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;
|
||||
|
||||
public enum EEngineNetworkVersionHistory
|
||||
public enum EEngineNetworkVersionHistory : uint
|
||||
{
|
||||
HISTORY_INITIAL = 1,
|
||||
HISTORY_REPLAY_BACKWARDS_COMPAT = 2, // Bump version to get rid of older replays before backwards compat was turned on officially
|
||||
|
||||
@@ -4,11 +4,11 @@ namespace Prospect.Unreal.Core;
|
||||
|
||||
public class FUrl
|
||||
{
|
||||
public string Protocol { get; init; } = "unreal";
|
||||
public IPAddress Host { get; init; } = IPAddress.Any;
|
||||
public int Port { get; init; } = 7777;
|
||||
public string Map { get; init; } = "GearStart";
|
||||
public string RedirectUrl { get; init; } = string.Empty;
|
||||
public List<string> Options { get; init; } = new List<string>();
|
||||
public string Portal { get; init; } = string.Empty;
|
||||
public string Protocol { get; set; } = "unreal";
|
||||
public IPAddress Host { get; set; } = IPAddress.Any;
|
||||
public int Port { get; set; } = 7777;
|
||||
public string Map { get; set; } = "GearStart";
|
||||
public string RedirectUrl { get; set; } = string.Empty;
|
||||
public List<string> Options { get; set; } = new List<string>();
|
||||
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))
|
||||
{
|
||||
result = new FReceivedPacketView(
|
||||
new FPacketDataView(packet.Buffer, packet.Buffer.Length),
|
||||
new FPacketDataView(packet.Buffer, packet.Buffer.Length, ECountUnits.Bytes),
|
||||
packet.Address,
|
||||
new FInPacketTraits());
|
||||
|
||||
@@ -58,10 +58,10 @@ public class FReceiveThreadRunnable : IAsyncDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.Buffer.Length > CoreNet.MaxPacketSize)
|
||||
if (result.Buffer.Length > UNetConnection.MaxPacketSize)
|
||||
{
|
||||
Logger.Warning("Received packet exceeding MaxPacketSize ({Size} > {Max}) from {Ip}",
|
||||
result.Buffer.Length, CoreNet.MaxPacketSize, result.RemoteEndPoint);
|
||||
result.Buffer.Length, UNetConnection.MaxPacketSize, result.RemoteEndPoint);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,11 @@ public class PacketHandler
|
||||
|
||||
if (!_bConnectionlessHandler)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
// TODO: Load from .ini file (GEngineIni)
|
||||
// %s PacketHandlerProfileConfig (Driver..)
|
||||
// Components
|
||||
|
||||
// If no matches, load from PacketHandlerComponents / Components in GEngineIni
|
||||
}
|
||||
|
||||
// TODO: FEncryptionComponent.
|
||||
@@ -93,6 +97,19 @@ public class PacketHandler
|
||||
return Outgoing_Internal(packet, countBits, traits, true, address);
|
||||
}
|
||||
|
||||
public void BeginHandshaking()
|
||||
{
|
||||
// bBeganHandshaking = true;
|
||||
|
||||
foreach (var component in _handlerComponents)
|
||||
{
|
||||
if (component.RequiresHandshake() && !component.IsInitialized())
|
||||
{
|
||||
component.NotifiyHandshakeBegin();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public HandlerComponent AddHandler<T>() where T : HandlerComponent
|
||||
{
|
||||
var result = (HandlerComponent) Activator.CreateInstance(typeof(T), this)!;
|
||||
@@ -102,6 +119,16 @@ public class PacketHandler
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool Incoming(FReceivedPacketView packetView)
|
||||
{
|
||||
return Incoming_Internal(packetView);
|
||||
}
|
||||
|
||||
public void IncomingHigh(FBitReader reader)
|
||||
{
|
||||
// NO-OP
|
||||
}
|
||||
|
||||
private bool Incoming_Internal(FReceivedPacketView packetView)
|
||||
{
|
||||
var returnVal = true;
|
||||
@@ -159,7 +186,7 @@ public class PacketHandler
|
||||
{
|
||||
ReplaceIncomingPacket(processPacketReader);
|
||||
|
||||
packetView.DataView = new FPacketDataView(_incomingPacket.GetBuffer(), _incomingPacket.GetBitsLeft());
|
||||
packetView.DataView = new FPacketDataView(_incomingPacket.GetBuffer(), _incomingPacket.GetBitsLeft(), ECountUnits.Bits);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using Prospect.Unreal.Core;
|
||||
|
||||
namespace Prospect.Unreal.Net;
|
||||
|
||||
@@ -8,15 +9,26 @@ public class FInPacketTraits
|
||||
public bool FromRecentlyDisconnected { get; set; }
|
||||
}
|
||||
|
||||
public class FPacketDataView
|
||||
public readonly struct FPacketDataView
|
||||
{
|
||||
private readonly byte[] _data;
|
||||
private readonly int _count;
|
||||
private readonly int _countBits;
|
||||
|
||||
public FPacketDataView(byte[] data, int length)
|
||||
public FPacketDataView(byte[] data, int length, ECountUnits unit)
|
||||
{
|
||||
_data = data;
|
||||
_countBits = length * 8;
|
||||
|
||||
if (unit == ECountUnits.Bits)
|
||||
{
|
||||
_count = FMath.DivideAndRoundUp(length, 8);
|
||||
_countBits = length;
|
||||
}
|
||||
else /* if (unit == ECountUnits.Bytes) */
|
||||
{
|
||||
_count = length;
|
||||
_countBits = length * 8;
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] GetData()
|
||||
@@ -31,7 +43,7 @@ public class FPacketDataView
|
||||
|
||||
public int NumBytes()
|
||||
{
|
||||
return _data.Length;
|
||||
return _count;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
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)
|
||||
@@ -181,14 +227,18 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
||||
var bRestartHandshake = false;
|
||||
var secretId = (byte) 0;
|
||||
var timestamp = 1.0d;
|
||||
var cookie = new byte[CookieByteSize];
|
||||
var origCookie = new byte[CookieByteSize];
|
||||
Span<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.Server)
|
||||
if (Handler.Mode == HandlerMode.Client)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
else if (Handler.Mode == HandlerMode.Server)
|
||||
{
|
||||
var bInitialConnect = timestamp == 0.0;
|
||||
if (bInitialConnect)
|
||||
@@ -206,7 +256,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
||||
if (bValidCookieLifetime && bValidSecretIdTimestamp)
|
||||
{
|
||||
// Regenerate the cookie from the packet info, and see if the received cookie matches the regenerated one
|
||||
var regenCookie = new byte[CookieByteSize];
|
||||
Span<byte> regenCookie = stackalloc byte[CookieByteSize];
|
||||
|
||||
GenerateCookie(address, secretId, timestamp, regenCookie);
|
||||
|
||||
@@ -216,16 +266,17 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
||||
{
|
||||
if (bRestartHandshake)
|
||||
{
|
||||
Buffer.BlockCopy(origCookie, 0, _authorisedCookie, 0, _authorisedCookie.Length);
|
||||
origCookie.CopyTo(_authorisedCookie);
|
||||
}
|
||||
else
|
||||
{
|
||||
var curSequence = BinaryPrimitives.ReadInt16LittleEndian(cookie);
|
||||
var seqA = BinaryPrimitives.ReadInt16LittleEndian(cookie);
|
||||
var seqB = BinaryPrimitives.ReadInt16LittleEndian(cookie.Slice(2));
|
||||
|
||||
_lastServerSequence = curSequence & (UNetConnection.MaxPacketId - 1);
|
||||
_lastClientSequence = (curSequence + 1) & (UNetConnection.MaxPacketId - 1);
|
||||
_lastServerSequence = seqA & (UNetConnection.MaxPacketId - 1);
|
||||
_lastClientSequence = seqB & (UNetConnection.MaxPacketId - 1);
|
||||
|
||||
Buffer.BlockCopy(cookie, 0, _authorisedCookie, 0, _authorisedCookie.Length);
|
||||
cookie.CopyTo(_authorisedCookie);
|
||||
}
|
||||
|
||||
_bRestartedHandshake = bRestartHandshake;
|
||||
@@ -262,7 +313,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
||||
FBitReader packet,
|
||||
ref bool bOutRestartHandshake,
|
||||
ref byte outSecretId,
|
||||
ref double outTimestamp, byte[] outCookie, byte[] outOrigCookie)
|
||||
ref double outTimestamp, Span<byte> outCookie, Span<byte> outOrigCookie)
|
||||
{
|
||||
var bValidPacket = false;
|
||||
var bitsLeft = packet.GetBitsLeft();
|
||||
@@ -314,7 +365,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
||||
var bHandshakePacket = (byte)1;
|
||||
var bRestartHandshake = (byte)0;
|
||||
var timestamp = _driver.GetElapsedTime();
|
||||
var cookie = new byte[CookieByteSize];
|
||||
Span<byte> cookie = stackalloc byte[CookieByteSize];
|
||||
|
||||
GenerateCookie(address, _activeSecret, timestamp, cookie);
|
||||
|
||||
@@ -329,7 +380,7 @@ public class StatelessConnectHandlerComponent : HandlerComponent
|
||||
challengePacket.WriteDouble(timestamp);
|
||||
challengePacket.Serialize(cookie, cookie.Length);
|
||||
|
||||
Logger.Verbose("SendConnectChallenge. Timestamp: {Timestamp}, Cookie: {Cookie}", timestamp, cookie);
|
||||
Logger.Verbose("SendConnectChallenge. Timestamp: {Timestamp}, Cookie: {Cookie}", timestamp, Convert.ToHexString(cookie));
|
||||
|
||||
CapHandshakePacket(challengePacket);
|
||||
|
||||
|
||||
@@ -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 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)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
base.InitBase(inDriver, inSocket, inURL, inState,
|
||||
(inMaxPacket == 0 || inMaxPacket > MaxPacketSize) ? MaxPacketSize : inMaxPacket,
|
||||
inPacketOverhead == 0 ? UdpHeaderSize : inPacketOverhead);
|
||||
|
||||
Socket = inSocket;
|
||||
}
|
||||
|
||||
public override void InitRemoteConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, IPEndPoint inRemoteAddr, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
InitBase(inDriver, inSocket, inURL, inState,
|
||||
(inMaxPacket == 0 || inMaxPacket > MaxPacketSize) ? MaxPacketSize : inMaxPacket,
|
||||
inPacketOverhead == 0 ? UdpHeaderSize : inPacketOverhead);
|
||||
|
||||
RemoteAddr = inRemoteAddr;
|
||||
Url.Host = RemoteAddr.Address;
|
||||
|
||||
SetClientLoginState(EClientLoginState.LoggingIn);
|
||||
SetExpectedClientLoginMsgType(0); // TODO: NMT_HELLO
|
||||
}
|
||||
|
||||
public override void InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState,
|
||||
int inMaxPacket = 0, int inPacketOverhead = 0)
|
||||
public override void InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits)
|
||||
@@ -47,9 +73,11 @@ public class UIpConnection : UNetConnection
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void ReceivedRawPacket(byte[] data, int count)
|
||||
public override void ReceivedRawPacket(FReceivedPacketView packetView)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
_socketErrorRecvDelayStartTime = 0;
|
||||
|
||||
base.ReceivedRawPacket(packetView);
|
||||
}
|
||||
|
||||
public override float GetTimeoutValue()
|
||||
|
||||
@@ -53,19 +53,27 @@ public class UIpNetDriver : UNetDriver
|
||||
{
|
||||
MappedClientConnections.TryGetValue(packet.Address, out connection);
|
||||
}
|
||||
|
||||
var bIgnorePacket = false;
|
||||
|
||||
// If we didn't find a client connection, maybe create a new one.
|
||||
if (connection == null)
|
||||
{
|
||||
connection = ProcessConnectionlessPacket(packet);
|
||||
// TODO: bIgnorePacket
|
||||
bIgnorePacket = packet.DataView.NumBytes() == 0;
|
||||
}
|
||||
|
||||
// Send the packet to the connection for processing.
|
||||
if (connection != null && !bIgnorePacket)
|
||||
{
|
||||
connection.ReceivedRawPacket(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UNetConnection? ProcessConnectionlessPacket(FReceivedPacketView packet)
|
||||
{
|
||||
UNetConnection? returnVal;
|
||||
UNetConnection? returnVal = null;
|
||||
var statelessConnect = StatelessConnectComponent;
|
||||
var address = packet.Address;
|
||||
var bPassedChallenge = false;
|
||||
@@ -95,7 +103,7 @@ public class UIpNetDriver : UNetDriver
|
||||
bIgnorePacket = false;
|
||||
}
|
||||
|
||||
packet.DataView = new FPacketDataView(workingData, newCountBytes);
|
||||
packet.DataView = new FPacketDataView(workingData, newCountBytes, ECountUnits.Bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,8 +128,13 @@ public class UIpNetDriver : UNetDriver
|
||||
}
|
||||
|
||||
returnVal.InitRemoteConnection(this, Socket, World != null ? World.Url : new FUrl(), address, EConnectionState.USOCK_Open);
|
||||
|
||||
// if (returnVal.Handler)
|
||||
|
||||
if (returnVal.Handler != null)
|
||||
{
|
||||
returnVal.Handler.BeginHandshaking();
|
||||
}
|
||||
|
||||
AddClientConnection(returnVal);
|
||||
}
|
||||
|
||||
if (statelessConnect != null)
|
||||
@@ -132,10 +145,10 @@ public class UIpNetDriver : UNetDriver
|
||||
|
||||
if (bIgnorePacket)
|
||||
{
|
||||
packet.DataView = new FPacketDataView(packet.DataView.GetData(), 0);
|
||||
packet.DataView = new FPacketDataView(packet.DataView.GetData(), 0, ECountUnits.Bits);
|
||||
}
|
||||
|
||||
return null;
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
public override void LowLevelSend(IPEndPoint address, byte[] data, int countBits, FOutPacketTraits traits)
|
||||
|
||||
@@ -1,15 +1,193 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using Prospect.Unreal.Core;
|
||||
using Prospect.Unreal.Core.Names;
|
||||
using Prospect.Unreal.Exceptions;
|
||||
using Prospect.Unreal.Net.Channels;
|
||||
using Prospect.Unreal.Net.Packets.Bunch;
|
||||
using Prospect.Unreal.Net.Packets.Header;
|
||||
using Prospect.Unreal.Net.Packets.Header.Sequence;
|
||||
using Prospect.Unreal.Serialization;
|
||||
using Serilog;
|
||||
|
||||
namespace Prospect.Unreal.Net;
|
||||
|
||||
public abstract class UNetConnection
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<UNetConnection>();
|
||||
|
||||
public const int ReliableBuffer = 256;
|
||||
public const int MaxPacketId = 16384;
|
||||
public const int MaxChSequence = 1024;
|
||||
public const int MaxBunchHeaderBits = 256;
|
||||
public const int MaxPacketSize = 1024;
|
||||
|
||||
public const int DefaultMaxChannelSize = 32767;
|
||||
|
||||
public const int MaxJitterClockTimeValue = 1023;
|
||||
public const int NumBitsForJitterClockTimeInHeader = 10;
|
||||
|
||||
public const EEngineNetworkVersionHistory DefaultEngineNetworkProtocolVersion = EEngineNetworkVersionHistory.HISTORY_ENGINENETVERSION_LATEST;
|
||||
public const uint DefaultGameNetworkProtocolVersion = 0;
|
||||
|
||||
private List<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 InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0);
|
||||
public abstract void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits);
|
||||
@@ -17,11 +195,491 @@ public abstract class UNetConnection
|
||||
public abstract string LowLevelDescribe();
|
||||
public abstract void Tick(float deltaSeconds);
|
||||
public abstract void CleanUp();
|
||||
public abstract void ReceivedRawPacket(byte[] data, int count);
|
||||
|
||||
public virtual void ReceivedRawPacket(FReceivedPacketView packetView)
|
||||
{
|
||||
if (Handler != null)
|
||||
{
|
||||
var result = Handler.Incoming(packetView);
|
||||
if (result)
|
||||
{
|
||||
if (packetView.DataView.NumBytes() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Close connection, malformed packet.
|
||||
Logger.Fatal("Connection should be closed here");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle an incoming raw packet from the driver.
|
||||
if (packetView.DataView.NumBytes() > 0)
|
||||
{
|
||||
var data = packetView.DataView.GetData();
|
||||
var count = packetView.DataView.NumBytes();
|
||||
|
||||
var lastByte = data[count - 1];
|
||||
if (lastByte != 0)
|
||||
{
|
||||
var bitSize = (count * 8) - 1;
|
||||
|
||||
// Bit streaming, starts at the Least Significant Bit, and ends at the MSB.
|
||||
while ((lastByte & 0x80) == 0)
|
||||
{
|
||||
lastByte *= 2;
|
||||
bitSize--;
|
||||
}
|
||||
|
||||
var reader = new FBitReader(data, bitSize);
|
||||
|
||||
reader.SetEngineNetVer(EngineNetworkProtocolVersion);
|
||||
reader.SetGameNetVer(GameNetworkProtocolVersion);
|
||||
|
||||
if (Handler != null)
|
||||
{
|
||||
Handler.IncomingHigh(reader);
|
||||
}
|
||||
|
||||
if (reader.GetBitsLeft() > 0)
|
||||
{
|
||||
ReceivedPacket(reader);
|
||||
|
||||
// TODO: Flush out of order cache
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReceivedPacket(FBitReader reader, bool bIsReinjectedPacket = false)
|
||||
{
|
||||
if (reader.IsError())
|
||||
{
|
||||
Logger.Error("Packet too small");
|
||||
return;
|
||||
}
|
||||
|
||||
var resetReaderMark = new FBitReaderMark(reader);
|
||||
|
||||
if (_bInternalAck)
|
||||
{
|
||||
++InPacketId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Read packet header.
|
||||
var header = new FNotificationHeader();
|
||||
|
||||
if (!PacketNotify.ReadHeader(ref header, reader))
|
||||
{
|
||||
// TODO: Close connection, malformed packet.
|
||||
Logger.Fatal("Failed to read PacketHeader");
|
||||
return;
|
||||
}
|
||||
|
||||
var bHasPacketInfoPayload = true;
|
||||
|
||||
if (reader.EngineNetVer() > EEngineNetworkVersionHistory.HISTORY_JITTER_IN_HEADER)
|
||||
{
|
||||
bHasPacketInfoPayload = reader.ReadBit();
|
||||
|
||||
if (bHasPacketInfoPayload)
|
||||
{
|
||||
var bitsReadPreJitterClock = reader.GetPosBits();
|
||||
|
||||
var packetJitterClockTimeMs = reader.ReadInt(MaxJitterClockTimeValue + 1);
|
||||
|
||||
if (reader.GetPosBits() - bitsReadPreJitterClock != NumBitsForJitterClockTimeInHeader)
|
||||
{
|
||||
throw new UnrealNetException("JitterClockTime did not read the expected amount of bits");
|
||||
}
|
||||
|
||||
if (!bIsReinjectedPacket)
|
||||
{
|
||||
ProcessJitter(packetJitterClockTimeMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var packetSequenceDelta = PacketNotify.GetSequenceDelta(header);
|
||||
if (packetSequenceDelta > 0)
|
||||
{
|
||||
var bPacketOrderCacheActive = !_bFlushingPacketOrderCache && _packetOrderCache != null;
|
||||
var bCheckForMissingSequence = bPacketOrderCacheActive && _packetOrderCacheCount == 0;
|
||||
var bFillingPacketOrderCache = bPacketOrderCacheActive && _packetOrderCacheCount > 0;
|
||||
|
||||
const int maxMissingPackets = 0; // CVarNetPacketOrderMaxMissingPackets
|
||||
var missingPacketCount = packetSequenceDelta - 1;
|
||||
|
||||
// Cache the packet if we are already caching, and begin caching if we just encountered a missing sequence, within range
|
||||
if (bFillingPacketOrderCache || (bCheckForMissingSequence && missingPacketCount > 0 && missingPacketCount <= maxMissingPackets))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (missingPacketCount > 10)
|
||||
{
|
||||
Logger.Verbose("High single frame packet loss, PacketsLost: {Amount}", missingPacketCount);
|
||||
}
|
||||
|
||||
// TODO: Increment things
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Increment things
|
||||
// TODO: PacketOrderCache
|
||||
return;
|
||||
}
|
||||
|
||||
// Update incoming sequence data and deliver packet notifications
|
||||
// Packet is only accepted if both the incoming sequence number and incoming ack data are valid
|
||||
PacketNotify.Update(header, (ackedSequence, delivered) =>
|
||||
{
|
||||
// TODO: Increment things
|
||||
|
||||
if (!new SequenceNumber((ushort)LastNotifiedPacketId).Equals(ackedSequence))
|
||||
{
|
||||
// TODO: Close connection.
|
||||
Logger.Fatal("LastNotifiedPacketId != AckedSequence");
|
||||
return;
|
||||
}
|
||||
|
||||
if (delivered)
|
||||
{
|
||||
// ReceivedAck(LastNotifiedPacketId, ChannelsToClose)
|
||||
Logger.Verbose("TODO: ReceivedAck");
|
||||
}
|
||||
else
|
||||
{
|
||||
// ReceivedNak(LastNotifiedPacketId)
|
||||
Logger.Verbose("TODO: ReceivedNak");
|
||||
}
|
||||
});
|
||||
|
||||
// Extra information associated with the header (read only after acks have been processed)
|
||||
if (packetSequenceDelta > 0 && !ReadPacketInfo(reader, bHasPacketInfoPayload))
|
||||
{
|
||||
// TODO: Close connection.
|
||||
Logger.Fatal("Failed to read PacketHeader");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var bIgnoreRPCS = Driver!.ShouldIgnoreRPCs();
|
||||
var bSickAcp = false;
|
||||
|
||||
// Track channels that were rejected while processing this packet - used to avoid sending multiple close-channel bunches,
|
||||
// which would cause a disconnect serverside
|
||||
var rejectedChans = new List<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 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.Net;
|
||||
using Prospect.Unreal.Net.Channels;
|
||||
using Prospect.Unreal.Runtime;
|
||||
|
||||
namespace Prospect.Unreal.Net;
|
||||
@@ -10,6 +11,15 @@ public abstract class UNetDriver : IAsyncDisposable
|
||||
|
||||
protected UNetDriver()
|
||||
{
|
||||
GuidCache = new FNetGUIDCache(this);
|
||||
// TODO: Load from Engine ini
|
||||
ChannelDefinitions = new List<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>();
|
||||
}
|
||||
|
||||
@@ -18,6 +28,18 @@ public abstract class UNetDriver : IAsyncDisposable
|
||||
/// </summary>
|
||||
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>
|
||||
/// Map of <see cref="IPEndPoint"/> to <see cref="UNetConnection"/>.
|
||||
/// </summary>
|
||||
@@ -75,6 +97,16 @@ public abstract class UNetDriver : IAsyncDisposable
|
||||
// TODO: AddInitialObjects?
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsServer()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual bool ShouldIgnoreRPCs()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public double GetElapsedTime()
|
||||
{
|
||||
@@ -85,7 +117,34 @@ public abstract class UNetDriver : IAsyncDisposable
|
||||
{
|
||||
_elapsedTime = 0.0;
|
||||
}
|
||||
|
||||
|
||||
private protected void AddClientConnection(UNetConnection newConnection)
|
||||
{
|
||||
ClientConnections.Add(newConnection);
|
||||
|
||||
if (newConnection.RemoteAddr != null)
|
||||
{
|
||||
MappedClientConnections[newConnection.RemoteAddr] = newConnection;
|
||||
|
||||
// TODO: RecentlyDisconnectedClients ?
|
||||
}
|
||||
|
||||
CreateInitialServerChannels(newConnection);
|
||||
|
||||
// TODO: NetworkObjectList > HandleConnectionAdded
|
||||
}
|
||||
|
||||
private void CreateInitialServerChannels(UNetConnection clientConnection)
|
||||
{
|
||||
foreach (var channelDef in ChannelDefinitions)
|
||||
{
|
||||
if (channelDef.InitialServer)
|
||||
{
|
||||
clientConnection.CreateChannelByName(channelDef.Name, EChannelCreateFlags.OpenedLocally, channelDef.StaticChannelIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual ValueTask DisposeAsync()
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
|
||||
@@ -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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal", "Prospect.Unreal\Prospect.Unreal.csproj", "{380DD490-8F01-4025-91D7-74C289AA5B75}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{168B6247-2D33-4942-AB3F-C5041B3BDFEA}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Tests", "Prospect.Unreal.Tests\Prospect.Unreal.Tests.csproj", "{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
@@ -73,6 +77,14 @@ Global
|
||||
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x64.Build.0 = Release|Any CPU
|
||||
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x86.Build.0 = Release|Any CPU
|
||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x64.Build.0 = Release|Any CPU
|
||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -87,5 +99,6 @@ Global
|
||||
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
|
||||
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
|
||||
{380DD490-8F01-4025-91D7-74C289AA5B75} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
|
||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34} = {168B6247-2D33-4942-AB3F-C5041B3BDFEA}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
Reference in New Issue
Block a user