Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs
Juan Pablo Arce 3f2c843247 Unstable v0.19.3.0
2022-09-02 15:10:56 -03:00

726 lines
26 KiB
C#

using Barotrauma.Extensions;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.IO;
using System.Linq;
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
{
internal static class Epoch
{
private static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Returns the current Unix Epoch (Coordinated Universal Time)
/// </summary>
public static int NowUTC
{
get
{
return (int)(DateTime.UtcNow.Subtract(epoch).TotalSeconds);
}
}
/// <summary>
/// Returns the current Unix Epoch (user's current time)
/// </summary>
public static int NowLocal
{
get
{
return (int)(DateTime.Now.Subtract(epoch).TotalSeconds);
}
}
/// <summary>
/// Convert an epoch to a datetime
/// </summary>
public static DateTime ToDateTime(decimal unixTime)
{
return epoch.AddSeconds((long)unixTime);
}
/// <summary>
/// Convert a DateTime to a unix time
/// </summary>
public static uint FromDateTime(DateTime dt)
{
return (uint)(dt.Subtract(epoch).TotalSeconds);
}
}
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.SaveFolder.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;
}
}
}