From f3b0a317618ed36d4901776995f63025c0f8312a Mon Sep 17 00:00:00 2001 From: AeonLucid Date: Mon, 10 Jan 2022 04:38:06 +0100 Subject: [PATCH] Started serializing bunches --- .../Net/Packets/Header/FPackedHeaderTests.cs | 30 + .../Prospect.Unreal.Tests.csproj | 21 + .../Core/EEngineNetworkVersionHistory.cs | 2 +- src/Prospect.Unreal/Core/FUrl.cs | 14 +- src/Prospect.Unreal/Core/Names/FName.cs | 36 + .../Core/Names/UnrealNameKey.cs | 202 ++++++ src/Prospect.Unreal/Core/Names/UnrealNames.cs | 231 ++++++ .../Net/Channels/EChannelCloseReason.cs | 12 + .../Net/Channels/EChannelCreateFlags.cs | 8 + .../Net/Channels/EChannelType.cs | 13 + src/Prospect.Unreal/Net/CoreNet.cs | 6 - src/Prospect.Unreal/Net/EClientLoginState.cs | 10 + src/Prospect.Unreal/Net/ECountUnits.cs | 7 + src/Prospect.Unreal/Net/FChannelDefinition.cs | 3 + src/Prospect.Unreal/Net/FNetGUIDCache.cs | 11 + .../Net/FReceiveThreadRunnable.cs | 6 +- src/Prospect.Unreal/Net/PacketHandler.cs | 31 +- src/Prospect.Unreal/Net/PacketView.cs | 20 +- .../Net/Packets/Bunch/FInBunch.cs | 95 +++ .../Net/Packets/Header/FNetPacketNotify.cs | 146 ++++ .../Net/Packets/Header/FNotificationHeader.cs | 11 + .../Net/Packets/Header/FPackedHeader.cs | 27 + .../Packets/Header/Sequence/FSentAckData.cs | 13 + .../Header/Sequence/SequenceHistory.cs | 46 ++ .../Packets/Header/Sequence/SequenceNumber.cs | 70 ++ .../Net/StatelessConnectHandlerComponent.cs | 77 +- src/Prospect.Unreal/Net/UChannel.cs | 8 + src/Prospect.Unreal/Net/UIpConnection.cs | 42 +- src/Prospect.Unreal/Net/UIpNetDriver.cs | 27 +- src/Prospect.Unreal/Net/UNetConnection.cs | 664 +++++++++++++++++- src/Prospect.Unreal/Net/UNetDriver.cs | 61 +- src/Prospect.Unreal/Net/UPackageMap.cs | 56 ++ src/Prospect.Unreal/Net/UPackageMapClient.cs | 9 + .../Serialization/FBitReaderMark.cs | 21 + .../Serialization/FNetBitReader.cs | 18 + src/Prospect.sln | 13 + 36 files changed, 2013 insertions(+), 54 deletions(-) create mode 100644 src/Prospect.Unreal.Tests/Net/Packets/Header/FPackedHeaderTests.cs create mode 100644 src/Prospect.Unreal.Tests/Prospect.Unreal.Tests.csproj create mode 100644 src/Prospect.Unreal/Core/Names/FName.cs create mode 100644 src/Prospect.Unreal/Core/Names/UnrealNameKey.cs create mode 100644 src/Prospect.Unreal/Core/Names/UnrealNames.cs create mode 100644 src/Prospect.Unreal/Net/Channels/EChannelCloseReason.cs create mode 100644 src/Prospect.Unreal/Net/Channels/EChannelCreateFlags.cs create mode 100644 src/Prospect.Unreal/Net/Channels/EChannelType.cs delete mode 100644 src/Prospect.Unreal/Net/CoreNet.cs create mode 100644 src/Prospect.Unreal/Net/EClientLoginState.cs create mode 100644 src/Prospect.Unreal/Net/ECountUnits.cs create mode 100644 src/Prospect.Unreal/Net/FChannelDefinition.cs create mode 100644 src/Prospect.Unreal/Net/FNetGUIDCache.cs create mode 100644 src/Prospect.Unreal/Net/Packets/Bunch/FInBunch.cs create mode 100644 src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs create mode 100644 src/Prospect.Unreal/Net/Packets/Header/FNotificationHeader.cs create mode 100644 src/Prospect.Unreal/Net/Packets/Header/FPackedHeader.cs create mode 100644 src/Prospect.Unreal/Net/Packets/Header/Sequence/FSentAckData.cs create mode 100644 src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs create mode 100644 src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceNumber.cs create mode 100644 src/Prospect.Unreal/Net/UChannel.cs create mode 100644 src/Prospect.Unreal/Net/UPackageMap.cs create mode 100644 src/Prospect.Unreal/Net/UPackageMapClient.cs create mode 100644 src/Prospect.Unreal/Serialization/FBitReaderMark.cs create mode 100644 src/Prospect.Unreal/Serialization/FNetBitReader.cs 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