Server LuaCsForBarotrauma
ToolBox.cs
3 using Microsoft.Xna.Framework;
4 using System;
5 using System.Collections.Generic;
6 using System.Collections.Immutable;
7 using System.Diagnostics.CodeAnalysis;
8 using Barotrauma.IO;
9 using System.Linq;
10 using System.Net;
11 using System.Reflection;
12 using System.Security.Cryptography;
13 using System.Text;
14 
15 namespace Barotrauma
16 {
17  [Obsolete("Use named tuples instead.")]
18  public class Pair<T1, T2>
19  {
20  public T1 First { get; set; }
21  public T2 Second { get; set; }
22 
23  public Pair(T1 first, T2 second)
24  {
25  First = first;
26  Second = second;
27  }
28  }
29 
30  internal readonly record struct SquareLine(Vector2[] Points, SquareLine.LineType Type)
31  {
32  internal enum LineType
33  {
42  FourPointForwardsLine,
51  SixPointBackwardsLine
52  }
53  }
54 
55  static partial class ToolBox
56  {
57  public static bool IsProperFilenameCase(string filename)
58  {
59  //File case only matters on Linux where the filesystem is case-sensitive, so we don't need these errors in release builds.
60  //It also seems Path.GetFullPath may return a path with an incorrect case on Windows when the case of any of the game's
61  //parent folders have been changed.
62 #if !DEBUG && !LINUX
63  return true;
64 #endif
65 
66  CorrectFilenameCase(filename, out bool corrected);
67 
68  return !corrected;
69  }
70 
71  private static readonly Dictionary<string, string> cachedFileNames = new Dictionary<string, string>();
72 
73  public static string CorrectFilenameCase(string filename, out bool corrected, string directory = "")
74  {
75  char[] delimiters = { '/', '\\' };
76  string[] subDirs = filename.Split(delimiters);
77  string originalFilename = filename;
78  filename = "";
79  corrected = false;
80 
81 #if !WINDOWS
82  if (File.Exists(originalFilename))
83  {
84  return originalFilename;
85  }
86 #endif
87  if (cachedFileNames.TryGetValue(originalFilename, out string existingName))
88  {
89  // Already processed and cached.
90  return existingName;
91  }
92 
93  string startPath = directory ?? "";
94 
95  string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/');
96  if (originalFilename.Replace('\\', '/').StartsWith(saveFolder))
97  {
98  //paths that lead to the save folder might have incorrect case,
99  //mainly if they come from a filelist
100  startPath = saveFolder.EndsWith('/') ? saveFolder : $"{saveFolder}/";
101  filename = startPath;
102  subDirs = subDirs.Skip(saveFolder.Split('/').Length).ToArray();
103  }
104  else if (Path.IsPathRooted(originalFilename))
105  {
106  #warning TODO: incorrect assumption or...? Figure out what this was actually supposed to fix, if anything. Might've been a perf thing.
107  return originalFilename; //assume that rooted paths have correct case since these are generated by the game
108  }
109 
110  for (int i = 0; i < subDirs.Length; i++)
111  {
112  if (i == subDirs.Length - 1 && string.IsNullOrEmpty(subDirs[i]))
113  {
114  break;
115  }
116 
117  string subDir = subDirs[i].TrimEnd();
118  string enumPath = Path.Combine(startPath, filename);
119 
120  if (string.IsNullOrWhiteSpace(filename))
121  {
122  enumPath = string.IsNullOrWhiteSpace(startPath) ? "./" : startPath;
123  }
124 
125  string[] filePaths = Directory.GetFileSystemEntries(enumPath).Select(Path.GetFileName).ToArray();
126 
127  if (filePaths.Any(s => s.Equals(subDir, StringComparison.Ordinal)))
128  {
129  filename += subDir;
130  }
131  else
132  {
133  string[] correctedPaths = filePaths.Where(s => s.Equals(subDir, StringComparison.OrdinalIgnoreCase)).ToArray();
134  if (correctedPaths.Any())
135  {
136  corrected = true;
137  filename += correctedPaths.First();
138  }
139  else
140  {
141  //DebugConsole.ThrowError($"File \"{originalFilename}\" not found!");
142  corrected = false;
143  return originalFilename;
144  }
145  }
146  if (i < subDirs.Length - 1) { filename += "/"; }
147  }
148 
149  cachedFileNames.Add(originalFilename, filename);
150  return filename;
151  }
152 
153  public static string RemoveInvalidFileNameChars(string fileName)
154  {
155  var invalidChars = Path.GetInvalidFileNameCharsCrossPlatform().Concat(new char[] {';'});
156  foreach (char invalidChar in invalidChars)
157  {
158  fileName = fileName.Replace(invalidChar.ToString(), "");
159  }
160  return fileName;
161  }
162 
163  private static readonly System.Text.RegularExpressions.Regex removeBBCodeRegex =
164  new System.Text.RegularExpressions.Regex(@"\[\/?(?:b|i|u|url|quote|code|img|color|size)*?.*?\]");
165 
166  public static string RemoveBBCodeTags(string str)
167  {
168  if (string.IsNullOrEmpty(str)) { return str; }
169  return removeBBCodeRegex.Replace(str, "");
170  }
171 
172  public static string RandomSeed(int length)
173  {
174  var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
175  return new string(
176  Enumerable.Repeat(chars, length)
177  .Select(s => s[Rand.Int(s.Length)])
178  .ToArray());
179  }
180 
181  public static int IdentifierToInt(Identifier id) => StringToInt(id.Value.ToLowerInvariant());
182 
186  public static int StringToInt(string str)
187  {
188  //deterministic hash function based on https://andrewlock.net/why-is-string-gethashcode-different-each-time-i-run-my-program-in-net-core/
189  unchecked
190  {
191  int hash1 = (5381 << 16) + 5381;
192  int hash2 = hash1;
193 
194  for (int i = 0; i < str.Length; i += 2)
195  {
196  hash1 = ((hash1 << 5) + hash1) ^ str[i];
197  if (i == str.Length - 1) { break; }
198  hash2 = ((hash2 << 5) + hash2) ^ str[i + 1];
199  }
200 
201  return hash1 + (hash2 * 1566083941);
202  }
203  }
204 
208  public static string ConvertInputType(string inputType)
209  {
210  if (inputType == "ActionHit" || inputType == "Action") return "Use";
211  if (inputType == "SecondaryHit" || inputType == "Secondary") return "Aim";
212 
213  return inputType;
214  }
215 
222  public static string GetDebugSymbol(bool isFinished, bool isRunning = false)
223  {
224  return isRunning ? "[‖color:243,162,50‖x‖color:end‖]" : $"[‖color:{(isFinished ? "0,255,0‖x" : "255,0,0‖o")}‖color:end‖]";
225  }
226 
232  public static string ColorizeObject(this object obj)
233  {
234  string color = obj switch
235  {
236  bool b => b ? "80,250,123" : "255,85,85",
237  string _ => "241,250,140",
238  Identifier _ => "241,250,140",
239  int _ => "189,147,249",
240  float _ => "189,147,249",
241  double _ => "189,147,249",
242  null => "255,85,85",
243  _ => "139,233,253"
244  };
245 
246  return obj is string || obj is Identifier
247  ? $"‖color:{color}‖\"{obj}\"‖color:end‖"
248  : $"‖color:{color}‖{obj ?? "null"}‖color:end‖";
249  }
250 
251  // Convert an RGB value into an HLS value.
252  public static Vector3 RgbToHLS(Vector3 color)
253  {
254  double h, l, s;
255 
256  double double_r = color.X;
257  double double_g = color.Y;
258  double double_b = color.Z;
259 
260  // Get the maximum and minimum RGB components.
261  double max = double_r;
262  if (max < double_g) max = double_g;
263  if (max < double_b) max = double_b;
264 
265  double min = double_r;
266  if (min > double_g) min = double_g;
267  if (min > double_b) min = double_b;
268 
269  double diff = max - min;
270  l = (max + min) / 2;
271  if (Math.Abs(diff) < 0.00001)
272  {
273  s = 0;
274  h = 0; // H is really undefined.
275  }
276  else
277  {
278  if (l <= 0.5) s = diff / (max + min);
279  else s = diff / (2 - max - min);
280 
281  double r_dist = (max - double_r) / diff;
282  double g_dist = (max - double_g) / diff;
283  double b_dist = (max - double_b) / diff;
284 
285  if (double_r == max) h = b_dist - g_dist;
286  else if (double_g == max) h = 2 + r_dist - b_dist;
287  else h = 4 + g_dist - r_dist;
288 
289  h = h * 60;
290  if (h < 0) h += 360;
291  }
292 
293  return new Vector3((float)h, (float)l, (float)s);
294  }
295 
299  public static int LevenshteinDistance(string s, string t)
300  {
301  int n = s.Length;
302  int m = t.Length;
303  int[,] d = new int[n + 1, m + 1];
304 
305  if (n == 0 || m == 0) return 0;
306 
307  for (int i = 0; i <= n; d[i, 0] = i++) ;
308  for (int j = 0; j <= m; d[0, j] = j++) ;
309 
310  for (int i = 1; i <= n; i++)
311  {
312  for (int j = 1; j <= m; j++)
313  {
314  int cost = (t[j - 1] == s[i - 1]) ? 0 : 1;
315 
316  d[i, j] = Math.Min(
317  Math.Min(d[i - 1, j] + 1, d[i, j - 1] + 1),
318  d[i - 1, j - 1] + cost);
319  }
320  }
321 
322  return d[n, m];
323  }
324 
325  public static LocalizedString SecondsToReadableTime(float seconds)
326  {
327  int s = (int)(seconds % 60.0f);
328  if (seconds < 60.0f)
329  {
330  return TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString());
331  }
332 
333  int h = (int)(seconds / (60.0f * 60.0f));
334  int m = (int)((seconds / 60.0f) % 60);
335 
336  LocalizedString text = "";
337  if (h != 0) { text = TextManager.GetWithVariable("timeformathours", "[hours]", h.ToString()); }
338  if (m != 0)
339  {
340  LocalizedString minutesText = TextManager.GetWithVariable("timeformatminutes", "[minutes]", m.ToString());
341  text = text.IsNullOrEmpty() ? minutesText : LocalizedString.Join(" ", text, minutesText);
342  }
343  if (s != 0)
344  {
345  LocalizedString secondsText = TextManager.GetWithVariable("timeformatseconds", "[seconds]", s.ToString());
346  text = text.IsNullOrEmpty() ? secondsText : LocalizedString.Join(" ", text, secondsText);
347  }
348  return text;
349  }
350 
351  private static Dictionary<string, List<string>> cachedLines = new Dictionary<string, List<string>>();
352  public static string GetRandomLine(string filePath, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
353  {
354  List<string> lines;
355  if (cachedLines.ContainsKey(filePath))
356  {
357  lines = cachedLines[filePath];
358  }
359  else
360  {
361  try
362  {
363  lines = File.ReadAllLines(filePath, catchUnauthorizedAccessExceptions: false).ToList();
364  cachedLines.Add(filePath, lines);
365  if (lines.Count == 0)
366  {
367  DebugConsole.ThrowError("File \"" + filePath + "\" is empty!");
368  return "";
369  }
370  }
371  catch (Exception e)
372  {
373  DebugConsole.ThrowError("Couldn't open file \"" + filePath + "\"!", e);
374  return "";
375  }
376  }
377 
378  if (lines.Count == 0) return "";
379  return lines[Rand.Range(0, lines.Count, randSync)];
380  }
381 
385  public static IReadMessage ExtractBits(this IReadMessage originalBuffer, int numberOfBits)
386  {
387  var buffer = new ReadWriteMessage();
388 
389  for (int i = 0; i < numberOfBits; i++)
390  {
391  bool bit = originalBuffer.ReadBoolean();
392  buffer.WriteBoolean(bit);
393  }
394  buffer.BitPosition = 0;
395 
396  return buffer;
397  }
398 
399  public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Rand.RandSync randSync)
400  {
401  return SelectWeightedRandom(objects, weightMethod, Rand.GetRNG(randSync));
402  }
403 
404  public static T SelectWeightedRandom<T>(IEnumerable<T> objects, Func<T, float> weightMethod, Random random)
405  {
406  if (typeof(PrefabWithUintIdentifier).IsAssignableFrom(typeof(T)))
407  {
408  objects = objects.OrderBy(p => (p as PrefabWithUintIdentifier)?.UintIdentifier ?? 0);
409  }
410  List<T> objectList = objects.ToList();
411  List<float> weights = objectList.Select(weightMethod).ToList();
412  return SelectWeightedRandom(objectList, weights, random);
413  }
414 
415  public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Rand.RandSync randSync)
416  {
417  return SelectWeightedRandom(objects, weights, Rand.GetRNG(randSync));
418  }
419 
420  public static T SelectWeightedRandom<T>(IList<T> objects, IList<float> weights, Random random)
421  {
422  if (objects.Count == 0) { return default; }
423 
424  if (objects.Count != weights.Count)
425  {
426  DebugConsole.ThrowError("Error in SelectWeightedRandom, number of objects does not match the number of weights.\n" + Environment.StackTrace.CleanupStackTrace());
427  return objects[0];
428  }
429 
430  float totalWeight = weights.Sum();
431  float randomNum = (float)(random.NextDouble() * totalWeight);
432  T objectWithNonZeroWeight = default;
433  for (int i = 0; i < objects.Count; i++)
434  {
435  if (weights[i] > 0)
436  {
437  objectWithNonZeroWeight = objects[i];
438  }
439  if (randomNum <= weights[i])
440  {
441  return objects[i];
442  }
443  randomNum -= weights[i];
444  }
445  //it's possible for rounding errors to cause an element to not get selected if we pick a random number very close to 1
446  //to work around that, always return some object with a non-zero weight if none gets returned in the loop above
447  return objectWithNonZeroWeight;
448  }
449 
453  public static T CreateCopy<T>(this T source, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) where T : new() => CopyValues(source, new T(), flags);
454  public static T CopyValuesTo<T>(this T source, T target, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public) => CopyValues(source, target, flags);
455 
459  public static T CopyValues<T>(T source, T destination, BindingFlags flags = BindingFlags.Instance | BindingFlags.Public)
460  {
461  if (source == null)
462  {
463  throw new Exception("Failed to copy object. Source is null.");
464  }
465  if (destination == null)
466  {
467  throw new Exception("Failed to copy object. Destination is null.");
468  }
469  Type type = source.GetType();
470  var properties = type.GetProperties(flags);
471  foreach (var property in properties)
472  {
473  if (property.CanWrite)
474  {
475  property.SetValue(destination, property.GetValue(source, null), null);
476  }
477  }
478  var fields = type.GetFields(flags);
479  foreach (var field in fields)
480  {
481  field.SetValue(destination, field.GetValue(source));
482  }
483  // Check that the fields match.Uncomment to apply the test, if in doubt.
484  //if (fields.Any(f => { var value = f.GetValue(destination); return value == null || !value.Equals(f.GetValue(source)); }))
485  //{
486  // throw new Exception("Failed to copy some of the fields.");
487  //}
488  return destination;
489  }
490 
491  public static void SiftElement<T>(this List<T> list, int from, int to)
492  {
493  if (from < 0 || from >= list.Count) { throw new ArgumentException($"from parameter out of range (from={from}, range=[0..{list.Count - 1}])"); }
494  if (to < 0 || to >= list.Count) { throw new ArgumentException($"to parameter out of range (to={to}, range=[0..{list.Count - 1}])"); }
495 
496  T elem = list[from];
497  if (from > to)
498  {
499  for (int i = from; i > to; i--)
500  {
501  list[i] = list[i - 1];
502  }
503  list[to] = elem;
504  }
505  else if (from < to)
506  {
507  for (int i = from; i < to; i++)
508  {
509  list[i] = list[i + 1];
510  }
511  list[to] = elem;
512  }
513  }
514 
515  public static string EscapeCharacters(string str)
516  {
517  return str.Replace("\\", "\\\\").Replace("\"", "\\\"");
518  }
519 
520  public static string UnescapeCharacters(string str)
521  {
522  string retVal = "";
523  for (int i = 0; i < str.Length; i++)
524  {
525  if (str[i] != '\\')
526  {
527  retVal += str[i];
528  }
529  else if (i+1<str.Length)
530  {
531  if (str[i+1] == '\\')
532  {
533  retVal += "\\";
534  }
535  else if (str[i+1] == '\"')
536  {
537  retVal += "\"";
538  }
539  i++;
540  }
541  }
542  return retVal;
543  }
544 
545  public static string[] SplitCommand(string command)
546  {
547  command = command.Trim();
548 
549  List<string> commands = new List<string>();
550  int escape = 0;
551  bool inQuotes = false;
552  string piece = "";
553 
554  for (int i = 0; i < command.Length; i++)
555  {
556  if (command[i] == '\\')
557  {
558  if (escape == 0) escape = 2;
559  else piece += '\\';
560  }
561  else if (command[i] == '"')
562  {
563  if (escape == 0) inQuotes = !inQuotes;
564  else piece += '"';
565  }
566  else if (command[i] == ' ' && !inQuotes)
567  {
568  if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece);
569  piece = "";
570  }
571  else if (escape == 0) piece += command[i];
572 
573  if (escape > 0) escape--;
574  }
575 
576  if (!string.IsNullOrWhiteSpace(piece)) commands.Add(piece); //add final piece
577 
578  return commands.ToArray();
579  }
580 
592  public static string CleanUpPathCrossPlatform(this string path, bool correctFilenameCase = true, string directory = "")
593  {
594  if (string.IsNullOrEmpty(path)) { return ""; }
595 
596  path = path
597  .Replace('\\', '/');
598  if (path.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
599  {
600  path = path.Substring("file:".Length);
601  }
602  while (path.IndexOf("//") >= 0)
603  {
604  path = path.Replace("//", "/");
605  }
606 
607  if (correctFilenameCase)
608  {
609  string correctedPath = CorrectFilenameCase(path, out _, directory);
610  if (!string.IsNullOrEmpty(correctedPath)) { path = correctedPath; }
611  }
612 
613  return path;
614  }
615 
624  public static string CleanUpPath(this string path)
625  {
626  return path.CleanUpPathCrossPlatform(
627  correctFilenameCase:
628 #if WINDOWS
629  false
630 #else
631  true
632 #endif
633  );
634  }
635 
636  public static float GetEasing(TransitionMode easing, float t)
637  {
638  return easing switch
639  {
640  TransitionMode.Smooth => MathUtils.SmoothStep(t),
641  TransitionMode.Smoother => MathUtils.SmootherStep(t),
642  TransitionMode.EaseIn => MathUtils.EaseIn(t),
643  TransitionMode.EaseOut => MathUtils.EaseOut(t),
644  TransitionMode.Exponential => t * t,
645  TransitionMode.Linear => t,
646  _ => t,
647  };
648  }
649 
650  public static Rectangle GetWorldBounds(Point center, Point size)
651  {
652  Point halfSize = size.Divide(2);
653  Point topLeft = new Point(center.X - halfSize.X, center.Y + halfSize.Y);
654  return new Rectangle(topLeft, size);
655  }
656 
657  public static void ThrowIfNull<T>([NotNull] T o)
658  {
659  if (o is null) { throw new ArgumentNullException(); }
660  }
661 
665  public static string GetFormattedPercentage(float v)
666  {
667  return TextManager.GetWithVariable("percentageformat", "[value]", ((int)MathF.Round(v * 100)).ToString()).Value;
668  }
669 
670  private static readonly ImmutableHashSet<char> affectedCharacters = ImmutableHashSet.Create('%', '+', '%');
671 
679  public static string ExtendColorToPercentageSigns(string original)
680  {
681  const string colorEnd = "‖color:end‖",
682  colorStart = "‖color:";
683 
684  const char definitionIndicator = '‖';
685 
686  char[] chars = original.ToCharArray();
687 
688  for (int i = 0; i < chars.Length; i++)
689  {
690  if (!TryGetAt(i, chars, out char currentChar) || !affectedCharacters.Contains(currentChar)) { continue; }
691 
692  // look behind
693  if (TryGetAt(i - 1, chars, out char c) && c is definitionIndicator)
694  {
695  int offset = colorEnd.Length;
696 
697  if (MatchesSequence(i - offset, colorEnd, chars))
698  {
699  // push the color end tag forwards until the character is within the tag
700  char prev = currentChar;
701  for (int k = i - offset; k <= i; k++)
702  {
703  if (!TryGetAt(k, chars, out c)) { continue; }
704 
705  chars[k] = prev;
706  prev = c;
707  }
708  continue;
709  }
710  }
711 
712  // look ahead
713  if (TryGetAt(i + 1, chars, out c) && c is definitionIndicator)
714  {
715  if (!MatchesSequence(i + 1, colorStart, chars)) { continue; }
716 
717  int offset = FindNextDefinitionOffset(i, colorStart.Length, chars);
718 
719  // we probably reached the end of the string
720  if (offset > chars.Length) { continue; }
721 
722  // push the color start tag back until the character is within the tag
723  char prev = currentChar;
724  for (int k = i + offset; k >= i; k--)
725  {
726  if (!TryGetAt(k, chars, out c)) { continue; }
727 
728  chars[k] = prev;
729  prev = c;
730  }
731 
732  // skip needlessly checking this section again since we already know what's ahead
733  i += offset;
734  }
735  }
736 
737  static int FindNextDefinitionOffset(int index, int initialOffset, char[] chars)
738  {
739  int offset = initialOffset;
740  while (TryGetAt(index + offset, chars, out char c) && c is not definitionIndicator) { offset++; }
741  return offset;
742  }
743 
744  static bool MatchesSequence(int index, string sequence, char[] chars)
745  {
746  for (int i = 0; i < sequence.Length; i++)
747  {
748  if (!TryGetAt(index + i, chars, out char c) || c != sequence[i]) { return false; }
749  }
750 
751  return true;
752  }
753 
754  static bool TryGetAt(int i, char[] chars, out char c)
755  {
756  if (i >= 0 && i < chars.Length)
757  {
758  c = chars[i];
759  return true;
760  }
761 
762  c = default;
763  return false;
764  }
765 
766  return new string(chars);
767  }
768 
769  public static bool StatIdentifierMatches(Identifier original, Identifier match)
770  {
771  if (original == match) { return true; }
772  return Matches(original, match) || Matches(match, original);
773 
774  static bool Matches(Identifier a, Identifier b)
775  {
776  for (int i = 0; i < b.Value.Length; i++)
777  {
778  if (i >= a.Value.Length) { return b[i] is '~'; }
779  if (!CharEquals(a[i], b[i])) { return false; }
780  }
781  return false;
782  }
783 
784  static bool CharEquals(char a, char b) => char.ToLowerInvariant(a) == char.ToLowerInvariant(b);
785  }
786 
787  public static bool EquivalentTo(this IPEndPoint self, IPEndPoint other)
788  => self.Address.EquivalentTo(other.Address) && self.Port == other.Port;
789 
790  public static bool EquivalentTo(this IPAddress self, IPAddress other)
791  {
792  if (self.IsIPv4MappedToIPv6) { self = self.MapToIPv4(); }
793  if (other.IsIPv4MappedToIPv6) { other = other.MapToIPv4(); }
794  return self.Equals(other);
795  }
796 
800  public static float ShortAudioSampleToFloat(short value)
801  {
802  return value / 32767f;
803  }
804 
808  public static short FloatToShortAudioSample(float value)
809  {
810  int temp = (int)(32767 * value);
811  if (temp > short.MaxValue)
812  {
813  temp = short.MaxValue;
814  }
815  else if (temp < short.MinValue)
816  {
817  temp = short.MinValue;
818  }
819  return (short)temp;
820  }
821 
823  public static SquareLine GetSquareLineBetweenPoints(Vector2 start, Vector2 end, float knobLength = 24f)
824  {
825  Vector2[] points = new Vector2[6];
826 
827  // set the start and end points
828  points[0] = points[1] = points[2] = start;
829  points[5] = points[4] = points[3] = end;
830 
831  points[2].X += (points[3].X - points[2].X) / 2;
832  points[2].X = Math.Max(points[2].X, points[0].X + knobLength);
833  points[3].X = points[2].X;
834 
835  bool isBehind = false;
836 
837  // if the node is "behind" us do some magic to make the line curve to prevent overlapping
838  if (points[2].X <= points[0].X + knobLength)
839  {
840  isBehind = true;
841  points[1].X += knobLength;
842  points[2].X = points[2].X;
843  points[2].Y += (points[4].Y - points[1].Y) / 2;
844  }
845 
846  if (points[3].X >= points[5].X - knobLength)
847  {
848  isBehind = true;
849  points[4].X -= knobLength;
850  points[3].X = points[4].X;
851  points[3].Y -= points[3].Y - points[2].Y;
852  }
853 
854  SquareLine.LineType type = isBehind
855  ? SquareLine.LineType.SixPointBackwardsLine
856  : SquareLine.LineType.FourPointForwardsLine;
857 
858  return new SquareLine(points, type);
859  }
860 
869  public static string BytesToHexString(byte[] bytes)
870  {
871  StringBuilder sb = new StringBuilder();
872  foreach (byte b in bytes)
873  {
874  sb.Append(b.ToString("X2"));
875  }
876  return sb.ToString();
877  }
878 
886  public static Vector2 GetClosestPointOnRectangle(RectangleF rect, Vector2 point)
887  {
888  Vector2 closest = new Vector2(
889  MathHelper.Clamp(point.X, rect.Left, rect.Right),
890  MathHelper.Clamp(point.Y, rect.Top, rect.Bottom));
891 
892  if (point.X < rect.Left)
893  {
894  closest.X = rect.Left;
895  }
896  else if (point.X > rect.Right)
897  {
898  closest.X = rect.Right;
899  }
900 
901  if (point.Y < rect.Top)
902  {
903  closest.Y = rect.Top;
904  }
905  else if (point.Y > rect.Bottom)
906  {
907  closest.Y = rect.Bottom;
908  }
909 
910  return closest;
911  }
912 
913  public static ImmutableArray<uint> PrefabCollectionToUintIdentifierArray(IEnumerable<PrefabWithUintIdentifier> prefabs)
914  => prefabs.Select(static p => p.UintIdentifier).ToImmutableArray();
915 
916  public static ImmutableArray<T> UintIdentifierArrayToPrefabCollection<T>(PrefabCollection<T> Prefabs, IEnumerable<uint> uintIdentifiers) where T : PrefabWithUintIdentifier
917  {
918  var builder = ImmutableArray.CreateBuilder<T>();
919 
920  foreach (uint uintIdentifier in uintIdentifiers)
921  {
922  var matchingPrefab = Prefabs.Find(p => p.UintIdentifier == uintIdentifier);
923  if (matchingPrefab == null)
924  {
925  DebugConsole.ThrowError($"Unable to find prefab with uint identifier {uintIdentifier}");
926  continue;
927  }
928  builder.Add(matchingPrefab);
929  }
930 
931  return builder.ToImmutable();
932  }
933  }
934 }
Pair(T1 first, T2 second)
Definition: ToolBox.cs:23
TransitionMode
Definition: Enums.cs:6