diff --git a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmaking.cs b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmaking.cs index c432176..1226100 100644 --- a/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmaking.cs +++ b/src/Prospect.Server.Api/Services/CloudScript/Functions/EnterMatchmaking.cs @@ -12,7 +12,7 @@ public class EnterMatchmaking : ICloudScriptFunction + { + Tick.Dispose(); + e.Cancel = true; + }; + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] ({SourceContext,-52}) {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + + Logger.Information("Starting Prospect.Server.Game"); + + await using (var driver = new UIpNetDriver()) + { + driver.Init(); + + // Tick all components. + while (await Tick.WaitForNextTickAsync()) + { + var deltaTime = TickRate; + + driver.TickDispatch(deltaTime); + // driver.PostTickDispatch(); + + if (driver.ConnectionlessHandler != null) + { + driver.ConnectionlessHandler.Tick(deltaTime); + } + } + } + + Logger.Information("Shutting down"); } } \ No newline at end of file diff --git a/src/Prospect.Server.Game/Prospect.Server.Game.csproj b/src/Prospect.Server.Game/Prospect.Server.Game.csproj index 398b2f7..f747e6b 100644 --- a/src/Prospect.Server.Game/Prospect.Server.Game.csproj +++ b/src/Prospect.Server.Game/Prospect.Server.Game.csproj @@ -12,4 +12,8 @@ + + + + diff --git a/src/Prospect.Unreal/Class1.cs b/src/Prospect.Unreal/Class1.cs deleted file mode 100644 index a3202ca..0000000 --- a/src/Prospect.Unreal/Class1.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Prospect.Unreal; - -public class Class1 -{ -} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/EEngineNetworkVersionHistory.cs b/src/Prospect.Unreal/Core/EEngineNetworkVersionHistory.cs new file mode 100644 index 0000000..5efb285 --- /dev/null +++ b/src/Prospect.Unreal/Core/EEngineNetworkVersionHistory.cs @@ -0,0 +1,24 @@ +namespace Prospect.Unreal.Core; + +public enum EEngineNetworkVersionHistory +{ + HISTORY_INITIAL = 1, + HISTORY_REPLAY_BACKWARDS_COMPAT = 2, // Bump version to get rid of older replays before backwards compat was turned on officially + HISTORY_MAX_ACTOR_CHANNELS_CUSTOMIZATION = 3, // Bump version because serialization of the actor channels changed + HISTORY_REPCMD_CHECKSUM_REMOVE_PRINTF = 4, // Bump version since the way FRepLayoutCmd::CompatibleChecksum was calculated changed due to an optimization + HISTORY_NEW_ACTOR_OVERRIDE_LEVEL = 5, // Bump version since a level reference was added to the new actor information + HISTORY_CHANNEL_NAMES = 6, // Bump version since channel type is now an fname + HISTORY_CHANNEL_CLOSE_REASON = 7, // Bump version to serialize a channel close reason in bunches instead of bDormant + HISTORY_ACKS_INCLUDED_IN_HEADER = 8, // Bump version since acks are now sent as part of the header + HISTORY_NETEXPORT_SERIALIZATION = 9, // Bump version due to serialization change to FNetFieldExport + HISTORY_NETEXPORT_SERIALIZE_FIX = 10, // Bump version to fix net field export name serialization + HISTORY_FAST_ARRAY_DELTA_STRUCT = 11, // Bump version to allow fast array serialization, delta struct serialization. + HISTORY_FIX_ENUM_SERIALIZATION = 12, // Bump version to fix enum net serialization issues. + HISTORY_OPTIONALLY_QUANTIZE_SPAWN_INFO = 13, // Bump version to conditionally disable quantization for Scale, Location, and Velocity when spawning network actors. + HISTORY_JITTER_IN_HEADER = 14, // Bump version since we added jitter clock time to packet headers and removed remote saturation + HISTORY_CLASSNETCACHE_FULLNAME = 15, // Bump version to use full paths in GetNetFieldExportGroupForClassNetCache + HISTORY_REPLAY_DORMANCY = 16, // Bump version to support dormancy properly in replays + + HISTORY_ENGINENETVERSION_PLUS_ONE, + HISTORY_ENGINENETVERSION_LATEST = HISTORY_ENGINENETVERSION_PLUS_ONE - 1, +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/FEngineVersion.cs b/src/Prospect.Unreal/Core/FEngineVersion.cs new file mode 100644 index 0000000..fb8df37 --- /dev/null +++ b/src/Prospect.Unreal/Core/FEngineVersion.cs @@ -0,0 +1,35 @@ +using Prospect.Unreal.Net; +using Prospect.Unreal.Serialization; + +namespace Prospect.Unreal.Core; + +public class FEngineVersion +{ + public ushort Major { get; private set; } + + public ushort Minor { get; private set; } + + public ushort Patch { get; private set; } + + public uint Changelist { get; private set; } + + public string Branch { get; private set; } + + public void Set(ushort major, ushort minor, ushort patch, uint changelist, string branch) + { + Major = major; + Minor = minor; + Patch = patch; + Changelist = changelist; + Branch = branch; + } + + public void Deserialize(FArchive archive) + { + Major = archive.ReadUInt16(); + Minor = archive.ReadUInt16(); + Patch = archive.ReadUInt16(); + Changelist = archive.ReadUInt32(); + Branch = FString.Deserialize(archive); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/FMath.cs b/src/Prospect.Unreal/Core/FMath.cs new file mode 100644 index 0000000..c29d2f8 --- /dev/null +++ b/src/Prospect.Unreal/Core/FMath.cs @@ -0,0 +1,14 @@ +namespace Prospect.Unreal.Core; + +public static class FMath +{ + public static int DivideAndRoundUp(int dividend, int divisor) + { + return (dividend + divisor - 1) / divisor; + } + + public static float FRandRange(float min, float max) + { + return min + (max - min) * Random.Shared.NextSingle(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/FString.cs b/src/Prospect.Unreal/Core/FString.cs new file mode 100644 index 0000000..ec84913 --- /dev/null +++ b/src/Prospect.Unreal/Core/FString.cs @@ -0,0 +1,105 @@ +using System.Text; +using Prospect.Unreal.Serialization; + +namespace Prospect.Unreal.Core; + +public static class FString +{ + private const int MaxSerializeSize = 1024; + + public static void Serialize(FArchive archive, string value) + { + var unicodeCount = Encoding.UTF8.GetByteCount(value); + var bSaveUnicodeChar = archive.IsForcingUnicode() || unicodeCount != value.Length; + if (bSaveUnicodeChar) + { + var num = unicodeCount + 1; + var saveNum = -num; + + archive.WriteInt32(saveNum); + + if (num != 0) + { + var valueBytesSize = num * 2; + var valueBytes = valueBytesSize > 128 ? new byte[valueBytesSize] : stackalloc byte[valueBytesSize]; + + Encoding.UTF8.GetBytes(value, valueBytes); + + if (!archive.IsByteSwapping()) + { + archive.Serialize(valueBytes, valueBytesSize); + } + else + { + throw new NotImplementedException(); + // for (int i = 0; i < num; i++) + // { + // valueBytes[i] = archive.ByteSwap(valueBytes[i]); + // } + } + } + } + else + { + var num = value.Length + 1; + var valueBytes = num > 128 ? new byte[num] : stackalloc byte[num]; + + Encoding.ASCII.GetBytes(value, valueBytes); + + archive.WriteInt32(num); + archive.Serialize(valueBytes, num); + } + } + + public static string Deserialize(FArchive archive) + { + // > 0 for ANSICHAR, < 0 for UCS2CHAR serialization + + string? result = null; + + var saveNum = archive.ReadInt32(); + var loadUcs2Char = saveNum < 0; + if (loadUcs2Char) + { + saveNum = -saveNum; + } + + // If SaveNum is still less than 0, they must have passed in MIN_INT. Archive is corrupted. + if (saveNum < 0) + { + throw new Exception("Archive is corrupted"); + } + + // Protect against network packets allocating too much memory + if (MaxSerializeSize > 0 && saveNum > MaxSerializeSize) + { + throw new Exception("String is too large"); + } + + if (saveNum != 0) + { + if (loadUcs2Char) + { + var bytes = archive.ReadBytes(saveNum * 2); + + // -2 to remove unicode null terminator. + result = Encoding.Unicode.GetString(bytes, 0, bytes.Length - 2); + } + else + { + var bytes = archive.ReadBytes(saveNum); + + // -1 to remove null terminator. + result = Encoding.ASCII.GetString(bytes, 0, bytes.Length - 1); + } + + // Throw away empty string. + if (saveNum == 1) + { + result = string.Empty; + } + } + + return result ?? string.Empty; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/FURL.cs b/src/Prospect.Unreal/Core/FURL.cs new file mode 100644 index 0000000..d8384e0 --- /dev/null +++ b/src/Prospect.Unreal/Core/FURL.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Core; + +public class FURL +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Exceptions/UnrealException.cs b/src/Prospect.Unreal/Exceptions/UnrealException.cs new file mode 100644 index 0000000..c4aca71 --- /dev/null +++ b/src/Prospect.Unreal/Exceptions/UnrealException.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace Prospect.Unreal.Exceptions; + +public class UnrealException : Exception +{ + public UnrealException() + { + } + + protected UnrealException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public UnrealException(string? message) : base(message) + { + } + + public UnrealException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Exceptions/UnrealSerializationException.cs b/src/Prospect.Unreal/Exceptions/UnrealSerializationException.cs new file mode 100644 index 0000000..ae044bf --- /dev/null +++ b/src/Prospect.Unreal/Exceptions/UnrealSerializationException.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace Prospect.Unreal.Exceptions; + +public class UnrealSerializationException : UnrealException +{ + public UnrealSerializationException() + { + } + + protected UnrealSerializationException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public UnrealSerializationException(string? message) : base(message) + { + } + + public UnrealSerializationException(string? message, Exception? innerException) : base(message, innerException) + { + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/CoreNet.cs b/src/Prospect.Unreal/Net/CoreNet.cs new file mode 100644 index 0000000..e197723 --- /dev/null +++ b/src/Prospect.Unreal/Net/CoreNet.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net; + +internal class CoreNet +{ + public const int MaxPacketSize = 1024; +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/EConnectionState.cs b/src/Prospect.Unreal/Net/EConnectionState.cs new file mode 100644 index 0000000..94f2c3a --- /dev/null +++ b/src/Prospect.Unreal/Net/EConnectionState.cs @@ -0,0 +1,9 @@ +namespace Prospect.Unreal.Net; + +public enum EConnectionState +{ + USOCK_Invalid = 0, // Connection is invalid, possibly uninitialized. + USOCK_Closed = 1, // Connection permanently closed. + USOCK_Pending = 2, // Connection is awaiting connection. + USOCK_Open = 3, // Connection is open. +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/EIncomingResult.cs b/src/Prospect.Unreal/Net/EIncomingResult.cs new file mode 100644 index 0000000..659d3ec --- /dev/null +++ b/src/Prospect.Unreal/Net/EIncomingResult.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net; + +public enum EIncomingResult +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FOutPacketTraits.cs b/src/Prospect.Unreal/Net/FOutPacketTraits.cs new file mode 100644 index 0000000..49e2796 --- /dev/null +++ b/src/Prospect.Unreal/Net/FOutPacketTraits.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net; + +public class FOutPacketTraits +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FReceiveThreadRunnable.cs b/src/Prospect.Unreal/Net/FReceiveThreadRunnable.cs new file mode 100644 index 0000000..9b7cd22 --- /dev/null +++ b/src/Prospect.Unreal/Net/FReceiveThreadRunnable.cs @@ -0,0 +1,95 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using Serilog; + +namespace Prospect.Unreal.Net; + +public record FReceivedPacket(IPEndPoint Address, byte[] Buffer, DateTimeOffset Timestamp); + +public class FReceiveThreadRunnable : IAsyncDisposable +{ + private static readonly ILogger Logger = Log.ForContext(); + + private readonly UIpNetDriver _driver; + private readonly CancellationTokenSource _cancellation; + private readonly ConcurrentQueue _receiveQueue; + + private Task? _receiveTask; + + public FReceiveThreadRunnable(UIpNetDriver driver) + { + _driver = driver; + _cancellation = new CancellationTokenSource(); + _receiveQueue = new ConcurrentQueue(); + } + + public void Start() + { + _receiveTask = ReceiveAsync(); + } + + public bool TryReceive([MaybeNullWhen(false)] out FReceivedPacketView result) + { + if (_receiveQueue.TryDequeue(out var packet)) + { + result = new FReceivedPacketView + { + DataView = new FPacketDataView(packet.Buffer, packet.Buffer.Length), + Address = packet.Address, + Traits = new FInPacketTraits() + }; + + return true; + } + + result = null; + return false; + } + + private async Task ReceiveAsync() + { + try + { + while (!_cancellation.IsCancellationRequested) + { + var result = await _driver.Socket.ReceiveAsync(_cancellation.Token); + + if (result.Buffer.Length == 0) + { + continue; + } + + if (result.Buffer.Length > CoreNet.MaxPacketSize) + { + Logger.Warning("Received packet exceeding MaxPacketSize ({Size} > {Max}) from {Ip}", + result.Buffer.Length, CoreNet.MaxPacketSize, result.RemoteEndPoint); + continue; + } + + _receiveQueue.Enqueue(new FReceivedPacket(result.RemoteEndPoint, result.Buffer, DateTimeOffset.UtcNow)); + } + } + catch (Exception e) + { + if (e is OperationCanceledException && !_cancellation.IsCancellationRequested) + { + Logger.Error(e, "Exception caught"); + } + } + + Logger.Debug("Stopped FReceiveThreadRunnable"); + } + + public async ValueTask DisposeAsync() + { + _cancellation.Cancel(); + + if (_receiveTask != null) + { + await _receiveTask; + } + + _cancellation.Dispose(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/HandlerComponent.cs b/src/Prospect.Unreal/Net/HandlerComponent.cs new file mode 100644 index 0000000..79b1936 --- /dev/null +++ b/src/Prospect.Unreal/Net/HandlerComponent.cs @@ -0,0 +1,108 @@ +using System.Net; +using Prospect.Unreal.Serialization; + +namespace Prospect.Unreal.Net; + +public abstract class HandlerComponent +{ + private bool _bRequiresHandshake; + private bool _bRequiresReliability; + private bool _bActive; + private bool _bInitialized; + private string _name; + + protected HandlerComponent(PacketHandler handler, string name) + { + _name = name; + _bRequiresHandshake = false; + _bRequiresReliability = false; + _bActive = false; + _bInitialized = false; + + Handler = handler; + State = HandlerComponentState.UnInitialized; + } + + protected PacketHandler Handler { get; } + protected HandlerComponentState State { get; private set; } + + public virtual bool IsActive() + { + return _bActive; + } + + public virtual bool IsValid() + { + return false; + } + + public bool IsInitialized() + { + return _bInitialized; + } + + public bool RequiresHandshake() + { + return _bRequiresHandshake; + } + + public bool RequiresReliability() + { + return _bRequiresReliability; + } + + public virtual void Incoming(FBitReader packet) + { + } + + public virtual void Outgoing(FBitWriter packet, FOutPacketTraits traits) + { + } + + public virtual void IncomingConnectionless(FIncomingPacketRef packetRef) + { + } + + public virtual void OutgoingConnectionless(IPEndPoint address, FBitWriter packet, FOutPacketTraits traits) + { + } + + public virtual bool CanReadUnaligned() + { + return false; + } + + public abstract void Initialize(); + + public virtual void NotifiyHandshakeBegin() + { + } + + public virtual void Tick(float deltaTime) + { + } + + public virtual void SetActive(bool active) + { + _bActive = active; + } + + public virtual int GetReservedPacketBits() + { + return 0; + } + + public virtual void CountBytes(FArchive ar) + { + } + + protected void SetState(HandlerComponentState state) + { + State = state; + } + + protected void Initialized() + { + _bInitialized = true; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/HandlerComponentState.cs b/src/Prospect.Unreal/Net/HandlerComponentState.cs new file mode 100644 index 0000000..7b58113 --- /dev/null +++ b/src/Prospect.Unreal/Net/HandlerComponentState.cs @@ -0,0 +1,9 @@ +namespace Prospect.Unreal.Net; + +public enum HandlerComponentState +{ + UnInitialized, // HandlerComponent not yet initialized + InitializedOnLocal, // Initialized on local instance + InitializeOnRemote, // Initialized on remote instance, not on local instance + Initialized // Initialized on both local and remote instances +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/HandlerMode.cs b/src/Prospect.Unreal/Net/HandlerMode.cs new file mode 100644 index 0000000..52c0a8e --- /dev/null +++ b/src/Prospect.Unreal/Net/HandlerMode.cs @@ -0,0 +1,7 @@ +namespace Prospect.Unreal.Net; + +public enum HandlerMode +{ + Client, // Clientside PacketHandler + Server // Serverside PacketHandler +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/HandlerState.cs b/src/Prospect.Unreal/Net/HandlerState.cs new file mode 100644 index 0000000..5d59a3f --- /dev/null +++ b/src/Prospect.Unreal/Net/HandlerState.cs @@ -0,0 +1,8 @@ +namespace Prospect.Unreal.Net; + +public enum HandlerState +{ + Uninitialized, // PacketHandler is uninitialized + InitializingComponents, // PacketHandler is initializing HandlerComponents + Initialized // PacketHandler and all HandlerComponents (if any) are initialized +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/PacketHandler.cs b/src/Prospect.Unreal/Net/PacketHandler.cs new file mode 100644 index 0000000..01d72c7 --- /dev/null +++ b/src/Prospect.Unreal/Net/PacketHandler.cs @@ -0,0 +1,265 @@ +using System.Net; +using Prospect.Unreal.Serialization; +using Serilog; + +namespace Prospect.Unreal.Net; + +public record FIncomingPacketRef(FBitReader Packet, IPEndPoint Address, FInPacketTraits Traits); + +public record ProcessedPacket(byte[] Data, int CountBits, bool Error = false); + +public class PacketHandler +{ + private static readonly ILogger Logger = Log.ForContext(); + + private readonly List _handlerComponents; + + private bool _bConnectionlessHandler; + private bool _bRawSend; + private HandlerState _state; + private ReliabilityHandlerComponent? _reliabilityComponent; + + private FBitWriter _outgoingPacket; + private FBitReader _incomingPacket; + + public PacketHandler() + { + _bConnectionlessHandler = false; + _state = HandlerState.Uninitialized; + _handlerComponents = new List(); + _reliabilityComponent = null; + _outgoingPacket = new FBitWriter(0); + _incomingPacket = new FBitReader(Array.Empty()); + + Mode = HandlerMode.Server; + } + + public HandlerMode Mode { get; } + + public void Tick(float deltaTime) + { + foreach (var component in _handlerComponents) + { + component.Tick(deltaTime); + } + } + + public void Initialize(bool bConnectionlessOnly) + { + _bConnectionlessHandler = bConnectionlessOnly; + + if (!_bConnectionlessHandler) + { + throw new NotImplementedException(); + } + + // TODO: FEncryptionComponent. + + // TODO: ReliabilityHandlerComponent. + } + + public void InitializeComponents() + { + if (_state == HandlerState.Uninitialized) + { + if (_handlerComponents.Count > 0) + { + SetState(HandlerState.InitializingComponents); + } + else + { + HandlerInitialized(); + } + } + + foreach (var component in _handlerComponents) + { + if (component.IsValid() && !component.IsInitialized()) + { + component.Initialize(); + } + } + } + + public bool IncomingConnectionless(FReceivedPacketView packetView) + { + packetView.Traits.ConnectionlessPacket = true; + + return Incoming_Internal(packetView); + } + + public ProcessedPacket OutgoingConnectionless(IPEndPoint address, byte[] packet, int countBits, FOutPacketTraits traits) + { + return Outgoing_Internal(packet, countBits, traits, true, address); + } + + public HandlerComponent AddHandler() where T : HandlerComponent + { + var result = (HandlerComponent) Activator.CreateInstance(typeof(T), this)!; + + _handlerComponents.Add(result); + + return result; + } + + private bool Incoming_Internal(FReceivedPacketView packetView) + { + var returnVal = true; + var dataView = packetView.DataView; + var countBits = dataView.NumBits(); + + if (_handlerComponents.Count > 0) + { + var data = dataView.GetData(); + var lastByte = data[dataView.NumBytes() - 1]; + if (lastByte != 0) + { + countBits--; + + while ((lastByte & 0x80) == 0) + { + lastByte *= 2; + countBits--; + } + } + else + { + returnVal = false; + } + } + + if (returnVal) + { + var processPacketReader = new FBitReader(dataView.GetData(), countBits); + var packetRef = new FIncomingPacketRef(processPacketReader, packetView.Address, packetView.Traits); + + if (_state == HandlerState.Uninitialized) + { + UpdateInitialState(); + } + + foreach (var component in _handlerComponents) + { + if (processPacketReader.GetPosBits() != 0 && !component.CanReadUnaligned()) + { + RealignPacket(processPacketReader); + } + + if (packetView.Traits.ConnectionlessPacket) + { + component.IncomingConnectionless(packetRef); + } + else + { + component.Incoming(processPacketReader); + } + } + + if (!processPacketReader.IsError()) + { + ReplaceIncomingPacket(processPacketReader); + + packetView.DataView = new FPacketDataView(_incomingPacket.GetBuffer(), _incomingPacket.GetBitsLeft()); + } + else + { + returnVal = false; + } + } + + return returnVal; + } + + private ProcessedPacket Outgoing_Internal(byte[] packet, int countBits, FOutPacketTraits traits, bool bConnectionLess, IPEndPoint address) + { + if (!_bRawSend) + { + throw new NotImplementedException(); + } + else + { + return new ProcessedPacket(packet, countBits); + } + } + + private void SetState(HandlerState state) + { + if (state == _state) + { + Logger.Fatal("Set PacketHandler state to the state it is currently in ({State})", state); + } + else + { + _state = state; + } + } + + private void UpdateInitialState() + { + if (_state == HandlerState.Uninitialized) + { + if (_handlerComponents.Count > 0) + { + InitializeComponents(); + } + else + { + HandlerInitialized(); + } + } + } + + private void HandlerInitialized() + { + if (_reliabilityComponent != null) + { + throw new NotImplementedException(); + } + + SetState(HandlerState.Initialized); + } + + private void ReplaceIncomingPacket(FBitReader replacementPacket) + { + if (replacementPacket.GetPosBits() == 0 || replacementPacket.GetBitsLeft() == 0) + { + _incomingPacket = replacementPacket; + } + else + { + var tempPacketData = new byte[replacementPacket.GetBytesLeft()]; + var newPacketSizeBits = replacementPacket.GetBitsLeft(); + + replacementPacket.SerializeBits(tempPacketData, newPacketSizeBits); + + _incomingPacket = new FBitReader(tempPacketData, newPacketSizeBits); + } + } + + private void RealignPacket(FBitReader packet) + { + Logger.Warning("Realigning packet, which is untested"); + + if (packet.GetPosBits() != 0) + { + var bitsLeft = packet.GetBitsLeft(); + if (bitsLeft > 0) + { + var tempPacketData = new byte[packet.GetBytesLeft()]; + + packet.SerializeBits(tempPacketData, bitsLeft); + packet.SetData(tempPacketData, bitsLeft); + } + } + } + + public void SetRawSend(bool enabled) + { + _bRawSend = enabled; + } + + public bool GetRawSend() + { + return _bRawSend; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/PacketView.cs b/src/Prospect.Unreal/Net/PacketView.cs new file mode 100644 index 0000000..9eddda8 --- /dev/null +++ b/src/Prospect.Unreal/Net/PacketView.cs @@ -0,0 +1,43 @@ +using System.Net; + +namespace Prospect.Unreal.Net; + +public class FInPacketTraits +{ + public bool ConnectionlessPacket { get; set; } + public bool FromRecentlyDisconnected { get; set; } +} + +public class FPacketDataView +{ + private readonly byte[] _data; + private readonly int _countBits; + + public FPacketDataView(byte[] data, int length) + { + _data = data; + _countBits = length * 8; + } + + public byte[] GetData() + { + return _data; + } + + public int NumBits() + { + return _countBits; + } + + public int NumBytes() + { + return _data.Length; + } +} + +public class FReceivedPacketView +{ + public FPacketDataView DataView { get; set; } + public IPEndPoint Address { get; set; } + public FInPacketTraits Traits { get; set; } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/ReliabilityHandlerComponent.cs b/src/Prospect.Unreal/Net/ReliabilityHandlerComponent.cs new file mode 100644 index 0000000..a77f66a --- /dev/null +++ b/src/Prospect.Unreal/Net/ReliabilityHandlerComponent.cs @@ -0,0 +1,13 @@ +namespace Prospect.Unreal.Net; + +public class ReliabilityHandlerComponent : HandlerComponent +{ + public ReliabilityHandlerComponent(PacketHandler handler) : base(handler, nameof(ReliabilityHandlerComponent)) + { + } + + public override void Initialize() + { + + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs b/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs new file mode 100644 index 0000000..cf2e4e9 --- /dev/null +++ b/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs @@ -0,0 +1,517 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Net; +using System.Security.Cryptography; +using Prospect.Unreal.Core; +using Prospect.Unreal.Serialization; +using Serilog; + +namespace Prospect.Unreal.Net; + +public class StatelessConnectHandlerComponent : HandlerComponent +{ + private static readonly ILogger Logger = Log.ForContext(); + + private const int SecretByteSize = 64; + private const int SecretCount = 2; + private const int CookieByteSize = 20; + + private const int HandshakePacketSizeBits = 227; + private const int RestartHandshakePacketSizeBits = 2; + private const int RestartResponseSizeBits = 387; + + private const float SecretUpdateTime = 15.0f; + private const float SecretUpdateTimeVariance = 5.0f; + + private const float MaxCookieLifetime = ((SecretUpdateTime + SecretUpdateTimeVariance) * SecretCount); + private const float MinCookieLifetime = SecretUpdateTime; + + private UNetDriver? _driver; + + /// + /// The serverside-only 'secret' value, used to help with generating cookies. + /// + private byte[][] _handshakeSecret; + + /// + /// Which of the two secret values above is active (values are changed frequently, to limit replay attacks) + /// + private byte _activeSecret; + + /// + /// The time of the last secret value update + /// + private double _lastSecretUpdateTimestamp; + + /// + /// The last address to successfully complete the handshake challenge + /// + private IPEndPoint? _lastChallengeSuccessAddress; + + /// + /// The initial server sequence value, from the last successful handshake + /// + private int _lastServerSequence; + + /// + /// The initial client sequence value, from the last successful handshake + /// + private int _lastClientSequence; + + /// + /// Client: Whether or not we are in the middle of a restarted handshake. + /// Server: Whether or not the last handshake was a restarted handshake. + /// + private bool _bRestartedHandshake; + + /// + /// The cookie which completed the connection handshake. + /// + private byte[] _authorisedCookie; + + /// + /// The magic header which is prepended to all packets + /// + private BitArray _magicHeader; + + public StatelessConnectHandlerComponent(PacketHandler handler) : base(handler, nameof(StatelessConnectHandlerComponent)) + { + _handshakeSecret = new byte[2][]; + _activeSecret = byte.MaxValue; + _lastChallengeSuccessAddress = null; + _lastServerSequence = 0; + _lastClientSequence = 0; + _bRestartedHandshake = false; + _authorisedCookie = new byte[CookieByteSize]; + _magicHeader = new BitArray(0); + } + + public void GetChallengeSequences(out int serverSequence, out int clientSequence) + { + serverSequence = _lastServerSequence; + clientSequence = _lastClientSequence; + } + + public void ResetChallengeData() + { + _lastChallengeSuccessAddress = null; + _bRestartedHandshake = false; + _lastServerSequence = 0; + _lastClientSequence = 0; + + for (var i = 0; i < _authorisedCookie.Length; i++) + { + _authorisedCookie[i] = 0; + } + } + + public void SetDriver(UNetDriver driver) + { + _driver = driver; + + if (Handler.Mode == HandlerMode.Server) + { + var statelessComponent = _driver.StatelessConnectComponent; + if (statelessComponent != null) + { + if (statelessComponent == this) + { + UpdateSecret(); + } + else + { + InitFromConnectionless(statelessComponent); + } + } + } + } + + public override void Initialize() + { + Logger.Debug("Initializing"); + + if (Handler.Mode == HandlerMode.Server) + { + Initialized(); + } + } + + private void InitFromConnectionless(StatelessConnectHandlerComponent connectionlessHandler) + { + Logger.Debug("InitFromConnectionless"); + + // Store the cookie/address used for the handshake, to enable server ack-retries + _lastChallengeSuccessAddress = connectionlessHandler._lastChallengeSuccessAddress; + + Buffer.BlockCopy(connectionlessHandler._authorisedCookie, 0, _authorisedCookie, 0, _authorisedCookie.Length); + } + + public override void CountBytes(FArchive ar) + { + throw new NotImplementedException(); + } + + public override void Incoming(FBitReader packet) + { + base.Incoming(packet); + } + + public override void Outgoing(FBitWriter packet, FOutPacketTraits traits) + { + base.Outgoing(packet, traits); + } + + public override void IncomingConnectionless(FIncomingPacketRef packetRef) + { + var packet = packetRef.Packet; + var address = packetRef.Address; + + if (_magicHeader.Length > 0) + { + // Skip magic header. + packet.Pos += _magicHeader.Length; + } + + var bHandshakePacket = packet.ReadBit() && !packet.IsError(); + + _lastChallengeSuccessAddress = null; + + if (bHandshakePacket) + { + var bRestartHandshake = false; + var secretId = (byte) 0; + var timestamp = 1.0d; + var cookie = new byte[CookieByteSize]; + var origCookie = new byte[CookieByteSize]; + + bHandshakePacket = ParseHandshakePacket(packet, ref bRestartHandshake, ref secretId, ref timestamp, cookie, origCookie); + + if (bHandshakePacket) + { + if (Handler.Mode == HandlerMode.Server) + { + var bInitialConnect = timestamp == 0.0; + if (bInitialConnect) + { + SendConnectChallenge(address); + } + else if (_driver != null) + { + var bChallengeSuccess = false; + var cookieDelta = _driver.GetElapsedTime() - timestamp; + var secretDelta = timestamp - _lastSecretUpdateTimestamp; + var bValidCookieLifetime = cookieDelta >= 0.0 && (MaxCookieLifetime - cookieDelta) > 0.0; + var bValidSecretIdTimestamp = (secretId == _activeSecret) ? (secretDelta >= 0.0) : (secretDelta <= 0.0); + + if (bValidCookieLifetime && bValidSecretIdTimestamp) + { + // Regenerate the cookie from the packet info, and see if the received cookie matches the regenerated one + var regenCookie = new byte[CookieByteSize]; + + GenerateCookie(address, secretId, timestamp, regenCookie); + + bChallengeSuccess = cookie.SequenceEqual(regenCookie); + + if (bChallengeSuccess) + { + if (bRestartHandshake) + { + Buffer.BlockCopy(origCookie, 0, _authorisedCookie, 0, _authorisedCookie.Length); + } + else + { + var curSequence = BinaryPrimitives.ReadInt16LittleEndian(cookie); + + _lastServerSequence = curSequence & (UNetConnection.MaxPacketId - 1); + _lastClientSequence = (curSequence + 1) & (UNetConnection.MaxPacketId - 1); + + Buffer.BlockCopy(cookie, 0, _authorisedCookie, 0, _authorisedCookie.Length); + } + + _bRestartedHandshake = bRestartHandshake; + _lastChallengeSuccessAddress = address; + + // Now ack the challenge response - the cookie is stored in AuthorisedCookie, to enable retries + SendChallengeAck(address, _authorisedCookie); + } + } + } + } + } + else + { + packet.SetError(); + + Logger.Error("Error reading handshake packet"); + } + } + else if (packet.IsError()) + { + Logger.Error("Error reading handshake bit from packet"); + } + // Late packets from recently disconnected clients may incorrectly trigger this code path, so detect and exclude those packets + else if (!packet.IsError() && !packetRef.Traits.FromRecentlyDisconnected) + { + // The packet was fine but not a handshake packet - an existing client might suddenly be communicating on a different address. + // If we get them to resend their cookie, we can update the connection's info with their new address. + SendRestartHandshakeRequest(address); + } + } + + private bool ParseHandshakePacket( + FBitReader packet, + ref bool bOutRestartHandshake, + ref byte outSecretId, + ref double outTimestamp, byte[] outCookie, byte[] outOrigCookie) + { + var bValidPacket = false; + var bitsLeft = packet.GetBitsLeft(); + var bHandshakePacketSize = bitsLeft == (HandshakePacketSizeBits - 1); + var bRestartResponsePacketSize = bitsLeft == (RestartHandshakePacketSizeBits - 1); + + if (bHandshakePacketSize || bRestartResponsePacketSize) + { + bOutRestartHandshake = packet.ReadBit(); + outSecretId = (byte)(packet.ReadBit() ? 1 : 0); + outTimestamp = packet.ReadDouble(); + packet.Serialize(outCookie, CookieByteSize); + + if (bRestartResponsePacketSize) + { + packet.Serialize(outOrigCookie, CookieByteSize); + } + + bValidPacket = !packet.IsError(); + } + else if (bitsLeft == (RestartHandshakePacketSizeBits - 1)) + { + bOutRestartHandshake = packet.ReadBit(); + bValidPacket = !packet.IsError() && bOutRestartHandshake && Handler.Mode == HandlerMode.Client; + } + + return bValidPacket; + } + + private void GenerateCookie(IPEndPoint clientAddress, byte secretId, double timestamp, Span outCookie) + { + using var writer = new FBitWriter(64 * 8); + + writer.WriteDouble(timestamp); + writer.WriteString(clientAddress.ToString()); + + HMACSHA1.HashData(_handshakeSecret[secretId], writer.GetData().AsSpan((int)writer.GetNumBytes()), outCookie); + } + + private void SendConnectChallenge(IPEndPoint address) + { + if (_driver == null) + { + Logger.Warning("Tried to send connect challenge without driver"); + return; + } + + using var challengePacket = new FBitWriter(GetAdjustedSizeBits(HandshakePacketSizeBits) + 1); + var bHandshakePacket = (byte)1; + var bRestartHandshake = (byte)0; + var timestamp = _driver.GetElapsedTime(); + var cookie = new byte[CookieByteSize]; + + GenerateCookie(address, _activeSecret, timestamp, cookie); + + if (_magicHeader.Length > 0) + { + challengePacket.SerializeBits(_magicHeader, _magicHeader.Count); + } + + challengePacket.WriteBit(bHandshakePacket); + challengePacket.WriteBit(bRestartHandshake); + challengePacket.WriteBit(_activeSecret); + challengePacket.WriteDouble(timestamp); + challengePacket.Serialize(cookie, cookie.Length); + + Logger.Verbose("SendConnectChallenge. Timestamp: {Timestamp}, Cookie: {Cookie}", timestamp, cookie); + + CapHandshakePacket(challengePacket); + + var connectionlessHandler = _driver.ConnectionlessHandler; + + connectionlessHandler?.SetRawSend(true); + + if (_driver.IsNetResourceValid()) + { + _driver.LowLevelSend(address, challengePacket.GetData(), (int)challengePacket.GetNumBits(), new FOutPacketTraits()); + } + + connectionlessHandler?.SetRawSend(false); + } + + private void SendChallengeAck(IPEndPoint address, byte[] inCookie) + { + if (_driver == null) + { + Logger.Warning("Tried to send challenge ack without driver"); + return; + } + + using var ackPacket = new FBitWriter(GetAdjustedSizeBits(HandshakePacketSizeBits) + 1); + var bHandshakePacket = (byte)1; + var bRestartHandshake = (byte)0; + var timestamp = -1.0d; + + if (_magicHeader.Length > 0) + { + ackPacket.SerializeBits(_magicHeader, _magicHeader.Count); + } + + ackPacket.WriteBit(bHandshakePacket); + ackPacket.WriteBit(bRestartHandshake); + ackPacket.WriteBit(bHandshakePacket); + ackPacket.WriteDouble(timestamp); + ackPacket.Serialize(inCookie, CookieByteSize); + + Logger.Verbose("SendChallengeAck. InCookie: {Cookie}", inCookie); + + CapHandshakePacket(ackPacket); + + var connectionlessHandler = _driver.ConnectionlessHandler; + + connectionlessHandler?.SetRawSend(true); + + if (_driver.IsNetResourceValid()) + { + _driver.LowLevelSend(address, ackPacket.GetData(), (int)ackPacket.GetNumBits(), new FOutPacketTraits()); + } + + connectionlessHandler?.SetRawSend(false); + } + + private void SendRestartHandshakeRequest(IPEndPoint address) + { + if (_driver == null) + { + Logger.Warning("Tried to send restart handshake without driver"); + return; + } + + using var restartPacket = new FBitWriter(GetAdjustedSizeBits(RestartHandshakePacketSizeBits) + 1); + var bHandshakePacket = (byte)1; + var bRestartHandshake = (byte)1; + + if (_magicHeader.Length > 0) + { + restartPacket.SerializeBits(_magicHeader, _magicHeader.Count); + } + + restartPacket.WriteBit(bHandshakePacket); + restartPacket.WriteBit(bRestartHandshake); + + CapHandshakePacket(restartPacket); + + var connectionlessHandler = _driver.ConnectionlessHandler; + + connectionlessHandler?.SetRawSend(true); + + if (_driver.IsNetResourceValid()) + { + _driver.LowLevelSend(address, restartPacket.GetData(), (int)restartPacket.GetNumBits(), new FOutPacketTraits()); + } + + connectionlessHandler?.SetRawSend(false); + } + + public override bool CanReadUnaligned() + { + return true; + } + + private void CapHandshakePacket(FBitWriter handshakePacket) + { + var numBits = handshakePacket.GetNumBits() - GetAdjustedSizeBits(0); + + if (numBits != HandshakePacketSizeBits && + numBits != RestartHandshakePacketSizeBits && + numBits != RestartResponseSizeBits) + { + Logger.Warning("Invalid handshake packet size bits"); + } + + // Termination bit. + handshakePacket.WriteBit(1); + } + + public override bool IsValid() + { + return true; + } + + private int GetAdjustedSizeBits(int inSizeBits) + { + return _magicHeader.Length + inSizeBits; + } + + public bool HasPassedChallenge(IPEndPoint address, out bool bOutRestartedHandshake) + { + bOutRestartedHandshake = _bRestartedHandshake; + + return _lastChallengeSuccessAddress != null && + _lastChallengeSuccessAddress.Equals(address); + } + + public void UpdateSecret() + { + _lastSecretUpdateTimestamp = _driver?.GetElapsedTime() ?? 0.0; + + if (_activeSecret == byte.MaxValue) + { + _handshakeSecret[0] = new byte[SecretByteSize]; + _handshakeSecret[1] = new byte[SecretByteSize]; + + // Randomize other secret. + var arr = _handshakeSecret[1]; + + for (var i = 0; i < SecretByteSize; i++) + { + arr[i] = (byte)(Random.Shared.Next() % 255); + } + + _activeSecret = 0; + } + else + { + _activeSecret = (byte)(_activeSecret == 1 ? 0 : 1); + } + + // Randomize current secret. + var curArray = _handshakeSecret[_activeSecret]; + + for (var i = 0; i < SecretByteSize; i++) + { + curArray[i] = (byte)(Random.Shared.Next() % 255); + } + } + + public override int GetReservedPacketBits() + { + return _magicHeader.Length + 1; + } + + public override void Tick(float deltaTime) + { + if (Handler.Mode == HandlerMode.Client) + { + throw new NotImplementedException(); + } + else + { + var bConnectionlessHandler = _driver != null && _driver.StatelessConnectComponent == this; + if (bConnectionlessHandler) + { + var curVariance = FMath.FRandRange(0, SecretUpdateTimeVariance); + if (((_driver!.GetElapsedTime() - _lastSecretUpdateTimestamp) - (SecretUpdateTime + curVariance)) > 0.0) + { + UpdateSecret(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UIpConnection.cs b/src/Prospect.Unreal/Net/UIpConnection.cs new file mode 100644 index 0000000..666773b --- /dev/null +++ b/src/Prospect.Unreal/Net/UIpConnection.cs @@ -0,0 +1,59 @@ +using System.Net; +using System.Net.Sockets; +using Prospect.Unreal.Core; + +namespace Prospect.Unreal.Net; + +public class UIpConnection : UNetConnection +{ + public override void InitBase(UNetDriver inDriver, UdpClient inSocket, FURL inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0) + { + throw new NotImplementedException(); + } + + public override void InitRemoteConnection(UNetDriver inDriver, UdpClient inSocket, FURL inURL, IPEndPoint inRemoteAddr, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0) + { + throw new NotImplementedException(); + } + + public override void InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FURL inURL, EConnectionState inState, + int inMaxPacket = 0, int inPacketOverhead = 0) + { + throw new NotImplementedException(); + } + + public override void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits) + { + throw new NotImplementedException(); + } + + public override string LowLevelGetRemoteAddress(bool bAppendPort = false) + { + throw new NotImplementedException(); + } + + public override string LowLevelDescribe() + { + throw new NotImplementedException(); + } + + public override void Tick(float deltaSeconds) + { + throw new NotImplementedException(); + } + + public override void CleanUp() + { + throw new NotImplementedException(); + } + + public override void ReceivedRawPacket(byte[] data, int count) + { + throw new NotImplementedException(); + } + + public override float GetTimeoutValue() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UIpNetDriver.cs b/src/Prospect.Unreal/Net/UIpNetDriver.cs new file mode 100644 index 0000000..b1ecf21 --- /dev/null +++ b/src/Prospect.Unreal/Net/UIpNetDriver.cs @@ -0,0 +1,179 @@ +using System.Net; +using System.Net.Sockets; +using Prospect.Unreal.Core; +using Serilog; + +namespace Prospect.Unreal.Net; + +public class UIpNetDriver : UNetDriver +{ + private static readonly ILogger Logger = Log.ForContext(); + + private bool _isDisposed; + + public UIpNetDriver() + { + ServerIp = new IPEndPoint(IPAddress.Any, 7777); + Socket = new UdpClient(ServerIp); + ReceiveThread = new FReceiveThreadRunnable(this); + } + + public IPEndPoint ServerIp { get; } + public UdpClient Socket { get; } + public FReceiveThreadRunnable ReceiveThread { get; } + + public bool Init() + { + // Initialize connectionless packet handler. + InitConnectionlessHandler(); + + // Start receiving packets. + ReceiveThread.Start(); + + return true; + } + + public override void TickDispatch(float deltaTime) + { + base.TickDispatch(deltaTime); + + while (ReceiveThread.TryReceive(out var packet)) + { + Logger.Information("Received {Buffer} from {Adress}", packet.DataView.GetData(), packet.Address); + + UNetConnection? connection = null; + + if (Equals(packet.Address, ServerIp)) + { + // TODO: Assign connection. + throw new NotImplementedException(); + } + + if (connection == null) + { + MappedClientConnections.TryGetValue(packet.Address, out connection); + } + + // If we didn't find a client connection, maybe create a new one. + if (connection == null) + { + connection = ProcessConnectionlessPacket(packet); + // TODO: bIgnorePacket + } + } + } + + private UNetConnection? ProcessConnectionlessPacket(FReceivedPacketView packet) + { + UNetConnection? returnVal; + var statelessConnect = StatelessConnectComponent; + var address = packet.Address; + var bPassedChallenge = false; + var bRestartedHandshake = false; + var bIgnorePacket = true; + + if (ConnectionlessHandler != null && statelessConnect != null) + { + var result = ConnectionlessHandler.IncomingConnectionless(packet); + if (result) + { + bPassedChallenge = statelessConnect.HasPassedChallenge(address, out bRestartedHandshake); + + if (bPassedChallenge) + { + if (bRestartedHandshake) + { + throw new NotImplementedException(); + } + + var newCountBytes = packet.DataView.NumBytes(); + var workingData = new byte[newCountBytes]; + + if (newCountBytes > 0) + { + Buffer.BlockCopy(packet.DataView.GetData(), 0, workingData, 0, newCountBytes); + bIgnorePacket = false; + } + + packet.DataView = new FPacketDataView(workingData, newCountBytes); + } + } + } + else + { + Logger.Warning("Invalid ConnectionlessHandler or StatelessConnectComponent, can't accept connections"); + } + + if (bPassedChallenge) + { + if (!bRestartedHandshake) + { + Logger.Verbose("Server accepting post-challenge connection from: {Address}", address); + + returnVal = new UIpConnection(); + + if (statelessConnect != null) + { + statelessConnect.GetChallengeSequences(out var serverSequence, out var clientSequence); + + returnVal.InitSequence(clientSequence, serverSequence); + } + + // TODO: World url + returnVal.InitRemoteConnection(this, Socket, null, address, EConnectionState.USOCK_Open); + + // if (returnVal.Handler) + } + + if (statelessConnect != null) + { + statelessConnect.ResetChallengeData(); + } + } + + if (bIgnorePacket) + { + packet.DataView = new FPacketDataView(packet.DataView.GetData(), 0); + } + + return null; + } + + public override void LowLevelSend(IPEndPoint address, byte[] data, int countBits, FOutPacketTraits traits) + { + if (ConnectionlessHandler != null) + { + var processedData = ConnectionlessHandler.OutgoingConnectionless(address, data, countBits, traits); + if (!processedData.Error) + { + data = processedData.Data; + countBits = processedData.CountBits; + } + else + { + countBits = 0; + } + } + + if (countBits > 0) + { + Socket.Send(data, FMath.DivideAndRoundUp(countBits, 8), address); + } + } + + public override bool IsNetResourceValid() + { + return !_isDisposed; + } + + public override async ValueTask DisposeAsync() + { + _isDisposed = true; + + await base.DisposeAsync(); + + Socket.Dispose(); + + await ReceiveThread.DisposeAsync(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UNetConnection.cs b/src/Prospect.Unreal/Net/UNetConnection.cs new file mode 100644 index 0000000..54d4672 --- /dev/null +++ b/src/Prospect.Unreal/Net/UNetConnection.cs @@ -0,0 +1,27 @@ +using System.Net; +using System.Net.Sockets; +using Prospect.Unreal.Core; + +namespace Prospect.Unreal.Net; + +public abstract class UNetConnection +{ + public const int ReliableBuffer = 256; + public const int MaxPacketId = 16384; + + public abstract void InitBase(UNetDriver inDriver, UdpClient inSocket, FURL inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0); + public abstract void InitRemoteConnection(UNetDriver inDriver, UdpClient inSocket, FURL inURL, IPEndPoint inRemoteAddr, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0); + public abstract void InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FURL inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0); + public abstract void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits); + public abstract string LowLevelGetRemoteAddress(bool bAppendPort = false); + public abstract string LowLevelDescribe(); + public abstract void Tick(float deltaSeconds); + public abstract void CleanUp(); + public abstract void ReceivedRawPacket(byte[] data, int count); + public abstract float GetTimeoutValue(); + + public void InitSequence(int clientSequence, int serverSequence) + { + + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UNetDriver.cs b/src/Prospect.Unreal/Net/UNetDriver.cs new file mode 100644 index 0000000..1140d95 --- /dev/null +++ b/src/Prospect.Unreal/Net/UNetDriver.cs @@ -0,0 +1,67 @@ +using System.Collections.Concurrent; +using System.Net; + +namespace Prospect.Unreal.Net; + +public abstract class UNetDriver : IAsyncDisposable +{ + private double _elapsedTime; + + protected UNetDriver() + { + MappedClientConnections = new ConcurrentDictionary(); + } + + /// + /// Map of to . + /// + public ConcurrentDictionary MappedClientConnections { get; } + + /// + /// Serverside PacketHandler for managing connectionless packets + /// + public PacketHandler? ConnectionlessHandler { get; private set; } + + /// + /// Reference to the PacketHandler component, for managing stateless connection handshakes + /// + public StatelessConnectHandlerComponent? StatelessConnectComponent { get; private set; } + + public void InitConnectionlessHandler() + { + ConnectionlessHandler = new PacketHandler(); + ConnectionlessHandler.Initialize(true); + + StatelessConnectComponent = (StatelessConnectHandlerComponent) ConnectionlessHandler.AddHandler(); + StatelessConnectComponent.SetDriver(this); + + ConnectionlessHandler.InitializeComponents(); + } + + public virtual void TickDispatch(float deltaTime) + { + _elapsedTime += deltaTime; + } + + public virtual void LowLevelSend(IPEndPoint address, byte[] data, int countBits, FOutPacketTraits traits) + { + throw new NotImplementedException(); + } + + public abstract bool IsNetResourceValid(); + + public double GetElapsedTime() + { + return _elapsedTime; + } + + public void ResetElapsedTime() + { + _elapsedTime = 0.0; + } + + public virtual ValueTask DisposeAsync() + { + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Prospect.Unreal.csproj b/src/Prospect.Unreal/Prospect.Unreal.csproj index 87866e3..799bfa0 100644 --- a/src/Prospect.Unreal/Prospect.Unreal.csproj +++ b/src/Prospect.Unreal/Prospect.Unreal.csproj @@ -4,6 +4,7 @@ net6.0 enable enable + true diff --git a/src/Prospect.Unreal/Serialization/FArchive.cs b/src/Prospect.Unreal/Serialization/FArchive.cs new file mode 100644 index 0000000..71e0b01 --- /dev/null +++ b/src/Prospect.Unreal/Serialization/FArchive.cs @@ -0,0 +1,731 @@ +using Prospect.Unreal.Core; + +namespace Prospect.Unreal.Serialization; + +public abstract class FArchive : IDisposable +{ + /// + /// Whether this archive is for loading data. + /// + public bool _arIsLoading; + + /// + /// Whether this archive is for saving data. + /// + public bool _arIsSaving; + + /// + /// Whether archive is transacting. + /// + public bool _arIsTransacting; + + /// + /// Whether this archive serializes to a text format. Text format archives should use high level constructs from FStructuredArchive for delimiting data rather than manually seeking through the file. + /// + public bool _arIsTextFormat; + + /// + /// Whether this archive wants properties to be serialized in binary form instead of tagged. + /// + public bool _arWantBinaryPropertySerialization; + + /// + /// Whether this archive wants to always save strings in unicode format + /// + public bool _arForceUnicode; + + /// + /// Whether this archive saves to persistent storage. + /// + public bool _arIsPersistent; + + /// + /// Whether this archive contains errors. + /// + public bool _arIsError; + + /// + /// Whether this archive contains critical errors. + /// + public bool _arIsCriticalError; + + /// + /// Quickly tell if an archive contains script code. + /// + public bool _arContainsCode; + + /// + /// Used to determine whether FArchive contains a level or world. + /// + public bool _arContainsMap; + + /// + /// Used to determine whether FArchive contains data required to be gathered for localization. + /// + public bool _arRequiresLocalizationGather; + + /// + /// Whether we should forcefully swap bytes. + /// + public bool _arForceByteSwapping; + + /// + /// If true, we will not serialize the ObjectArchetype reference in UObject. + /// + public bool _arIgnoreArchetypeRef; + + /// + /// If true, we will not serialize the ObjectArchetype reference in UObject. + /// + public bool _arNoDelta; + + /// + /// If true, we will not serialize the Outer reference in UObject. + /// + public bool _arIgnoreOuterRef; + + /// + /// If true, we will not serialize ClassGeneratedBy reference in UClass. + /// + public bool _arIgnoreClassGeneratedByRef; + + /// + /// If true, UObject::Serialize will skip serialization of the Class property. + /// + public bool _arIgnoreClassRef; + + /// + /// Whether to allow lazy loading. + /// + public bool _arAllowLazyLoading; + + /// + /// Whether this archive only cares about serializing object references. + /// + public bool _arIsObjectReferenceCollector; + + /// + /// Whether a reference collector is modifying the references and wants both weak and strong ones + /// + public bool _arIsModifyingWeakAndStrongReferences; + + /// + /// Whether this archive is counting memory and therefore wants e.g. TMaps to be serialized. + /// + public bool _arIsCountingMemory; + + /// + /// Whether bulk data serialization should be skipped or not. + /// + public bool _arShouldSkipBulkData; + + /// + /// Whether editor only properties are being filtered from the archive (or has been filtered). + /// + public bool _arIsFilterEditorOnly; + + /// + /// Whether this archive is saving/loading game state + /// + public bool _arIsSaveGame; + + /// + /// Whether or not this archive is sending/receiving network data + /// + public bool _arIsNetArchive; + + /// + /// Set TRUE to use the custom property list attribute for serialization. + /// + public bool _arUseCustomPropertyList; + + /// + /// Whether we are currently serializing defaults. > 0 means yes, <= 0 means no. + /// + public int _arSerializingDefaults; + + /// + /// Modifier flags that be used when serializing UProperties + /// + public uint _arPortFlags; + + /// + /// Max size of data that this archive is allowed to serialize. + /// + public long _arMaxSerializeSize; + + /// + /// Holds the archive version. + /// + protected int _arUE4Ver; + + /// + /// Holds the archive version for licensees. + /// + protected int _arLicenseeUE4Ver; + + /// + /// Holds the engine version. + /// + protected FEngineVersion _arEngineVer; + + /// + /// Holds the engine network protocol version. + /// + protected EEngineNetworkVersionHistory _arEngineNetVer; + + /// + /// Holds the game network protocol version. + /// + protected uint _arGameNetVer; + + public virtual bool ReadBit() + { + throw new NotImplementedException(); + } + + public virtual unsafe byte ReadByte() + { + byte value; + Serialize(&value, 1); + return value; + } + + public virtual unsafe byte[] ReadBytes(long amount) + { + byte[] value = new byte[amount]; + + fixed (byte* pValue = value) + { + Serialize(pValue, amount); + } + + return value; + } + + public virtual unsafe ushort ReadUInt16() + { + ushort value; + ByteOrderSerialize(&value, sizeof(ushort)); + return value; + } + + public virtual unsafe short ReadInt16() + { + short value; + ByteOrderSerialize(&value, sizeof(short)); + return value; + } + + public virtual unsafe uint ReadUInt32() + { + uint value; + ByteOrderSerialize(&value, sizeof(uint)); + return value; + } + + public virtual unsafe int ReadInt32() + { + int value; + ByteOrderSerialize(&value, sizeof(int)); + return value; + } + + public unsafe void WriteInt32(int value) + { + ByteOrderSerialize((uint*)&value, sizeof(uint)); + } + + public virtual unsafe uint ReadInt(uint max) + { + throw new NotImplementedException(); + } + + public virtual unsafe uint ReadUInt32Packed() + { + uint value; + SerializeIntPacked(&value); + return value; + } + + public virtual unsafe ulong ReadUInt64() + { + ulong value; + ByteOrderSerialize(&value, sizeof(ulong)); + return value; + } + + public virtual unsafe long ReadInt64() + { + long value; + ByteOrderSerialize(&value, sizeof(long)); + return value; + } + + public virtual unsafe float ReadFloat() + { + float value; + ByteOrderSerialize(&value, sizeof(float)); + return value; + } + + public virtual unsafe double ReadDouble() + { + double value; + ByteOrderSerialize(&value, sizeof(double)); + return value; + } + + public unsafe void WriteDouble(double value) + { + ByteOrderSerialize((ulong*)&value, sizeof(ulong)); + } + + public string ReadString() + { + return FString.Deserialize(this); + } + + public void WriteString(string value) + { + FString.Serialize(this, value); + } + + public unsafe void Serialize(Span value, long num) + { + fixed (byte* pBuffer = value) + { + Serialize(pBuffer, num); + } + } + + public unsafe void Serialize(byte[] value, long num) + { + fixed (byte* pBuffer = value) + { + Serialize(pBuffer, num); + } + } + + public virtual unsafe void Serialize(void* value, long num) + { + throw new NotImplementedException(); + } + + public unsafe void SerializeBits(byte[] value, long lengthBits) + { + fixed (byte* pBuffer = value) + { + SerializeBits(pBuffer, lengthBits); + } + } + + /// + /// + /// + /// + /// + public virtual unsafe void SerializeBits(void* value, long lengthBits) + { + Serialize(value, (lengthBits + 7) / 8); + + if (IsLoading() && (lengthBits % 8) != 0) + { + ((byte*) value)[lengthBits / 8] &= (byte) ((1 << (int) (lengthBits & 7)) - 1); + } + } + + public virtual unsafe void SerializeInt(uint* value, uint max) + { + throw new NotImplementedException(); + ByteOrderSerialize(&value, sizeof(uint)); + } + + /// + /// Packs int value into bytes of 7 bits with 8th bit for 'more'. + /// + /// + public virtual unsafe void SerializeIntPacked(uint* value) + { + if (IsLoading()) + { + byte count = 0; + byte more = 1; + + while (more != 0) + { + byte nextByte; + Serialize(&nextByte, 1); // Read next byte + + more = (byte)(nextByte & 1); // Check 1 bit to see if theres more after this + nextByte = (byte)(nextByte >> 1); // Shift to get actual 7 bit value + *value += (uint)nextByte << (7 * count++); // Add to total value + } + } + else + { + throw new NotImplementedException(); + } + } + + public virtual string GetArchiveName() + { + return "FArchive"; + } + + public virtual long Tell() + { + return -1; + } + + public virtual long TotalSize() + { + return -1; + } + + public virtual bool AtEnd() + { + var pos = Tell(); + + return pos != -1 && pos >= TotalSize(); + } + + public virtual void Seek(long inPos) + { + throw new NotImplementedException(); + } + + public virtual void Flush() + { + throw new NotImplementedException(); + } + + public virtual bool Close() + { + return !_arIsError; + } + + public virtual bool GetError() + { + return _arIsError; + } + + public void SetError() + { + _arIsError = true; + } + + public bool IsByteSwapping() + { + return BitConverter.IsLittleEndian + ? _arForceByteSwapping + : _arIsPersistent; + } + + /// + /// Used to do byte swapping on small items. This does not happen usually, so we don't want it inline + /// + /// + /// + public unsafe void ByteSwap(void* v, int length) + { + byte* ptr = (byte*) v; + int top = length - 1; + int bottom = 0; + + while (bottom < top) + { + var aPos = top--; + var bPos = bottom++; + + (ptr[aPos], ptr[bPos]) = (ptr[bPos], ptr[aPos]); + } + } + + public unsafe void ByteOrderSerialize(void* v, int length) + { + if (!IsByteSwapping()) + { + Serialize(v, length); + return; + } + + SerializeByteOrderSwapped(v, length); + } + + private unsafe void SerializeByteOrderSwapped(void* v, int length) + { + if (IsLoading()) + { + Serialize(v, length); + ByteSwap(v, length); + } + else + { + ByteSwap(v, length); + Serialize(v, length); + ByteSwap(v, length); + } + } + + public void ThisContainsCode() + { + _arContainsCode = true; + } + + public void ThisContainsMap() + { + _arContainsMap = true; + } + + public void ThisRequiresLocalizationGather() + { + _arRequiresLocalizationGather = true; + } + + public void StartSerializingDefaults() + { + _arSerializingDefaults++; + } + + public void StopSerializingDefaults() + { + _arSerializingDefaults--; + } + + public int UE4Ver() + { + return _arUE4Ver; + } + + public int LicenseeUE4Ver() + { + return _arLicenseeUE4Ver; + } + + public FEngineVersion EngineVer() + { + return _arEngineVer; + } + + public EEngineNetworkVersionHistory EngineNetVer() + { + return _arEngineNetVer; + } + + public uint GameNetVer() + { + return _arGameNetVer; + } + + public bool IsLoading() + { + return _arIsLoading; + } + + public bool IsSaving() + { + return _arIsSaving; + } + + public bool IsTransacting() + { + // Misses FPlatformProperties::HasEditorOnlyData. + + return _arIsTransacting; + } + + public bool IsTextFormat() + { + return _arIsTextFormat; + } + + public bool WantBinaryPropertySerialization() + { + return _arWantBinaryPropertySerialization; + } + + public bool IsForcingUnicode() + { + return _arForceUnicode; + } + + public bool IsPersistent() + { + return _arIsPersistent; + } + + public bool IsError() + { + return _arIsError; + } + + public bool IsCriticalError() + { + return _arIsCriticalError; + } + + public bool ContainsCode() + { + return _arContainsCode; + } + + public bool ContainsMap() + { + return _arContainsMap; + } + + public bool RequiresLocalizationGather() + { + return _arRequiresLocalizationGather; + } + + public bool ForceByteSwapping() + { + return _arForceByteSwapping; + } + + public bool IsSerializingDefaults() + { + return _arSerializingDefaults > 0; + } + + public bool IsIgnoringArchetypeRef() + { + return _arIgnoreArchetypeRef; + } + + public bool DoDelta() + { + return !_arNoDelta; + } + + public bool IsIgnoringOuterRef() + { + return _arIgnoreOuterRef; + } + + public bool IsIgnoringClassGeneratedByRef() + { + return _arIgnoreClassGeneratedByRef; + } + + public bool IsIgnoringClassRef() + { + return _arIgnoreClassRef; + } + + public bool IsAllowingLazyLoading() + { + return _arAllowLazyLoading; + } + + public bool IsObjectReferenceCollector() + { + return _arIsObjectReferenceCollector; + } + + public bool IsModifyingWeakAndStrongReferences() + { + return _arIsModifyingWeakAndStrongReferences; + } + + public bool IsCountingMemory() + { + return _arIsCountingMemory; + } + + public uint GetPortFlags() + { + return _arPortFlags; + } + + public bool HasAnyPortFlags(uint flags) + { + return (_arPortFlags & flags) != 0; + } + + public bool HasAllPortFlags(uint flags) + { + return (_arPortFlags & flags) == flags; + } + + public bool ShouldSkipBulkData() + { + return _arShouldSkipBulkData; + } + + public long GetMaxSerializeSize() + { + return _arMaxSerializeSize; + } + + /// + /// Sets the archive version number. Used by the code that makes sure that FLinkerLoad's + /// internal archive versions match the file reader it creates. + /// + /// new version number + public void SetUE4Ver(int inVer) + { + _arUE4Ver = inVer; + } + + /// + /// Sets the archive licensee version number. Used by the code that makes sure that FLinkerLoad's + /// internal archive versions match the file reader it creates. + /// + /// new version number + public void SetLicenseeUE4Ver(int inVer) + { + _arLicenseeUE4Ver = inVer; + } + + /// + /// Sets the archive engine version. Used by the code that makes sure that FLinkerLoad's + /// internal archive versions match the file reader it creates. + /// + /// new version number + public void SetEngineVer(FEngineVersion inVer) + { + _arEngineVer = inVer; + } + + /// + /// Sets the archive engine network version. + /// + /// + public void SetEngineNetVer(EEngineNetworkVersionHistory inEngineNetVer) + { + _arEngineNetVer = inEngineNetVer; + } + + /// + /// Sets the archive game network version. + /// + /// + public void SetGameNetVer(uint inGameNetVer) + { + _arGameNetVer = inGameNetVer; + } + + /// + /// Toggle saving as Unicode. This is needed when we need to make sure ANSI strings are saved as Unicode + /// + /// set to true to force saving as Unicode + public void SetForceUnicode(bool enabled) + { + _arForceUnicode = enabled; + } + + public virtual bool IsFilterEditorOnly() + { + return _arIsFilterEditorOnly; + } + + public virtual void SetFilterEditorOnly(bool inFilterEditorOnly) + { + _arIsFilterEditorOnly = inFilterEditorOnly; + } + + public abstract void Dispose(); +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Serialization/FBitReader.cs b/src/Prospect.Unreal/Serialization/FBitReader.cs new file mode 100644 index 0000000..240ed9a --- /dev/null +++ b/src/Prospect.Unreal/Serialization/FBitReader.cs @@ -0,0 +1,234 @@ +using System.Collections; +using Prospect.Unreal.Exceptions; + +namespace Prospect.Unreal.Serialization; + +public class FBitReader : FArchive +{ + public FBitReader(byte[] src) : this(src, src?.Length << 3 ?? 0) + { + } + + public FBitReader(byte[]? src, int num) + { + _arIsPersistent = true; + _arIsLoading = true; + _arIsNetArchive = true; + + Num = num; + + if (src != null) + { + Buffer = src; + BufferBits = new BitArray(src); + + ApplyMask(); + } + else + { + Buffer = Array.Empty(); + BufferBits = new BitArray(Buffer); + } + } + + private byte[] Buffer { get; set; } + + private BitArray BufferBits { get; set; } + + public long Pos { get; set; } + + private long Num { get; set; } + + public unsafe void SetData(byte[] src, long countBits) + { + Pos = 0; + Num = countBits; + Buffer = src; + + if ((Num & 7) != 0) + { + Buffer[Num >> 3] &= FBitUtil.GMask[Num & 7]; + } + + BufferBits = new BitArray(Buffer); + } + + public unsafe void SetData(FBitReader src, long countBits) + { + Pos = 0; + Num = countBits; + + if (src.GetBitsLeft() < countBits) + { + throw new UnrealSerializationException($"SetData overflow. ({src.GetBitsLeft()} < {countBits})"); + } + + SetEngineNetVer(src.EngineNetVer()); + SetGameNetVer(src.GameNetVer()); + + Buffer = new byte[(countBits + 7) >> 3]; + + fixed (byte* pBuffer = Buffer) + { + src.SerializeBits(pBuffer, countBits); + } + + BufferBits = new BitArray(Buffer); + } + + public override unsafe void SerializeBits(void* dest, long lengthBits) + { + if (lengthBits == 1) + { + ((byte*) dest)[0] = (byte) (BufferBits[(int) Pos++] ? 0x01 : 0x00); + } + else if (lengthBits != 0) + { + ((byte*) dest)[0] = 0; + + fixed (byte* pBuffer = Buffer) + { + FBitUtil.AppBitsCpy((byte*) dest, 0, pBuffer, (int) Pos, (int) lengthBits); + } + + Pos += lengthBits; + } + } + + public override unsafe void SerializeInt(uint* outValue, uint valueMax) + { + uint value = 0; + var localPos = Pos; + var localNum = Num; + + for (uint mask = 1; (value + mask) < valueMax && (mask != 0); mask *= 2, localPos++) + { + if (localPos >= localNum) + { + throw new UnrealSerializationException("BitReader::SerializeInt Overflow."); + } + + if (BufferBits[(int) localPos]) + { + value |= mask; + } + } + + Pos = localPos; + *outValue = value; + } + + public override unsafe uint ReadInt(uint max) + { + uint value = 0; + SerializeInt(&value, max); + return value; + } + + public override bool ReadBit() + { + return BufferBits[(int) Pos++]; + } + + public override unsafe void Serialize(void* dest, long lengthBytes) + { + SerializeBits(dest, lengthBytes * 8); + } + + public unsafe byte* GetData() + { + fixed (byte* pBuffer = Buffer) + { + return pBuffer; + } + } + + public byte[] GetBuffer() + { + return Buffer; + } + + public int GetBufferPosChecked() + { + if (Pos % 8 != 0) + { + throw new UnrealSerializationException("FBitReader Pos % 8 != 0"); + } + + return (int) (Pos >> 3); + } + + public int GetBytesLeft() + { + return (int) ((Num - Pos + 7) >> 3); + } + + public int GetBitsLeft() + { + return Math.Max((int) (Num - Pos), 0); + } + + public bool AtEnd() + { + return _arIsError || Pos >= Num; + } + + public int GetNumBytes() + { + return (int) ((Num + 7) >> 3); + } + + public int GetNumBits() + { + return (int) Num; + } + + public int GetPosBits() + { + return (int) Pos; + } + + public void AppendDataFromChecked(int inBufferPos, byte[] inBuffer, int inBitsCount) + { + if (Num % 8 != 0) + { + throw new UnrealSerializationException("FBitReader Pos % 8 != 0"); + } + + // Copy to buffer. + var merged = new byte[Buffer.Length + inBuffer.Length - inBufferPos]; + + System.Buffer.BlockCopy(Buffer, 0, merged, 0, Buffer.Length); + System.Buffer.BlockCopy(inBuffer, inBufferPos, merged, Buffer.Length, inBuffer.Length - inBufferPos); + + Buffer = merged; + BufferBits = new BitArray(Buffer); + + Num += inBitsCount; + + ApplyMask(); + } + + private void ApplyMask() + { + if ((Num & 7) != 0) + { + var mask = FBitUtil.GMask[Num & 7]; + var num = (int) Num; + + // Apply to bytes. + Buffer[num >> 3] &= mask; + + // Apply to bits. + for (int i = 0; i <= 7; i++) + { + BufferBits[num - i] &= ((mask >> 7 - i) & 0x1) == 1; + } + } + } + + public override void Dispose() + { + + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Serialization/FBitUtil.cs b/src/Prospect.Unreal/Serialization/FBitUtil.cs new file mode 100644 index 0000000..883a77b --- /dev/null +++ b/src/Prospect.Unreal/Serialization/FBitUtil.cs @@ -0,0 +1,107 @@ +namespace Prospect.Unreal.Serialization; + +public class FBitUtil +{ + public static readonly byte[] GShift = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; + + public static readonly byte[] GMask = {0x00, 0x01, 0x03, 0x07, 0x0f, 0x1f, 0x3f, 0x7f}; + + public static unsafe void AppBitsCpy(byte* dest, int destBit, byte* src, int srcBit, int bitCount) + { + if (bitCount == 0) return; + + // Special case - always at least one bit to copy, + // a maximum of 2 bytes to read, 2 to write - only touch bytes that are actually used. + if (bitCount <= 8) + { + int aDestIndex = destBit / 8; + int aSrcIndex = srcBit / 8; + int aLastDest = (destBit + bitCount - 1) / 8; + int aLastSrc = (srcBit + bitCount - 1) / 8; + int shiftSrc = srcBit & 7; + int shiftDest = destBit & 7; + int firstMask = 0xFF << shiftDest; + int lastMask = 0xFE << ((destBit + bitCount - 1) & 7); // Pre-shifted left by 1. + int accu; + + if (aSrcIndex == aLastSrc) + accu = (src[aSrcIndex] >> shiftSrc); + else + accu = ((src[aSrcIndex] >> shiftSrc) | (src[aLastSrc] << (8 - shiftSrc))); + + // One byte. + if (aDestIndex == aLastDest) + { + int multiMask = firstMask & ~lastMask; + dest[aDestIndex] = (byte) ((dest[aDestIndex] & ~multiMask) | ((accu << shiftDest) & multiMask)); + } + // Two bytes. + else + { + dest[aDestIndex] = (byte) ((dest[aDestIndex] & ~firstMask) | ((accu << shiftDest) & firstMask)); + dest[aLastDest] = (byte) ((dest[aLastDest] & lastMask) | ((accu >> (8 - shiftDest)) & ~lastMask)); + } + + return; + } + + // Main copier, uses byte sized shifting. Minimum size is 9 bits, so at least 2 reads and 2 writes. + int destIndex = destBit / 8; + int firstSrcMask = 0xFF << (destBit & 7); + int lastDest = (destBit + bitCount) / 8; + int lastSrcMask = 0xFF << ((destBit + bitCount) & 7); + int srcIndex = srcBit / 8; + int lastSrc = (srcBit + bitCount) / 8; + int shiftCount = (destBit & 7) - (srcBit & 7); + int destLoop = lastDest - destIndex; + int srcLoop = lastSrc - srcIndex; + int fullLoop; + int bitAccu; + + // Lead-in needs to read 1 or 2 source bytes depending on alignment. + if (shiftCount >= 0) + { + fullLoop = Math.Max(destLoop, srcLoop); + bitAccu = src[srcIndex] << shiftCount; + shiftCount += 8; //prepare for the inner loop. + } + else + { + shiftCount += 8; // turn shifts -7..-1 into +1..+7 + fullLoop = Math.Max(destLoop, srcLoop - 1); + bitAccu = src[srcIndex] << shiftCount; + srcIndex++; + shiftCount += 8; // Prepare for inner loop. + bitAccu = ((src[srcIndex] << shiftCount) + (bitAccu)) >> 8; + } + + // Lead-in - first copy. + dest[destIndex] = (byte)((bitAccu & firstSrcMask) | (dest[destIndex] & ~firstSrcMask)); + srcIndex++; + destIndex++; + + // Fast inner loop. + for (; fullLoop > 1; fullLoop--) + { // ShiftCount ranges from 8 to 15 - all reads are relevant. + bitAccu = ((src[srcIndex] << shiftCount) + (bitAccu)) >> 8; // Copy in the new, discard the old. + srcIndex++; + dest[destIndex] = (byte)bitAccu; // Copy low 8 bits. + destIndex++; + } + + // Lead-out. + if (lastSrcMask != 0xFF) + { + if ((srcBit + bitCount - 1) / 8 == srcIndex) // Last legal byte ? + { + bitAccu = ((src[srcIndex] << shiftCount) + (bitAccu)) >> 8; + } + else + { + bitAccu = bitAccu >> 8; + } + + dest[destIndex] = (byte)((dest[destIndex] & lastSrcMask) | (bitAccu & ~lastSrcMask)); + } + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Serialization/FBitWriter.cs b/src/Prospect.Unreal/Serialization/FBitWriter.cs new file mode 100644 index 0000000..db3c97d --- /dev/null +++ b/src/Prospect.Unreal/Serialization/FBitWriter.cs @@ -0,0 +1,137 @@ +using System.Buffers; +using System.Collections; +using Prospect.Unreal.Core; +using Serilog; + +namespace Prospect.Unreal.Serialization; + +public class FBitWriter : FArchive +{ + private static readonly ILogger Logger = Log.ForContext(); + private static readonly ArrayPool Pool = ArrayPool.Create(); + + public FBitWriter(int inMaxBits, bool inAllowResize = false) + { + Num = 0; + Max = inMaxBits; + AllowResize = inAllowResize; + Buffer = Pool.Rent((inMaxBits + 7) >> 3); + + _arIsSaving = true; + _arIsPersistent = true; + _arIsNetArchive = true; + } + + private byte[] Buffer { get; set; } + private long Num { get; set; } + private long Max { get; set; } + private bool AllowResize { get; } + private bool AllowOverflow { get; } + + public override void Dispose() + { + Pool.Return(Buffer, true); + } + + public override unsafe void Serialize(void* src, long lengthBytes) + { + var lengthBits = lengthBytes * 8; + if (AllowAppend(lengthBits)) + { + fixed (byte* pBuffer = Buffer) + { + FBitUtil.AppBitsCpy(pBuffer, (int)Num, (byte*)src, 0, (int)lengthBits); + } + + Num += lengthBits; + } + else + { + SetOverflowed(lengthBits); + } + } + + public void SerializeBits(BitArray bits, int lengthBits) + { + if (AllowAppend(lengthBits)) + { + for (var i = 0; i < lengthBits; i++) + { + WriteBit((byte)(bits.Get(i) ? 1 : 0)); + } + } + else + { + SetOverflowed(lengthBits); + } + } + + public void WriteBit(byte value) + { + if (AllowAppend(1)) + { + if (value != 0) + { + Buffer[Num >> 3] |= FBitUtil.GShift[Num & 7]; + } + + Num++; + } + else + { + SetOverflowed(1); + } + } + + private void SetOverflowed(long lengthBits) + { + if (!AllowOverflow) + { + Logger.Error("FBitWriter overflowed (WriteLen: {Len}, Remaining: {Remaining}, Max: {Max})", lengthBits, (Max - Num), Max); + } + + SetError(); + } + + private bool AllowAppend(long lengthBits) + { + if (Num + lengthBits > Max) + { + if (AllowResize) + { + throw new NotImplementedException(); + } + else + { + return false; + } + } + + return true; + } + + public byte[] GetData() + { + if (IsError()) + { + Logger.Error("Retrieved data from a BitWriter that had an error"); + } + + return Buffer; + } + + public long GetNumBytes() + { + return (Num + 7) >> 3; + } + + public long GetNumBits() + { + return Num; + } + + public long GetMaxBits() + { + return Max; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Serialization/FMemoryWriter.cs b/src/Prospect.Unreal/Serialization/FMemoryWriter.cs new file mode 100644 index 0000000..04659e9 --- /dev/null +++ b/src/Prospect.Unreal/Serialization/FMemoryWriter.cs @@ -0,0 +1,20 @@ +using System.Buffers; + +namespace Prospect.Unreal.Serialization; + +public class FMemoryWriter : FArchive +{ + private static readonly ArrayPool Pool = ArrayPool.Create(); + + public FMemoryWriter() + { + + } + + + + public override void Dispose() + { + + } +} \ No newline at end of file