Started on UE4 server, semi working handshake
This commit is contained in:
@@ -12,7 +12,7 @@ public class EnterMatchmaking : ICloudScriptFunction<FYEnterMatchAzureFunction,
|
|||||||
Success = true,
|
Success = true,
|
||||||
Address = "127.0.0.1",
|
Address = "127.0.0.1",
|
||||||
ErrorMessage = "",
|
ErrorMessage = "",
|
||||||
Port = 30229,
|
Port = 7777,
|
||||||
ShardIndex = 0,
|
ShardIndex = 0,
|
||||||
SingleplayerStation = false,
|
SingleplayerStation = false,
|
||||||
MaintenanceMode = false,
|
MaintenanceMode = false,
|
||||||
|
|||||||
@@ -1,9 +1,50 @@
|
|||||||
namespace Prospect.Server.Game;
|
using Prospect.Unreal.Net;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Prospect.Server.Game;
|
||||||
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
private const float TickRate = (1000.0f / 60.0f) / 1000.0f;
|
||||||
|
|
||||||
|
private static readonly ILogger Logger = Log.ForContext(typeof(Program));
|
||||||
|
private static readonly PeriodicTimer Tick = new PeriodicTimer(TimeSpan.FromSeconds(TickRate));
|
||||||
|
|
||||||
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Hello, World!");
|
Console.CancelKeyPress += (s, e) =>
|
||||||
|
{
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,4 +12,8 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Prospect.Unreal\Prospect.Unreal.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
namespace Prospect.Unreal;
|
|
||||||
|
|
||||||
public class Class1
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Prospect.Unreal.Core;
|
||||||
|
|
||||||
|
public class FURL
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
|
internal class CoreNet
|
||||||
|
{
|
||||||
|
public const int MaxPacketSize = 1024;
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
|
public enum EIncomingResult
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
|
public class FOutPacketTraits
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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<FReceiveThreadRunnable>();
|
||||||
|
|
||||||
|
private readonly UIpNetDriver _driver;
|
||||||
|
private readonly CancellationTokenSource _cancellation;
|
||||||
|
private readonly ConcurrentQueue<FReceivedPacket> _receiveQueue;
|
||||||
|
|
||||||
|
private Task? _receiveTask;
|
||||||
|
|
||||||
|
public FReceiveThreadRunnable(UIpNetDriver driver)
|
||||||
|
{
|
||||||
|
_driver = driver;
|
||||||
|
_cancellation = new CancellationTokenSource();
|
||||||
|
_receiveQueue = new ConcurrentQueue<FReceivedPacket>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
|
public enum HandlerMode
|
||||||
|
{
|
||||||
|
Client, // Clientside PacketHandler
|
||||||
|
Server // Serverside PacketHandler
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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<PacketHandler>();
|
||||||
|
|
||||||
|
private readonly List<HandlerComponent> _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<HandlerComponent>();
|
||||||
|
_reliabilityComponent = null;
|
||||||
|
_outgoingPacket = new FBitWriter(0);
|
||||||
|
_incomingPacket = new FBitReader(Array.Empty<byte>());
|
||||||
|
|
||||||
|
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<T>() 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
|
public class ReliabilityHandlerComponent : HandlerComponent
|
||||||
|
{
|
||||||
|
public ReliabilityHandlerComponent(PacketHandler handler) : base(handler, nameof(ReliabilityHandlerComponent))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<StatelessConnectHandlerComponent>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The serverside-only 'secret' value, used to help with generating cookies.
|
||||||
|
/// </summary>
|
||||||
|
private byte[][] _handshakeSecret;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Which of the two secret values above is active (values are changed frequently, to limit replay attacks)
|
||||||
|
/// </summary>
|
||||||
|
private byte _activeSecret;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time of the last secret value update
|
||||||
|
/// </summary>
|
||||||
|
private double _lastSecretUpdateTimestamp;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last address to successfully complete the handshake challenge
|
||||||
|
/// </summary>
|
||||||
|
private IPEndPoint? _lastChallengeSuccessAddress;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The initial server sequence value, from the last successful handshake
|
||||||
|
/// </summary>
|
||||||
|
private int _lastServerSequence;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The initial client sequence value, from the last successful handshake
|
||||||
|
/// </summary>
|
||||||
|
private int _lastClientSequence;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client: Whether or not we are in the middle of a restarted handshake.
|
||||||
|
/// Server: Whether or not the last handshake was a restarted handshake.
|
||||||
|
/// </summary>
|
||||||
|
private bool _bRestartedHandshake;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The cookie which completed the connection handshake.
|
||||||
|
/// </summary>
|
||||||
|
private byte[] _authorisedCookie;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The magic header which is prepended to all packets
|
||||||
|
/// </summary>
|
||||||
|
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<byte> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<UIpNetDriver>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<IPEndPoint, UNetConnection>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map of <see cref="IPEndPoint"/> to <see cref="UNetConnection"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ConcurrentDictionary<IPEndPoint, UNetConnection> MappedClientConnections { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Serverside PacketHandler for managing connectionless packets
|
||||||
|
/// </summary>
|
||||||
|
public PacketHandler? ConnectionlessHandler { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to the PacketHandler component, for managing stateless connection handshakes
|
||||||
|
/// </summary>
|
||||||
|
public StatelessConnectHandlerComponent? StatelessConnectComponent { get; private set; }
|
||||||
|
|
||||||
|
public void InitConnectionlessHandler()
|
||||||
|
{
|
||||||
|
ConnectionlessHandler = new PacketHandler();
|
||||||
|
ConnectionlessHandler.Initialize(true);
|
||||||
|
|
||||||
|
StatelessConnectComponent = (StatelessConnectHandlerComponent) ConnectionlessHandler.AddHandler<StatelessConnectHandlerComponent>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,731 @@
|
|||||||
|
using Prospect.Unreal.Core;
|
||||||
|
|
||||||
|
namespace Prospect.Unreal.Serialization;
|
||||||
|
|
||||||
|
public abstract class FArchive : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive is for loading data.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsLoading;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive is for saving data.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsSaving;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether archive is transacting.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsTransacting;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsTextFormat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive wants properties to be serialized in binary form instead of tagged.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arWantBinaryPropertySerialization;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive wants to always save strings in unicode format
|
||||||
|
/// </summary>
|
||||||
|
public bool _arForceUnicode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive saves to persistent storage.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsPersistent;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive contains errors.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsError;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive contains critical errors.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsCriticalError;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quickly tell if an archive contains script code.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arContainsCode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to determine whether FArchive contains a level or world.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arContainsMap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to determine whether FArchive contains data required to be gathered for localization.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arRequiresLocalizationGather;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether we should forcefully swap bytes.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arForceByteSwapping;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, we will not serialize the ObjectArchetype reference in UObject.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIgnoreArchetypeRef;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, we will not serialize the ObjectArchetype reference in UObject.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arNoDelta;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, we will not serialize the Outer reference in UObject.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIgnoreOuterRef;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, we will not serialize ClassGeneratedBy reference in UClass.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIgnoreClassGeneratedByRef;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, UObject::Serialize will skip serialization of the Class property.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIgnoreClassRef;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to allow lazy loading.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arAllowLazyLoading;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive only cares about serializing object references.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsObjectReferenceCollector;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a reference collector is modifying the references and wants both weak and strong ones
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsModifyingWeakAndStrongReferences;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive is counting memory and therefore wants e.g. TMaps to be serialized.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsCountingMemory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether bulk data serialization should be skipped or not.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arShouldSkipBulkData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether editor only properties are being filtered from the archive (or has been filtered).
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsFilterEditorOnly;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this archive is saving/loading game state
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsSaveGame;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not this archive is sending/receiving network data
|
||||||
|
/// </summary>
|
||||||
|
public bool _arIsNetArchive;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set TRUE to use the custom property list attribute for serialization.
|
||||||
|
/// </summary>
|
||||||
|
public bool _arUseCustomPropertyList;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether we are currently serializing defaults. > 0 means yes, <= 0 means no.
|
||||||
|
/// </summary>
|
||||||
|
public int _arSerializingDefaults;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modifier flags that be used when serializing UProperties
|
||||||
|
/// </summary>
|
||||||
|
public uint _arPortFlags;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Max size of data that this archive is allowed to serialize.
|
||||||
|
/// </summary>
|
||||||
|
public long _arMaxSerializeSize;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the archive version.
|
||||||
|
/// </summary>
|
||||||
|
protected int _arUE4Ver;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the archive version for licensees.
|
||||||
|
/// </summary>
|
||||||
|
protected int _arLicenseeUE4Ver;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the engine version.
|
||||||
|
/// </summary>
|
||||||
|
protected FEngineVersion _arEngineVer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the engine network protocol version.
|
||||||
|
/// </summary>
|
||||||
|
protected EEngineNetworkVersionHistory _arEngineNetVer;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Holds the game network protocol version.
|
||||||
|
/// </summary>
|
||||||
|
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<byte> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <param name="lengthBits"></param>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Packs int value into bytes of 7 bits with 8th bit for 'more'.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to do byte swapping on small items. This does not happen usually, so we don't want it inline
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="v"></param>
|
||||||
|
/// <param name="length"></param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the archive version number. Used by the code that makes sure that FLinkerLoad's
|
||||||
|
/// internal archive versions match the file reader it creates.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inVer">new version number</param>
|
||||||
|
public void SetUE4Ver(int inVer)
|
||||||
|
{
|
||||||
|
_arUE4Ver = inVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inVer">new version number</param>
|
||||||
|
public void SetLicenseeUE4Ver(int inVer)
|
||||||
|
{
|
||||||
|
_arLicenseeUE4Ver = inVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the archive engine version. Used by the code that makes sure that FLinkerLoad's
|
||||||
|
/// internal archive versions match the file reader it creates.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inVer">new version number</param>
|
||||||
|
public void SetEngineVer(FEngineVersion inVer)
|
||||||
|
{
|
||||||
|
_arEngineVer = inVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the archive engine network version.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inEngineNetVer"></param>
|
||||||
|
public void SetEngineNetVer(EEngineNetworkVersionHistory inEngineNetVer)
|
||||||
|
{
|
||||||
|
_arEngineNetVer = inEngineNetVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the archive game network version.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inGameNetVer"></param>
|
||||||
|
public void SetGameNetVer(uint inGameNetVer)
|
||||||
|
{
|
||||||
|
_arGameNetVer = inGameNetVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Toggle saving as Unicode. This is needed when we need to make sure ANSI strings are saved as Unicode
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enabled">set to true to force saving as Unicode</param>
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -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<byte>();
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FBitWriter>();
|
||||||
|
private static readonly ArrayPool<byte> Pool = ArrayPool<byte>.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Buffers;
|
||||||
|
|
||||||
|
namespace Prospect.Unreal.Serialization;
|
||||||
|
|
||||||
|
public class FMemoryWriter : FArchive
|
||||||
|
{
|
||||||
|
private static readonly ArrayPool<byte> Pool = ArrayPool<byte>.Create();
|
||||||
|
|
||||||
|
public FMemoryWriter()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user