Started serializing bunches
This commit is contained in:
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||||
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||||
|
<PackageReference Include="coverlet.collector" Version="3.1.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Prospect.Unreal\Prospect.Unreal.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Prospect.Unreal.Core;
|
namespace Prospect.Unreal.Core;
|
||||||
|
|
||||||
public enum EEngineNetworkVersionHistory
|
public enum EEngineNetworkVersionHistory : uint
|
||||||
{
|
{
|
||||||
HISTORY_INITIAL = 1,
|
HISTORY_INITIAL = 1,
|
||||||
HISTORY_REPLAY_BACKWARDS_COMPAT = 2, // Bump version to get rid of older replays before backwards compat was turned on officially
|
HISTORY_REPLAY_BACKWARDS_COMPAT = 2, // Bump version to get rid of older replays before backwards compat was turned on officially
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ namespace Prospect.Unreal.Core;
|
|||||||
|
|
||||||
public class FUrl
|
public class FUrl
|
||||||
{
|
{
|
||||||
public string Protocol { get; init; } = "unreal";
|
public string Protocol { get; set; } = "unreal";
|
||||||
public IPAddress Host { get; init; } = IPAddress.Any;
|
public IPAddress Host { get; set; } = IPAddress.Any;
|
||||||
public int Port { get; init; } = 7777;
|
public int Port { get; set; } = 7777;
|
||||||
public string Map { get; init; } = "GearStart";
|
public string Map { get; set; } = "GearStart";
|
||||||
public string RedirectUrl { get; init; } = string.Empty;
|
public string RedirectUrl { get; set; } = string.Empty;
|
||||||
public List<string> Options { get; init; } = new List<string>();
|
public List<string> Options { get; set; } = new List<string>();
|
||||||
public string Portal { get; init; } = string.Empty;
|
public string Portal { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<int, string>();
|
||||||
|
|
||||||
|
// 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<int, string> Names { get; }
|
||||||
|
|
||||||
|
public static IReadOnlyDictionary<UnrealNameKey, FName> FNames { get; }
|
||||||
|
|
||||||
|
public static int MaxHardcodedNameIndex { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Prospect.Unreal.Net.Channels;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum EChannelCreateFlags
|
||||||
|
{
|
||||||
|
None = (1 << 0),
|
||||||
|
OpenedLocally = (1 << 1)
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Prospect.Unreal.Net;
|
|
||||||
|
|
||||||
internal class CoreNet
|
|
||||||
{
|
|
||||||
public const int MaxPacketSize = 1024;
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
|
public enum ECountUnits
|
||||||
|
{
|
||||||
|
Bits,
|
||||||
|
Bytes
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
|
public class FNetGUIDCache
|
||||||
|
{
|
||||||
|
public FNetGUIDCache(UNetDriver driver)
|
||||||
|
{
|
||||||
|
Driver = driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public UNetDriver Driver { get; }
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ public class FReceiveThreadRunnable : IAsyncDisposable
|
|||||||
if (_receiveQueue.TryDequeue(out var packet))
|
if (_receiveQueue.TryDequeue(out var packet))
|
||||||
{
|
{
|
||||||
result = new FReceivedPacketView(
|
result = new FReceivedPacketView(
|
||||||
new FPacketDataView(packet.Buffer, packet.Buffer.Length),
|
new FPacketDataView(packet.Buffer, packet.Buffer.Length, ECountUnits.Bytes),
|
||||||
packet.Address,
|
packet.Address,
|
||||||
new FInPacketTraits());
|
new FInPacketTraits());
|
||||||
|
|
||||||
@@ -58,10 +58,10 @@ public class FReceiveThreadRunnable : IAsyncDisposable
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.Buffer.Length > CoreNet.MaxPacketSize)
|
if (result.Buffer.Length > UNetConnection.MaxPacketSize)
|
||||||
{
|
{
|
||||||
Logger.Warning("Received packet exceeding MaxPacketSize ({Size} > {Max}) from {Ip}",
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ public class PacketHandler
|
|||||||
|
|
||||||
if (!_bConnectionlessHandler)
|
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.
|
// TODO: FEncryptionComponent.
|
||||||
@@ -93,6 +97,19 @@ public class PacketHandler
|
|||||||
return Outgoing_Internal(packet, countBits, traits, true, address);
|
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<T>() where T : HandlerComponent
|
public HandlerComponent AddHandler<T>() where T : HandlerComponent
|
||||||
{
|
{
|
||||||
var result = (HandlerComponent) Activator.CreateInstance(typeof(T), this)!;
|
var result = (HandlerComponent) Activator.CreateInstance(typeof(T), this)!;
|
||||||
@@ -102,6 +119,16 @@ public class PacketHandler
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Incoming(FReceivedPacketView packetView)
|
||||||
|
{
|
||||||
|
return Incoming_Internal(packetView);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void IncomingHigh(FBitReader reader)
|
||||||
|
{
|
||||||
|
// NO-OP
|
||||||
|
}
|
||||||
|
|
||||||
private bool Incoming_Internal(FReceivedPacketView packetView)
|
private bool Incoming_Internal(FReceivedPacketView packetView)
|
||||||
{
|
{
|
||||||
var returnVal = true;
|
var returnVal = true;
|
||||||
@@ -159,7 +186,7 @@ public class PacketHandler
|
|||||||
{
|
{
|
||||||
ReplaceIncomingPacket(processPacketReader);
|
ReplaceIncomingPacket(processPacketReader);
|
||||||
|
|
||||||
packetView.DataView = new FPacketDataView(_incomingPacket.GetBuffer(), _incomingPacket.GetBitsLeft());
|
packetView.DataView = new FPacketDataView(_incomingPacket.GetBuffer(), _incomingPacket.GetBitsLeft(), ECountUnits.Bits);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
|
using Prospect.Unreal.Core;
|
||||||
|
|
||||||
namespace Prospect.Unreal.Net;
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
@@ -8,16 +9,27 @@ public class FInPacketTraits
|
|||||||
public bool FromRecentlyDisconnected { get; set; }
|
public bool FromRecentlyDisconnected { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FPacketDataView
|
public readonly struct FPacketDataView
|
||||||
{
|
{
|
||||||
private readonly byte[] _data;
|
private readonly byte[] _data;
|
||||||
|
private readonly int _count;
|
||||||
private readonly int _countBits;
|
private readonly int _countBits;
|
||||||
|
|
||||||
public FPacketDataView(byte[] data, int length)
|
public FPacketDataView(byte[] data, int length, ECountUnits unit)
|
||||||
{
|
{
|
||||||
_data = data;
|
_data = data;
|
||||||
|
|
||||||
|
if (unit == ECountUnits.Bits)
|
||||||
|
{
|
||||||
|
_count = FMath.DivideAndRoundUp(length, 8);
|
||||||
|
_countBits = length;
|
||||||
|
}
|
||||||
|
else /* if (unit == ECountUnits.Bytes) */
|
||||||
|
{
|
||||||
|
_count = length;
|
||||||
_countBits = length * 8;
|
_countBits = length * 8;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public byte[] GetData()
|
public byte[] GetData()
|
||||||
{
|
{
|
||||||
@@ -31,7 +43,7 @@ public class FPacketDataView
|
|||||||
|
|
||||||
public int NumBytes()
|
public int NumBytes()
|
||||||
{
|
{
|
||||||
return _data.Length;
|
return _count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Close, but go dormant.
|
||||||
|
/// </summary>
|
||||||
|
public bool bDormant { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replication on this channel is being paused by the server.
|
||||||
|
/// </summary>
|
||||||
|
public bool bIsReplicationPaused { get; set; }
|
||||||
|
|
||||||
|
public bool bReliable { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Not a complete bunch
|
||||||
|
/// </summary>
|
||||||
|
public bool bPartial { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The first bunch of a partial bunch
|
||||||
|
/// </summary>
|
||||||
|
public bool bPartialInitial { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The final bunch of a partial bunch
|
||||||
|
/// </summary>
|
||||||
|
public bool bPartialFinal { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This bunch has networkGUID name/id pairs.
|
||||||
|
/// </summary>
|
||||||
|
public bool bHasPackageMapExports { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This bunch has guids that must be mapped before we can process this bunch.
|
||||||
|
/// </summary>
|
||||||
|
public bool bHasMustBeMappedGUIDs { get; set; }
|
||||||
|
|
||||||
|
public bool bIgnoreRPCs { get; set; }
|
||||||
|
|
||||||
|
public EChannelCloseReason CloseReason { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FNetPacketNotify>();
|
||||||
|
|
||||||
|
public const int SequenceNumberBits = 14;
|
||||||
|
public const int MaxSequenceHistoryLength = 256;
|
||||||
|
|
||||||
|
private readonly Queue<FSentAckData> _ackRecord = new Queue<FSentAckData>();
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -153,7 +153,53 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
|||||||
|
|
||||||
public override void Incoming(FBitReader packet)
|
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<byte> cookie = stackalloc byte[CookieByteSize];
|
||||||
|
Span<byte> 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)
|
public override void Outgoing(FBitWriter packet, FOutPacketTraits traits)
|
||||||
@@ -181,14 +227,18 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
|||||||
var bRestartHandshake = false;
|
var bRestartHandshake = false;
|
||||||
var secretId = (byte) 0;
|
var secretId = (byte) 0;
|
||||||
var timestamp = 1.0d;
|
var timestamp = 1.0d;
|
||||||
var cookie = new byte[CookieByteSize];
|
Span<byte> cookie = stackalloc byte[CookieByteSize];
|
||||||
var origCookie = new byte[CookieByteSize];
|
Span<byte> origCookie = stackalloc byte[CookieByteSize];
|
||||||
|
|
||||||
bHandshakePacket = ParseHandshakePacket(packet, ref bRestartHandshake, ref secretId, ref timestamp, cookie, origCookie);
|
bHandshakePacket = ParseHandshakePacket(packet, ref bRestartHandshake, ref secretId, ref timestamp, cookie, origCookie);
|
||||||
|
|
||||||
if (bHandshakePacket)
|
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;
|
var bInitialConnect = timestamp == 0.0;
|
||||||
if (bInitialConnect)
|
if (bInitialConnect)
|
||||||
@@ -206,7 +256,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
|||||||
if (bValidCookieLifetime && bValidSecretIdTimestamp)
|
if (bValidCookieLifetime && bValidSecretIdTimestamp)
|
||||||
{
|
{
|
||||||
// Regenerate the cookie from the packet info, and see if the received cookie matches the regenerated one
|
// Regenerate the cookie from the packet info, and see if the received cookie matches the regenerated one
|
||||||
var regenCookie = new byte[CookieByteSize];
|
Span<byte> regenCookie = stackalloc byte[CookieByteSize];
|
||||||
|
|
||||||
GenerateCookie(address, secretId, timestamp, regenCookie);
|
GenerateCookie(address, secretId, timestamp, regenCookie);
|
||||||
|
|
||||||
@@ -216,16 +266,17 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
|||||||
{
|
{
|
||||||
if (bRestartHandshake)
|
if (bRestartHandshake)
|
||||||
{
|
{
|
||||||
Buffer.BlockCopy(origCookie, 0, _authorisedCookie, 0, _authorisedCookie.Length);
|
origCookie.CopyTo(_authorisedCookie);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var curSequence = BinaryPrimitives.ReadInt16LittleEndian(cookie);
|
var seqA = BinaryPrimitives.ReadInt16LittleEndian(cookie);
|
||||||
|
var seqB = BinaryPrimitives.ReadInt16LittleEndian(cookie.Slice(2));
|
||||||
|
|
||||||
_lastServerSequence = curSequence & (UNetConnection.MaxPacketId - 1);
|
_lastServerSequence = seqA & (UNetConnection.MaxPacketId - 1);
|
||||||
_lastClientSequence = (curSequence + 1) & (UNetConnection.MaxPacketId - 1);
|
_lastClientSequence = seqB & (UNetConnection.MaxPacketId - 1);
|
||||||
|
|
||||||
Buffer.BlockCopy(cookie, 0, _authorisedCookie, 0, _authorisedCookie.Length);
|
cookie.CopyTo(_authorisedCookie);
|
||||||
}
|
}
|
||||||
|
|
||||||
_bRestartedHandshake = bRestartHandshake;
|
_bRestartedHandshake = bRestartHandshake;
|
||||||
@@ -262,7 +313,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
|||||||
FBitReader packet,
|
FBitReader packet,
|
||||||
ref bool bOutRestartHandshake,
|
ref bool bOutRestartHandshake,
|
||||||
ref byte outSecretId,
|
ref byte outSecretId,
|
||||||
ref double outTimestamp, byte[] outCookie, byte[] outOrigCookie)
|
ref double outTimestamp, Span<byte> outCookie, Span<byte> outOrigCookie)
|
||||||
{
|
{
|
||||||
var bValidPacket = false;
|
var bValidPacket = false;
|
||||||
var bitsLeft = packet.GetBitsLeft();
|
var bitsLeft = packet.GetBitsLeft();
|
||||||
@@ -314,7 +365,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
|||||||
var bHandshakePacket = (byte)1;
|
var bHandshakePacket = (byte)1;
|
||||||
var bRestartHandshake = (byte)0;
|
var bRestartHandshake = (byte)0;
|
||||||
var timestamp = _driver.GetElapsedTime();
|
var timestamp = _driver.GetElapsedTime();
|
||||||
var cookie = new byte[CookieByteSize];
|
Span<byte> cookie = stackalloc byte[CookieByteSize];
|
||||||
|
|
||||||
GenerateCookie(address, _activeSecret, timestamp, cookie);
|
GenerateCookie(address, _activeSecret, timestamp, cookie);
|
||||||
|
|
||||||
@@ -329,7 +380,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
|||||||
challengePacket.WriteDouble(timestamp);
|
challengePacket.WriteDouble(timestamp);
|
||||||
challengePacket.Serialize(cookie, cookie.Length);
|
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);
|
CapHandshakePacket(challengePacket);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Prospect.Unreal.Core.Names;
|
||||||
|
|
||||||
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
|
public class UChannel
|
||||||
|
{
|
||||||
|
public FName ChName { get; }
|
||||||
|
}
|
||||||
@@ -6,20 +6,46 @@ namespace Prospect.Unreal.Net;
|
|||||||
|
|
||||||
public class UIpConnection : UNetConnection
|
public class UIpConnection : UNetConnection
|
||||||
{
|
{
|
||||||
|
public const int IpHeaderSize = 20;
|
||||||
|
public const int UdpHeaderSize = IpHeaderSize + 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cached time of the first send socket error that will be used to compute disconnect delay.
|
||||||
|
/// </summary>
|
||||||
|
private double _socketErrorSendDelayStartTime;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cached time of the first recv socket error that will be used to compute disconnect delay.
|
||||||
|
/// </summary>
|
||||||
|
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)
|
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)
|
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,
|
public override void InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0)
|
||||||
int inMaxPacket = 0, int inPacketOverhead = 0)
|
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits)
|
public override void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits)
|
||||||
@@ -47,9 +73,11 @@ public class UIpConnection : UNetConnection
|
|||||||
throw new NotImplementedException();
|
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()
|
public override float GetTimeoutValue()
|
||||||
|
|||||||
@@ -54,18 +54,26 @@ public class UIpNetDriver : UNetDriver
|
|||||||
MappedClientConnections.TryGetValue(packet.Address, out connection);
|
MappedClientConnections.TryGetValue(packet.Address, out connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var bIgnorePacket = false;
|
||||||
|
|
||||||
// If we didn't find a client connection, maybe create a new one.
|
// If we didn't find a client connection, maybe create a new one.
|
||||||
if (connection == null)
|
if (connection == null)
|
||||||
{
|
{
|
||||||
connection = ProcessConnectionlessPacket(packet);
|
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)
|
private UNetConnection? ProcessConnectionlessPacket(FReceivedPacketView packet)
|
||||||
{
|
{
|
||||||
UNetConnection? returnVal;
|
UNetConnection? returnVal = null;
|
||||||
var statelessConnect = StatelessConnectComponent;
|
var statelessConnect = StatelessConnectComponent;
|
||||||
var address = packet.Address;
|
var address = packet.Address;
|
||||||
var bPassedChallenge = false;
|
var bPassedChallenge = false;
|
||||||
@@ -95,7 +103,7 @@ public class UIpNetDriver : UNetDriver
|
|||||||
bIgnorePacket = false;
|
bIgnorePacket = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
packet.DataView = new FPacketDataView(workingData, newCountBytes);
|
packet.DataView = new FPacketDataView(workingData, newCountBytes, ECountUnits.Bytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,7 +129,12 @@ public class UIpNetDriver : UNetDriver
|
|||||||
|
|
||||||
returnVal.InitRemoteConnection(this, Socket, World != null ? World.Url : new FUrl(), address, EConnectionState.USOCK_Open);
|
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)
|
if (statelessConnect != null)
|
||||||
@@ -132,10 +145,10 @@ public class UIpNetDriver : UNetDriver
|
|||||||
|
|
||||||
if (bIgnorePacket)
|
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)
|
public override void LowLevelSend(IPEndPoint address, byte[] data, int countBits, FOutPacketTraits traits)
|
||||||
|
|||||||
@@ -1,15 +1,193 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using Prospect.Unreal.Core;
|
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;
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
public abstract class UNetConnection
|
public abstract class UNetConnection
|
||||||
{
|
{
|
||||||
|
private static readonly ILogger Logger = Log.ForContext<UNetConnection>();
|
||||||
|
|
||||||
public const int ReliableBuffer = 256;
|
public const int ReliableBuffer = 256;
|
||||||
public const int MaxPacketId = 16384;
|
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<FBitReader>? _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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owning net driver
|
||||||
|
/// </summary>
|
||||||
|
public UNetDriver? Driver { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Package map between local and remote. (negotiates net serialization)
|
||||||
|
/// </summary>
|
||||||
|
public UPackageMapClient? PackageMap { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum packet size.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxPacket { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL of the other side.
|
||||||
|
/// </summary>
|
||||||
|
public FUrl Url { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The remote address of this connection, typically generated from the URL.
|
||||||
|
/// </summary>
|
||||||
|
public IPEndPoint? RemoteAddr { get; protected set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// State this connection is in.
|
||||||
|
/// </summary>
|
||||||
|
public EConnectionState State { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PacketHandler, for managing layered handler components, which modify packets as they are sent/received
|
||||||
|
/// </summary>
|
||||||
|
public PacketHandler? Handler { get; private set; }
|
||||||
|
|
||||||
|
public EClientLoginState ClientLoginState { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to determine what the next expected control channel msg type should be from a connecting client
|
||||||
|
/// </summary>
|
||||||
|
public byte ExpectedClientLoginMsgType { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to the PacketHandler component, for managing stateless connection handshakes
|
||||||
|
/// </summary>
|
||||||
|
public StatelessConnectHandlerComponent? StatelessConnectComponent { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bytes overhead per packet sent.
|
||||||
|
/// </summary>
|
||||||
|
public int PacketOverhead { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full incoming packet index.
|
||||||
|
/// </summary>
|
||||||
|
public int InPacketId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Most recently sent packet.
|
||||||
|
/// </summary>
|
||||||
|
public int OutPacketId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Most recently acked outgoing packet.
|
||||||
|
/// </summary>
|
||||||
|
public int OutAckPacketId { get; private set; }
|
||||||
|
|
||||||
|
public bool LastHasServerFrameTime { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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 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 InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0);
|
||||||
public abstract void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits);
|
public abstract void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits);
|
||||||
@@ -17,11 +195,491 @@ public abstract class UNetConnection
|
|||||||
public abstract string LowLevelDescribe();
|
public abstract string LowLevelDescribe();
|
||||||
public abstract void Tick(float deltaSeconds);
|
public abstract void Tick(float deltaSeconds);
|
||||||
public abstract void CleanUp();
|
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<int>();
|
||||||
|
|
||||||
|
|
||||||
|
// 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 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<StatelessConnectHandlerComponent>();
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using Prospect.Unreal.Net.Channels;
|
||||||
using Prospect.Unreal.Runtime;
|
using Prospect.Unreal.Runtime;
|
||||||
|
|
||||||
namespace Prospect.Unreal.Net;
|
namespace Prospect.Unreal.Net;
|
||||||
@@ -10,6 +11,15 @@ public abstract class UNetDriver : IAsyncDisposable
|
|||||||
|
|
||||||
protected UNetDriver()
|
protected UNetDriver()
|
||||||
{
|
{
|
||||||
|
GuidCache = new FNetGUIDCache(this);
|
||||||
|
// TODO: Load from Engine ini
|
||||||
|
ChannelDefinitions = new List<FChannelDefinition>
|
||||||
|
{
|
||||||
|
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<UNetConnection>();
|
||||||
MappedClientConnections = new ConcurrentDictionary<IPEndPoint, UNetConnection>();
|
MappedClientConnections = new ConcurrentDictionary<IPEndPoint, UNetConnection>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +28,18 @@ public abstract class UNetDriver : IAsyncDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public UWorld? World { get; private set; }
|
public UWorld? World { get; private set; }
|
||||||
|
|
||||||
|
public FNetGUIDCache GuidCache { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to specify available channel types and their associated UClass
|
||||||
|
/// </summary>
|
||||||
|
public List<FChannelDefinition> ChannelDefinitions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Array of connections to clients (this net driver is a host) - unsorted, and ordering changes depending on actor replication
|
||||||
|
/// </summary>
|
||||||
|
public List<UNetConnection> ClientConnections { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Map of <see cref="IPEndPoint"/> to <see cref="UNetConnection"/>.
|
/// Map of <see cref="IPEndPoint"/> to <see cref="UNetConnection"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -76,6 +98,16 @@ public abstract class UNetDriver : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsServer()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool ShouldIgnoreRPCs()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public double GetElapsedTime()
|
public double GetElapsedTime()
|
||||||
{
|
{
|
||||||
return _elapsedTime;
|
return _elapsedTime;
|
||||||
@@ -86,6 +118,33 @@ public abstract class UNetDriver : IAsyncDisposable
|
|||||||
_elapsedTime = 0.0;
|
_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()
|
public virtual ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Prospect.Unreal.Net;
|
||||||
|
|
||||||
|
public class UPackageMapClient
|
||||||
|
{
|
||||||
|
public void Initialize(UNetConnection connection, FNetGUIDCache guidCache)
|
||||||
|
{
|
||||||
|
// TODO: Implement
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -19,6 +19,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Steam", "Prospect.
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal", "Prospect.Unreal\Prospect.Unreal.csproj", "{380DD490-8F01-4025-91D7-74C289AA5B75}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal", "Prospect.Unreal\Prospect.Unreal.csproj", "{380DD490-8F01-4025-91D7-74C289AA5B75}"
|
||||||
EndProject
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|x64 = Debug|x64
|
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|x64.Build.0 = Release|Any CPU
|
||||||
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x86.ActiveCfg = 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
|
{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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -87,5 +99,6 @@ Global
|
|||||||
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
|
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
|
||||||
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
|
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
|
||||||
{380DD490-8F01-4025-91D7-74C289AA5B75} = {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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
Reference in New Issue
Block a user