Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Utils/ToolBox.cs
2026-04-30 21:59:54 +08:00

938 lines
35 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.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,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 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();
}
}
}