3 using Microsoft.Xna.Framework;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Diagnostics.CodeAnalysis;
11 using System.Reflection;
12 using System.Security.Cryptography;
17 [Obsolete(
"Use named tuples instead.")]
23 public Pair(T1 first, T2 second)
30 internal readonly record
struct SquareLine(Vector2[] Points, SquareLine.LineType Type)
32 internal enum LineType
42 FourPointForwardsLine,
55 static partial class ToolBox
57 public static bool IsProperFilenameCase(
string filename)
66 CorrectFilenameCase(filename, out
bool corrected);
71 private static readonly Dictionary<string, string> cachedFileNames =
new Dictionary<string, string>();
73 public static string CorrectFilenameCase(
string filename, out
bool corrected,
string directory =
"")
75 char[] delimiters = {
'/',
'\\' };
76 string[] subDirs = filename.Split(delimiters);
77 string originalFilename = filename;
82 if (File.Exists(originalFilename))
84 return originalFilename;
87 if (cachedFileNames.TryGetValue(originalFilename, out
string existingName))
93 string startPath = directory ??
"";
95 string saveFolder = SaveUtil.DefaultSaveFolder.Replace(
'\\',
'/');
96 if (originalFilename.Replace(
'\\',
'/').StartsWith(saveFolder))
100 startPath = saveFolder.EndsWith(
'/') ? saveFolder : $
"{saveFolder}/";
101 filename = startPath;
102 subDirs = subDirs.Skip(saveFolder.Split(
'/').Length).ToArray();
104 else if (Path.IsPathRooted(originalFilename))
106 #warning TODO: incorrect assumption or...? Figure out what this was actually supposed to fix, if anything. Might've been a perf thing.
107 return originalFilename;
110 for (
int i = 0; i < subDirs.Length; i++)
112 if (i == subDirs.Length - 1 &&
string.IsNullOrEmpty(subDirs[i]))
117 string subDir = subDirs[i].TrimEnd();
118 string enumPath = Path.Combine(startPath, filename);
120 if (
string.IsNullOrWhiteSpace(filename))
122 enumPath =
string.IsNullOrWhiteSpace(startPath) ?
"./" : startPath;
125 string[] filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToArray();
127 if (filePaths.Any(s => s.Equals(subDir, StringComparison.Ordinal)))
133 string[] correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToArray();
134 if (correctedPaths.Any())
137 filename += correctedPaths.First();
143 return originalFilename;
146 if (i < subDirs.Length - 1) { filename +=
"/"; }
149 cachedFileNames.Add(originalFilename, filename);
153 public static string RemoveInvalidFileNameChars(
string fileName)
155 var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform().Concat(
new char[] {
';'});
156 foreach (
char invalidChar
in invalidChars)
158 fileName = fileName.Replace(invalidChar.ToString(),
"");
163 private static readonly System.Text.RegularExpressions.Regex removeBBCodeRegex =
164 new System.Text.RegularExpressions.Regex(
@"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]");
166 public static string RemoveBBCodeTags(
string str)
168 if (
string.IsNullOrEmpty(str)) {
return str; }
169 return removeBBCodeRegex.Replace(str,
"");
172 public static string RandomSeed(
int length)
174 var chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
176 Enumerable.Repeat(chars, length)
177 .Select(s => s[Rand.Int(s.Length)])
181 public static int IdentifierToInt(Identifier
id) => StringToInt(
id.Value.ToLowerInvariant());
186 public static int StringToInt(
string str)
191 int hash1 = (5381 << 16) + 5381;
194 for (
int i = 0; i < str.Length; i += 2)
196 hash1 = ((hash1 << 5) + hash1) ^ str[i];
197 if (i == str.Length - 1) {
break; }
198 hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
201 return hash1 + (hash2 * 1566083941);
208 public static string ConvertInputType(
string inputType)
210 if (inputType ==
"ActionHit" || inputType ==
"Action")
return "Use";
211 if (inputType ==
"SecondaryHit" || inputType ==
"Secondary")
return "Aim";
222 public static string GetDebugSymbol(
bool isFinished,
bool isRunning =
false)
224 return isRunning ?
"[‖color:243,162,50‖x‖color:end‖]" : $
"[‖color:{(isFinished ? "0,255,0‖x
" : "255,0,0‖o
")}‖color:end‖]";
232 public static string ColorizeObject(
this object obj)
234 string color = obj
switch
236 bool b => b ?
"80,250,123" :
"255,85,85",
237 string _ =>
"241,250,140",
238 Identifier _ =>
"241,250,140",
239 int _ =>
"189,147,249",
240 float _ =>
"189,147,249",
241 double _ =>
"189,147,249",
246 return obj is
string || obj is Identifier
247 ? $
"‖color:{color}‖\"{obj}\"‖color:end‖"
248 : $
"‖color:{color}‖{obj ?? "null"}‖color:end‖";
252 public static Vector3 RgbToHLS(Vector3 color)
256 double double_r = color.X;
257 double double_g = color.Y;
258 double double_b = color.Z;
261 double max = double_r;
262 if (max < double_g) max = double_g;
263 if (max < double_b) max = double_b;
265 double min = double_r;
266 if (min > double_g) min = double_g;
267 if (min > double_b) min = double_b;
269 double diff = max - min;
271 if (Math.Abs(diff) < 0.00001)
278 if (l <= 0.5) s = diff / (max + min);
279 else s = diff / (2 - max - min);
281 double r_dist = (max - double_r) / diff;
282 double g_dist = (max - double_g) / diff;
283 double b_dist = (max - double_b) / diff;
285 if (double_r == max) h = b_dist - g_dist;
286 else if (double_g == max) h = 2 + r_dist - b_dist;
287 else h = 4 + g_dist - r_dist;
293 return new Vector3((
float)h, (
float)l, (
float)s);
299 public static int LevenshteinDistance(
string s,
string t)
303 int[,] d =
new int[n + 1, m + 1];
305 if (n == 0 || m == 0)
return 0;
307 for (
int i = 0; i <= n; d[i, 0] = i++) ;
308 for (
int j = 0; j <= m; d[0, j] = j++) ;
310 for (
int i = 1; i <= n; i++)
312 for (
int j = 1; j <= m; j++)
314 int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
317 Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
318 d[i - 1, j - 1] + cost);
325 public static LocalizedString SecondsToReadableTime(
float seconds)
327 int s = (int)(seconds % 60.0f);
330 return TextManager.GetWithVariable(
"timeformatseconds",
"[seconds]", s.ToString());
333 int h = (int)(seconds / (60.0f * 60.0f));
334 int m = (int)((seconds / 60.0f) % 60);
336 LocalizedString text =
"";
337 if (h != 0) { text = TextManager.GetWithVariable(
"timeformathours",
"[hours]", h.ToString()); }
340 LocalizedString minutesText = TextManager.GetWithVariable(
"timeformatminutes",
"[minutes]", m.ToString());
341 text = text.IsNullOrEmpty() ? minutesText : LocalizedString.Join(
" ", text, minutesText);
345 LocalizedString secondsText = TextManager.GetWithVariable(
"timeformatseconds",
"[seconds]", s.ToString());
346 text = text.IsNullOrEmpty() ? secondsText : LocalizedString.Join(
" ", text, secondsText);
351 private static Dictionary<string, List<string>> cachedLines =
new Dictionary<string, List<string>>();
352 public static string GetRandomLine(
string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
355 if (cachedLines.ContainsKey(filePath))
357 lines = cachedLines[filePath];
363 lines = File.ReadAllLines(filePath, catchUnauthorizedAccessExceptions:
false).ToList();
364 cachedLines.Add(filePath, lines);
365 if (lines.Count == 0)
367 DebugConsole.ThrowError(
"File \"" + filePath +
"\" is empty!");
373 DebugConsole.ThrowError(
"Couldn't open file \"" + filePath +
"\"!", e);
378 if (lines.Count == 0)
return "";
379 return lines[Rand.Range(0, lines.Count, randSync)];
387 var buffer =
new ReadWriteMessage();
389 for (
int i = 0; i < numberOfBits; i++)
392 buffer.WriteBoolean(bit);
394 buffer.BitPosition = 0;
399 public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Rand.RandSync randSync)
401 return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync));
404 public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Random random)
406 if (typeof(PrefabWithUintIdentifier).IsAssignableFrom(typeof(T)))
408 objects = objects.OrderBy(p => (p as PrefabWithUintIdentifier)?.UintIdentifier ?? 0);
410 List<T> objectList = objects.ToList();
411 List<float> weights = objectList.Select(weightMethod).ToList();
412 return SelectWeightedRandom(objectList, weights, random);
415 public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Rand.RandSync randSync)
417 return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync));
420 public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Random random)
422 if (objects.Count == 0) {
return default; }
424 if (objects.Count != weights.Count)
426 DebugConsole.ThrowError(
"Error in SelectWeightedRandom, number of objects does not match the number of weights.\n" + Environment.StackTrace.CleanupStackTrace());
430 float totalWeight = weights.Sum();
431 float randomNum = (float)(random.NextDouble() * totalWeight);
432 T objectWithNonZeroWeight =
default;
433 for (
int i = 0; i < objects.Count; i++)
437 objectWithNonZeroWeight = objects[i];
439 if (randomNum <= weights[i])
443 randomNum -= weights[i];
447 return objectWithNonZeroWeight;
453 public static T CreateCopy<T>(
this T source, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) where T : new() => CopyValues(source, new T(), flags);
454 public static T CopyValuesTo<T>(this T source, T target, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) => CopyValues(source, target, flags);
459 public static T CopyValues<T>(T source, T destination, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public)
463 throw new Exception(
"Failed to copy object. Source is null.");
465 if (destination ==
null)
467 throw new Exception(
"Failed to copy object. Destination is null.");
469 Type type = source.GetType();
470 var properties = type.GetProperties(flags);
471 foreach (var property
in properties)
473 if (property.CanWrite)
475 property.SetValue(destination, property.GetValue(source,
null),
null);
478 var fields = type.GetFields(flags);
479 foreach (var field
in fields)
481 field.SetValue(destination, field.GetValue(source));
491 public static void SiftElement<T>(
this List<T> list,
int from,
int to)
493 if (from < 0 || from >= list.Count) {
throw new ArgumentException($
"from parameter out of range (from={from}, range=[0..{list.Count - 1}])"); }
494 if (to < 0 || to >= list.Count) {
throw new ArgumentException($
"to parameter out of range (to={to}, range=[0..{list.Count - 1}])"); }
499 for (
int i = from; i > to; i--)
501 list[i] = list[i - 1];
507 for (
int i = from; i < to; i++)
509 list[i] = list[i + 1];
515 public static string EscapeCharacters(
string str)
517 return str.Replace(
"\\",
"\\\\").Replace(
"\"",
"\\\"");
520 public static string UnescapeCharacters(
string str)
523 for (
int i = 0; i < str.Length; i++)
529 else if (i+1<str.Length)
531 if (str[i+1] ==
'\\')
535 else if (str[i+1] ==
'\"')
545 public static string[] SplitCommand(
string command)
547 command = command.Trim();
549 List<string> commands =
new List<string>();
551 bool inQuotes =
false;
554 for (
int i = 0; i < command.Length; i++)
556 if (command[i] ==
'\\')
558 if (escape == 0) escape = 2;
561 else if (command[i] ==
'"')
563 if (escape == 0) inQuotes = !inQuotes;
566 else if (command[i] ==
' ' && !inQuotes)
568 if (!
string.IsNullOrWhiteSpace(piece)) commands.Add(piece);
571 else if (escape == 0) piece += command[i];
573 if (escape > 0) escape--;
576 if (!
string.IsNullOrWhiteSpace(piece)) commands.Add(piece);
578 return commands.ToArray();
592 public static string CleanUpPathCrossPlatform(
this string path,
bool correctFilenameCase =
true,
string directory =
"")
594 if (
string.IsNullOrEmpty(path)) {
return ""; }
598 if (path.StartsWith(
"file:", StringComparison.OrdinalIgnoreCase))
600 path = path.Substring(
"file:".Length);
602 while (path.IndexOf(
"//") >= 0)
604 path = path.Replace(
"//",
"/");
607 if (correctFilenameCase)
609 string correctedPath = CorrectFilenameCase(path, out _, directory);
610 if (!
string.IsNullOrEmpty(correctedPath)) { path = correctedPath; }
624 public static string CleanUpPath(
this string path)
626 return path.CleanUpPathCrossPlatform(
650 public static Rectangle GetWorldBounds(Point center, Point size)
652 Point halfSize = size.Divide(2);
653 Point topLeft =
new Point(center.X - halfSize.X, center.Y + halfSize.Y);
654 return new Rectangle(topLeft, size);
657 public static void ThrowIfNull<T>([NotNull] T o)
659 if (o is
null) {
throw new ArgumentNullException(); }
665 public static string GetFormattedPercentage(
float v)
667 return TextManager.GetWithVariable(
"percentageformat",
"[value]", ((
int)MathF.Round(v * 100)).ToString()).Value;
670 private static readonly ImmutableHashSet<char> affectedCharacters = ImmutableHashSet.Create(
'%',
'+',
'%');
679 public static string ExtendColorToPercentageSigns(
string original)
681 const string colorEnd =
"‖color:end‖",
682 colorStart =
"‖color:";
684 const char definitionIndicator =
'‖';
686 char[] chars = original.ToCharArray();
688 for (
int i = 0; i < chars.Length; i++)
690 if (!TryGetAt(i, chars, out
char currentChar) || !affectedCharacters.Contains(currentChar)) {
continue; }
693 if (TryGetAt(i - 1, chars, out
char c) && c is definitionIndicator)
695 int offset = colorEnd.Length;
697 if (MatchesSequence(i - offset, colorEnd, chars))
700 char prev = currentChar;
701 for (
int k = i - offset; k <= i; k++)
703 if (!TryGetAt(k, chars, out c)) {
continue; }
713 if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator)
715 if (!MatchesSequence(i + 1, colorStart, chars)) {
continue; }
717 int offset = FindNextDefinitionOffset(i, colorStart.Length, chars);
720 if (offset > chars.Length) {
continue; }
723 char prev = currentChar;
724 for (
int k = i + offset; k >= i; k--)
726 if (!TryGetAt(k, chars, out c)) {
continue; }
737 static int FindNextDefinitionOffset(
int index,
int initialOffset,
char[] chars)
739 int offset = initialOffset;
740 while (TryGetAt(index + offset, chars, out
char c) && c is not definitionIndicator) { offset++; }
744 static bool MatchesSequence(
int index,
string sequence,
char[] chars)
746 for (
int i = 0; i < sequence.Length; i++)
748 if (!TryGetAt(index + i, chars, out
char c) || c != sequence[i]) {
return false; }
754 static bool TryGetAt(
int i,
char[] chars, out
char c)
756 if (i >= 0 && i < chars.Length)
766 return new string(chars);
769 public static bool StatIdentifierMatches(Identifier original, Identifier match)
771 if (original == match) {
return true; }
772 return Matches(original, match) || Matches(match, original);
774 static bool Matches(Identifier a, Identifier b)
776 for (
int i = 0; i < b.Value.Length; i++)
778 if (i >= a.Value.Length) {
return b[i] is
'~'; }
779 if (!CharEquals(a[i], b[i])) {
return false; }
784 static bool CharEquals(
char a,
char b) =>
char.ToLowerInvariant(a) ==
char.ToLowerInvariant(b);
787 public static bool EquivalentTo(
this IPEndPoint
self, IPEndPoint other)
788 =>
self.Address.EquivalentTo(other.Address) &&
self.Port == other.Port;
790 public static bool EquivalentTo(
this IPAddress
self, IPAddress other)
792 if (
self.IsIPv4MappedToIPv6) {
self =
self.MapToIPv4(); }
793 if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); }
794 return self.Equals(other);
800 public static float ShortAudioSampleToFloat(
short value)
802 return value / 32767f;
808 public static short FloatToShortAudioSample(
float value)
810 int temp = (int)(32767 * value);
811 if (temp >
short.MaxValue)
813 temp =
short.MaxValue;
815 else if (temp <
short.MinValue)
817 temp =
short.MinValue;
823 public static SquareLine GetSquareLineBetweenPoints(Vector2 start, Vector2 end,
float knobLength = 24f)
825 Vector2[] points =
new Vector2[6];
828 points[0] = points[1] = points[2] = start;
829 points[5] = points[4] = points[3] = end;
831 points[2].X += (points[3].X - points[2].X) / 2;
832 points[2].X = Math.Max(points[2].X, points[0].X + knobLength);
833 points[3].X = points[2].X;
835 bool isBehind =
false;
838 if (points[2].X <= points[0].X + knobLength)
841 points[1].X += knobLength;
842 points[2].X = points[2].X;
843 points[2].Y += (points[4].Y - points[1].Y) / 2;
846 if (points[3].X >= points[5].X - knobLength)
849 points[4].X -= knobLength;
850 points[3].X = points[4].X;
851 points[3].Y -= points[3].Y - points[2].Y;
854 SquareLine.LineType type = isBehind
855 ? SquareLine.LineType.SixPointBackwardsLine
856 : SquareLine.LineType.FourPointForwardsLine;
858 return new SquareLine(points, type);
869 public static string BytesToHexString(
byte[] bytes)
871 StringBuilder sb =
new StringBuilder();
872 foreach (
byte b
in bytes)
874 sb.Append(b.ToString(
"X2"));
876 return sb.ToString();
886 public static Vector2 GetClosestPointOnRectangle(RectangleF rect, Vector2 point)
888 Vector2 closest =
new Vector2(
889 MathHelper.Clamp(point.X, rect.Left, rect.Right),
890 MathHelper.Clamp(point.Y, rect.Top, rect.Bottom));
892 if (point.X < rect.Left)
894 closest.X = rect.Left;
896 else if (point.X > rect.Right)
898 closest.X = rect.Right;
901 if (point.Y < rect.Top)
903 closest.Y = rect.Top;
905 else if (point.Y > rect.Bottom)
907 closest.Y = rect.Bottom;
913 public static ImmutableArray<uint> PrefabCollectionToUintIdentifierArray(IEnumerable<PrefabWithUintIdentifier> prefabs)
914 => prefabs.Select(
static p => p.UintIdentifier).ToImmutableArray();
916 public static ImmutableArray<T> UintIdentifierArrayToPrefabCollection<T>(PrefabCollection<T> Prefabs, IEnumerable<uint> uintIdentifiers) where T : PrefabWithUintIdentifier
918 var builder = ImmutableArray.CreateBuilder<T>();
920 foreach (uint uintIdentifier
in uintIdentifiers)
922 var matchingPrefab = Prefabs.Find(p => p.UintIdentifier == uintIdentifier);
923 if (matchingPrefab ==
null)
925 DebugConsole.ThrowError($
"Unable to find prefab with uint identifier {uintIdentifier}");
928 builder.Add(matchingPrefab);
931 return builder.ToImmutable();
Pair(T1 first, T2 second)