using Barotrauma.Extensions; using Barotrauma.Networking; using Microsoft.Xna.Framework; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Barotrauma.IO; using System.Linq; using System.Net; using System.Reflection; using System.Security.Cryptography; using System.Text; namespace Barotrauma { [Obsolete("Use named tuples instead.")] public class Pair { public T1 First { get; set; } public T2 Second { get; set; } public Pair(T1 first, T2 second) { First = first; Second = second; } } internal readonly record struct SquareLine(Vector2[] Points, SquareLine.LineType Type) { internal enum LineType { /// /// Normal 4 point line /// /// /// ┏━━━ end /// ┃ /// start ━━━┛ /// FourPointForwardsLine, /// /// A line where the end is behind the start and 2 extra points are used to draw it /// /// /// start ━┓ /// ┏━━━━━━┛ /// ┗━ end /// SixPointBackwardsLine } } static partial class ToolBox { public static bool IsProperFilenameCase(string filename) { //File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds. //It also seems Path.GetFullPath may return a path with an incorrect case on Windows when the case of any of the game's //parent folders have been changed. #if !DEBUG && !LINUX return true; #endif CorrectFilenameCase(filename, out bool corrected); return !corrected; } public static string CorrectFilenameCase(string filename, out bool corrected, string directory = "") { char[] delimiters = { '/', '\\' }; string[] subDirs = filename.Split(delimiters); string originalFilename = filename; filename = ""; corrected = false; #if !WINDOWS if (File.Exists(originalFilename)) { return originalFilename; } #endif string startPath = directory ?? ""; string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/'); if (originalFilename.Replace('\\', '/').StartsWith(saveFolder)) { //paths that lead to the save folder might have incorrect case, //mainly if they come from a filelist startPath = saveFolder.EndsWith('/') ? saveFolder : $"{saveFolder}/"; filename = startPath; subDirs = subDirs.Skip(saveFolder.Split('/').Length).ToArray(); } else if (Path.IsPathRooted(originalFilename)) { #warning TODO: incorrect assumption or...? Figure out what this was actually supposed to fix, if anything. Might've been a perf thing. return originalFilename; //assume that rooted paths have correct case since these are generated by the game } for (int i = 0; i < subDirs.Length; i++) { if (i == subDirs.Length - 1 && string.IsNullOrEmpty(subDirs[i])) { break; } string subDir = subDirs[i].TrimEnd(); string enumPath = Path.Combine(startPath, filename); if (string.IsNullOrWhiteSpace(filename)) { enumPath = string.IsNullOrWhiteSpace(startPath) ? "./" : startPath; } string[] filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToArray(); if (filePaths.Any(s => s.Equals(subDir, StringComparison.Ordinal))) { filename += subDir; } else { string[] correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToArray(); if (correctedPaths.Any()) { corrected = true; filename += correctedPaths.First(); } else { //DebugConsole.ThrowError($"File \"{originalFilename}\" not found!"); corrected = false; return originalFilename; } } if (i < subDirs.Length - 1) { filename += "/"; } } return filename; } public static string RemoveInvalidFileNameChars(string fileName) { var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform().Concat(new char[] {';'}); foreach (char invalidChar in invalidChars) { fileName = fileName.Replace(invalidChar.ToString(), ""); } return fileName; } private static readonly System.Text.RegularExpressions.Regex removeBBCodeRegex = new System.Text.RegularExpressions.Regex(@"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]"); public static string RemoveBBCodeTags(string str) { if (string.IsNullOrEmpty(str)) { return str; } return removeBBCodeRegex.Replace(str, ""); } public static string RandomSeed(int length) { var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; return new string( Enumerable.Repeat(chars, length) .Select(s => s[Rand.Int(s.Length)]) .ToArray()); } public static int IdentifierToInt(Identifier id) => StringToInt(id.Value.ToLowerInvariant()); public static int StringToInt(string str) { str = str.Substring(0, Math.Min(str.Length, 32)); str = str.PadLeft(4, 'a'); byte[] asciiBytes = Encoding.ASCII.GetBytes(str); for (int i = 4; i < asciiBytes.Length; i++) { asciiBytes[i % 4] ^= asciiBytes[i]; } return BitConverter.ToInt32(asciiBytes, 0); } /// /// a method for changing inputtypes with old names to the new ones to ensure backwards compatibility with older subs /// public static string ConvertInputType(string inputType) { if (inputType == "ActionHit" || inputType == "Action") return "Use"; if (inputType == "SecondaryHit" || inputType == "Secondary") return "Aim"; return inputType; } /// /// Returns either a green [x] or a red [o] /// /// /// /// public static string GetDebugSymbol(bool isFinished, bool isRunning = false) { return isRunning ? "[‖color:243,162,50‖x‖color:end‖]" : $"[‖color:{(isFinished ? "0,255,0‖x" : "255,0,0‖o")}‖color:end‖]"; } /// /// Turn the object into a string and give it rich color based on the object type /// /// /// public static string ColorizeObject(this object obj) { string color = obj switch { bool b => b ? "80,250,123" : "255,85,85", string _ => "241,250,140", Identifier _ => "241,250,140", int _ => "189,147,249", float _ => "189,147,249", double _ => "189,147,249", null => "255,85,85", _ => "139,233,253" }; return obj is string || obj is Identifier ? $"‖color:{color}‖\"{obj}\"‖color:end‖" : $"‖color:{color}‖{obj ?? "null"}‖color:end‖"; } // Convert an RGB value into an HLS value. public static Vector3 RgbToHLS(Vector3 color) { double h, l, s; double double_r = color.X; double double_g = color.Y; double double_b = color.Z; // Get the maximum and minimum RGB components. double max = double_r; if (max < double_g) max = double_g; if (max < double_b) max = double_b; double min = double_r; if (min > double_g) min = double_g; if (min > double_b) min = double_b; double diff = max - min; l = (max + min) / 2; if (Math.Abs(diff) < 0.00001) { s = 0; h = 0; // H is really undefined. } else { if (l <= 0.5) s = diff / (max + min); else s = diff / (2 - max - min); double r_dist = (max - double_r) / diff; double g_dist = (max - double_g) / diff; double b_dist = (max - double_b) / diff; if (double_r == max) h = b_dist - g_dist; else if (double_g == max) h = 2 + r_dist - b_dist; else h = 4 + g_dist - r_dist; h = h * 60; if (h < 0) h += 360; } return new Vector3((float)h, (float)l, (float)s); } /// /// Calculates the minimum number of single-character edits (i.e. insertions, deletions or substitutions) required to change one string into the other /// public static int LevenshteinDistance(string s, string t) { int n = s.Length; int m = t.Length; int[,] d = new int[n + 1, m + 1]; if (n == 0 || m == 0) return 0; for (int i = 0; i <= n; d[i, 0] = i++) ; for (int j = 0; j <= m; d[0, j] = j++) ; for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { int cost = (t[j - 1] == s[i - 1]) ? 0 : 1; d[i, j] = Math.Min( Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1), d[i - 1, j - 1] + cost); } } return d[n, m]; } public static LocalizedString SecondsToReadableTime(float seconds) { int s = (int)(seconds % 60.0f); if (seconds < 60.0f) { return TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString()); } int h = (int)(seconds / (60.0f * 60.0f)); int m = (int)((seconds / 60.0f) % 60); LocalizedString text = ""; if (h != 0) { text = TextManager.GetWithVariable("timeformathours", "[hours]", h.ToString()); } if (m != 0) { LocalizedString minutesText = TextManager.GetWithVariable("timeformatminutes", "[minutes]", m.ToString()); text = text.IsNullOrEmpty() ? minutesText : LocalizedString.Join(" ", text, minutesText); } if (s != 0) { LocalizedString secondsText = TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString()); text = text.IsNullOrEmpty() ? secondsText : LocalizedString.Join(" ", text, secondsText); } return text; } private static Dictionary> cachedLines = new Dictionary>(); public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient) { List lines; if (cachedLines.ContainsKey(filePath)) { lines = cachedLines[filePath]; } else { try { lines = File.ReadAllLines(filePath).ToList(); cachedLines.Add(filePath, lines); if (lines.Count == 0) { DebugConsole.ThrowError("File \"" + filePath + "\" is empty!"); return ""; } } catch (Exception e) { DebugConsole.ThrowError("Couldn't open file \"" + filePath + "\"!", e); return ""; } } if (lines.Count == 0) return ""; return lines[Rand.Range(0, lines.Count, randSync)]; } /// /// Reads a number of bits from the buffer and inserts them to a new NetBuffer instance /// public static IReadMessage ExtractBits(this IReadMessage originalBuffer, int numberOfBits) { var buffer = new ReadWriteMessage(); for (int i = 0; i < numberOfBits; i++) { bool bit = originalBuffer.ReadBoolean(); buffer.WriteBoolean(bit); } buffer.BitPosition = 0; return buffer; } public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Rand.RandSync randSync) { return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync)); } public static T SelectWeightedRandom(IEnumerable objects, Func weightMethod, Random random) { if (typeof(PrefabWithUintIdentifier).IsAssignableFrom(typeof(T))) { objects = objects.OrderBy(p => (p as PrefabWithUintIdentifier)?.UintIdentifier ?? 0); } List objectList = objects.ToList(); List weights = objectList.Select(weightMethod).ToList(); return SelectWeightedRandom(objectList, weights, random); } public static T SelectWeightedRandom(IList objects, IList weights, Rand.RandSync randSync) { return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync)); } public static T SelectWeightedRandom(IList objects, IList weights, Random random) { if (objects.Count == 0) { return default; } if (objects.Count != weights.Count) { DebugConsole.ThrowError("Error in SelectWeightedRandom, number of objects does not match the number of weights.\n" + Environment.StackTrace.CleanupStackTrace()); return objects[0]; } float totalWeight = weights.Sum(); float randomNum = (float)(random.NextDouble() * totalWeight); for (int i = 0; i < objects.Count; i++) { if (randomNum <= weights[i]) { return objects[i]; } randomNum -= weights[i]; } return default; } /// /// Returns a new instance of the class with all properties and fields copied. /// public static T CreateCopy(this T source, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) where T : new() => CopyValues(source, new T(), flags); public static T CopyValuesTo(this T source, T target, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) => CopyValues(source, target, flags); /// /// Copies the values of the source to the destination. May not work, if the source is of higher inheritance class than the destination. Does not work with virtual properties. /// public static T CopyValues(T source, T destination, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) { if (source == null) { throw new Exception("Failed to copy object. Source is null."); } if (destination == null) { throw new Exception("Failed to copy object. Destination is null."); } Type type = source.GetType(); var properties = type.GetProperties(flags); foreach (var property in properties) { if (property.CanWrite) { property.SetValue(destination, property.GetValue(source, null), null); } } var fields = type.GetFields(flags); foreach (var field in fields) { field.SetValue(destination, field.GetValue(source)); } // Check that the fields match.Uncomment to apply the test, if in doubt. //if (fields.Any(f => { var value = f.GetValue(destination); return value == null || !value.Equals(f.GetValue(source)); })) //{ // throw new Exception("Failed to copy some of the fields."); //} return destination; } public static void SiftElement(this List list, int from, int to) { if (from < 0 || from >= list.Count) { throw new ArgumentException($"from parameter out of range (from={from}, range=[0..{list.Count - 1}])"); } if (to < 0 || to >= list.Count) { throw new ArgumentException($"to parameter out of range (to={to}, range=[0..{list.Count - 1}])"); } T elem = list[from]; if (from > to) { for (int i = from; i > to; i--) { list[i] = list[i - 1]; } list[to] = elem; } else if (from < to) { for (int i = from; i < to; i++) { list[i] = list[i + 1]; } list[to] = elem; } } public static string EscapeCharacters(string str) { return str.Replace("\\", "\\\\").Replace("\"", "\\\""); } public static string UnescapeCharacters(string str) { string retVal = ""; for (int i = 0; i < str.Length; i++) { if (str[i] != '\\') { retVal += str[i]; } else if (i+1 commands = new List(); int escape = 0; bool inQuotes = false; string piece = ""; for (int i = 0; i < command.Length; i++) { if (command[i] == '\\') { if (escape == 0) escape = 2; else piece += '\\'; } else if (command[i] == '"') { if (escape == 0) inQuotes = !inQuotes; else piece += '"'; } else if (command[i] == ' ' && !inQuotes) { if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece); piece = ""; } else if (escape == 0) piece += command[i]; if (escape > 0) escape--; } if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece); //add final piece return commands.ToArray(); } /// /// Cleans up a path by replacing backslashes with forward slashes, and /// optionally corrects the casing of the path. Recommended when serializing /// paths to a human-readable file to force case correction on all platforms. /// Also useful when working with paths to files that currently don't exist, /// i.e. case cannot be corrected. /// /// Path to clean up /// Should the case be corrected to match the filesystem? /// Directories that the path should be found in, not returned. /// Path with corrected slashes, and corrected case if requested. public static string CleanUpPathCrossPlatform(this string path, bool correctFilenameCase = true, string directory = "") { if (string.IsNullOrEmpty(path)) { return ""; } path = path .Replace('\\', '/'); if (path.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) { path = path.Substring("file:".Length); } while (path.IndexOf("//") >= 0) { path = path.Replace("//", "/"); } if (correctFilenameCase) { string correctedPath = CorrectFilenameCase(path, out _, directory); if (!string.IsNullOrEmpty(correctedPath)) { path = correctedPath; } } return path; } /// /// Cleans up a path by replacing backslashes with forward slashes, and /// corrects the casing of the path on non-Windows platforms. Recommended /// when loading a path from a file, to make sure that it is found on all /// platforms when attempting to open it. /// /// Path to clean up /// Path with corrected slashes, and corrected case if required by the platform. public static string CleanUpPath(this string path) { return path.CleanUpPathCrossPlatform( correctFilenameCase: #if WINDOWS false #else true #endif ); } public static float GetEasing(TransitionMode easing, float t) { return easing switch { TransitionMode.Smooth => MathUtils.SmoothStep(t), TransitionMode.Smoother => MathUtils.SmootherStep(t), TransitionMode.EaseIn => MathUtils.EaseIn(t), TransitionMode.EaseOut => MathUtils.EaseOut(t), TransitionMode.Exponential => t * t, TransitionMode.Linear => t, _ => t, }; } public static Rectangle GetWorldBounds(Point center, Point size) { Point halfSize = size.Divide(2); Point topLeft = new Point(center.X - halfSize.X, center.Y + halfSize.Y); return new Rectangle(topLeft, size); } public static void ThrowIfNull([NotNull] T o) { if (o is null) { throw new ArgumentNullException(); } } /// /// Converts a percentage value in the 0-1 range to a string representation in the format "x %" according to the grammar rules of the selected language /// public static string GetFormattedPercentage(float v) { return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value; } private static readonly ImmutableHashSet affectedCharacters = ImmutableHashSet.Create('%', '+', '%'); /// /// Extends % and + characters to color tags in talent name tooltips to make them look nicer. /// This obviously does not work in languages like French where a non breaking space is used /// so it's just a a bit extra for the languages it works with. /// /// /// public static string ExtendColorToPercentageSigns(string original) { const string colorEnd = "‖color:end‖", colorStart = "‖color:"; const char definitionIndicator = '‖'; char[] chars = original.ToCharArray(); for (int i = 0; i < chars.Length; i++) { if (!TryGetAt(i, chars, out char currentChar) || !affectedCharacters.Contains(currentChar)) { continue; } // look behind if (TryGetAt(i - 1, chars, out char c) && c is definitionIndicator) { int offset = colorEnd.Length; if (MatchesSequence(i - offset, colorEnd, chars)) { // push the color end tag forwards until the character is within the tag char prev = currentChar; for (int k = i - offset; k <= i; k++) { if (!TryGetAt(k, chars, out c)) { continue; } chars[k] = prev; prev = c; } continue; } } // look ahead if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator) { if (!MatchesSequence(i + 1, colorStart, chars)) { continue; } int offset = FindNextDefinitionOffset(i, colorStart.Length, chars); // we probably reached the end of the string if (offset > chars.Length) { continue; } // push the color start tag back until the character is within the tag char prev = currentChar; for (int k = i + offset; k >= i; k--) { if (!TryGetAt(k, chars, out c)) { continue; } chars[k] = prev; prev = c; } // skip needlessly checking this section again since we already know what's ahead i += offset; } } static int FindNextDefinitionOffset(int index, int initialOffset, char[] chars) { int offset = initialOffset; while (TryGetAt(index + offset, chars, out char c) && c is not definitionIndicator) { offset++; } return offset; } static bool MatchesSequence(int index, string sequence, char[] chars) { for (int i = 0; i < sequence.Length; i++) { if (!TryGetAt(index + i, chars, out char c) || c != sequence[i]) { return false; } } return true; } static bool TryGetAt(int i, char[] chars, out char c) { if (i >= 0 && i < chars.Length) { c = chars[i]; return true; } c = default; return false; } return new string(chars); } public static bool StatIdentifierMatches(Identifier original, Identifier match) { if (original == match) { return true; } return Matches(original, match) || Matches(match, original); static bool Matches(Identifier a, Identifier b) { for (int i = 0; i < b.Value.Length; i++) { if (i >= a.Value.Length) { return b[i] is '~'; } if (!CharEquals(a[i], b[i])) { return false; } } return false; } static bool CharEquals(char a, char b) => char.ToLowerInvariant(a) == char.ToLowerInvariant(b); } public static bool EquivalentTo(this IPEndPoint self, IPEndPoint other) => self.Address.EquivalentTo(other.Address) && self.Port == other.Port; public static bool EquivalentTo(this IPAddress self, IPAddress other) { if (self.IsIPv4MappedToIPv6) { self = self.MapToIPv4(); } if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); } return self.Equals(other); } /// /// Converts a 16-bit audio sample to float value between -1 and 1. /// public static float ShortAudioSampleToFloat(short value) { return value / 32767f; } /// /// Converts a float value between -1 and 1 to a 16-bit audio sample. /// public static short FloatToShortAudioSample(float value) { int temp = (int)(32767 * value); if (temp > short.MaxValue) { temp = short.MaxValue; } else if (temp < short.MinValue) { temp = short.MinValue; } return (short)temp; } /// = points[5].X - knobLength) { isBehind = true; points[4].X -= knobLength; points[3].X = points[4].X; points[3].Y -= points[3].Y - points[2].Y; } SquareLine.LineType type = isBehind ? SquareLine.LineType.SixPointBackwardsLine : SquareLine.LineType.FourPointForwardsLine; return new SquareLine(points, type); } /// /// Returns closest point on a rectangle to a given point. /// If the point is inside the rectangle, the point itself is returned. /// /// /// /// public static Vector2 GetClosestPointOnRectangle(RectangleF rect, Vector2 point) { Vector2 closest = new Vector2( MathHelper.Clamp(point.X, rect.Left, rect.Right), MathHelper.Clamp(point.Y, rect.Top, rect.Bottom)); if (point.X < rect.Left) { closest.X = rect.Left; } else if (point.X > rect.Right) { closest.X = rect.Right; } if (point.Y < rect.Top) { closest.Y = rect.Top; } else if (point.Y > rect.Bottom) { closest.Y = rect.Bottom; } return closest; } } }