diff --git a/src/Prospect.Unreal.Tests/Net/Packets/Header/FPackedHeaderTests.cs b/src/Prospect.Unreal.Tests/Net/Packets/Header/FPackedHeaderTests.cs
new file mode 100644
index 0000000..e62d99f
--- /dev/null
+++ b/src/Prospect.Unreal.Tests/Net/Packets/Header/FPackedHeaderTests.cs
@@ -0,0 +1,30 @@
+using System.Diagnostics.CodeAnalysis;
+using NUnit.Framework;
+using Prospect.Unreal.Net.Packets.Header;
+
+namespace Prospect.Unreal.Tests.Net.Packets.Header;
+
+[SuppressMessage("ReSharper", "HeapView.BoxingAllocation")]
+public class FPackedHeaderTests
+{
+ [Test]
+ [TestCase((uint)1221642592, (ushort)4660)]
+ public void TestGetSeq(uint input, ushort output)
+ {
+ Assert.AreEqual(output, FPackedHeader.GetSeq(input).Value);
+ }
+
+ [Test]
+ [TestCase((uint)1221642592, (ushort)3222)]
+ public void TestGetAckedSeq(uint input, ushort output)
+ {
+ Assert.AreEqual(output, FPackedHeader.GetAckedSeq(input).Value);
+ }
+
+ [Test]
+ [TestCase((uint)1221642592, (uint)0)]
+ public void TestGetHistoryWordCount(uint input, uint output)
+ {
+ Assert.AreEqual(output, FPackedHeader.GetHistoryWordCount(input));
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal.Tests/Prospect.Unreal.Tests.csproj b/src/Prospect.Unreal.Tests/Prospect.Unreal.Tests.csproj
new file mode 100644
index 0000000..d986c73
--- /dev/null
+++ b/src/Prospect.Unreal.Tests/Prospect.Unreal.Tests.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net6.0
+ enable
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Prospect.Unreal/Core/EEngineNetworkVersionHistory.cs b/src/Prospect.Unreal/Core/EEngineNetworkVersionHistory.cs
index 5efb285..e6515b5 100644
--- a/src/Prospect.Unreal/Core/EEngineNetworkVersionHistory.cs
+++ b/src/Prospect.Unreal/Core/EEngineNetworkVersionHistory.cs
@@ -1,6 +1,6 @@
namespace Prospect.Unreal.Core;
-public enum EEngineNetworkVersionHistory
+public enum EEngineNetworkVersionHistory : uint
{
HISTORY_INITIAL = 1,
HISTORY_REPLAY_BACKWARDS_COMPAT = 2, // Bump version to get rid of older replays before backwards compat was turned on officially
diff --git a/src/Prospect.Unreal/Core/FUrl.cs b/src/Prospect.Unreal/Core/FUrl.cs
index 132b0d8..dab6c3f 100644
--- a/src/Prospect.Unreal/Core/FUrl.cs
+++ b/src/Prospect.Unreal/Core/FUrl.cs
@@ -4,11 +4,11 @@ namespace Prospect.Unreal.Core;
public class FUrl
{
- public string Protocol { get; init; } = "unreal";
- public IPAddress Host { get; init; } = IPAddress.Any;
- public int Port { get; init; } = 7777;
- public string Map { get; init; } = "GearStart";
- public string RedirectUrl { get; init; } = string.Empty;
- public List Options { get; init; } = new List();
- public string Portal { get; init; } = string.Empty;
+ public string Protocol { get; set; } = "unreal";
+ public IPAddress Host { get; set; } = IPAddress.Any;
+ public int Port { get; set; } = 7777;
+ public string Map { get; set; } = "GearStart";
+ public string RedirectUrl { get; set; } = string.Empty;
+ public List Options { get; set; } = new List();
+ public string Portal { get; set; } = string.Empty;
}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Core/Names/FName.cs b/src/Prospect.Unreal/Core/Names/FName.cs
new file mode 100644
index 0000000..ee5e24d
--- /dev/null
+++ b/src/Prospect.Unreal/Core/Names/FName.cs
@@ -0,0 +1,36 @@
+namespace Prospect.Unreal.Core.Names;
+
+public class FName
+{
+ public FName()
+ {
+ NameIndex = -1; // ?
+ Number = -1; // ?
+ Str = "None";
+ }
+
+ public FName(string str)
+ {
+ NameIndex = -1;
+ Number = -1;
+ Str = str;
+ }
+
+ public FName(string str, int number)
+ {
+ NameIndex = -1;
+ Number = number;
+ Str = str;
+ }
+
+ public Int32 NameIndex { get; set; }
+
+ public Int32 Number { get; set; }
+
+ public string Str { get; set; }
+
+ public override string ToString()
+ {
+ return Str;
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Core/Names/UnrealNameKey.cs b/src/Prospect.Unreal/Core/Names/UnrealNameKey.cs
new file mode 100644
index 0000000..6ee4777
--- /dev/null
+++ b/src/Prospect.Unreal/Core/Names/UnrealNameKey.cs
@@ -0,0 +1,202 @@
+namespace Prospect.Unreal.Core.Names
+{
+ public enum UnrealNameKey
+ {
+ None = 0,
+
+ // Class property types (name indices are significant for serialization).
+ ByteProperty = 1,
+ IntProperty = 2,
+ BoolProperty = 3,
+ FloatProperty = 4,
+ ObjectProperty = 5, // ClassProperty shares the same tag
+ NameProperty = 6,
+ DelegateProperty = 7,
+ DoubleProperty = 8,
+ ArrayProperty = 9,
+ StructProperty = 10,
+ VectorProperty = 11,
+ RotatorProperty = 12,
+ StrProperty = 13,
+ TextProperty = 14,
+ InterfaceProperty = 15,
+ MulticastDelegateProperty = 16,
+ //Available = 17,
+ LazyObjectProperty = 18,
+ SoftObjectProperty = 19, // SoftClassProperty shares the same tag
+ UInt64Property = 20,
+ UInt32Property = 21,
+ UInt16Property = 22,
+ Int64Property = 23,
+ Int16Property = 25,
+ Int8Property = 26,
+ //Available = 27,
+ MapProperty = 28,
+ SetProperty = 29,
+
+ // Special packages.
+ Core = 30,
+ Engine = 31,
+ Editor = 32,
+ CoreUObject = 33,
+
+ // More class properties
+ EnumProperty = 34,
+
+ // Special types.
+ Cylinder = 50,
+ BoxSphereBounds = 51,
+ Sphere = 52,
+ Box = 53,
+ Vector2D = 54,
+ IntRect = 55,
+ IntPoint = 56,
+ Vector4 = 57,
+ Name = 58,
+ Vector = 59,
+ Rotator = 60,
+ SHVector = 61,
+ Color = 62,
+ Plane = 63,
+ Matrix = 64,
+ LinearColor = 65,
+ AdvanceFrame = 66,
+ Pointer = 67,
+ Double = 68,
+ Quat = 69,
+ Self = 70,
+ Transform = 71,
+
+ // Object class names.
+ Object = 100,
+ Camera = 101,
+ Actor = 102,
+ ObjectRedirector = 103,
+ ObjectArchetype = 104,
+ Class = 105,
+ ScriptStruct = 106,
+ Function = 107,
+
+ // Misc.
+ State = 200,
+ TRUE = 201,
+ FALSE = 202,
+ Enum = 203,
+ Default = 204,
+ Skip = 205,
+ Input = 206,
+ Package = 207,
+ Groups = 208,
+ Interface = 209,
+ Components = 210,
+ Global = 211,
+ Super = 212,
+ Outer = 213,
+ Map = 214,
+ Role = 215,
+ RemoteRole = 216,
+ PersistentLevel = 217,
+ TheWorld = 218,
+ PackageMetaData = 219,
+ InitialState = 220,
+ Game = 221,
+ SelectionColor = 222,
+ UI = 223,
+ ExecuteUbergraph = 224,
+ DeviceID = 225,
+ RootStat = 226,
+ MoveActor = 227,
+ All = 230,
+ MeshEmitterVertexColor = 231,
+ TextureOffsetParameter = 232,
+ TextureScaleParameter = 233,
+ ImpactVel = 234,
+ SlideVel = 235,
+ TextureOffset1Parameter = 236,
+ MeshEmitterDynamicParameter = 237,
+ ExpressionInput = 238,
+ Untitled = 239,
+ Timer = 240,
+ Team = 241,
+ Low = 242,
+ High = 243,
+ NetworkGUID = 244,
+ GameThread = 245,
+ RenderThread = 246,
+ OtherChildren = 247,
+ Location = 248,
+ Rotation = 249,
+ BSP = 250,
+ EditorSettings = 251,
+ AudioThread = 252,
+ ID = 253,
+ UserDefinedEnum = 254,
+ Control = 255,
+ Voice = 256,
+ Zlib = 257,
+ Gzip = 258,
+
+ // Online
+ DGram = 280,
+ Stream = 281,
+ GameNetDriver = 282,
+ PendingNetDriver = 283,
+ BeaconNetDriver = 284,
+ FlushNetDormancy = 285,
+ DemoNetDriver = 286,
+ GameSession = 287,
+ PartySession = 288,
+ GamePort = 289,
+ BeaconPort = 290,
+ MeshPort = 291,
+ MeshNetDriver = 292,
+
+ // Texture settings.
+ Linear = 300,
+ Point = 301,
+ Aniso = 302,
+ LightMapResolution = 303,
+
+ // Sound.
+ // = 310,
+ UnGrouped = 311,
+ VoiceChat = 312,
+
+ // Optimized replication.
+ Playing = 320,
+ Spectating = 322,
+ Inactive = 325,
+
+ // Log messages.
+ PerfWarning = 350,
+ Info = 351,
+ Init = 352,
+ Exit = 353,
+ Cmd = 354,
+ Warning = 355,
+ Error = 356,
+
+ // File format backwards-compatibility.
+ FontCharacter = 400,
+ InitChild2StartBone = 401,
+ SoundCueLocalized = 402,
+ SoundCue = 403,
+ RawDistributionFloat = 404,
+ RawDistributionVector = 405,
+ InterpCurveFloat = 406,
+ InterpCurveVector2D = 407,
+ InterpCurveVector = 408,
+ InterpCurveTwoVectors = 409,
+ InterpCurveQuat = 410,
+
+ AI = 450,
+ NavMesh = 451,
+
+ PerformanceCapture = 500,
+
+ // Special config names - not required to be consistent for network replication
+ EditorLayout = 600,
+ EditorKeyBindings = 601,
+ GameUserSettings = 602
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Core/Names/UnrealNames.cs b/src/Prospect.Unreal/Core/Names/UnrealNames.cs
new file mode 100644
index 0000000..22cac42
--- /dev/null
+++ b/src/Prospect.Unreal/Core/Names/UnrealNames.cs
@@ -0,0 +1,231 @@
+namespace Prospect.Unreal.Core.Names
+{
+ public static class UnrealNames
+ {
+ public const int MaxNetworkedHardcodedName = 410;
+
+ static UnrealNames()
+ {
+ // ReSharper disable once UseObjectOrCollectionInitializer
+ var names = new Dictionary();
+
+ // Special zero value, meaning no name.
+ names.Add(0, "None");
+
+ // Class property types (name indices are significant for serialization).
+ names.Add(1, "ByteProperty");
+ names.Add(2, "IntProperty");
+ names.Add(3, "BoolProperty");
+ names.Add(4, "FloatProperty");
+ names.Add(5, "ObjectProperty"); // ClassProperty shares the same tag
+ names.Add(6, "NameProperty");
+ names.Add(7, "DelegateProperty");
+ names.Add(8, "DoubleProperty");
+ names.Add(9, "ArrayProperty");
+ names.Add(10, "StructProperty");
+ names.Add(11, "VectorProperty");
+ names.Add(12, "RotatorProperty");
+ names.Add(13, "StrProperty");
+ names.Add(14, "TextProperty");
+ names.Add(15, "InterfaceProperty");
+ names.Add(16, "MulticastDelegateProperty");
+ //Names.Add(17, "Available");
+ names.Add(18, "LazyObjectProperty");
+ names.Add(19, "SoftObjectProperty"); // SoftClassProperty shares the same tag
+ names.Add(20, "UInt64Property");
+ names.Add(21, "UInt32Property");
+ names.Add(22, "UInt16Property");
+ names.Add(23, "Int64Property");
+ names.Add(25, "Int16Property");
+ names.Add(26, "Int8Property");
+ //Names.Add(27, "Available");
+ names.Add(28, "MapProperty");
+ names.Add(29, "SetProperty");
+
+ // Special packages.
+ names.Add(30, "Core");
+ names.Add(31, "Engine");
+ names.Add(32, "Editor");
+ names.Add(33, "CoreUObject");
+
+ // More class properties
+ names.Add(34, "EnumProperty");
+
+ // Special types.
+ names.Add(50, "Cylinder");
+ names.Add(51, "BoxSphereBounds");
+ names.Add(52, "Sphere");
+ names.Add(53, "Box");
+ names.Add(54, "Vector2D");
+ names.Add(55, "IntRect");
+ names.Add(56, "IntPoint");
+ names.Add(57, "Vector4");
+ names.Add(58, "Name");
+ names.Add(59, "Vector");
+ names.Add(60, "Rotator");
+ names.Add(61, "SHVector");
+ names.Add(62, "Color");
+ names.Add(63, "Plane");
+ names.Add(64, "Matrix");
+ names.Add(65, "LinearColor");
+ names.Add(66, "AdvanceFrame");
+ names.Add(67, "Pointer");
+ names.Add(68, "Double");
+ names.Add(69, "Quat");
+ names.Add(70, "Self");
+ names.Add(71, "Transform");
+
+ // Object class names.
+ names.Add(100, "Object");
+ names.Add(101, "Camera");
+ names.Add(102, "Actor");
+ names.Add(103, "ObjectRedirector");
+ names.Add(104, "ObjectArchetype");
+ names.Add(105, "Class");
+ names.Add(106, "ScriptStruct");
+ names.Add(107, "Function");
+ names.Add(108, "Pawn");
+
+ // Misc.
+ names.Add(200, "State");
+ names.Add(201, "TRUE");
+ names.Add(202, "FALSE");
+ names.Add(203, "Enum");
+ names.Add(204, "Default");
+ names.Add(205, "Skip");
+ names.Add(206, "Input");
+ names.Add(207, "Package");
+ names.Add(208, "Groups");
+ names.Add(209, "Interface");
+ names.Add(210, "Components");
+ names.Add(211, "Global");
+ names.Add(212, "Super");
+ names.Add(213, "Outer");
+ names.Add(214, "Map");
+ names.Add(215, "Role");
+ names.Add(216, "RemoteRole");
+ names.Add(217, "PersistentLevel");
+ names.Add(218, "TheWorld");
+ names.Add(219, "PackageMetaData");
+ names.Add(220, "InitialState");
+ names.Add(221, "Game");
+ names.Add(222, "SelectionColor");
+ names.Add(223, "UI");
+ names.Add(224, "ExecuteUbergraph");
+ names.Add(225, "DeviceID");
+ names.Add(226, "RootStat");
+ names.Add(227, "MoveActor");
+ names.Add(230, "All");
+ names.Add(231, "MeshEmitterVertexColor");
+ names.Add(232, "TextureOffsetParameter");
+ names.Add(233, "TextureScaleParameter");
+ names.Add(234, "ImpactVel");
+ names.Add(235, "SlideVel");
+ names.Add(236, "TextureOffset1Parameter");
+ names.Add(237, "MeshEmitterDynamicParameter");
+ names.Add(238, "ExpressionInput");
+ names.Add(239, "Untitled");
+ names.Add(240, "Timer");
+ names.Add(241, "Team");
+ names.Add(242, "Low");
+ names.Add(243, "High");
+ names.Add(244, "NetworkGUID");
+ names.Add(245, "GameThread");
+ names.Add(246, "RenderThread");
+ names.Add(247, "OtherChildren");
+ names.Add(248, "Location");
+ names.Add(249, "Rotation");
+ names.Add(250, "BSP");
+ names.Add(251, "EditorSettings");
+ names.Add(252, "AudioThread");
+ names.Add(253, "ID");
+ names.Add(254, "UserDefinedEnum");
+ names.Add(255, "Control");
+ names.Add(256, "Voice");
+ names.Add(257, " Zlib");
+ names.Add(258, " Gzip");
+ names.Add(259, " LZ4");
+ names.Add(260, " Mobile");
+
+ // Online
+ names.Add(280, "DGram");
+ names.Add(281, "Stream");
+ names.Add(282, "GameNetDriver");
+ names.Add(283, "PendingNetDriver");
+ names.Add(284, "BeaconNetDriver");
+ names.Add(285, "FlushNetDormancy");
+ names.Add(286, "DemoNetDriver");
+ names.Add(287, "GameSession");
+ names.Add(288, "PartySession");
+ names.Add(289, "GamePort");
+ names.Add(290, "BeaconPort");
+ names.Add(291, "MeshPort");
+ names.Add(292, "MeshNetDriver");
+ names.Add(293, "LiveStreamVoice");
+ names.Add(294, "LiveStreamAnimation");
+
+ // Texture settings.
+ names.Add(300, "Linear");
+ names.Add(301, "Point");
+ names.Add(302, "Aniso");
+ names.Add(303, "LightMapResolution");
+
+ // Sound.
+ //Names.Add(310, "");
+ names.Add(311, "UnGrouped");
+ names.Add(312, "VoiceChat");
+
+ // Optimized replication.
+ names.Add(320, "Playing");
+ names.Add(322, "Spectating");
+ names.Add(325, "Inactive");
+
+ // Log messages.
+ names.Add(350, "PerfWarning");
+ names.Add(351, "Info");
+ names.Add(352, "Init");
+ names.Add(353, "Exit");
+ names.Add(354, "Cmd");
+ names.Add(355, "Warning");
+ names.Add(356, "Error");
+
+ // File format backwards-compatibility.
+ names.Add(400, "FontCharacter");
+ names.Add(401, "InitChild2StartBone");
+ names.Add(402, "SoundCueLocalized");
+ names.Add(403, "SoundCue");
+ names.Add(404, "RawDistributionFloat");
+ names.Add(405, "RawDistributionVector");
+ names.Add(406, "InterpCurveFloat");
+ names.Add(407, "InterpCurveVector2D");
+ names.Add(408, "InterpCurveVector");
+ names.Add(409, "InterpCurveTwoVectors");
+ names.Add(410, "InterpCurveQuat");
+
+ names.Add(450, "AI");
+ names.Add(451, "NavMesh");
+
+ names.Add(500, "PerformanceCapture");
+
+ // Special config names - not required to be consistent for network replication
+ names.Add(600, "EditorLayout");
+ names.Add(601, "EditorKeyBindings");
+ names.Add(602, "GameUserSettings");
+
+ names.Add(700, "Filename");
+ names.Add(701, "Lerp");
+ names.Add(702, "Root");
+
+ // Save.
+ Names = names;
+ FNames = Names.ToDictionary(x => (UnrealNameKey) x.Key, y => new FName(y.Value, y.Key));
+ MaxHardcodedNameIndex = Names.Last().Key + 1;
+ }
+
+ public static IReadOnlyDictionary Names { get; }
+
+ public static IReadOnlyDictionary FNames { get; }
+
+ public static int MaxHardcodedNameIndex { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/Channels/EChannelCloseReason.cs b/src/Prospect.Unreal/Net/Channels/EChannelCloseReason.cs
new file mode 100644
index 0000000..4b02e76
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Channels/EChannelCloseReason.cs
@@ -0,0 +1,12 @@
+namespace Prospect.Unreal.Net.Channels;
+
+public enum EChannelCloseReason
+{
+ Destroyed,
+ Dormancy,
+ LevelUnloaded,
+ Relevancy,
+ TearOff,
+ /* reserved */
+ MAX = 15 // this value is used for serialization, modifying it may require a network version change
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/Channels/EChannelCreateFlags.cs b/src/Prospect.Unreal/Net/Channels/EChannelCreateFlags.cs
new file mode 100644
index 0000000..9c4c827
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Channels/EChannelCreateFlags.cs
@@ -0,0 +1,8 @@
+namespace Prospect.Unreal.Net.Channels;
+
+[Flags]
+public enum EChannelCreateFlags
+{
+ None = (1 << 0),
+ OpenedLocally = (1 << 1)
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/Channels/EChannelType.cs b/src/Prospect.Unreal/Net/Channels/EChannelType.cs
new file mode 100644
index 0000000..cc9f2a0
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Channels/EChannelType.cs
@@ -0,0 +1,13 @@
+namespace Prospect.Unreal.Net.Channels;
+
+public static class EChannelType
+{
+ public const int CHTYPE_None = 0; // Invalid type.
+ public const int CHTYPE_Control = 1; // Connection control.
+ public const int CHTYPE_Actor = 2; // Actor-update channel.
+
+ public const int CHTYPE_File = 3; // Binary file transfer.
+
+ public const int CHTYPE_Voice = 4; // VoIP data channel
+ public const int CHTYPE_MAX = 8; // Maximum.
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/CoreNet.cs b/src/Prospect.Unreal/Net/CoreNet.cs
deleted file mode 100644
index e197723..0000000
--- a/src/Prospect.Unreal/Net/CoreNet.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace Prospect.Unreal.Net;
-
-internal class CoreNet
-{
- public const int MaxPacketSize = 1024;
-}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/EClientLoginState.cs b/src/Prospect.Unreal/Net/EClientLoginState.cs
new file mode 100644
index 0000000..84920cc
--- /dev/null
+++ b/src/Prospect.Unreal/Net/EClientLoginState.cs
@@ -0,0 +1,10 @@
+namespace Prospect.Unreal.Net;
+
+public enum EClientLoginState
+{
+ Invalid = 0, // This must be a client (which doesn't use this state) or uninitialized.
+ LoggingIn = 1, // The client is currently logging in.
+ Welcomed = 2, // Told client to load map and will respond with SendJoin
+ ReceivedJoin = 3, // NMT_Join received and a player controller has been created
+ CleanedUp = 4 // Cleanup has been called at least once, the connection is considered abandoned/terminated/gone
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/ECountUnits.cs b/src/Prospect.Unreal/Net/ECountUnits.cs
new file mode 100644
index 0000000..bdfa149
--- /dev/null
+++ b/src/Prospect.Unreal/Net/ECountUnits.cs
@@ -0,0 +1,7 @@
+namespace Prospect.Unreal.Net;
+
+public enum ECountUnits
+{
+ Bits,
+ Bytes
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/FChannelDefinition.cs b/src/Prospect.Unreal/Net/FChannelDefinition.cs
new file mode 100644
index 0000000..d3b35ef
--- /dev/null
+++ b/src/Prospect.Unreal/Net/FChannelDefinition.cs
@@ -0,0 +1,3 @@
+namespace Prospect.Unreal.Net;
+
+public record FChannelDefinition(string Name, Type Class, int StaticChannelIndex, bool TickOnCreate, bool ServerOpen, bool ClientOpen, bool InitialServer, bool InitialClient);
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/FNetGUIDCache.cs b/src/Prospect.Unreal/Net/FNetGUIDCache.cs
new file mode 100644
index 0000000..8e506e3
--- /dev/null
+++ b/src/Prospect.Unreal/Net/FNetGUIDCache.cs
@@ -0,0 +1,11 @@
+namespace Prospect.Unreal.Net;
+
+public class FNetGUIDCache
+{
+ public FNetGUIDCache(UNetDriver driver)
+ {
+ Driver = driver;
+ }
+
+ public UNetDriver Driver { get; }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/FReceiveThreadRunnable.cs b/src/Prospect.Unreal/Net/FReceiveThreadRunnable.cs
index 4f0e472..96800c4 100644
--- a/src/Prospect.Unreal/Net/FReceiveThreadRunnable.cs
+++ b/src/Prospect.Unreal/Net/FReceiveThreadRunnable.cs
@@ -34,7 +34,7 @@ public class FReceiveThreadRunnable : IAsyncDisposable
if (_receiveQueue.TryDequeue(out var packet))
{
result = new FReceivedPacketView(
- new FPacketDataView(packet.Buffer, packet.Buffer.Length),
+ new FPacketDataView(packet.Buffer, packet.Buffer.Length, ECountUnits.Bytes),
packet.Address,
new FInPacketTraits());
@@ -58,10 +58,10 @@ public class FReceiveThreadRunnable : IAsyncDisposable
continue;
}
- if (result.Buffer.Length > CoreNet.MaxPacketSize)
+ if (result.Buffer.Length > UNetConnection.MaxPacketSize)
{
Logger.Warning("Received packet exceeding MaxPacketSize ({Size} > {Max}) from {Ip}",
- result.Buffer.Length, CoreNet.MaxPacketSize, result.RemoteEndPoint);
+ result.Buffer.Length, UNetConnection.MaxPacketSize, result.RemoteEndPoint);
continue;
}
diff --git a/src/Prospect.Unreal/Net/PacketHandler.cs b/src/Prospect.Unreal/Net/PacketHandler.cs
index 01d72c7..eae62eb 100644
--- a/src/Prospect.Unreal/Net/PacketHandler.cs
+++ b/src/Prospect.Unreal/Net/PacketHandler.cs
@@ -50,7 +50,11 @@ public class PacketHandler
if (!_bConnectionlessHandler)
{
- throw new NotImplementedException();
+ // TODO: Load from .ini file (GEngineIni)
+ // %s PacketHandlerProfileConfig (Driver..)
+ // Components
+
+ // If no matches, load from PacketHandlerComponents / Components in GEngineIni
}
// TODO: FEncryptionComponent.
@@ -93,6 +97,19 @@ public class PacketHandler
return Outgoing_Internal(packet, countBits, traits, true, address);
}
+ public void BeginHandshaking()
+ {
+ // bBeganHandshaking = true;
+
+ foreach (var component in _handlerComponents)
+ {
+ if (component.RequiresHandshake() && !component.IsInitialized())
+ {
+ component.NotifiyHandshakeBegin();
+ }
+ }
+ }
+
public HandlerComponent AddHandler() where T : HandlerComponent
{
var result = (HandlerComponent) Activator.CreateInstance(typeof(T), this)!;
@@ -102,6 +119,16 @@ public class PacketHandler
return result;
}
+ public bool Incoming(FReceivedPacketView packetView)
+ {
+ return Incoming_Internal(packetView);
+ }
+
+ public void IncomingHigh(FBitReader reader)
+ {
+ // NO-OP
+ }
+
private bool Incoming_Internal(FReceivedPacketView packetView)
{
var returnVal = true;
@@ -159,7 +186,7 @@ public class PacketHandler
{
ReplaceIncomingPacket(processPacketReader);
- packetView.DataView = new FPacketDataView(_incomingPacket.GetBuffer(), _incomingPacket.GetBitsLeft());
+ packetView.DataView = new FPacketDataView(_incomingPacket.GetBuffer(), _incomingPacket.GetBitsLeft(), ECountUnits.Bits);
}
else
{
diff --git a/src/Prospect.Unreal/Net/PacketView.cs b/src/Prospect.Unreal/Net/PacketView.cs
index 3b02cd5..9a0a19c 100644
--- a/src/Prospect.Unreal/Net/PacketView.cs
+++ b/src/Prospect.Unreal/Net/PacketView.cs
@@ -1,4 +1,5 @@
using System.Net;
+using Prospect.Unreal.Core;
namespace Prospect.Unreal.Net;
@@ -8,15 +9,26 @@ public class FInPacketTraits
public bool FromRecentlyDisconnected { get; set; }
}
-public class FPacketDataView
+public readonly struct FPacketDataView
{
private readonly byte[] _data;
+ private readonly int _count;
private readonly int _countBits;
- public FPacketDataView(byte[] data, int length)
+ public FPacketDataView(byte[] data, int length, ECountUnits unit)
{
_data = data;
- _countBits = length * 8;
+
+ if (unit == ECountUnits.Bits)
+ {
+ _count = FMath.DivideAndRoundUp(length, 8);
+ _countBits = length;
+ }
+ else /* if (unit == ECountUnits.Bytes) */
+ {
+ _count = length;
+ _countBits = length * 8;
+ }
}
public byte[] GetData()
@@ -31,7 +43,7 @@ public class FPacketDataView
public int NumBytes()
{
- return _data.Length;
+ return _count;
}
}
diff --git a/src/Prospect.Unreal/Net/Packets/Bunch/FInBunch.cs b/src/Prospect.Unreal/Net/Packets/Bunch/FInBunch.cs
new file mode 100644
index 0000000..6a3ee3d
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Packets/Bunch/FInBunch.cs
@@ -0,0 +1,95 @@
+using Prospect.Unreal.Core.Names;
+using Prospect.Unreal.Net.Channels;
+using Prospect.Unreal.Serialization;
+
+namespace Prospect.Unreal.Net.Packets.Bunch
+{
+ public class FInBunch : FNetBitReader
+ {
+ public FInBunch(UNetConnection inConnection, byte[]? src = null, int countBits = 0) : base(inConnection.PackageMap, src, countBits)
+ {
+ PacketId = 0;
+ Next = null;
+ Connection = inConnection;
+ ChIndex = 0;
+ ChType = 0; // TODO: CHTYPE_None
+ ChName = UnrealNames.FNames[UnrealNameKey.None]; // TODO: NAME_None
+ ChSequence = 0;
+ bOpen = false;
+ bClose = false;
+ bDormant = false;
+ bReliable = false;
+ bPartial = false;
+ bPartialInitial = false;
+ bPartialFinal = false;
+ bHasPackageMapExports = false;
+ bHasMustBeMappedGUIDs = false;
+ bIgnoreRPCs = false;
+ CloseReason = EChannelCloseReason.Destroyed;
+
+ // TODO: SetByteSwapping(Connection->bNeedsByteSwapping);
+
+ SetEngineNetVer(Connection.EngineNetworkProtocolVersion);
+ SetGameNetVer(Connection.GameNetworkProtocolVersion);
+ }
+
+ public int PacketId { get; set; }
+
+ public FInBunch? Next { get; set; }
+
+ public UNetConnection Connection { get; }
+
+ public int ChIndex { get; set; }
+
+ public int ChType { get; set; }
+
+ public FName ChName { get; set; }
+
+ public int ChSequence { get; set; }
+
+ public bool bOpen { get; set; }
+
+ public bool bClose { get; set; }
+
+ ///
+ /// Close, but go dormant.
+ ///
+ public bool bDormant { get; set; }
+
+ ///
+ /// Replication on this channel is being paused by the server.
+ ///
+ public bool bIsReplicationPaused { get; set; }
+
+ public bool bReliable { get; set; }
+
+ ///
+ /// Not a complete bunch
+ ///
+ public bool bPartial { get; set; }
+
+ ///
+ /// The first bunch of a partial bunch
+ ///
+ public bool bPartialInitial { get; set; }
+
+ ///
+ /// The final bunch of a partial bunch
+ ///
+ public bool bPartialFinal { get; set; }
+
+ ///
+ /// This bunch has networkGUID name/id pairs.
+ ///
+ public bool bHasPackageMapExports { get; set; }
+
+ ///
+ /// This bunch has guids that must be mapped before we can process this bunch.
+ ///
+ public bool bHasMustBeMappedGUIDs { get; set; }
+
+ public bool bIgnoreRPCs { get; set; }
+
+ public EChannelCloseReason CloseReason { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs b/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs
new file mode 100644
index 0000000..0dfe9cd
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs
@@ -0,0 +1,146 @@
+using Prospect.Unreal.Net.Packets.Header.Sequence;
+using Prospect.Unreal.Serialization;
+using Serilog;
+
+namespace Prospect.Unreal.Net.Packets.Header;
+
+public delegate void HandlePacketNotification(SequenceNumber ackedSequence, bool delivered);
+
+public class FNetPacketNotify
+{
+ private static readonly ILogger Logger = Log.ForContext();
+
+ public const int SequenceNumberBits = 14;
+ public const int MaxSequenceHistoryLength = 256;
+
+ private readonly Queue _ackRecord = new Queue();
+
+ private readonly SequenceHistory _inSeqHistory = new SequenceHistory();
+ private SequenceNumber _inSeq;
+ private SequenceNumber _inAckSeq;
+ private SequenceNumber _inAckSeqAck;
+
+ private SequenceNumber _outSeq;
+ private SequenceNumber _outAckSeq;
+
+ public void Init(SequenceNumber initialInSeq, SequenceNumber initialOutSeq)
+ {
+ _inSeqHistory.Reset();
+ _inSeq = initialInSeq;
+ _inAckSeq = initialInSeq;
+ _inAckSeqAck = initialInSeq;
+ _outSeq = initialOutSeq;
+ _outAckSeq = new SequenceNumber((ushort)(initialOutSeq.Value - 1));
+ }
+
+ public bool ReadHeader(ref FNotificationHeader data, FBitReader reader)
+ {
+ var packedHeader = reader.ReadUInt32();
+
+ data.Seq = FPackedHeader.GetSeq(packedHeader);
+ data.AckedSeq = FPackedHeader.GetAckedSeq(packedHeader);
+ data.HistoryWordCount = FPackedHeader.GetHistoryWordCount(packedHeader) + 1;
+ data.History = new SequenceHistory();
+ data.History.Read(reader, data.HistoryWordCount);
+
+ return !reader.IsError();
+ }
+
+ public int GetSequenceDelta(FNotificationHeader notificationData)
+ {
+ if (!notificationData.Seq.Greater(_inSeq))
+ {
+ return 0;
+ }
+
+ if (!notificationData.AckedSeq.GreaterEq(_outAckSeq))
+ {
+ return 0;
+ }
+
+ if (!_outSeq.Greater(notificationData.AckedSeq))
+ {
+ return 0;
+ }
+
+ return SequenceNumber.Diff(notificationData.Seq, _inSeq);
+ }
+
+ public int Update(FNotificationHeader notificationData, HandlePacketNotification func)
+ {
+ var inSeqDelta = GetSequenceDelta(notificationData);
+ if (inSeqDelta > 0)
+ {
+ Logger.Verbose("FNetPacketNotify::Update - Seq {Seq}, InSeq {InSeq}", notificationData.Seq.Value, _inSeq.Value);
+
+ ProcessReceivedAcks(notificationData, func);
+
+ _inSeq = notificationData.Seq;
+
+ return inSeqDelta;
+ }
+
+ return 0;
+ }
+
+ private void ProcessReceivedAcks(FNotificationHeader notificationData, HandlePacketNotification func)
+ {
+ if (notificationData.AckedSeq.Greater(_outAckSeq))
+ {
+ Logger.Verbose("ProcessReceivedAcks - AckedSeq {AckedSeq}, OutAckSeq {OutAckSeq}", notificationData.AckedSeq.Value, _outAckSeq.Value);
+
+ var ackCount = SequenceNumber.Diff(notificationData.AckedSeq, _outAckSeq);
+
+ // Update InAckSeqAck used to track the needed number of bits to transmit our ack history
+ _inAckSeqAck = UpdateInAckSeqAck(ackCount, notificationData.AckedSeq);
+
+ // ExpectedAck = OutAckSeq + 1
+ var currentAck = new SequenceNumber(_outAckSeq.Value).IncrementAndGet();
+
+ if (ackCount > SequenceHistory.Size)
+ {
+ Logger.Warning("ProcessReceivedAcks - Missed Acks");
+ }
+
+ // Everything not found in the history buffer is treated as lost
+ while (ackCount > SequenceHistory.Size)
+ {
+ --ackCount;
+ func(currentAck, false);
+ currentAck = currentAck.IncrementAndGet();
+ }
+
+ // For sequence numbers contained in the history we lookup the delivery status from the history
+ while (ackCount > 0)
+ {
+ --ackCount;
+ func(currentAck, notificationData.History.IsDelivered(ackCount));
+ currentAck = currentAck.IncrementAndGet();
+ }
+
+ _outAckSeq = notificationData.AckedSeq;
+ }
+ }
+
+ private SequenceNumber UpdateInAckSeqAck(int ackCount, SequenceNumber ackedSeq)
+ {
+ if (ackCount <= _ackRecord.Count)
+ {
+ if (ackCount > 1)
+ {
+ for (var i = 0; i < ackCount - 1; i++)
+ {
+ _ackRecord.Dequeue();
+ }
+ }
+
+ var ackData = _ackRecord.Dequeue();
+ if (ackData.OutSeq.Equals(ackedSeq))
+ {
+ return ackData.InAckSeq;
+ }
+ }
+
+ return new SequenceNumber((ushort)(ackedSeq.Value - MaxSequenceHistoryLength));
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/Packets/Header/FNotificationHeader.cs b/src/Prospect.Unreal/Net/Packets/Header/FNotificationHeader.cs
new file mode 100644
index 0000000..b7e97d8
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Packets/Header/FNotificationHeader.cs
@@ -0,0 +1,11 @@
+using Prospect.Unreal.Net.Packets.Header.Sequence;
+
+namespace Prospect.Unreal.Net.Packets.Header;
+
+public ref struct FNotificationHeader
+{
+ public SequenceHistory History;
+ public uint HistoryWordCount;
+ public SequenceNumber Seq;
+ public SequenceNumber AckedSeq;
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/Packets/Header/FPackedHeader.cs b/src/Prospect.Unreal/Net/Packets/Header/FPackedHeader.cs
new file mode 100644
index 0000000..c1da7ce
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Packets/Header/FPackedHeader.cs
@@ -0,0 +1,27 @@
+using Prospect.Unreal.Net.Packets.Header.Sequence;
+
+namespace Prospect.Unreal.Net.Packets.Header;
+
+public class FPackedHeader
+{
+ public const int HistoryWordCountBits = 4;
+ public const int SeqMask = (1 << FNetPacketNotify.SequenceNumberBits) - 1;
+ public const int HistoryWordCountMask = (1 << HistoryWordCountBits) - 1;
+ public const int AckSeqShift = HistoryWordCountBits;
+ public const int SeqShift = AckSeqShift + FNetPacketNotify.SequenceNumberBits;
+
+ public static SequenceNumber GetSeq(uint packed)
+ {
+ return new SequenceNumber((ushort)(packed >> SeqShift & SeqMask));
+ }
+
+ public static SequenceNumber GetAckedSeq(uint packed)
+ {
+ return new SequenceNumber((ushort)(packed >> AckSeqShift & SeqMask));
+ }
+
+ public static uint GetHistoryWordCount(uint packed)
+ {
+ return packed & HistoryWordCountMask;
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/Packets/Header/Sequence/FSentAckData.cs b/src/Prospect.Unreal/Net/Packets/Header/Sequence/FSentAckData.cs
new file mode 100644
index 0000000..b3aabf8
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Packets/Header/Sequence/FSentAckData.cs
@@ -0,0 +1,13 @@
+namespace Prospect.Unreal.Net.Packets.Header.Sequence;
+
+public readonly struct FSentAckData
+{
+ public FSentAckData(SequenceNumber outSeq, SequenceNumber inAckSeq)
+ {
+ OutSeq = outSeq;
+ InAckSeq = inAckSeq;
+ }
+
+ public SequenceNumber OutSeq { get; }
+ public SequenceNumber InAckSeq { get; }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs b/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs
new file mode 100644
index 0000000..517dee5
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs
@@ -0,0 +1,46 @@
+using Prospect.Unreal.Serialization;
+
+namespace Prospect.Unreal.Net.Packets.Header.Sequence;
+
+public readonly struct SequenceHistory
+{
+ // Hardcoded values for FNetPacketNotify
+ public const uint HistorySize = FNetPacketNotify.MaxSequenceHistoryLength;
+
+ public const uint BitsPerWord = sizeof(uint) * 8;
+ public const uint WordCount = HistorySize / BitsPerWord;
+ public const uint MaxSizeInBits = WordCount * BitsPerWord;
+ public const uint Size = HistorySize;
+
+ private readonly uint[] _storage;
+
+ public SequenceHistory()
+ {
+ _storage = new uint[WordCount];
+ }
+
+ public void Read(FBitReader reader, uint numWords)
+ {
+ numWords = Math.Min(numWords, WordCount);
+ for (var i = 0; i < numWords; i++)
+ {
+ _storage[i] = reader.ReadUInt32();
+ }
+ }
+
+ public void Reset()
+ {
+ for (var i = 0; i < _storage.Length; i++)
+ {
+ _storage[i] = 0;
+ }
+ }
+
+ public bool IsDelivered(int index)
+ {
+ var wordIndex = (int)(index / BitsPerWord);
+ var wordMask = 1 << (int)(index & (BitsPerWord - 1));
+
+ return (_storage[wordIndex] & wordMask) != 0;
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceNumber.cs b/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceNumber.cs
new file mode 100644
index 0000000..7265ed7
--- /dev/null
+++ b/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceNumber.cs
@@ -0,0 +1,70 @@
+namespace Prospect.Unreal.Net.Packets.Header.Sequence;
+
+public readonly struct SequenceNumber
+{
+ // Hardcoded values for FNetPacketNotify
+ public const int NumBits = FNetPacketNotify.SequenceNumberBits;
+
+ public const int SeqNumberBits = NumBits;
+ public const ushort SeqNumberCount = 1 << NumBits;
+ public const ushort SeqNumberHalf = 1 << (NumBits - 1);
+ public const ushort SeqNumberMax = SeqNumberCount - 1;
+ public const ushort SeqNumberMask = SeqNumberMax;
+
+ public SequenceNumber(ushort value)
+ {
+ Value = (ushort)(value & SeqNumberMask);
+ }
+
+ public ushort Value { get; }
+
+ public bool Greater(SequenceNumber other)
+ {
+ return (Value != other.Value) && (((Value - other.Value) & SeqNumberMask) < SeqNumberHalf);
+ }
+
+ public bool GreaterEq(SequenceNumber other)
+ {
+ return ((Value - other.Value) & SeqNumberMask) < SeqNumberHalf;
+ }
+
+ public bool Equals(SequenceNumber other)
+ {
+ return Value == other.Value;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is SequenceNumber other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ return Value.GetHashCode();
+ }
+
+ public static int Diff(SequenceNumber a, SequenceNumber b)
+ {
+ const int shiftValue = sizeof(int) * 8 - NumBits;
+
+ var valueA = a.Value;
+ var valueB = b.Value;
+
+ return ((valueA - valueB) << shiftValue) >> shiftValue;
+ }
+
+ public SequenceNumber IncrementAndGet()
+ {
+ return new SequenceNumber((ushort)(Value + 1));
+ }
+
+ public readonly struct Difference
+ {
+ public Difference(int value)
+ {
+ Value = value;
+ }
+
+ public int Value { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs b/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs
index cf2e4e9..cd20e2a 100644
--- a/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs
+++ b/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs
@@ -153,7 +153,53 @@ public class StatelessConnectHandlerComponent : HandlerComponent
public override void Incoming(FBitReader packet)
{
- base.Incoming(packet);
+ if (_magicHeader.Length > 0)
+ {
+ // Skip magic header.
+ packet.Pos += _magicHeader.Length;
+ }
+
+ var bHandshakePacket = packet.ReadBit() && !packet.IsError();
+ if (bHandshakePacket)
+ {
+ var bRestartHandshake = false;
+ var secretId = (byte) 0;
+ var timestamp = 1.0d;
+ Span cookie = stackalloc byte[CookieByteSize];
+ Span origCookie = stackalloc byte[CookieByteSize];
+
+ bHandshakePacket = ParseHandshakePacket(packet, ref bRestartHandshake, ref secretId, ref timestamp, cookie, origCookie);
+
+ if (bHandshakePacket)
+ {
+ if (Handler.Mode == HandlerMode.Client)
+ {
+ throw new NotSupportedException();
+ }
+ else if (Handler.Mode == HandlerMode.Server)
+ {
+ if (_lastChallengeSuccessAddress != null)
+ {
+ // The server should not be receiving handshake packets at this stage - resend the ack in case it was lost.
+ // In this codepath, this component is linked to a UNetConnection, and the Last* values below, cache the handshake info.
+ SendChallengeAck(_lastChallengeSuccessAddress, _authorisedCookie);
+ }
+ }
+ }
+ else
+ {
+ packet.SetError();
+ Logger.Error("Incoming: Error reading handshake packet");
+ }
+ }
+ else if (packet.IsError())
+ {
+ Logger.Error("Incoming: Error reading handshake bit from packet");
+ }
+ else if (_lastChallengeSuccessAddress != null && Handler.Mode == HandlerMode.Server)
+ {
+ _lastChallengeSuccessAddress = null;
+ }
}
public override void Outgoing(FBitWriter packet, FOutPacketTraits traits)
@@ -181,14 +227,18 @@ public class StatelessConnectHandlerComponent : HandlerComponent
var bRestartHandshake = false;
var secretId = (byte) 0;
var timestamp = 1.0d;
- var cookie = new byte[CookieByteSize];
- var origCookie = new byte[CookieByteSize];
+ Span cookie = stackalloc byte[CookieByteSize];
+ Span origCookie = stackalloc byte[CookieByteSize];
bHandshakePacket = ParseHandshakePacket(packet, ref bRestartHandshake, ref secretId, ref timestamp, cookie, origCookie);
if (bHandshakePacket)
{
- if (Handler.Mode == HandlerMode.Server)
+ if (Handler.Mode == HandlerMode.Client)
+ {
+ throw new NotSupportedException();
+ }
+ else if (Handler.Mode == HandlerMode.Server)
{
var bInitialConnect = timestamp == 0.0;
if (bInitialConnect)
@@ -206,7 +256,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
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];
+ Span regenCookie = stackalloc byte[CookieByteSize];
GenerateCookie(address, secretId, timestamp, regenCookie);
@@ -216,16 +266,17 @@ public class StatelessConnectHandlerComponent : HandlerComponent
{
if (bRestartHandshake)
{
- Buffer.BlockCopy(origCookie, 0, _authorisedCookie, 0, _authorisedCookie.Length);
+ origCookie.CopyTo(_authorisedCookie);
}
else
{
- var curSequence = BinaryPrimitives.ReadInt16LittleEndian(cookie);
+ var seqA = BinaryPrimitives.ReadInt16LittleEndian(cookie);
+ var seqB = BinaryPrimitives.ReadInt16LittleEndian(cookie.Slice(2));
- _lastServerSequence = curSequence & (UNetConnection.MaxPacketId - 1);
- _lastClientSequence = (curSequence + 1) & (UNetConnection.MaxPacketId - 1);
+ _lastServerSequence = seqA & (UNetConnection.MaxPacketId - 1);
+ _lastClientSequence = seqB & (UNetConnection.MaxPacketId - 1);
- Buffer.BlockCopy(cookie, 0, _authorisedCookie, 0, _authorisedCookie.Length);
+ cookie.CopyTo(_authorisedCookie);
}
_bRestartedHandshake = bRestartHandshake;
@@ -262,7 +313,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
FBitReader packet,
ref bool bOutRestartHandshake,
ref byte outSecretId,
- ref double outTimestamp, byte[] outCookie, byte[] outOrigCookie)
+ ref double outTimestamp, Span outCookie, Span outOrigCookie)
{
var bValidPacket = false;
var bitsLeft = packet.GetBitsLeft();
@@ -314,7 +365,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
var bHandshakePacket = (byte)1;
var bRestartHandshake = (byte)0;
var timestamp = _driver.GetElapsedTime();
- var cookie = new byte[CookieByteSize];
+ Span cookie = stackalloc byte[CookieByteSize];
GenerateCookie(address, _activeSecret, timestamp, cookie);
@@ -329,7 +380,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
challengePacket.WriteDouble(timestamp);
challengePacket.Serialize(cookie, cookie.Length);
- Logger.Verbose("SendConnectChallenge. Timestamp: {Timestamp}, Cookie: {Cookie}", timestamp, cookie);
+ Logger.Verbose("SendConnectChallenge. Timestamp: {Timestamp}, Cookie: {Cookie}", timestamp, Convert.ToHexString(cookie));
CapHandshakePacket(challengePacket);
diff --git a/src/Prospect.Unreal/Net/UChannel.cs b/src/Prospect.Unreal/Net/UChannel.cs
new file mode 100644
index 0000000..6835a06
--- /dev/null
+++ b/src/Prospect.Unreal/Net/UChannel.cs
@@ -0,0 +1,8 @@
+using Prospect.Unreal.Core.Names;
+
+namespace Prospect.Unreal.Net;
+
+public class UChannel
+{
+ public FName ChName { get; }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/UIpConnection.cs b/src/Prospect.Unreal/Net/UIpConnection.cs
index 5a9006a..cfa16ea 100644
--- a/src/Prospect.Unreal/Net/UIpConnection.cs
+++ b/src/Prospect.Unreal/Net/UIpConnection.cs
@@ -6,20 +6,46 @@ namespace Prospect.Unreal.Net;
public class UIpConnection : UNetConnection
{
+ public const int IpHeaderSize = 20;
+ public const int UdpHeaderSize = IpHeaderSize + 8;
+
+ ///
+ /// Cached time of the first send socket error that will be used to compute disconnect delay.
+ ///
+ private double _socketErrorSendDelayStartTime;
+
+ ///
+ /// Cached time of the first recv socket error that will be used to compute disconnect delay.
+ ///
+ private double _socketErrorRecvDelayStartTime;
+
+ public UdpClient? Socket { get; private set; }
+
public override void InitBase(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0)
{
- throw new NotImplementedException();
+ base.InitBase(inDriver, inSocket, inURL, inState,
+ (inMaxPacket == 0 || inMaxPacket > MaxPacketSize) ? MaxPacketSize : inMaxPacket,
+ inPacketOverhead == 0 ? UdpHeaderSize : inPacketOverhead);
+
+ Socket = inSocket;
}
public override void InitRemoteConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, IPEndPoint inRemoteAddr, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0)
{
- throw new NotImplementedException();
+ InitBase(inDriver, inSocket, inURL, inState,
+ (inMaxPacket == 0 || inMaxPacket > MaxPacketSize) ? MaxPacketSize : inMaxPacket,
+ inPacketOverhead == 0 ? UdpHeaderSize : inPacketOverhead);
+
+ RemoteAddr = inRemoteAddr;
+ Url.Host = RemoteAddr.Address;
+
+ SetClientLoginState(EClientLoginState.LoggingIn);
+ SetExpectedClientLoginMsgType(0); // TODO: NMT_HELLO
}
- public override void InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState,
- int inMaxPacket = 0, int inPacketOverhead = 0)
+ public override void InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0)
{
- throw new NotImplementedException();
+ throw new NotSupportedException();
}
public override void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits)
@@ -47,9 +73,11 @@ public class UIpConnection : UNetConnection
throw new NotImplementedException();
}
- public override void ReceivedRawPacket(byte[] data, int count)
+ public override void ReceivedRawPacket(FReceivedPacketView packetView)
{
- throw new NotImplementedException();
+ _socketErrorRecvDelayStartTime = 0;
+
+ base.ReceivedRawPacket(packetView);
}
public override float GetTimeoutValue()
diff --git a/src/Prospect.Unreal/Net/UIpNetDriver.cs b/src/Prospect.Unreal/Net/UIpNetDriver.cs
index 257fcde..1549330 100644
--- a/src/Prospect.Unreal/Net/UIpNetDriver.cs
+++ b/src/Prospect.Unreal/Net/UIpNetDriver.cs
@@ -53,19 +53,27 @@ public class UIpNetDriver : UNetDriver
{
MappedClientConnections.TryGetValue(packet.Address, out connection);
}
+
+ var bIgnorePacket = false;
// If we didn't find a client connection, maybe create a new one.
if (connection == null)
{
connection = ProcessConnectionlessPacket(packet);
- // TODO: bIgnorePacket
+ bIgnorePacket = packet.DataView.NumBytes() == 0;
+ }
+
+ // Send the packet to the connection for processing.
+ if (connection != null && !bIgnorePacket)
+ {
+ connection.ReceivedRawPacket(packet);
}
}
}
private UNetConnection? ProcessConnectionlessPacket(FReceivedPacketView packet)
{
- UNetConnection? returnVal;
+ UNetConnection? returnVal = null;
var statelessConnect = StatelessConnectComponent;
var address = packet.Address;
var bPassedChallenge = false;
@@ -95,7 +103,7 @@ public class UIpNetDriver : UNetDriver
bIgnorePacket = false;
}
- packet.DataView = new FPacketDataView(workingData, newCountBytes);
+ packet.DataView = new FPacketDataView(workingData, newCountBytes, ECountUnits.Bytes);
}
}
}
@@ -120,8 +128,13 @@ public class UIpNetDriver : UNetDriver
}
returnVal.InitRemoteConnection(this, Socket, World != null ? World.Url : new FUrl(), address, EConnectionState.USOCK_Open);
-
- // if (returnVal.Handler)
+
+ if (returnVal.Handler != null)
+ {
+ returnVal.Handler.BeginHandshaking();
+ }
+
+ AddClientConnection(returnVal);
}
if (statelessConnect != null)
@@ -132,10 +145,10 @@ public class UIpNetDriver : UNetDriver
if (bIgnorePacket)
{
- packet.DataView = new FPacketDataView(packet.DataView.GetData(), 0);
+ packet.DataView = new FPacketDataView(packet.DataView.GetData(), 0, ECountUnits.Bits);
}
- return null;
+ return returnVal;
}
public override void LowLevelSend(IPEndPoint address, byte[] data, int countBits, FOutPacketTraits traits)
diff --git a/src/Prospect.Unreal/Net/UNetConnection.cs b/src/Prospect.Unreal/Net/UNetConnection.cs
index e679d6d..0ce4b67 100644
--- a/src/Prospect.Unreal/Net/UNetConnection.cs
+++ b/src/Prospect.Unreal/Net/UNetConnection.cs
@@ -1,15 +1,193 @@
using System.Net;
using System.Net.Sockets;
using Prospect.Unreal.Core;
+using Prospect.Unreal.Core.Names;
+using Prospect.Unreal.Exceptions;
+using Prospect.Unreal.Net.Channels;
+using Prospect.Unreal.Net.Packets.Bunch;
+using Prospect.Unreal.Net.Packets.Header;
+using Prospect.Unreal.Net.Packets.Header.Sequence;
+using Prospect.Unreal.Serialization;
+using Serilog;
namespace Prospect.Unreal.Net;
public abstract class UNetConnection
{
+ private static readonly ILogger Logger = Log.ForContext();
+
public const int ReliableBuffer = 256;
public const int MaxPacketId = 16384;
+ public const int MaxChSequence = 1024;
+ public const int MaxBunchHeaderBits = 256;
+ public const int MaxPacketSize = 1024;
+
+ public const int DefaultMaxChannelSize = 32767;
+
+ public const int MaxJitterClockTimeValue = 1023;
+ public const int NumBitsForJitterClockTimeInHeader = 10;
+
+ public const EEngineNetworkVersionHistory DefaultEngineNetworkProtocolVersion = EEngineNetworkVersionHistory.HISTORY_ENGINENETVERSION_LATEST;
+ public const uint DefaultGameNetworkProtocolVersion = 0;
+
+ private List? _packetOrderCache;
+ private int _packetOrderCacheStartIdx;
+ private int _packetOrderCacheCount;
+ private bool _bFlushingPacketOrderCache;
+
+ private bool _bInternalAck;
+ private bool _bReplay;
+
+ public UNetConnection()
+ {
+ _packetOrderCache = null;
+ _packetOrderCacheStartIdx = 0;
+ _packetOrderCacheCount = 0;
+ _bFlushingPacketOrderCache = false;
+ _bInternalAck = false;
+ _bReplay = false;
+
+ Driver = null;
+ PackageMap = null;
+ MaxPacket = 0;
+ Url = new FUrl();
+ RemoteAddr = new IPEndPoint(IPAddress.None, 0);
+ State = EConnectionState.USOCK_Invalid;
+ Handler = null;
+ ClientLoginState = EClientLoginState.Invalid;
+ ExpectedClientLoginMsgType = 0;
+ PacketOverhead = 0;
+ InPacketId = -1;
+ OutPacketId = 0;
+ OutAckPacketId = -1;
+ MaxChannelSize = DefaultMaxChannelSize;
+ Channels = new UChannel[DefaultMaxChannelSize];
+ OutReliable = new int[DefaultMaxChannelSize];
+ InReliable = new int[DefaultMaxChannelSize];
+ PendingOutRec = new int[DefaultMaxChannelSize];
+
+ EngineNetworkProtocolVersion = DefaultEngineNetworkProtocolVersion;
+ GameNetworkProtocolVersion = DefaultGameNetworkProtocolVersion;
+
+ PacketNotify = new FNetPacketNotify();
+ PacketNotify.Init(
+ new SequenceNumber((ushort)InPacketId),
+ new SequenceNumber((ushort)OutPacketId));
+ }
+
+ ///
+ /// Owning net driver
+ ///
+ public UNetDriver? Driver { get; private set; }
+
+ ///
+ /// Package map between local and remote. (negotiates net serialization)
+ ///
+ public UPackageMapClient? PackageMap { get; private set; }
+
+ ///
+ /// Maximum packet size.
+ ///
+ public int MaxPacket { get; private set; }
+
+ ///
+ /// URL of the other side.
+ ///
+ public FUrl Url { get; }
+
+ ///
+ /// The remote address of this connection, typically generated from the URL.
+ ///
+ public IPEndPoint? RemoteAddr { get; protected set; }
+
+ ///
+ /// State this connection is in.
+ ///
+ public EConnectionState State { get; private set; }
+
+ ///
+ /// PacketHandler, for managing layered handler components, which modify packets as they are sent/received
+ ///
+ public PacketHandler? Handler { get; private set; }
+
+ public EClientLoginState ClientLoginState { get; private set; }
+
+ ///
+ /// Used to determine what the next expected control channel msg type should be from a connecting client
+ ///
+ public byte ExpectedClientLoginMsgType { get; private set; }
+
+ ///
+ /// Reference to the PacketHandler component, for managing stateless connection handshakes
+ ///
+ public StatelessConnectHandlerComponent? StatelessConnectComponent { get; private set; }
+
+ ///
+ /// Bytes overhead per packet sent.
+ ///
+ public int PacketOverhead { get; private set; }
+
+ ///
+ /// Full incoming packet index.
+ ///
+ public int InPacketId { get; private set; }
+
+ ///
+ /// Most recently sent packet.
+ ///
+ public int OutPacketId { get; private set; }
+
+ ///
+ /// Most recently acked outgoing packet.
+ ///
+ public int OutAckPacketId { get; private set; }
+
+ public bool LastHasServerFrameTime { get; private set; }
+
+ ///
+ /// Full PacketId of last sent packet that we have received notification for (i.e. we know if it was delivered or not).
+ /// Related to OutAckPacketId which is tha last successfully delivered PacketId.
+ ///
+ public int LastNotifiedPacketId { get; private set; }
+
+ public int MaxChannelSize { get; }
+ public UChannel?[] Channels { get; }
+ public int[] OutReliable { get; }
+ public int[] InReliable { get; }
+ public int[] PendingOutRec { get; }
+ public int InitOutReliable { get; private set; }
+ public int InitInReliable { get; private set; }
+
+ public EEngineNetworkVersionHistory EngineNetworkProtocolVersion { get; private set; }
+ public uint GameNetworkProtocolVersion { get; private set; }
+
+ public FNetPacketNotify PacketNotify { get; }
+
+ public virtual void InitBase(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0)
+ {
+ Driver = inDriver;
+
+ // TODO: ConnectionId
+
+ State = inState;
+
+ Url.Protocol = inURL.Protocol;
+ Url.Host = inURL.Host;
+ Url.Port = inURL.Port;
+ Url.Map = inURL.Map;
+ Url.RedirectUrl = inURL.RedirectUrl;
+ Url.Options = inURL.Options;
+ Url.Portal = inURL.Portal;
+
+ MaxPacket = inMaxPacket;
+ PacketOverhead = inPacketOverhead;
+
+ InitHandler();
+
+ PackageMap = new UPackageMapClient();
+ PackageMap.Initialize(this, Driver.GuidCache);
+ }
- 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);
@@ -17,11 +195,491 @@ public abstract class UNetConnection
public abstract string LowLevelDescribe();
public abstract void Tick(float deltaSeconds);
public abstract void CleanUp();
- public abstract void ReceivedRawPacket(byte[] data, int count);
+
+ public virtual void ReceivedRawPacket(FReceivedPacketView packetView)
+ {
+ if (Handler != null)
+ {
+ var result = Handler.Incoming(packetView);
+ if (result)
+ {
+ if (packetView.DataView.NumBytes() == 0)
+ {
+ return;
+ }
+ }
+ else
+ {
+ // TODO: Close connection, malformed packet.
+ Logger.Fatal("Connection should be closed here");
+ return;
+ }
+ }
+
+ // Handle an incoming raw packet from the driver.
+ if (packetView.DataView.NumBytes() > 0)
+ {
+ var data = packetView.DataView.GetData();
+ var count = packetView.DataView.NumBytes();
+
+ var lastByte = data[count - 1];
+ if (lastByte != 0)
+ {
+ var bitSize = (count * 8) - 1;
+
+ // Bit streaming, starts at the Least Significant Bit, and ends at the MSB.
+ while ((lastByte & 0x80) == 0)
+ {
+ lastByte *= 2;
+ bitSize--;
+ }
+
+ var reader = new FBitReader(data, bitSize);
+
+ reader.SetEngineNetVer(EngineNetworkProtocolVersion);
+ reader.SetGameNetVer(GameNetworkProtocolVersion);
+
+ if (Handler != null)
+ {
+ Handler.IncomingHigh(reader);
+ }
+
+ if (reader.GetBitsLeft() > 0)
+ {
+ ReceivedPacket(reader);
+
+ // TODO: Flush out of order cache
+ }
+ }
+ }
+ }
+
+ private void ReceivedPacket(FBitReader reader, bool bIsReinjectedPacket = false)
+ {
+ if (reader.IsError())
+ {
+ Logger.Error("Packet too small");
+ return;
+ }
+
+ var resetReaderMark = new FBitReaderMark(reader);
+
+ if (_bInternalAck)
+ {
+ ++InPacketId;
+ }
+ else
+ {
+ // Read packet header.
+ var header = new FNotificationHeader();
+
+ if (!PacketNotify.ReadHeader(ref header, reader))
+ {
+ // TODO: Close connection, malformed packet.
+ Logger.Fatal("Failed to read PacketHeader");
+ return;
+ }
+
+ var bHasPacketInfoPayload = true;
+
+ if (reader.EngineNetVer() > EEngineNetworkVersionHistory.HISTORY_JITTER_IN_HEADER)
+ {
+ bHasPacketInfoPayload = reader.ReadBit();
+
+ if (bHasPacketInfoPayload)
+ {
+ var bitsReadPreJitterClock = reader.GetPosBits();
+
+ var packetJitterClockTimeMs = reader.ReadInt(MaxJitterClockTimeValue + 1);
+
+ if (reader.GetPosBits() - bitsReadPreJitterClock != NumBitsForJitterClockTimeInHeader)
+ {
+ throw new UnrealNetException("JitterClockTime did not read the expected amount of bits");
+ }
+
+ if (!bIsReinjectedPacket)
+ {
+ ProcessJitter(packetJitterClockTimeMs);
+ }
+ }
+ }
+
+ var packetSequenceDelta = PacketNotify.GetSequenceDelta(header);
+ if (packetSequenceDelta > 0)
+ {
+ var bPacketOrderCacheActive = !_bFlushingPacketOrderCache && _packetOrderCache != null;
+ var bCheckForMissingSequence = bPacketOrderCacheActive && _packetOrderCacheCount == 0;
+ var bFillingPacketOrderCache = bPacketOrderCacheActive && _packetOrderCacheCount > 0;
+
+ const int maxMissingPackets = 0; // CVarNetPacketOrderMaxMissingPackets
+ var missingPacketCount = packetSequenceDelta - 1;
+
+ // Cache the packet if we are already caching, and begin caching if we just encountered a missing sequence, within range
+ if (bFillingPacketOrderCache || (bCheckForMissingSequence && missingPacketCount > 0 && missingPacketCount <= maxMissingPackets))
+ {
+ throw new NotImplementedException();
+ }
+
+ if (missingPacketCount > 10)
+ {
+ Logger.Verbose("High single frame packet loss, PacketsLost: {Amount}", missingPacketCount);
+ }
+
+ // TODO: Increment things
+ }
+ else
+ {
+ // TODO: Increment things
+ // TODO: PacketOrderCache
+ return;
+ }
+
+ // Update incoming sequence data and deliver packet notifications
+ // Packet is only accepted if both the incoming sequence number and incoming ack data are valid
+ PacketNotify.Update(header, (ackedSequence, delivered) =>
+ {
+ // TODO: Increment things
+
+ if (!new SequenceNumber((ushort)LastNotifiedPacketId).Equals(ackedSequence))
+ {
+ // TODO: Close connection.
+ Logger.Fatal("LastNotifiedPacketId != AckedSequence");
+ return;
+ }
+
+ if (delivered)
+ {
+ // ReceivedAck(LastNotifiedPacketId, ChannelsToClose)
+ Logger.Verbose("TODO: ReceivedAck");
+ }
+ else
+ {
+ // ReceivedNak(LastNotifiedPacketId)
+ Logger.Verbose("TODO: ReceivedNak");
+ }
+ });
+
+ // Extra information associated with the header (read only after acks have been processed)
+ if (packetSequenceDelta > 0 && !ReadPacketInfo(reader, bHasPacketInfoPayload))
+ {
+ // TODO: Close connection.
+ Logger.Fatal("Failed to read PacketHeader");
+ return;
+ }
+ }
+
+ var bIgnoreRPCS = Driver!.ShouldIgnoreRPCs();
+ var bSickAcp = false;
+
+ // Track channels that were rejected while processing this packet - used to avoid sending multiple close-channel bunches,
+ // which would cause a disconnect serverside
+ var rejectedChans = new List();
+
+
+ // Disassemble and dispatch all bunches in the packet.
+ while (!reader.AtEnd() && State != EConnectionState.USOCK_Closed)
+ {
+ if (_bInternalAck && EngineNetworkProtocolVersion < EEngineNetworkVersionHistory.HISTORY_ACKS_INCLUDED_IN_HEADER)
+ {
+ _ = reader.ReadBit();
+ }
+
+ // Parse the bunch.
+ var startPos = reader.GetPosBits();
+
+ // Process Received data
+ {
+ // Parse the incoming data.
+ var bunch = new FInBunch(this);
+
+ var incomingStartPos = reader.GetPosBits();
+
+ var bControl = reader.ReadBit();
+
+ bunch.PacketId = InPacketId;
+ bunch.bOpen = bControl && reader.ReadBit();
+ bunch.bClose = bControl && reader.ReadBit();
+
+ if (bunch.EngineNetVer() < EEngineNetworkVersionHistory.HISTORY_CHANNEL_CLOSE_REASON)
+ {
+ bunch.bDormant = bunch.bClose && reader.ReadBit();
+ bunch.CloseReason = bunch.bDormant
+ ? EChannelCloseReason.Dormancy
+ : EChannelCloseReason.Destroyed;
+ }
+ else
+ {
+ bunch.CloseReason = bunch.bClose
+ ? (EChannelCloseReason)reader.ReadInt((uint)EChannelCloseReason.MAX)
+ : EChannelCloseReason.Destroyed;
+ bunch.bDormant = bunch.CloseReason == EChannelCloseReason.Dormancy;
+ }
+
+ bunch.bIsReplicationPaused = reader.ReadBit();
+ bunch.bReliable = reader.ReadBit();
+
+ if (bunch.EngineNetVer() < EEngineNetworkVersionHistory.HISTORY_MAX_ACTOR_CHANNELS_CUSTOMIZATION)
+ {
+ const int oldMaxActorChannels = 10240;
+ bunch.ChIndex = (int)reader.ReadInt(oldMaxActorChannels);
+ }
+ else
+ {
+ bunch.ChIndex = (int)reader.ReadUInt32Packed();
+
+ if (bunch.ChIndex >= MaxChannelSize)
+ {
+ throw new Exception("Bunch channel index exceeds channel limit");
+ }
+ }
+
+ // if flag is set, remap channel index values, we're fast forwarding a replay checkpoint
+ // and there should be no bunches for existing channels
+ if (_bInternalAck /* && bAllowExistingChannelIndex */ && (bunch.EngineNetVer() >= EEngineNetworkVersionHistory.HISTORY_REPLAY_DORMANCY))
+ {
+ throw new NotSupportedException("Replay code");
+ }
+
+ bunch.bHasPackageMapExports = reader.ReadBit();
+ bunch.bHasMustBeMappedGUIDs = reader.ReadBit();
+ bunch.bPartial = reader.ReadBit();
+
+ if (bunch.bReliable)
+ {
+ if (_bInternalAck)
+ {
+ // We can derive the sequence for 100% reliable connections.
+ bunch.ChSequence = InReliable[bunch.ChIndex] + 1;
+ }
+ else
+ {
+ bunch.ChSequence = MakeRelative((int)reader.ReadInt(MaxChSequence), InReliable[bunch.ChIndex], MaxChSequence);
+ }
+ }
+ else if (bunch.bPartial)
+ {
+ // If this is an unreliable partial bunch, we simply use packet sequence since we already have it
+ bunch.ChSequence = InPacketId;
+ }
+ else
+ {
+ bunch.ChSequence = 0;
+ }
+
+ bunch.bPartialInitial = bunch.bPartial && reader.ReadBit();
+ bunch.bPartialFinal = bunch.bPartial && reader.ReadBit();
+
+ if (bunch.EngineNetVer() < EEngineNetworkVersionHistory.HISTORY_CHANNEL_NAMES)
+ {
+ bunch.ChType = (bunch.bReliable || bunch.bOpen) ? (int) reader.ReadInt(EChannelType.CHTYPE_MAX) : EChannelType.CHTYPE_None;
+
+ switch (bunch.ChType)
+ {
+ case EChannelType.CHTYPE_Control:
+ bunch.ChName = UnrealNames.FNames[UnrealNameKey.Control];
+ break;
+ case EChannelType.CHTYPE_Voice:
+ bunch.ChName = UnrealNames.FNames[UnrealNameKey.Voice];
+ break;
+ case EChannelType.CHTYPE_Actor:
+ bunch.ChName = UnrealNames.FNames[UnrealNameKey.Actor];
+ break;
+ }
+ }
+ else
+ {
+ if (bunch.bReliable || bunch.bOpen)
+ {
+ if (!UPackageMap.StaticSerializeName(reader, out var chName) || reader.IsError())
+ {
+ // TODO: Close connection
+ Logger.Fatal("Channel name serialization failed");
+ return;
+ }
+
+ bunch.ChName = chName;
+
+ switch ((UnrealNameKey) bunch.ChName.Number)
+ {
+ case UnrealNameKey.Control:
+ bunch.ChType = EChannelType.CHTYPE_Control;
+ break;
+
+ case UnrealNameKey.Voice:
+ bunch.ChType = EChannelType.CHTYPE_Voice;
+ break;
+
+ case UnrealNameKey.Actor:
+ bunch.ChType = EChannelType.CHTYPE_Actor;
+ break;
+ }
+ }
+ else
+ {
+ bunch.ChType = EChannelType.CHTYPE_None;
+ bunch.ChName = UnrealNames.FNames[UnrealNameKey.None];
+ }
+ }
+
+ var channel = Channels[bunch.ChIndex];
+
+ // If there's an existing channel and the bunch specified it's channel type, make sure they match.
+ if (channel != null &&
+ (bunch.ChName.Number != (int)UnrealNameKey.None) &&
+ (bunch.ChName.Number != channel.ChName.Number))
+ {
+ Logger.Error("Existing channel at index {ChIndex} with type \"{ChName}\" differs from the incoming bunch's expected channel type, \"{BunchChName}\"",
+ bunch.ChIndex, channel.ChName.Str, bunch.ChName.Str);
+ // TODO: Close();
+ return;
+ }
+ }
+ }
+
+ throw new NotImplementedException();
+ }
+
+ private bool ReadPacketInfo(FBitReader reader, bool bHasPacketInfoPayload)
+ {
+ if (!bHasPacketInfoPayload)
+ {
+ var bCanContinueReading = reader.IsError() == false;
+ return bCanContinueReading;
+ }
+
+ var bHasServerFrameTime = reader.ReadBit();
+ var serverFrameTime = 0.0d;
+
+ if (!Driver!.IsServer())
+ {
+ throw new NotImplementedException("Client code");
+ }
+ else
+ {
+ LastHasServerFrameTime = bHasServerFrameTime;
+ }
+
+ if (reader.EngineNetVer() < EEngineNetworkVersionHistory.HISTORY_JITTER_IN_HEADER)
+ {
+ // RemoteInKBytesPerSecondByte
+ reader.ReadByte();
+ }
+
+ if (reader.IsError())
+ {
+ return false;
+ }
+
+ // TODO: Update ping, lag measuring
+
+ return true;
+ }
+
+ private void ProcessJitter(uint packetJitterClockTimeMs)
+ {
+ if (packetJitterClockTimeMs >= MaxJitterClockTimeValue)
+ {
+ return;
+ }
+
+ Logger.Verbose("Jitter calculations missing");
+ }
+
public abstract float GetTimeoutValue();
- public void InitSequence(int clientSequence, int serverSequence)
+ public void InitSequence(int incomingSequence, int outgoingSequence)
{
+ if (InPacketId == -1)
+ {
+ // Initialize the base UNetConnection packet sequence (not very useful/effective at preventing attacks)
+ InPacketId = incomingSequence - 1;
+ OutPacketId = outgoingSequence;
+ OutAckPacketId = outgoingSequence - 1;
+ LastNotifiedPacketId = OutAckPacketId;
+
+ // Initialize the reliable packet sequence (more useful/effective at preventing attacks)
+ InitInReliable = incomingSequence & (MaxChSequence - 1);
+ InitOutReliable = outgoingSequence & (MaxChSequence - 1);
+
+ for (var i = 0; i < InReliable.Length; i++)
+ {
+ InReliable[i] = InitInReliable;
+ }
+
+ for (var i = 0; i < OutReliable.Length; i++)
+ {
+ OutReliable[i] = InitOutReliable;
+ }
+
+ PacketNotify.Init(
+ new SequenceNumber((ushort)InPacketId),
+ new SequenceNumber((ushort)OutPacketId));
+
+ Logger.Verbose("InitSequence: IncomingSequence: {SeqA}, OutgoingSequence: {SeqB}", incomingSequence, outgoingSequence);
+ Logger.Verbose("InitSequence: InitInReliable: {In}, InitOutReliable: {Out}", InitInReliable, InitOutReliable);
+ }
+ }
+
+ private void InitHandler()
+ {
+ if (Handler != null)
+ {
+ return;
+ }
+
+ if (Driver == null)
+ {
+ throw new UnrealNetException("Driver was null");
+ }
+ Handler = new PacketHandler();
+ Handler.Initialize(false);
+
+ StatelessConnectComponent = (StatelessConnectHandlerComponent) Handler.AddHandler();
+ StatelessConnectComponent.SetDriver(Driver);
+
+ Handler.InitializeComponents();
+ }
+
+ public void CreateChannelByName(string chName, EChannelCreateFlags createFlags, int channelIndex)
+ {
+ // TODO: Implement
+ Logger.Verbose("Creating channel {Name} with index {Index}", chName, channelIndex);
+ }
+
+ private protected void SetClientLoginState(EClientLoginState newState)
+ {
+ if (ClientLoginState == newState)
+ {
+ Logger.Verbose("SetClientLoginState: State same: {Old}", newState);
+ return;
+ }
+
+ Logger.Verbose("SetClientLoginState: State changing from {Old} to {New}", ClientLoginState, newState);
+
+ ClientLoginState = newState;
+ }
+
+ private protected void SetExpectedClientLoginMsgType(byte newType)
+ {
+ if (ExpectedClientLoginMsgType == newType)
+ {
+ Logger.Verbose("SetExpectedClientLoginMsgType: Type same: [{Old}]", newType);
+ return;
+ }
+
+ Logger.Verbose("SetExpectedClientLoginMsgType: Type changing from [{Old}] to [{New}]", ExpectedClientLoginMsgType, newType);
+
+ ExpectedClientLoginMsgType = newType;
+ }
+
+ private static int BestSignedDifference(int value, int reference, int max)
+ {
+ return ((value - reference + max / 2) & (max - 1)) - max / 2;
+ }
+
+ private static int MakeRelative(int value, int reference, int max)
+ {
+ return reference + BestSignedDifference(value, reference, max);
}
}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/UNetDriver.cs b/src/Prospect.Unreal/Net/UNetDriver.cs
index fffdae6..5569df7 100644
--- a/src/Prospect.Unreal/Net/UNetDriver.cs
+++ b/src/Prospect.Unreal/Net/UNetDriver.cs
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Net;
+using Prospect.Unreal.Net.Channels;
using Prospect.Unreal.Runtime;
namespace Prospect.Unreal.Net;
@@ -10,6 +11,15 @@ public abstract class UNetDriver : IAsyncDisposable
protected UNetDriver()
{
+ GuidCache = new FNetGUIDCache(this);
+ // TODO: Load from Engine ini
+ ChannelDefinitions = new List
+ {
+ new FChannelDefinition("Control", typeof(string), 0, true, false, true, false, true),
+ new FChannelDefinition("Voice", typeof(string), 1, true, true, true, true, true),
+ new FChannelDefinition("Actor", typeof(string), -1, false, true, false, false, false)
+ };
+ ClientConnections = new List();
MappedClientConnections = new ConcurrentDictionary();
}
@@ -18,6 +28,18 @@ public abstract class UNetDriver : IAsyncDisposable
///
public UWorld? World { get; private set; }
+ public FNetGUIDCache GuidCache { get; }
+
+ ///
+ /// Used to specify available channel types and their associated UClass
+ ///
+ public List ChannelDefinitions { get; }
+
+ ///
+ /// Array of connections to clients (this net driver is a host) - unsorted, and ordering changes depending on actor replication
+ ///
+ public List ClientConnections { get; }
+
///
/// Map of to .
///
@@ -75,6 +97,16 @@ public abstract class UNetDriver : IAsyncDisposable
// TODO: AddInitialObjects?
}
}
+
+ public bool IsServer()
+ {
+ return true;
+ }
+
+ public virtual bool ShouldIgnoreRPCs()
+ {
+ return false;
+ }
public double GetElapsedTime()
{
@@ -85,7 +117,34 @@ public abstract class UNetDriver : IAsyncDisposable
{
_elapsedTime = 0.0;
}
-
+
+ private protected void AddClientConnection(UNetConnection newConnection)
+ {
+ ClientConnections.Add(newConnection);
+
+ if (newConnection.RemoteAddr != null)
+ {
+ MappedClientConnections[newConnection.RemoteAddr] = newConnection;
+
+ // TODO: RecentlyDisconnectedClients ?
+ }
+
+ CreateInitialServerChannels(newConnection);
+
+ // TODO: NetworkObjectList > HandleConnectionAdded
+ }
+
+ private void CreateInitialServerChannels(UNetConnection clientConnection)
+ {
+ foreach (var channelDef in ChannelDefinitions)
+ {
+ if (channelDef.InitialServer)
+ {
+ clientConnection.CreateChannelByName(channelDef.Name, EChannelCreateFlags.OpenedLocally, channelDef.StaticChannelIndex);
+ }
+ }
+ }
+
public virtual ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
diff --git a/src/Prospect.Unreal/Net/UPackageMap.cs b/src/Prospect.Unreal/Net/UPackageMap.cs
new file mode 100644
index 0000000..8615617
--- /dev/null
+++ b/src/Prospect.Unreal/Net/UPackageMap.cs
@@ -0,0 +1,56 @@
+using System.Diagnostics.CodeAnalysis;
+using Prospect.Unreal.Core;
+using Prospect.Unreal.Core.Names;
+using Prospect.Unreal.Serialization;
+
+namespace Prospect.Unreal.Net;
+
+public class UPackageMap
+{
+ public static unsafe bool StaticSerializeName(FBitReader ar, [MaybeNullWhen(false)] out FName name)
+ {
+ if (!ar.IsLoading())
+ {
+ throw new NotImplementedException();
+ }
+
+ name = null;
+
+ bool bHardcoded;
+ ar.SerializeBits(&bHardcoded, 1);
+
+ if (bHardcoded)
+ {
+ // replicated by hardcoded index
+ uint nameIndex;
+
+ if (ar.EngineNetVer() < EEngineNetworkVersionHistory.HISTORY_CHANNEL_NAMES)
+ {
+ ar.SerializeInt(&nameIndex, UnrealNames.MaxNetworkedHardcodedName);
+ }
+ else
+ {
+ ar.SerializeIntPacked(&nameIndex);
+ }
+
+ if (nameIndex < UnrealNames.MaxHardcodedNameIndex)
+ {
+ // hardcoded names never have a Number
+ name = UnrealNames.FNames[(UnrealNameKey) nameIndex];
+ }
+ else
+ {
+ ar.SetError();
+ }
+ }
+ else
+ {
+ // replicated by string
+ var inString = FString.Deserialize(ar);
+ var inNumber = ar.ReadInt32();
+ name = new FName(inString, inNumber);
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Net/UPackageMapClient.cs b/src/Prospect.Unreal/Net/UPackageMapClient.cs
new file mode 100644
index 0000000..f3a6ac3
--- /dev/null
+++ b/src/Prospect.Unreal/Net/UPackageMapClient.cs
@@ -0,0 +1,9 @@
+namespace Prospect.Unreal.Net;
+
+public class UPackageMapClient
+{
+ public void Initialize(UNetConnection connection, FNetGUIDCache guidCache)
+ {
+ // TODO: Implement
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Serialization/FBitReaderMark.cs b/src/Prospect.Unreal/Serialization/FBitReaderMark.cs
new file mode 100644
index 0000000..bff3e44
--- /dev/null
+++ b/src/Prospect.Unreal/Serialization/FBitReaderMark.cs
@@ -0,0 +1,21 @@
+namespace Prospect.Unreal.Serialization;
+
+public class FBitReaderMark
+{
+ private long _pos;
+
+ public FBitReaderMark(FBitReader reader)
+ {
+ _pos = reader.Pos;
+ }
+
+ public long GetPos()
+ {
+ return _pos;
+ }
+
+ public void Pop(FBitReader reader)
+ {
+ reader.Pos = _pos;
+ }
+}
\ No newline at end of file
diff --git a/src/Prospect.Unreal/Serialization/FNetBitReader.cs b/src/Prospect.Unreal/Serialization/FNetBitReader.cs
new file mode 100644
index 0000000..349680d
--- /dev/null
+++ b/src/Prospect.Unreal/Serialization/FNetBitReader.cs
@@ -0,0 +1,18 @@
+using Prospect.Unreal.Net;
+
+namespace Prospect.Unreal.Serialization;
+
+public class FNetBitReader : FBitReader
+{
+ public FNetBitReader(byte[] src) : base(src)
+ {
+ throw new NotSupportedException();
+ }
+
+ public FNetBitReader(UPackageMapClient? inPackageMap, byte[]? src, int num) : base(src, num)
+ {
+ PackageMap = inPackageMap;
+ }
+
+ public UPackageMapClient? PackageMap { get; }
+}
\ No newline at end of file
diff --git a/src/Prospect.sln b/src/Prospect.sln
index 866406f..4c08147 100644
--- a/src/Prospect.sln
+++ b/src/Prospect.sln
@@ -19,6 +19,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Steam", "Prospect.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal", "Prospect.Unreal\Prospect.Unreal.csproj", "{380DD490-8F01-4025-91D7-74C289AA5B75}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{168B6247-2D33-4942-AB3F-C5041B3BDFEA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Tests", "Prospect.Unreal.Tests\Prospect.Unreal.Tests.csproj", "{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -73,6 +77,14 @@ Global
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x64.Build.0 = Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x86.ActiveCfg = Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x86.Build.0 = Release|Any CPU
+ {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x64.Build.0 = Debug|Any CPU
+ {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x86.Build.0 = Debug|Any CPU
+ {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x64.ActiveCfg = Release|Any CPU
+ {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x64.Build.0 = Release|Any CPU
+ {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x86.ActiveCfg = Release|Any CPU
+ {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -87,5 +99,6 @@ Global
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
{380DD490-8F01-4025-91D7-74C289AA5B75} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
+ {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34} = {168B6247-2D33-4942-AB3F-C5041B3BDFEA}
EndGlobalSection
EndGlobal