Merge remote-tracking branch 'upstream/dev' into develop
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user