Merge remote-tracking branch 'upstream/dev' into develop

This commit is contained in:
EvilFactory
2022-12-09 17:33:44 -03:00
416 changed files with 12674 additions and 5862 deletions
@@ -36,9 +36,7 @@ namespace Barotrauma
public EitherT(T value) { Value = value; }
public override string? ToString()
{
return Value.ToString();
}
=> $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(T).NameWithGenerics()})";
public override bool TryGet(out T t) { t = Value; return true; }
public override bool TryGet(out U u) { u = default!; return false; }
@@ -75,9 +73,7 @@ namespace Barotrauma
public EitherU(U value) { Value = value; }
public override string? ToString()
{
return Value.ToString();
}
=> $"Either<{typeof(T).NameWithGenerics()}, {typeof(U).NameWithGenerics()}>({Value}: {typeof(U).NameWithGenerics()})";
public override bool TryGet(out T t) { t = default!; return false; }
public override bool TryGet(out U u) { u = Value; return true; }
@@ -104,6 +104,8 @@ namespace Barotrauma
(float)Math.Floor(value / div) * div;
}
public static int RoundToInt(float v) => (int)MathF.Round(v);
public static float RoundTowardsClosest(float value, float div)
{
return (float)Math.Round(value / div) * div;
@@ -560,35 +562,45 @@ namespace Barotrauma
public static double LineSegmentToPointDistanceSquared(Point lineA, Point lineB, Point point)
{
double xDiff = lineB.X - lineA.X;
double yDiff = lineB.Y - lineA.Y;
return LineSegmentToPointDistanceSquared(lineA.X, lineA.Y, lineB.X, lineB.Y, point.X, point.Y);
}
public static float LineSegmentToPointDistanceSquared(Vector2 lineA, Vector2 lineB, Vector2 point)
{
return (float)LineSegmentToPointDistanceSquared(lineA.X, lineA.Y, lineB.X, lineB.Y, point.X, point.Y);
}
private static double LineSegmentToPointDistanceSquared(double line1X, double line1Y, double line2X, double line2Y, double pointX, double pointY)
{
double xDiff = line2X - line1X;
double yDiff = line2Y - line1Y;
if (xDiff == 0 && yDiff == 0)
{
double v1 = lineA.X - point.X;
double v2 = lineA.Y - point.Y;
double v1 = line1X - pointX;
double v2 = line1Y - pointY;
return (v1 * v1) + (v2 * v2);
}
// Calculate the t that minimizes the distance.
double t = ((point.X - lineA.X) * xDiff + (point.Y - lineA.Y) * yDiff) / (xDiff * xDiff + yDiff * yDiff);
double t = ((pointX - line1X) * xDiff + (pointY - line1Y) * yDiff) / (xDiff * xDiff + yDiff * yDiff);
// See if this represents one of the segment's
// end points or a point in the middle.
if (t < 0)
{
xDiff = point.X - lineA.X;
yDiff = point.Y - lineA.Y;
xDiff = pointX - line1X;
yDiff = pointY - line1Y;
}
else if (t > 1)
{
xDiff = point.X - lineB.X;
yDiff = point.Y - lineB.Y;
xDiff = pointX - line2X;
yDiff = pointY - line2Y;
}
else
{
xDiff = point.X - (lineA.X + t * xDiff);
yDiff = point.Y - (lineA.Y + t * yDiff);
xDiff = pointX - (line1X + t * xDiff);
yDiff = pointY - (line1Y + t * yDiff);
}
return xDiff * xDiff + yDiff * yDiff;
@@ -0,0 +1,17 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
namespace Barotrauma
{
[NetworkSerialize]
public readonly record struct NetCollection<T>(ImmutableArray<T> Array) : INetSerializableStruct, IEnumerable<T>
{
public static readonly NetCollection<T> Empty = new(ImmutableArray<T>.Empty);
public NetCollection(params T[] elements) : this(elements.ToImmutableArray()) { }
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)Array).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)Array).GetEnumerator();
}
}
@@ -63,5 +63,21 @@ namespace Barotrauma
=> !(a == b);
public abstract override string ToString();
public static implicit operator Option<T>(Option.UnspecifiedNone _)
=> None();
}
public static class Option
{
public sealed class UnspecifiedNone
{
private UnspecifiedNone() { }
internal static readonly UnspecifiedNone Instance = new();
}
public static UnspecifiedNone None => UnspecifiedNone.Instance;
public static Option<T> Some<T>(T value) => Option<T>.Some(value);
}
}
@@ -101,5 +101,14 @@ namespace Barotrauma
return derivedTypes.Select(parseOfType).FirstOrDefault(t => t.IsSome()) ?? none();
}
public static string NameWithGenerics(this Type t)
{
if (!t.IsGenericType) { return t.Name; }
string result = t.Name[..t.Name.IndexOf('`')];
result += $"<{string.Join(", ", t.GetGenericArguments().Select(NameWithGenerics))}>";
return result;
}
}
}
@@ -1,4 +1,7 @@
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma
{
public abstract class Result<T, TError>
@@ -13,6 +16,14 @@ namespace Barotrauma
public static Failure<T, TError> Failure(TError error)
=> new Failure<T, TError>(error);
public abstract bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value);
public abstract bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value);
public abstract override string? ToString();
public static (Func<T, Result<T, TError>> Success, Func<TError, Result<T, TError>> Failure) GetFactoryMethods()
=> (Success, Failure);
}
public sealed class Success<T, TError> : Result<T, TError>
@@ -22,6 +33,21 @@ namespace Barotrauma
public readonly T Value;
public override bool IsSuccess => true;
public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value)
{
value = Value;
return true;
}
public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value)
{
value = default;
return false;
}
public override string ToString()
=> $"Success<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Value})";
public Success(T value)
{
Value = value;
@@ -35,6 +61,21 @@ namespace Barotrauma
public readonly TError Error;
public override bool IsSuccess => false;
public override bool TryUnwrapSuccess([MaybeNullWhen(returnValue: false)] out T value)
{
value = default;
return false;
}
public override bool TryUnwrapFailure([MaybeNullWhen(returnValue: false)] out TError value)
{
value = Error;
return true;
}
public override string ToString()
=> $"Failure<{typeof(T).NameWithGenerics()}, {typeof(TError).NameWithGenerics()}>({Error})";
public Failure(TError error)
{
@@ -314,6 +314,19 @@ namespace Barotrauma.IO
//TODO: validate recursion?
System.IO.Directory.Delete(path, recursive);
}
public static bool TryDelete(string path, bool recursive = true)
{
try
{
Directory.Delete(path, recursive);
return true;
}
catch
{
return false;
}
}
public static DateTime GetLastWriteTime(string path)
{
@@ -18,7 +18,7 @@ namespace Barotrauma
#if OSX
//"/*user*/Library/Application Support/Daedalic Entertainment GmbH/" on Mac
public static string SaveFolder = Path.Combine(
public static readonly string DefaultSaveFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
"Library",
"Application Support",
@@ -27,13 +27,13 @@ namespace Barotrauma
#else
//"C:/Users/*user*/AppData/Local/Daedalic Entertainment GmbH/" on Windows
//"/home/*user*/.local/share/Daedalic Entertainment GmbH/" on Linux
public static string SaveFolder = Path.Combine(
public static readonly string DefaultSaveFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Daedalic Entertainment GmbH",
"Barotrauma");
#endif
public static string MultiplayerSaveFolder = Path.Combine(SaveFolder, "Multiplayer");
public static string DefaultMultiplayerSaveFolder = Path.Combine(DefaultSaveFolder, "Multiplayer");
public static readonly string SubmarineDownloadFolder = Path.Combine("Submarines", "Downloaded");
public static readonly string CampaignDownloadFolder = Path.Combine("Data", "Saves", "Multiplayer_Downloaded");
@@ -43,9 +43,9 @@ namespace Barotrauma
public static string TempPath
{
#if SERVER
get { return Path.Combine(SaveFolder, "temp_server"); }
get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp_server"); }
#else
get { return Path.Combine(SaveFolder, "temp"); }
get { return Path.Combine(GetSaveFolder(SaveType.Singleplayer), "temp"); }
#endif
}
@@ -198,7 +198,10 @@ namespace Barotrauma
}
//deleting a multiplayer save file -> also delete character data
if (Path.GetFullPath(Path.GetDirectoryName(filePath)).Equals(Path.GetFullPath(MultiplayerSaveFolder)))
var fullPath = Path.GetFullPath(Path.GetDirectoryName(filePath));
if (fullPath.Equals(Path.GetFullPath(DefaultMultiplayerSaveFolder)) ||
fullPath == Path.GetFullPath(GetSaveFolder(SaveType.Multiplayer)))
{
string characterDataSavePath = MultiPlayerCampaign.GetCharacterDataSavePath(filePath);
if (File.Exists(characterDataSavePath))
@@ -215,35 +218,70 @@ namespace Barotrauma
}
}
public static string GetSavePath(SaveType saveType, string saveName)
public static string GetSaveFolder(SaveType saveType)
{
string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder;
return Path.Combine(folder, saveName);
string folder = string.Empty;
if (!string.IsNullOrEmpty(GameSettings.CurrentConfig.SavePath))
{
folder = GameSettings.CurrentConfig.SavePath;
if (saveType == SaveType.Multiplayer)
{
folder = Path.Combine(folder, "Multiplayer");
}
if (!Directory.Exists(folder))
{
DebugConsole.AddWarning($"Could not find the custom save folder \"{folder}\", creating the folder...");
try
{
Directory.CreateDirectory(folder);
}
catch (Exception e)
{
DebugConsole.ThrowError($"Could not find the custom save folder \"{folder}\". Using the default save path instead.", e);
folder = string.Empty;
}
}
}
if (string.IsNullOrEmpty(folder))
{
folder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
}
return folder;
}
public static IReadOnlyList<CampaignMode.SaveInfo> GetSaveFiles(SaveType saveType, bool includeInCompatible = true)
{
string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder;
if (!Directory.Exists(folder))
string defaultFolder = saveType == SaveType.Singleplayer ? DefaultSaveFolder : DefaultMultiplayerSaveFolder;
if (!Directory.Exists(defaultFolder))
{
DebugConsole.Log("Save folder \"" + folder + " not found! Attempting to create a new folder...");
DebugConsole.Log("Save folder \"" + defaultFolder + " not found! Attempting to create a new folder...");
try
{
Directory.CreateDirectory(folder);
Directory.CreateDirectory(defaultFolder);
}
catch (Exception e)
{
DebugConsole.ThrowError("Failed to create the folder \"" + folder + "\"!", e);
DebugConsole.ThrowError("Failed to create the folder \"" + defaultFolder + "\"!", e);
}
}
List<string> files = Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList();
List<string> files = Directory.GetFiles(defaultFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly).ToList();
var folder = GetSaveFolder(saveType);
if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder))
{
files.AddRange(Directory.GetFiles(folder, "*.save", System.IO.SearchOption.TopDirectoryOnly));
}
string legacyFolder = saveType == SaveType.Singleplayer ? LegacySaveFolder : LegacyMultiplayerSaveFolder;
if (Directory.Exists(legacyFolder))
{
files.AddRange(Directory.GetFiles(legacyFolder, "*.save", System.IO.SearchOption.TopDirectoryOnly));
}
files = files.Distinct().ToList();
List<CampaignMode.SaveInfo> saveInfos = new List<CampaignMode.SaveInfo>();
foreach (string file in files)
{
@@ -305,7 +343,7 @@ namespace Barotrauma
{
fileName = ToolBox.RemoveInvalidFileNameChars(fileName);
string folder = saveType == SaveType.Singleplayer ? SaveFolder : MultiplayerSaveFolder;
string folder = GetSaveFolder(saveType);
if (fileName == "Save_Default")
{
fileName = TextManager.Get("SaveFile.DefaultName").Value;
@@ -0,0 +1,336 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
namespace Barotrauma.Networking;
/*
* What are segment tables for?
*
* Segment tables help make our networking packet reading code more robust by
* clearly stating where part of a message begins. Previously we would've done
* something like:
*
* msg.WriteByte(SegmentType.A);
* ...
* msg.WriteByte(SegmentType.B);
* ...
* msg.WriteByte(SegmentType.EndOfMessage);
*
* The problem with this design is that it's hard to debug when the writing and reading
* code do not align for whatever reason. INetSerializableStruct is an awesome way
* of avoiding that problem, but deploying it on a broad scale means rewriting most
* of the netcode. That isn't going to happen any time soon, so this exists as an easier
* way of increasing robustness.
*
* A segment table is laid out as follows:
*
* [TablePointer: UInt16]
* [Segment: arbitrary]
* ...
* [Segment: arbitrary]
* [NumberOfSegments: UInt16]
* [(Identifier, SegmentPointer): (T, UInt16)]
* ...
* [(Identifier, SegmentPointer): (T, UInt16)]
*
* A pointer in this context is an offset relative to the BitPosition where the TablePointer is written.
*
* It is used as follows:
*
* using (var segmentTable = SegmentTableWriter<T>.StartWriting(outMsg))
* {
* segmentTable.StartNewSegment(T.A);
* ... write segment to outMsg ...
* segmentTable.StartNewSegment(T.B);
* ... write segment to outMsg ...
* }
* peer.SendMessage(outMsg);
*
* ...
*
* SegmentTableReader<T>.Read(inc,
* segmentDataReader: (segment, inc) =>
* {
* switch (segment)
* {
* ... read segments ...
* }
* }
* }
*
* The advantages of this approach are:
* - If a message is truncated or corrupted near the end, it becomes far more obvious because the table
* would not be read properly and look like garbage when printed to the console.
* - If the reading and writing code for a segment disagree on something, issues will be isolated to that
* one segment.
* - The code no longer has to fiddle with padding and temporary buffers because the segment table is able
* to handle content that is not byte-aligned just fine.
* - Exception handling is far easier when using a segment table, when combined with a using statement
* any uncaught exception will result in the entire table being skipped, allowing the remainder of the
* message to still be read.
* - It's harder to make mistakes in the implementation of segments themselves with this approach. By using
* the SegmentTableWriter and SegmentTableReader types, you get a type-safe way of delimiting segments
* and it's harder to forget to finalize a packet.
*/
[NetworkSerialize]
public readonly record struct Segment<T>(T Identifier, UInt16 Pointer) : INetSerializableStruct where T : struct;
readonly ref struct SegmentTableWriter<T> where T : struct
{
private readonly IWriteMessage message;
private readonly List<Segment<T>> segments;
public readonly int PointerLocation;
private SegmentTableWriter(IWriteMessage message, int pointerLocation)
{
this.message = message;
this.PointerLocation = pointerLocation;
this.segments = new List<Segment<T>>();
}
public static SegmentTableWriter<T> StartWriting(IWriteMessage msg)
{
var retVal = new SegmentTableWriter<T>(msg, msg.BitPosition);
msg.WriteUInt16(0); //reserve space for the table pointer
return retVal;
}
private void ThrowOnInvalidState()
{
if (segments.Count >= UInt16.MaxValue)
{
throw new InvalidOperationException($"Too many segments in SegmentTable<{typeof(T).Name}>");
}
if (message.BitPosition - PointerLocation > UInt16.MaxValue)
{
throw new OverflowException(
$"Too much data is being stored in SegmentTable<{typeof(T).Name}> ({segments.Count} segments)");
}
}
public void StartNewSegment(T value)
{
ThrowOnInvalidState();
segments.Add(new Segment<T>(value, (UInt16)(message.BitPosition-PointerLocation)));
}
public void Dispose()
{
ThrowOnInvalidState();
int tablePosition = message.BitPosition;
//rewrite the table pointer now that we know where the table ends
message.BitPosition = PointerLocation;
message.WriteUInt16((UInt16)(tablePosition-PointerLocation));
//write the table
message.BitPosition = tablePosition;
message.WriteUInt16((UInt16)segments.Count);
foreach (var segment in segments)
{
message.WriteNetSerializableStruct(segment);
}
}
}
readonly ref struct SegmentTableReader<T> where T : struct
{
private class SegmentReadMsg : IReadMessage
{
private readonly IReadMessage underlyingMsg;
private readonly IReadOnlyList<Segment<T>> segments;
private readonly int segmentIndex;
private readonly int offset;
private readonly int lengthBits;
public SegmentReadMsg(IReadMessage underlyingMsg, IReadOnlyList<Segment<T>> segments, int segmentIndex, int offset, int lengthBits)
{
this.underlyingMsg = underlyingMsg;
this.segments = segments;
this.segmentIndex = segmentIndex;
this.offset = offset;
this.lengthBits = lengthBits;
if (offset + lengthBits >= underlyingMsg.LengthBits)
{
throw new Exception(
$"Segment table is corrupt, segment length is invalid: {offset} + {lengthBits} >= {underlyingMsg.LengthBits}");
}
}
private void Check()
{
if (BitPosition > lengthBits)
{
throw new Exception($"Tried to read too much data from segment.");
}
}
private TRead Check<TRead>(TRead v)
{
Check();
return v;
}
public bool ReadBoolean() => Check(underlyingMsg.ReadBoolean());
public void ReadPadBits()
{
Check(); underlyingMsg.ReadPadBits();
}
public byte ReadByte() => Check(underlyingMsg.ReadByte());
public byte PeekByte() => Check(underlyingMsg.PeekByte());
public ushort ReadUInt16() => Check(underlyingMsg.ReadUInt16());
public short ReadInt16() => Check(underlyingMsg.ReadInt16());
public uint ReadUInt32() => Check(underlyingMsg.ReadUInt32());
public int ReadInt32() => Check(underlyingMsg.ReadInt32());
public ulong ReadUInt64() => Check(underlyingMsg.ReadUInt64());
public long ReadInt64() => Check(underlyingMsg.ReadInt64());
public float ReadSingle() => Check(underlyingMsg.ReadSingle());
public double ReadDouble() => Check(underlyingMsg.ReadDouble());
public uint ReadVariableUInt32() => Check(underlyingMsg.ReadVariableUInt32());
public string ReadString() => Check(underlyingMsg.ReadString());
public Identifier ReadIdentifier() => Check(underlyingMsg.ReadIdentifier());
public Color ReadColorR8G8B8() => Check(underlyingMsg.ReadColorR8G8B8());
public Color ReadColorR8G8B8A8() => Check(underlyingMsg.ReadColorR8G8B8A8());
public int ReadRangedInteger(int min, int max) => Check(underlyingMsg.ReadRangedInteger(min, max));
public float ReadRangedSingle(float min, float max, int bitCount) => Check(underlyingMsg.ReadRangedSingle(min, max, bitCount));
public byte[] ReadBytes(int numberOfBytes) => Check(underlyingMsg.ReadBytes(numberOfBytes));
public int BitPosition
{
get => underlyingMsg.BitPosition - offset;
set => Check(underlyingMsg.BitPosition = value + offset);
}
public int BytePosition => BitPosition / 8;
public byte[] Buffer => underlyingMsg.Buffer;
public int LengthBits
{
get => lengthBits;
set => throw new InvalidOperationException($"Cannot resize {nameof(SegmentReadMsg)}");
}
public int LengthBytes => lengthBits / 8;
public NetworkConnection Sender => underlyingMsg.Sender;
}
private readonly IReadMessage message;
private readonly List<Segment<T>> segments;
private readonly int exitLocation;
public readonly int PointerLocation;
private SegmentTableReader(IReadMessage message, List<Segment<T>> segments, int pointerLocation, int exitLocation)
{
this.message = message;
this.segments = segments;
this.PointerLocation = pointerLocation;
this.exitLocation = exitLocation;
}
public IReadOnlyList<Segment<T>> Segments => segments;
public enum BreakSegmentReading
{
No,
Yes
}
public delegate BreakSegmentReading SegmentDataReader(
T segmentHeader,
IReadMessage incMsg);
public delegate void ExceptionHandler(
Segment<T> segmentWithError,
Segment<T>[] previousSegments,
Exception exceptionThrown);
public static void Read(
IReadMessage msg,
SegmentDataReader segmentDataReader,
ExceptionHandler? exceptionHandler = null)
{
int pointerLocation = msg.BitPosition;
int tablePointer = msg.ReadUInt16();
int tableLocation = pointerLocation + tablePointer;
int returnPosition = msg.BitPosition;
//read the table
var segments = new List<Segment<T>>();
msg.BitPosition = tableLocation;
int numSegments = msg.ReadUInt16();
for (int i = 0; i < numSegments; i++)
{
segments.Add(INetSerializableStruct.Read<Segment<T>>(msg));
}
//store the exit location and go back to the top
int exitLocation = msg.BitPosition;
msg.BitPosition = returnPosition;
using var segmentTable = new SegmentTableReader<T>(msg, segments, pointerLocation, exitLocation);
for (int i = 0; i < segmentTable.Segments.Count; i++)
{
var segment = segmentTable.Segments[i];
msg.BitPosition = segmentTable.PointerLocation + segment.Pointer;
try
{
if (segmentDataReader(segment.Identifier, new SegmentReadMsg(
msg,
segments,
i,
offset: segmentTable.PointerLocation + segment.Pointer,
lengthBits: (i < segmentTable.Segments.Count - 1 ? segments[i + 1].Pointer : tablePointer) -
segment.Pointer))
is BreakSegmentReading.Yes)
{
break;
}
}
catch (Exception e)
{
var prevSegments = segments.Take(i).ToArray();
if (exceptionHandler is not null)
{
exceptionHandler(segment, prevSegments, e);
}
else
{
throw new Exception(
$"Exception thrown while reading segment {segment.Identifier} at position {segment.Pointer}." +
(prevSegments.Any() ? $" Previous segments: {string.Join(", ", prevSegments)}." : ""),
e);
}
}
}
}
public void Dispose()
{
message.BitPosition = exitLocation;
}
}
@@ -14,5 +14,17 @@ namespace Barotrauma
result = default;
return false;
}
public static async Task<T> WaitForLoadingScreen<T>(this Task<T> task)
{
var result = await task;
#if CLIENT
while (GameMain.Instance.LoadingScreenOpen)
{
await Task.Delay((int)(1000 * Timing.Step));
}
#endif
return result;
}
}
}
@@ -3,9 +3,11 @@ using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Barotrauma.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
@@ -101,7 +103,7 @@ namespace Barotrauma
string startPath = directory ?? "";
string saveFolder = SaveUtil.SaveFolder.Replace('\\', '/');
string saveFolder = SaveUtil.DefaultSaveFolder.Replace('\\', '/');
if (originalFilename.Replace('\\', '/').StartsWith(saveFolder))
{
//paths that lead to the save folder might have incorrect case,
@@ -721,5 +723,132 @@ namespace Barotrauma
{
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);
}
}
}