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 public static string CorrectFilenameCase(
string filename, out
bool corrected,
string directory =
"")
73 char[] delimiters = {
'/',
'\\' };
74 string[] subDirs = filename.Split(delimiters);
75 string originalFilename = filename;
80 if (File.Exists(originalFilename))
82 return originalFilename;
86 string startPath = directory ??
"";
88 string saveFolder = SaveUtil.DefaultSaveFolder.Replace(
'\\',
'/');
89 if (originalFilename.Replace(
'\\',
'/').StartsWith(saveFolder))
93 startPath = saveFolder.EndsWith(
'/') ? saveFolder : $
"{saveFolder}/";
95 subDirs = subDirs.Skip(saveFolder.Split(
'/').Length).ToArray();
97 else if (Path.IsPathRooted(originalFilename))
99 #warning TODO: incorrect assumption or...? Figure out what this was actually supposed to fix, if anything. Might've been a perf thing.
100 return originalFilename;
103 for (
int i = 0; i < subDirs.Length; i++)
105 if (i == subDirs.Length - 1 &&
string.IsNullOrEmpty(subDirs[i]))
110 string subDir = subDirs[i].TrimEnd();
111 string enumPath = Path.Combine(startPath, filename);
113 if (
string.IsNullOrWhiteSpace(filename))
115 enumPath =
string.IsNullOrWhiteSpace(startPath) ?
"./" : startPath;
118 string[] filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToArray();
120 if (filePaths.Any(s => s.Equals(subDir, StringComparison.Ordinal)))
126 string[] correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToArray();
127 if (correctedPaths.Any())
130 filename += correctedPaths.First();
136 return originalFilename;
139 if (i < subDirs.Length - 1) { filename +=
"/"; }
145 public static string RemoveInvalidFileNameChars(
string fileName)
147 var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform().Concat(
new char[] {
';'});
148 foreach (
char invalidChar
in invalidChars)
150 fileName = fileName.Replace(invalidChar.ToString(),
"");
155 private static readonly System.Text.RegularExpressions.Regex removeBBCodeRegex =
156 new System.Text.RegularExpressions.Regex(
@"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]");
158 public static string RemoveBBCodeTags(
string str)
160 if (
string.IsNullOrEmpty(str)) {
return str; }
161 return removeBBCodeRegex.Replace(str,
"");
164 public static string RandomSeed(
int length)
166 var chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
168 Enumerable.Repeat(chars, length)
169 .Select(s => s[Rand.Int(s.Length)])
173 public static int IdentifierToInt(Identifier
id) => StringToInt(
id.Value.ToLowerInvariant());
175 public static int StringToInt(
string str)
177 str = str.Substring(0, Math.Min(str.Length, 32));
179 str = str.PadLeft(4,
'a');
181 byte[] asciiBytes = Encoding.ASCII.GetBytes(str);
183 for (
int i = 4; i < asciiBytes.Length; i++)
185 asciiBytes[i % 4] ^= asciiBytes[i];
188 return BitConverter.ToInt32(asciiBytes, 0);
194 public static string ConvertInputType(
string inputType)
196 if (inputType ==
"ActionHit" || inputType ==
"Action")
return "Use";
197 if (inputType ==
"SecondaryHit" || inputType ==
"Secondary")
return "Aim";
208 public static string GetDebugSymbol(
bool isFinished,
bool isRunning =
false)
210 return isRunning ?
"[‖color:243,162,50‖x‖color:end‖]" : $
"[‖color:{(isFinished ? "0,255,0‖x
" : "255,0,0‖o
")}‖color:end‖]";
218 public static string ColorizeObject(
this object obj)
220 string color = obj
switch
222 bool b => b ?
"80,250,123" :
"255,85,85",
223 string _ =>
"241,250,140",
224 Identifier _ =>
"241,250,140",
225 int _ =>
"189,147,249",
226 float _ =>
"189,147,249",
227 double _ =>
"189,147,249",
232 return obj is
string || obj is Identifier
233 ? $
"‖color:{color}‖\"{obj}\"‖color:end‖"
234 : $
"‖color:{color}‖{obj ?? "null"}‖color:end‖";
238 public static Vector3 RgbToHLS(Vector3 color)
242 double double_r = color.X;
243 double double_g = color.Y;
244 double double_b = color.Z;
247 double max = double_r;
248 if (max < double_g) max = double_g;
249 if (max < double_b) max = double_b;
251 double min = double_r;
252 if (min > double_g) min = double_g;
253 if (min > double_b) min = double_b;
255 double diff = max - min;
257 if (Math.Abs(diff) < 0.00001)
264 if (l <= 0.5) s = diff / (max + min);
265 else s = diff / (2 - max - min);
267 double r_dist = (max - double_r) / diff;
268 double g_dist = (max - double_g) / diff;
269 double b_dist = (max - double_b) / diff;
271 if (double_r == max) h = b_dist - g_dist;
272 else if (double_g == max) h = 2 + r_dist - b_dist;
273 else h = 4 + g_dist - r_dist;
279 return new Vector3((
float)h, (
float)l, (
float)s);
285 public static int LevenshteinDistance(
string s,
string t)
289 int[,] d =
new int[n + 1, m + 1];
291 if (n == 0 || m == 0)
return 0;
293 for (
int i = 0; i <= n; d[i, 0] = i++) ;
294 for (
int j = 0; j <= m; d[0, j] = j++) ;
296 for (
int i = 1; i <= n; i++)
298 for (
int j = 1; j <= m; j++)
300 int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
303 Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
304 d[i - 1, j - 1] + cost);
311 public static LocalizedString SecondsToReadableTime(
float seconds)
313 int s = (int)(seconds % 60.0f);
316 return TextManager.GetWithVariable(
"timeformatseconds",
"[seconds]", s.ToString());
319 int h = (int)(seconds / (60.0f * 60.0f));
320 int m = (int)((seconds / 60.0f) % 60);
322 LocalizedString text =
"";
323 if (h != 0) { text = TextManager.GetWithVariable(
"timeformathours",
"[hours]", h.ToString()); }
326 LocalizedString minutesText = TextManager.GetWithVariable(
"timeformatminutes",
"[minutes]", m.ToString());
327 text = text.IsNullOrEmpty() ? minutesText : LocalizedString.Join(
" ", text, minutesText);
331 LocalizedString secondsText = TextManager.GetWithVariable(
"timeformatseconds",
"[seconds]", s.ToString());
332 text = text.IsNullOrEmpty() ? secondsText : LocalizedString.Join(
" ", text, secondsText);
337 private static Dictionary<string, List<string>> cachedLines =
new Dictionary<string, List<string>>();
338 public static string GetRandomLine(
string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
341 if (cachedLines.ContainsKey(filePath))
343 lines = cachedLines[filePath];
349 lines = File.ReadAllLines(filePath).ToList();
350 cachedLines.Add(filePath, lines);
351 if (lines.Count == 0)
353 DebugConsole.ThrowError(
"File \"" + filePath +
"\" is empty!");
359 DebugConsole.ThrowError(
"Couldn't open file \"" + filePath +
"\"!", e);
364 if (lines.Count == 0)
return "";
365 return lines[Rand.Range(0, lines.Count, randSync)];
373 var buffer =
new ReadWriteMessage();
375 for (
int i = 0; i < numberOfBits; i++)
378 buffer.WriteBoolean(bit);
380 buffer.BitPosition = 0;
385 public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Rand.RandSync randSync)
387 return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync));
390 public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Random random)
392 if (typeof(PrefabWithUintIdentifier).IsAssignableFrom(typeof(T)))
394 objects = objects.OrderBy(p => (p as PrefabWithUintIdentifier)?.UintIdentifier ?? 0);
396 List<T> objectList = objects.ToList();
397 List<float> weights = objectList.Select(weightMethod).ToList();
398 return SelectWeightedRandom(objectList, weights, random);
401 public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Rand.RandSync randSync)
403 return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync));
406 public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Random random)
408 if (objects.Count == 0) {
return default; }
410 if (objects.Count != weights.Count)
412 DebugConsole.ThrowError(
"Error in SelectWeightedRandom, number of objects does not match the number of weights.\n" + Environment.StackTrace.CleanupStackTrace());
416 float totalWeight = weights.Sum();
418 float randomNum = (float)(random.NextDouble() * totalWeight);
419 for (
int i = 0; i < objects.Count; i++)
421 if (randomNum <= weights[i])
425 randomNum -= weights[i];
433 public static T CreateCopy<T>(
this T source, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) where T : new() => CopyValues(source, new T(), flags);
434 public static T CopyValuesTo<T>(this T source, T target, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) => CopyValues(source, target, flags);
439 public static T CopyValues<T>(T source, T destination, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public)
443 throw new Exception(
"Failed to copy object. Source is null.");
445 if (destination ==
null)
447 throw new Exception(
"Failed to copy object. Destination is null.");
449 Type type = source.GetType();
450 var properties = type.GetProperties(flags);
451 foreach (var property
in properties)
453 if (property.CanWrite)
455 property.SetValue(destination, property.GetValue(source,
null),
null);
458 var fields = type.GetFields(flags);
459 foreach (var field
in fields)
461 field.SetValue(destination, field.GetValue(source));
471 public static void SiftElement<T>(
this List<T> list,
int from,
int to)
473 if (from < 0 || from >= list.Count) {
throw new ArgumentException($
"from parameter out of range (from={from}, range=[0..{list.Count - 1}])"); }
474 if (to < 0 || to >= list.Count) {
throw new ArgumentException($
"to parameter out of range (to={to}, range=[0..{list.Count - 1}])"); }
479 for (
int i = from; i > to; i--)
481 list[i] = list[i - 1];
487 for (
int i = from; i < to; i++)
489 list[i] = list[i + 1];
495 public static string EscapeCharacters(
string str)
497 return str.Replace(
"\\",
"\\\\").Replace(
"\"",
"\\\"");
500 public static string UnescapeCharacters(
string str)
503 for (
int i = 0; i < str.Length; i++)
509 else if (i+1<str.Length)
511 if (str[i+1] ==
'\\')
515 else if (str[i+1] ==
'\"')
525 public static string[] SplitCommand(
string command)
527 command = command.Trim();
529 List<string> commands =
new List<string>();
531 bool inQuotes =
false;
534 for (
int i = 0; i < command.Length; i++)
536 if (command[i] ==
'\\')
538 if (escape == 0) escape = 2;
541 else if (command[i] ==
'"')
543 if (escape == 0) inQuotes = !inQuotes;
546 else if (command[i] ==
' ' && !inQuotes)
548 if (!
string.IsNullOrWhiteSpace(piece)) commands.Add(piece);
551 else if (escape == 0) piece += command[i];
553 if (escape > 0) escape--;
556 if (!
string.IsNullOrWhiteSpace(piece)) commands.Add(piece);
558 return commands.ToArray();
572 public static string CleanUpPathCrossPlatform(
this string path,
bool correctFilenameCase =
true,
string directory =
"")
574 if (
string.IsNullOrEmpty(path)) {
return ""; }
578 if (path.StartsWith(
"file:", StringComparison.OrdinalIgnoreCase))
580 path = path.Substring(
"file:".Length);
582 while (path.IndexOf(
"//") >= 0)
584 path = path.Replace(
"//",
"/");
587 if (correctFilenameCase)
589 string correctedPath = CorrectFilenameCase(path, out _, directory);
590 if (!
string.IsNullOrEmpty(correctedPath)) { path = correctedPath; }
604 public static string CleanUpPath(
this string path)
606 return path.CleanUpPathCrossPlatform(
630 public static Rectangle GetWorldBounds(Point center, Point size)
632 Point halfSize = size.Divide(2);
633 Point topLeft =
new Point(center.X - halfSize.X, center.Y + halfSize.Y);
637 public static void ThrowIfNull<T>([NotNull] T o)
639 if (o is
null) {
throw new ArgumentNullException(); }
645 public static string GetFormattedPercentage(
float v)
647 return TextManager.GetWithVariable(
"percentageformat",
"[value]", ((
int)MathF.Round(v * 100)).ToString()).Value;
650 private static readonly ImmutableHashSet<char> affectedCharacters = ImmutableHashSet.Create(
'%',
'+',
'%');
659 public static string ExtendColorToPercentageSigns(
string original)
661 const string colorEnd =
"‖color:end‖",
662 colorStart =
"‖color:";
664 const char definitionIndicator =
'‖';
666 char[] chars = original.ToCharArray();
668 for (
int i = 0; i < chars.Length; i++)
670 if (!TryGetAt(i, chars, out
char currentChar) || !affectedCharacters.Contains(currentChar)) {
continue; }
673 if (TryGetAt(i - 1, chars, out
char c) && c is definitionIndicator)
675 int offset = colorEnd.Length;
677 if (MatchesSequence(i - offset, colorEnd, chars))
680 char prev = currentChar;
681 for (
int k = i - offset; k <= i; k++)
683 if (!TryGetAt(k, chars, out c)) {
continue; }
693 if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator)
695 if (!MatchesSequence(i + 1, colorStart, chars)) {
continue; }
697 int offset = FindNextDefinitionOffset(i, colorStart.Length, chars);
700 if (offset > chars.Length) {
continue; }
703 char prev = currentChar;
704 for (
int k = i + offset; k >= i; k--)
706 if (!TryGetAt(k, chars, out c)) {
continue; }
717 static int FindNextDefinitionOffset(
int index,
int initialOffset,
char[] chars)
719 int offset = initialOffset;
720 while (TryGetAt(index + offset, chars, out
char c) && c is not definitionIndicator) { offset++; }
724 static bool MatchesSequence(
int index,
string sequence,
char[] chars)
726 for (
int i = 0; i < sequence.Length; i++)
728 if (!TryGetAt(index + i, chars, out
char c) || c != sequence[i]) {
return false; }
734 static bool TryGetAt(
int i,
char[] chars, out
char c)
736 if (i >= 0 && i < chars.Length)
746 return new string(chars);
749 public static bool StatIdentifierMatches(Identifier original, Identifier match)
751 if (original == match) {
return true; }
752 return Matches(original, match) || Matches(match, original);
754 static bool Matches(Identifier a, Identifier b)
756 for (
int i = 0; i < b.Value.Length; i++)
758 if (i >= a.Value.Length) {
return b[i] is
'~'; }
759 if (!CharEquals(a[i], b[i])) {
return false; }
764 static bool CharEquals(
char a,
char b) =>
char.ToLowerInvariant(a) ==
char.ToLowerInvariant(b);
767 public static bool EquivalentTo(
this IPEndPoint
self, IPEndPoint other)
768 =>
self.Address.EquivalentTo(other.Address) &&
self.Port == other.Port;
770 public static bool EquivalentTo(
this IPAddress
self, IPAddress other)
772 if (
self.IsIPv4MappedToIPv6) {
self =
self.MapToIPv4(); }
773 if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); }
774 return self.Equals(other);
780 public static float ShortAudioSampleToFloat(
short value)
782 return value / 32767f;
788 public static short FloatToShortAudioSample(
float value)
790 int temp = (int)(32767 * value);
791 if (temp >
short.MaxValue)
793 temp =
short.MaxValue;
795 else if (temp <
short.MinValue)
797 temp =
short.MinValue;
803 public static SquareLine GetSquareLineBetweenPoints(Vector2 start, Vector2 end,
float knobLength = 24f)
805 Vector2[] points =
new Vector2[6];
808 points[0] = points[1] = points[2] = start;
809 points[5] = points[4] = points[3] = end;
811 points[2].X += (points[3].X - points[2].X) / 2;
812 points[2].X = Math.Max(points[2].X, points[0].X + knobLength);
813 points[3].X = points[2].X;
815 bool isBehind =
false;
818 if (points[2].X <= points[0].X + knobLength)
821 points[1].X += knobLength;
822 points[2].X = points[2].X;
823 points[2].Y += (points[4].Y - points[1].Y) / 2;
826 if (points[3].X >= points[5].X - knobLength)
829 points[4].X -= knobLength;
830 points[3].X = points[4].X;
831 points[3].Y -= points[3].Y - points[2].Y;
834 SquareLine.LineType type = isBehind
835 ? SquareLine.LineType.SixPointBackwardsLine
836 : SquareLine.LineType.FourPointForwardsLine;
838 return new SquareLine(points, type);
848 public static Vector2 GetClosestPointOnRectangle(RectangleF rect, Vector2 point)
850 Vector2 closest =
new Vector2(
851 MathHelper.Clamp(point.X, rect.Left, rect.Right),
852 MathHelper.Clamp(point.Y, rect.Top, rect.Bottom));
854 if (point.X < rect.Left)
856 closest.X = rect.Left;
858 else if (point.X > rect.Right)
860 closest.X = rect.Right;
863 if (point.Y < rect.Top)
865 closest.Y = rect.Top;
867 else if (point.Y > rect.Bottom)
869 closest.Y = rect.Bottom;
Pair(T1 first, T2 second)