Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs
2023-01-31 18:08:26 +02:00

812 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<T1, T2>
{
public T1 First { get; set; }
public T2 Second { get; set; }
public Pair(T1 first, T2 second)
{
First = first;
Second = second;
}
}
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);
}
/// <summary>
/// a method for changing inputtypes with old names to the new ones to ensure backwards compatibility with older subs
/// </summary>
public static string ConvertInputType(string inputType)
{
if (inputType == "ActionHit" || inputType == "Action") return "Use";
if (inputType == "SecondaryHit" || inputType == "Secondary") return "Aim";
return inputType;
}
/// <summary>
/// Convert a HSV value into a RGB value.
/// </summary>
/// <param name="hue">Value between 0 and 360</param>
/// <param name="saturation">Value between 0 and 1</param>
/// <param name="value">Value between 0 and 1</param>
/// <see href="https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB">Reference</see>
/// <returns></returns>
public static Color HSVToRGB(float hue, float saturation, float value)
{
float c = value * saturation;
float h = Math.Clamp(hue, 0, 360) / 60f;
float x = c * (1 - Math.Abs(h % 2 - 1));
float r = 0,
g = 0,
b = 0;
if (0 <= h && h <= 1) { r = c; g = x; b = 0; }
else if (1 < h && h <= 2) { r = x; g = c; b = 0; }
else if (2 < h && h <= 3) { r = 0; g = c; b = x; }
else if (3 < h && h <= 4) { r = 0; g = x; b = c; }
else if (4 < h && h <= 5) { r = x; g = 0; b = c; }
else if (5 < h && h <= 6) { r = c; g = 0; b = x; }
float m = value - c;
return new Color(r + m, g + m, b + m);
}
/// <summary>
/// Returns either a green [x] or a red [o]
/// </summary>
/// <param name="isFinished"></param>
/// <param name="isRunning"></param>
/// <returns></returns>
public static string GetDebugSymbol(bool isFinished, bool isRunning = false)
{
return isRunning ? "[‖color:243,162,50‖x‖color:end‖]" : $"[‖color:{(isFinished ? "0,255,0x" : "255,0,0o")}‖color:end‖]";
}
/// <summary>
/// Turn the object into a string and give it rich color based on the object type
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
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);
}
/// <summary>
/// Calculates the minimum number of single-character edits (i.e. insertions, deletions or substitutions) required to change one string into the other
/// </summary>
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<string, List<string>> cachedLines = new Dictionary<string, List<string>>();
public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
{
List<string> 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)];
}
/// <summary>
/// Reads a number of bits from the buffer and inserts them to a new NetBuffer instance
/// </summary>
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<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Rand.RandSync randSync)
{
return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync));
}
public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Random random)
{
List<T> objectList = objects.ToList();
List<float> weights = objectList.Select(o => weightMethod(o)).ToList();
return SelectWeightedRandom(objectList, weights, random);
}
public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Rand.RandSync randSync)
{
return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync));
}
public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Random random)
{
if (objects.Count == 0) return default(T);
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(T);
}
public static UInt32 IdentifierToUint32Hash(Identifier id, MD5 md5)
=> StringToUInt32Hash(id.Value.ToLowerInvariant(), md5);
public static UInt32 StringToUInt32Hash(string str, MD5 md5)
{
//calculate key based on MD5 hash instead of string.GetHashCode
//to ensure consistent results across platforms
byte[] inputBytes = Encoding.UTF8.GetBytes(str);
byte[] hash = md5.ComputeHash(inputBytes);
UInt32 key = (UInt32)((str.Length & 0xff) << 24); //could use more of the hash here instead?
key |= (UInt32)(hash[hash.Length - 3] << 16);
key |= (UInt32)(hash[hash.Length - 2] << 8);
key |= (UInt32)(hash[hash.Length - 1]);
return key;
}
/// <summary>
/// Returns a new instance of the class with all properties and fields copied.
/// </summary>
public static T CreateCopy<T>(this T source, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) where T : new() => CopyValues(source, new T(), flags);
public static T CopyValuesTo<T>(this T source, T target, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) => CopyValues(source, target, flags);
/// <summary>
/// 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.
/// </summary>
public static T CopyValues<T>(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<T>(this List<T> 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 ByteArrayToString(byte[] ba)
{
StringBuilder hex = new StringBuilder(ba.Length * 2);
foreach (byte b in ba)
hex.AppendFormat("{0:x2}", b);
return hex.ToString();
}
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<str.Length)
{
if (str[i+1] == '\\')
{
retVal += "\\";
}
else if (str[i+1] == '\"')
{
retVal += "\"";
}
i++;
}
}
return retVal;
}
public static string[] SplitCommand(string command)
{
command = command.Trim();
List<string> commands = new List<string>();
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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="path">Path to clean up</param>
/// <param name="correctFilenameCase">Should the case be corrected to match the filesystem?</param>
/// <param name="directory">Directories that the path should be found in, not returned.</param>
/// <returns>Path with corrected slashes, and corrected case if requested.</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="path">Path to clean up</param>
/// <returns>Path with corrected slashes, and corrected case if required by the platform.</returns>
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 Exception GetInnermost(this Exception e)
{
while (e.InnerException != null) { e = e.InnerException; }
return e;
}
public static void ThrowIfNull<T>([NotNull] T o)
{
if (o is null) { throw new ArgumentNullException(); }
}
public static string GetFormattedPercentage(float v)
{
return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value;
}
private static readonly ImmutableHashSet<char> affectedCharacters = ImmutableHashSet.Create('%', '+', '');
/// <summary>
/// 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.
/// </summary>
/// <param name="original"></param>
/// <returns></returns>
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);
}
}
}