938 lines
35 KiB
C#
938 lines
35 KiB
C#
using Barotrauma.Extensions;
|
||
using Barotrauma.Networking;
|
||
using Microsoft.Xna.Framework;
|
||
using System;
|
||
using System.Collections.Concurrent;
|
||
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;
|
||
using System.Collections.Concurrent;
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
internal readonly record struct SquareLine(Vector2[] Points, SquareLine.LineType Type)
|
||
{
|
||
internal enum LineType
|
||
{
|
||
/// <summary>
|
||
/// Normal 4 point line
|
||
/// </summary>
|
||
/// <example>
|
||
/// ┏━━━ end
|
||
/// ┃
|
||
/// start ━━━┛
|
||
/// </example>
|
||
FourPointForwardsLine,
|
||
/// <summary>
|
||
/// A line where the end is behind the start and 2 extra points are used to draw it
|
||
/// </summary>
|
||
/// <example>
|
||
/// start ━┓
|
||
/// ┏━━━━━━┛
|
||
/// ┗━ end
|
||
/// </example>
|
||
SixPointBackwardsLine
|
||
}
|
||
}
|
||
|
||
static partial class ToolBox
|
||
{
|
||
/// <summary>
|
||
/// Returns the Barotrauma.dll assembly.
|
||
/// Used with <see cref="ReflectionUtils.GetTypeWithBackwardsCompatibility"/>
|
||
/// </summary>
|
||
public static Assembly BarotraumaAssembly
|
||
=> Assembly.GetAssembly(typeof(GameMain));
|
||
|
||
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;
|
||
}
|
||
|
||
private static readonly ConcurrentDictionary<string, string> cachedFileNames = new ConcurrentDictionary<string, string>();
|
||
|
||
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
|
||
if (cachedFileNames.TryGetValue(originalFilename, out string existingName))
|
||
{
|
||
// Already processed and cached.
|
||
return existingName;
|
||
}
|
||
|
||
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 += "/"; }
|
||
}
|
||
|
||
cachedFileNames.TryAdd(originalFilename, 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());
|
||
|
||
/// <summary>
|
||
/// Convert the specified string to an integer using a deterministic formula. The same string always provides the same number, and different strings should generally provide a different number.
|
||
/// </summary>
|
||
public static int StringToInt(string str)
|
||
{
|
||
//deterministic hash function based on https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
|
||
unchecked
|
||
{
|
||
int hash1 = (5381 << 16) + 5381;
|
||
int hash2 = hash1;
|
||
|
||
for (int i = 0; i < str.Length; i += 2)
|
||
{
|
||
hash1 = ((hash1 << 5) + hash1) ^ str[i];
|
||
if (i == str.Length - 1) { break; }
|
||
hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
|
||
}
|
||
|
||
return hash1 + (hash2 * 1566083941);
|
||
}
|
||
}
|
||
|
||
/// <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>
|
||
/// 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,0‖x" : "255,0,0‖o")}‖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 readonly ConcurrentDictionary<string, List<string>> cachedLines = new ConcurrentDictionary<string, List<string>>();
|
||
public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
|
||
{
|
||
List<string> lines = cachedLines.GetOrAdd(filePath, path =>
|
||
{
|
||
try
|
||
{
|
||
var fileLines = File.ReadAllLines(path, catchUnauthorizedAccessExceptions: false).ToList();
|
||
if (fileLines.Count == 0)
|
||
{
|
||
DebugConsole.ThrowError("File \"" + path + "\" is empty!");
|
||
}
|
||
return fileLines;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
DebugConsole.ThrowError("Couldn't open file \"" + path + "\"!", e);
|
||
return new List<string>();
|
||
}
|
||
});
|
||
|
||
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)
|
||
{
|
||
if (typeof(PrefabWithUintIdentifier).IsAssignableFrom(typeof(T)))
|
||
{
|
||
objects = objects.OrderBy(p => (p as PrefabWithUintIdentifier)?.UintIdentifier ?? 0);
|
||
}
|
||
List<T> objectList = objects.ToList();
|
||
List<float> weights = objectList.Select(weightMethod).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; }
|
||
|
||
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);
|
||
T objectWithNonZeroWeight = default;
|
||
for (int i = 0; i < objects.Count; i++)
|
||
{
|
||
if (weights[i] > 0)
|
||
{
|
||
objectWithNonZeroWeight = objects[i];
|
||
}
|
||
if (randomNum <= weights[i])
|
||
{
|
||
return objects[i];
|
||
}
|
||
randomNum -= weights[i];
|
||
}
|
||
//it's possible for rounding errors to cause an element to not get selected if we pick a random number very close to 1
|
||
//to work around that, always return some object with a non-zero weight if none gets returned in the loop above
|
||
return objectWithNonZeroWeight;
|
||
}
|
||
|
||
/// <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 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 void ThrowIfNull<T>([NotNull] T o)
|
||
{
|
||
if (o is null) { throw new ArgumentNullException(); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Converts a 16-bit audio sample to float value between -1 and 1.
|
||
/// </summary>
|
||
public static float ShortAudioSampleToFloat(short value)
|
||
{
|
||
return value / 32767f;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Converts a float value between -1 and 1 to a 16-bit audio sample.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary
|
||
public static SquareLine GetSquareLineBetweenPoints(Vector2 start, Vector2 end, float knobLength = 24f)
|
||
{
|
||
Vector2[] points = new Vector2[6];
|
||
|
||
// set the start and end points
|
||
points[0] = points[1] = points[2] = start;
|
||
points[5] = points[4] = points[3] = end;
|
||
|
||
points[2].X += (points[3].X - points[2].X) / 2;
|
||
points[2].X = Math.Max(points[2].X, points[0].X + knobLength);
|
||
points[3].X = points[2].X;
|
||
|
||
bool isBehind = false;
|
||
|
||
// if the node is "behind" us do some magic to make the line curve to prevent overlapping
|
||
if (points[2].X <= points[0].X + knobLength)
|
||
{
|
||
isBehind = true;
|
||
points[1].X += knobLength;
|
||
points[2].X = points[2].X;
|
||
points[2].Y += (points[4].Y - points[1].Y) / 2;
|
||
}
|
||
|
||
if (points[3].X >= 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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Converts a byte array to a string of hex values.
|
||
/// </summary>
|
||
/// <example>
|
||
/// { 4, 3, 75, 80 } -> "04034B50"
|
||
/// </example>
|
||
/// <param name="bytes"></param>
|
||
/// <returns></returns>
|
||
public static string BytesToHexString(byte[] bytes)
|
||
{
|
||
StringBuilder sb = new StringBuilder();
|
||
foreach (byte b in bytes)
|
||
{
|
||
sb.Append(b.ToString("X2"));
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Returns closest point on a rectangle to a given point.
|
||
/// If the point is inside the rectangle, the point itself is returned.
|
||
/// </summary>
|
||
/// <param name="rect"></param>
|
||
/// <param name="point"></param>
|
||
/// <returns></returns>
|
||
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;
|
||
}
|
||
|
||
public static ImmutableArray<uint> PrefabCollectionToUintIdentifierArray(IEnumerable<PrefabWithUintIdentifier> prefabs)
|
||
=> prefabs.Select(static p => p.UintIdentifier).ToImmutableArray();
|
||
|
||
public static ImmutableArray<T> UintIdentifierArrayToPrefabCollection<T>(PrefabCollection<T> Prefabs, IEnumerable<uint> uintIdentifiers) where T : PrefabWithUintIdentifier
|
||
{
|
||
var builder = ImmutableArray.CreateBuilder<T>();
|
||
|
||
foreach (uint uintIdentifier in uintIdentifiers)
|
||
{
|
||
var matchingPrefab = Prefabs.Find(p => p.UintIdentifier == uintIdentifier);
|
||
if (matchingPrefab == null)
|
||
{
|
||
DebugConsole.ThrowError($"Unable to find prefab with uint identifier {uintIdentifier}");
|
||
continue;
|
||
}
|
||
builder.Add(matchingPrefab);
|
||
}
|
||
|
||
return builder.ToImmutable();
|
||
}
|
||
}
|
||
}
|