From 7c6353c475bd0a238e2f2bc6f4a0d4335f41cd03 Mon Sep 17 00:00:00 2001 From: AeonLucid Date: Thu, 13 Jan 2022 01:04:54 +0100 Subject: [PATCH] Handle NMT Login packet --- src/Prospect.Server.Game/Program.cs | 3 + .../NetControlMessageSourceGenerator.cs | 1 + .../Core/FStringTests.cs | 27 +++ src/Prospect.Unreal/Core/FEngineVersion.cs | 3 +- src/Prospect.Unreal/Core/FString.cs | 20 +- src/Prospect.Unreal/Core/FUrl.cs | 69 ++++++- src/Prospect.Unreal/Net/Actors/AActor.cs | 6 + .../Net/Actors/AGameModeBase.cs | 10 + src/Prospect.Unreal/Net/Actors/AInfo.cs | 6 + .../{Player => Actors}/APlayerController.cs | 0 .../Net/EUniqueIdEncodingFlags.cs | 23 +++ src/Prospect.Unreal/Net/FUniqueNetId.cs | 16 ++ src/Prospect.Unreal/Net/FUniqueNetIdRepl.cs | 152 +++++++++++++- .../Net/Packets/Header/FNetPacketNotify.cs | 29 ++- src/Prospect.Unreal/Net/UChildConnection.cs | 34 ++++ src/Prospect.Unreal/Net/UNetConnection.cs | 162 +++++++++++---- src/Prospect.Unreal/Net/UNetDriver.cs | 5 +- src/Prospect.Unreal/Net/UPackageMap.cs | 10 + src/Prospect.Unreal/Runtime/UGameInstance.cs | 13 ++ .../{Net/Player => Runtime}/UPlayer.cs | 2 + src/Prospect.Unreal/Runtime/UWorld.cs | 188 +++++++++++++++++- 21 files changed, 722 insertions(+), 57 deletions(-) create mode 100644 src/Prospect.Unreal.Tests/Core/FStringTests.cs create mode 100644 src/Prospect.Unreal/Net/Actors/AActor.cs create mode 100644 src/Prospect.Unreal/Net/Actors/AGameModeBase.cs create mode 100644 src/Prospect.Unreal/Net/Actors/AInfo.cs rename src/Prospect.Unreal/Net/{Player => Actors}/APlayerController.cs (100%) create mode 100644 src/Prospect.Unreal/Net/EUniqueIdEncodingFlags.cs create mode 100644 src/Prospect.Unreal/Net/FUniqueNetId.cs create mode 100644 src/Prospect.Unreal/Net/UChildConnection.cs create mode 100644 src/Prospect.Unreal/Runtime/UGameInstance.cs rename src/Prospect.Unreal/{Net/Player => Runtime}/UPlayer.cs (71%) diff --git a/src/Prospect.Server.Game/Program.cs b/src/Prospect.Server.Game/Program.cs index a0a93c4..85fda31 100644 --- a/src/Prospect.Server.Game/Program.cs +++ b/src/Prospect.Server.Game/Program.cs @@ -1,4 +1,5 @@ using Prospect.Unreal.Core; +using Prospect.Unreal.Runtime; using Serilog; namespace Prospect.Server.Game; @@ -33,6 +34,8 @@ internal static class Program await using (var world = new ProspectWorld(worldUrl)) { + world.SetGameInstance(new UGameInstance()); + world.SetGameMode(worldUrl); world.Listen(); while (await Tick.WaitForNextTickAsync()) diff --git a/src/Prospect.Unreal.Generator/NetControlMessageSourceGenerator.cs b/src/Prospect.Unreal.Generator/NetControlMessageSourceGenerator.cs index 5c84e83..80fee1f 100644 --- a/src/Prospect.Unreal.Generator/NetControlMessageSourceGenerator.cs +++ b/src/Prospect.Unreal.Generator/NetControlMessageSourceGenerator.cs @@ -103,6 +103,7 @@ namespace Prospect.Unreal.Generator { "int", new ParamDef("bunch.ReadInt32()", "bunch.WriteInt32({0})") }, { "uint", new ParamDef("bunch.ReadUInt32()", "bunch.WriteUInt32({0})") }, { "FString", new ParamDef("bunch.ReadString()", "bunch.WriteString({0})") }, + { "FUniqueNetIdRepl", new ParamDef("FUniqueNetIdRepl.Read(bunch)", "FUniqueNetIdRepl.Write(bunch, {0})") } }; public static string SendParams(List args) diff --git a/src/Prospect.Unreal.Tests/Core/FStringTests.cs b/src/Prospect.Unreal.Tests/Core/FStringTests.cs new file mode 100644 index 0000000..8871d8a --- /dev/null +++ b/src/Prospect.Unreal.Tests/Core/FStringTests.cs @@ -0,0 +1,27 @@ +using NUnit.Framework; +using Prospect.Unreal.Core; +using Prospect.Unreal.Serialization; + +namespace Prospect.Unreal.Tests.Core; + +public class FStringTests +{ + [Test] + [TestCase("")] + [TestCase("TestString")] + [TestCase("/Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap")] + [TestCase("/Script/ThirdPersonMP.ThirdPersonMPGameMode")] + public void TestWriteRead(string testValue) + { + // Write a string. + using var writer = new FNetBitWriter(4096); + + FString.Serialize(writer, testValue); + + // Create a reader. + var reader = new FNetBitReader(null, writer.GetData(), (int)writer.GetNumBits()); + var read = FString.Deserialize(reader); + + Assert.AreEqual(testValue, read); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/FEngineVersion.cs b/src/Prospect.Unreal/Core/FEngineVersion.cs index fb8df37..cf76699 100644 --- a/src/Prospect.Unreal/Core/FEngineVersion.cs +++ b/src/Prospect.Unreal/Core/FEngineVersion.cs @@ -1,5 +1,4 @@ -using Prospect.Unreal.Net; -using Prospect.Unreal.Serialization; +using Prospect.Unreal.Serialization; namespace Prospect.Unreal.Core; diff --git a/src/Prospect.Unreal/Core/FString.cs b/src/Prospect.Unreal/Core/FString.cs index ec84913..c6d8a62 100644 --- a/src/Prospect.Unreal/Core/FString.cs +++ b/src/Prospect.Unreal/Core/FString.cs @@ -41,13 +41,23 @@ public static class FString } else { - var num = value.Length + 1; - var valueBytes = num > 128 ? new byte[num] : stackalloc byte[num]; + var num = value.Length; + if (num != 0) + { + // Add null terminator. + num += 1; + + var valueBytes = num > 128 ? new byte[num] : stackalloc byte[num]; - Encoding.ASCII.GetBytes(value, valueBytes); + Encoding.ASCII.GetBytes(value, valueBytes); - archive.WriteInt32(num); - archive.Serialize(valueBytes, num); + archive.WriteInt32(num); + archive.Serialize(valueBytes, num); + } + else + { + archive.WriteInt32(0); + } } } diff --git a/src/Prospect.Unreal/Core/FUrl.cs b/src/Prospect.Unreal/Core/FUrl.cs index dab6c3f..a2dd510 100644 --- a/src/Prospect.Unreal/Core/FUrl.cs +++ b/src/Prospect.Unreal/Core/FUrl.cs @@ -1,14 +1,75 @@ using System.Net; +using System.Text; namespace Prospect.Unreal.Core; public class FUrl { - 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"; + private const string DefaultProtocol = "unreal"; + private static readonly IPAddress DefaultHost = IPAddress.Any; + private const int DefaultPort = 7777; + + public string Protocol { get; set; } = DefaultProtocol; + public IPAddress Host { get; set; } = DefaultHost; + public int Port { get; set; } = DefaultPort; + public string Map { get; set; } = "GearStart"; // TODO: UGameMapsSettings::GetGameDefaultMap() public string RedirectUrl { get; set; } = string.Empty; public List Options { get; set; } = new List(); public string Portal { get; set; } = string.Empty; + public bool Valid { get; set; } = true; + + public string ToString(bool fullyQualified) + { + var result = new StringBuilder(); + + // Emit protocol. + if ((Protocol != DefaultProtocol) || fullyQualified) + { + result.Append(Protocol); + result.Append(':'); + + if (!Equals(Host, DefaultHost)) + { + result.Append("//"); + } + } + + // Emit host and port + if (!Equals(Host, DefaultHost) || (Port != DefaultPort)) + { + result.Append(Port); + + if (!Map.StartsWith("/") && !Map.StartsWith("\\")) + { + result.Append('/'); + } + } + + // Emit map. + if (!string.IsNullOrEmpty(Map)) + { + result.Append(Map); + } + + // Emit options. + foreach (var option in Options) + { + result.Append('?'); + result.Append(option); + } + + // Emit portal. + if (!string.IsNullOrEmpty(Portal)) + { + result.Append('#'); + result.Append(Portal); + } + + return result.ToString(); + } + + public override string ToString() + { + return ToString(false); + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Actors/AActor.cs b/src/Prospect.Unreal/Net/Actors/AActor.cs new file mode 100644 index 0000000..7897d12 --- /dev/null +++ b/src/Prospect.Unreal/Net/Actors/AActor.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net.Actors; + +public class AActor +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Actors/AGameModeBase.cs b/src/Prospect.Unreal/Net/Actors/AGameModeBase.cs new file mode 100644 index 0000000..16252d3 --- /dev/null +++ b/src/Prospect.Unreal/Net/Actors/AGameModeBase.cs @@ -0,0 +1,10 @@ +namespace Prospect.Unreal.Net.Actors; + +public class AGameModeBase : AInfo +{ + public void PreLogin(string options, string address, FUniqueNetIdRepl uniqueId, out string? errorMessage) + { + // Login unique id must match server expected unique id type OR No unique id could mean game doesn't use them + errorMessage = null; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Actors/AInfo.cs b/src/Prospect.Unreal/Net/Actors/AInfo.cs new file mode 100644 index 0000000..912fe5e --- /dev/null +++ b/src/Prospect.Unreal/Net/Actors/AInfo.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net.Actors; + +public class AInfo : AActor +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Player/APlayerController.cs b/src/Prospect.Unreal/Net/Actors/APlayerController.cs similarity index 100% rename from src/Prospect.Unreal/Net/Player/APlayerController.cs rename to src/Prospect.Unreal/Net/Actors/APlayerController.cs diff --git a/src/Prospect.Unreal/Net/EUniqueIdEncodingFlags.cs b/src/Prospect.Unreal/Net/EUniqueIdEncodingFlags.cs new file mode 100644 index 0000000..9050a28 --- /dev/null +++ b/src/Prospect.Unreal/Net/EUniqueIdEncodingFlags.cs @@ -0,0 +1,23 @@ +namespace Prospect.Unreal.Net; + +[Flags] +public enum EUniqueIdEncodingFlags +{ + /** Default, nothing encoded, use normal FString serialization */ + NotEncoded = 0, + /** Data is optimized based on some assumptions (even number of [0-9][a-f][A-F] that can be packed into nibbles) */ + IsEncoded = (1 << 0), + /** This unique id is empty or invalid, nothing further to serialize */ + IsEmpty = (1 << 1), + /** Reserved for future use */ + Unused1 = (1 << 2), + /** Remaining bits are used for encoding the type without requiring another byte */ + Reserved1 = (1 << 3), + Reserved2 = (1 << 4), + Reserved3 = (1 << 5), + Reserved4 = (1 << 6), + Reserved5 = (1 << 7), + /** Helper masks */ + FlagsMask = (Reserved1 - 1), + TypeMask = (255 ^ FlagsMask) +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FUniqueNetId.cs b/src/Prospect.Unreal/Net/FUniqueNetId.cs new file mode 100644 index 0000000..9c381d4 --- /dev/null +++ b/src/Prospect.Unreal/Net/FUniqueNetId.cs @@ -0,0 +1,16 @@ +namespace Prospect.Unreal.Net; + +public class FUniqueNetId +{ + public FUniqueNetId(string contents) + { + Contents = contents; + } + + public string Contents { get; } + + public bool IsValid() + { + return true; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FUniqueNetIdRepl.cs b/src/Prospect.Unreal/Net/FUniqueNetIdRepl.cs index ae85707..6a30249 100644 --- a/src/Prospect.Unreal/Net/FUniqueNetIdRepl.cs +++ b/src/Prospect.Unreal/Net/FUniqueNetIdRepl.cs @@ -1,5 +1,155 @@ -namespace Prospect.Unreal.Net; +using Prospect.Unreal.Core.Names; +using Prospect.Unreal.Serialization; +using Serilog; + +namespace Prospect.Unreal.Net; public class FUniqueNetIdRepl { + private static readonly ILogger Logger = Log.ForContext(); + private const int TypeHash_Other = 31; + + public FUniqueNetId? UniqueNetId { get; private set; } + + public bool IsValid() + { + return UniqueNetId != null && UniqueNetId.IsValid(); + } + + public string ToDebugString() + { + // TODO: Add type + return IsValid() ? $"{UniqueNetId!.Contents}" : "INVALID"; + } + + public static void Write(FArchive ar, FUniqueNetIdRepl value) + { + Serialize(ar, value); + } + + public static FUniqueNetIdRepl Read(FArchive ar) + { + var result = new FUniqueNetIdRepl(); + Serialize(ar, result); + return result; + } + + private void NetSerialize(FArchive ar, UPackageMap? packageMap, out bool bOutSuccess) + { + // TODO: Get back to this later when we understand FName / FNamePool and UOnlineEngineInterface better. + // FUniqueNetIdRepl::NetSerialize + + bOutSuccess = false; + + if (ar.IsSaving()) + { + throw new NotImplementedException(); + } + else if (ar.IsLoading()) + { + UniqueNetId = null; + + var encodingFlags = (EUniqueIdEncodingFlags) ar.ReadByte(); + + if (!ar.IsError()) + { + if ((encodingFlags & EUniqueIdEncodingFlags.IsEncoded) != 0) + { + if ((encodingFlags & EUniqueIdEncodingFlags.IsEmpty) == 0) + { + // Non empty and hex encoded + var typeHash = GetTypeHashFromEncoding(encodingFlags); + if (typeHash == 0) + { + // If no type was encoded, assume default + throw new NotImplementedException(); + // TypeHash = UOnlineEngineInterface::Get()->GetReplicationHashForSubsystem(UOnlineEngineInterface::Get()->GetDefaultOnlineSubsystemName()); + } + + var bValidTypeHash = typeHash != 0; + + if (typeHash == TypeHash_Other) + { + var typeString = ar.ReadString(); + var type = new FName(typeString); + // TODO: Add FName into FNamePool + throw new NotImplementedException(); + if (ar.IsError() || type.Number == (int)UnrealNameKey.None) + { + bValidTypeHash = false; + } + } + else + { + // Type = UOnlineEngineInterface::Get()->GetSubsystemFromReplicationHash(TypeHash); + } + + throw new NotImplementedException(); + } + else + { + bOutSuccess = true; + } + } + else + { + // Original FString serialization goes here + var typeHash = GetTypeHashFromEncoding(encodingFlags); + if (typeHash == 0) + { + // If no type was encoded, assume default + throw new NotImplementedException(); + // TypeHash = UOnlineEngineInterface::Get()->GetReplicationHashForSubsystem(UOnlineEngineInterface::Get()->GetDefaultOnlineSubsystemName()); + } + + var bValidTypeHash = typeHash != 0; + if (typeHash == TypeHash_Other) + { + + } + else + { + // TODO: Type = UOnlineEngineInterface::Get()->GetSubsystemFromReplicationHash(TypeHash); + } + + if (bValidTypeHash) + { + var contents = ar.ReadString(); + if (!ar.IsError()) + { + // TODO: Check if type != none + UniqueNetId = new FUniqueNetId(contents); + bOutSuccess = true; + } + } + else + { + Logger.Warning("Error with encoded type hash"); + } + } + } + else + { + Logger.Warning("Error serializing unique id"); + } + } + } + + private static void Serialize(FArchive ar, FUniqueNetIdRepl uniqueNetId) + { + if (!ar.IsPersistent() || ar._arIsNetArchive) + { + uniqueNetId.NetSerialize(ar, null, out _); + } + else + { + throw new NotSupportedException(); + } + } + + private static byte GetTypeHashFromEncoding(EUniqueIdEncodingFlags inFlags) + { + var typeHash = (byte) ((byte) (inFlags & EUniqueIdEncodingFlags.TypeMask) >> 3); + return (byte)(typeHash < 32 ? typeHash : 0); + } } diff --git a/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs b/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs index ace27bb..530094d 100644 --- a/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs +++ b/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs @@ -4,7 +4,19 @@ using Serilog; namespace Prospect.Unreal.Net.Packets.Header; -public delegate void HandlePacketNotification(SequenceNumber ackedSequence, bool delivered); +public delegate void HandlePacketNotification(PacketNotifyUpdateContext context, SequenceNumber ackedSequence, bool delivered); + +public readonly ref struct PacketNotifyUpdateContext +{ + public readonly HandlePacketNotification Func; + public readonly List ChannelToClose; + + public PacketNotifyUpdateContext(HandlePacketNotification func, List channelToClose) + { + Func = func; + ChannelToClose = channelToClose; + } +} public class FNetPacketNotify { @@ -133,14 +145,14 @@ public class FNetPacketNotify return SequenceNumber.Diff(notificationData.Seq, _inSeq); } - public int Update(FNotificationHeader notificationData, HandlePacketNotification func) + public int Update(FNotificationHeader notificationData, PacketNotifyUpdateContext context) { var inSeqDelta = GetSequenceDelta(notificationData); if (inSeqDelta > 0) { Logger.Verbose("Update - Seq {Seq}, InSeq {InSeq}", notificationData.Seq.Value, _inSeq.Value); - ProcessReceivedAcks(notificationData, func); + ProcessReceivedAcks(notificationData, context); _inSeq = notificationData.Seq; @@ -150,7 +162,7 @@ public class FNetPacketNotify return 0; } - private void ProcessReceivedAcks(FNotificationHeader notificationData, HandlePacketNotification func) + private void ProcessReceivedAcks(FNotificationHeader notificationData, PacketNotifyUpdateContext context) { if (notificationData.AckedSeq.Greater(_outAckSeq)) { @@ -173,7 +185,7 @@ public class FNetPacketNotify while (ackCount > SequenceHistory.Size) { --ackCount; - func(currentAck, false); + context.Func(context, currentAck, false); currentAck = currentAck.IncrementAndGet(); } @@ -181,7 +193,7 @@ public class FNetPacketNotify while (ackCount > 0) { --ackCount; - func(currentAck, notificationData.History.IsDelivered(ackCount)); + context.Func(context, currentAck, notificationData.History.IsDelivered(ackCount)); currentAck = currentAck.IncrementAndGet(); } @@ -231,7 +243,7 @@ public class FNetPacketNotify { while (ackedSeq.Greater(_inAckSeq)) { - _inAckSeq.IncrementAndGet(); + _inAckSeq = _inAckSeq.IncrementAndGet(); var bReportAcked = _inAckSeq.Equals(ackedSeq) ? isAck : false; @@ -246,7 +258,8 @@ public class FNetPacketNotify // Add entry to the ack-record so that we can update the InAckSeqAck when we received the ack for this OutSeq. _ackRecord.Enqueue(new FSentAckData(_outSeq, _writtenInAckSeq)); _writtenHistoryWordCount = 0; + _outSeq = _outSeq.IncrementAndGet(); - return _outSeq.IncrementAndGet(); + return _outSeq; } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UChildConnection.cs b/src/Prospect.Unreal/Net/UChildConnection.cs new file mode 100644 index 0000000..4f83946 --- /dev/null +++ b/src/Prospect.Unreal/Net/UChildConnection.cs @@ -0,0 +1,34 @@ +namespace Prospect.Unreal.Net; + +public class UChildConnection : UNetConnection +{ + public override void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits) + { + throw new NotImplementedException(); + } + + public override string LowLevelGetRemoteAddress(bool bAppendPort = false) + { + throw new NotImplementedException(); + } + + public override string LowLevelDescribe() + { + throw new NotImplementedException(); + } + + public override void Tick(float deltaSeconds) + { + throw new NotImplementedException(); + } + + public override void CleanUp() + { + throw new NotImplementedException(); + } + + public override float GetTimeoutValue() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UNetConnection.cs b/src/Prospect.Unreal/Net/UNetConnection.cs index bddfe8e..590a78d 100644 --- a/src/Prospect.Unreal/Net/UNetConnection.cs +++ b/src/Prospect.Unreal/Net/UNetConnection.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using System.Net; +using System.Net; using System.Net.Sockets; using Prospect.Unreal.Core; using Prospect.Unreal.Core.Names; @@ -38,7 +37,19 @@ public abstract class UNetConnection : UPlayer public const EEngineNetworkVersionHistory DefaultEngineNetworkProtocolVersion = EEngineNetworkVersionHistory.HISTORY_ENGINENETVERSION_LATEST; public const uint DefaultGameNetworkProtocolVersion = 0; + /// + /// The channels that need ticking. This will be a subset of OpenChannels, only including + /// channels that need to process either dormancy or queued bunches. Should be a significant + /// optimization over ticking and calling virtual functions on the potentially hundreds of + /// OpenChannels every frame. + /// private HashSet _channelsToTick; + + /// + /// Online platform ID of remote player on this connection. Only valid on client connections (server side). + /// + private FName _playerOnlinePlatformName; + private List? _packetOrderCache; private int _packetOrderCacheStartIdx; private int _packetOrderCacheCount; @@ -48,6 +59,7 @@ public abstract class UNetConnection : UPlayer private bool _bReplay; private double _statUpdateTime; + private double _lastReceiveTime; private double _lastReceiveRealTime; private double _lastGoodPacketRealtime; @@ -73,16 +85,21 @@ public abstract class UNetConnection : UPlayer private bool _bFlushedNetThisFrame; private bool _bAutoFlush; + private readonly HandlePacketNotification _packetNotifyUpdateDelegate; + public UNetConnection() { _channelsToTick = new HashSet(); + _playerOnlinePlatformName = UnrealNames.FNames[UnrealNameKey.None]; _packetOrderCache = null; _packetOrderCacheStartIdx = 0; _packetOrderCacheCount = 0; _bFlushingPacketOrderCache = false; _bInternalAck = false; _bReplay = false; - + _packetNotifyUpdateDelegate = PacketNotifyUpdate; + + Children = new List(); Driver = null; PackageMap = null; OpenChannels = new List(); @@ -116,6 +133,11 @@ public abstract class UNetConnection : UPlayer new SequenceNumber((ushort)OutPacketId)); } + /// + /// Child connections for secondary viewports + /// + public List Children { get; private set; } + /// /// Owning net driver /// @@ -198,6 +220,11 @@ public abstract class UNetConnection : UPlayer /// public StatelessConnectHandlerComponent? StatelessConnectComponent { get; private set; } + /// + /// Net id of remote player on this connection. Only valid on client connections (server side). + /// + public FUniqueNetIdRepl? PlayerId { get; set; } + /// /// Bytes overhead per packet sent. /// @@ -208,6 +235,21 @@ public abstract class UNetConnection : UPlayer /// public string Challenge { get; private set; } + /// + /// Client-generated response. + /// + public string ClientResponse { get; set; } + + /// + /// The last time an ack was received + /// + public float LastRecvAckTime { get; set; } + + /// + /// The last time an ack was received + /// + public double LastRecvAckTimestamp { get; set; } + /// /// Queued up bits waiting to send /// @@ -321,8 +363,16 @@ public abstract class UNetConnection : UPlayer PackageMap = packageMapClient; } - 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 virtual void InitRemoteConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, IPEndPoint inRemoteAddr, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0) + { + throw new NotImplementedException(); + } + + public virtual void InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0) + { + throw new NotImplementedException(); + } + public abstract void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits); public abstract string LowLevelGetRemoteAddress(bool bAppendPort = false); public abstract string LowLevelDescribe(); @@ -463,44 +513,25 @@ public abstract class UNetConnection : UPlayer } // TODO: Increment things + InPacketId += packetSequenceDelta; } else { // TODO: Increment things // TODO: PacketOrderCache + Logger.Warning("Received out of order packet"); 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"); - } - }); + PacketNotify.Update(header, new PacketNotifyUpdateContext(_packetNotifyUpdateDelegate, channelsToClose)); // 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"); + Close(); return; } } @@ -698,16 +729,16 @@ public abstract class UNetConnection : UPlayer if (bunch.bReliable) { - Logger.Verbose(" Reliable Bunch, Channel {Ch} Sequence {Seq}: Size {A.0}+{B.0}", bunch.ChIndex, bunch.ChSequence, (headerPos - incomingStartPos)/8.0f, (reader.GetPosBits()-headerPos)/8.0f); + Logger.Verbose(" Reliable Bunch, Channel {Ch} Sequence {Seq}: Size {A:###.0}+{B:###.0}", bunch.ChIndex, bunch.ChSequence, (headerPos - incomingStartPos)/8.0f, (reader.GetPosBits()-headerPos)/8.0f); } else { - Logger.Verbose(" Unreliable Bunch, Channel {Ch}: Size {A.0}+{B.0}", bunch.ChIndex, (headerPos - incomingStartPos)/8.0f, (reader.GetPosBits()-headerPos)/8.0f); + Logger.Verbose(" Unreliable Bunch, Channel {Ch}: Size {A:###.0}+{B:###.0}", bunch.ChIndex, (headerPos - incomingStartPos)/8.0f, (reader.GetPosBits()-headerPos)/8.0f); } if (bunch.bOpen) { - Logger.Verbose(" bOpen Bunch, Channel {Ch} Sequence {Seq}: Size {A.0}+{B.0}", bunch.ChIndex, bunch.ChSequence, (headerPos - incomingStartPos)/8.0f, (reader.GetPosBits()-headerPos)/8.0f); + Logger.Verbose(" bOpen Bunch, Channel {Ch} Sequence {Seq}: Size {A:###.0}+{B:###.0}", bunch.ChIndex, bunch.ChSequence, (headerPos - incomingStartPos)/8.0f, (reader.GetPosBits()-headerPos)/8.0f); } if (Channels[bunch.ChIndex] == null && (bunch.ChIndex != 0 || bunch.ChName != UnrealNames.FNames[UnrealNameKey.Control])) @@ -902,6 +933,59 @@ public abstract class UNetConnection : UPlayer } } + private void PacketNotifyUpdate(PacketNotifyUpdateContext context, SequenceNumber ackedSequence, bool delivered) + { + ++LastNotifiedPacketId; + // TODO: Increment things + + if (!new SequenceNumber((ushort)LastNotifiedPacketId).Equals(ackedSequence)) + { + Close(); + Logger.Fatal("LastNotifiedPacketId != AckedSequence"); + return; + } + + if (delivered) + { + ReceivedAck(LastNotifiedPacketId, context.ChannelToClose); + Logger.Verbose("TODO: ReceivedAck"); + } + else + { + ReceivedNak(LastNotifiedPacketId); + Logger.Verbose("TODO: ReceivedNak"); + } + } + + private void ReceivedAck(int ackPacketId, List outChannelsToClose) + { + Logger.Verbose("Received ack {AckPacketId}", ackPacketId); + + // Advance OutAckPacketId + OutAckPacketId = ackPacketId; + + // Process the bunch. + LastRecvAckTime = (float)Driver!.GetElapsedTime(); + LastRecvAckTimestamp = Driver.GetElapsedTime(); + + if (PackageMap != null) + { + PackageMap.ReceivedAck(ackPacketId); + } + + // TODO: Invoke AckChannelFunc on all channels written for this PacketId + } + + private void ReceivedNak(int nakPacketId) + { + Logger.Verbose("Received nak {NakPacketId}", nakPacketId); + + // Update pending NetGUIDs + PackageMap!.ReceivedNak(nakPacketId); + + // TODO: Invoke NakChannelFunc on all channels written for this PacketId + } + private bool ReadPacketInfo(FBitReader reader, bool bHasPacketInfoPayload) { if (!bHasPacketInfoPayload) @@ -1165,8 +1249,11 @@ public abstract class UNetConnection : UPlayer { // TOOD: Increment things } - - FlushNet(); + + if (_bAutoFlush) + { + FlushNet(); + } return bunch.PacketId; } @@ -1420,7 +1507,12 @@ public abstract class UNetConnection : UPlayer NumPaddingBits = 0; } - private void FlushNet() + public void SetPlayerOnlinePlatformName(FName inPlayerOnlinePlatformName) + { + _playerOnlinePlatformName = inPlayerOnlinePlatformName; + } + + public void FlushNet(bool bIgnoreSimulation = false) { if (Driver == null) { @@ -1554,7 +1646,7 @@ public abstract class UNetConnection : UPlayer _channelsToTick.Remove(channel); } - private protected void SetClientLoginState(EClientLoginState newState) + internal void SetClientLoginState(EClientLoginState newState) { if (ClientLoginState == newState) { diff --git a/src/Prospect.Unreal/Net/UNetDriver.cs b/src/Prospect.Unreal/Net/UNetDriver.cs index e5782ad..5cbfdd0 100644 --- a/src/Prospect.Unreal/Net/UNetDriver.cs +++ b/src/Prospect.Unreal/Net/UNetDriver.cs @@ -34,8 +34,11 @@ public abstract class UNetDriver : IAsyncDisposable } } - // From Engine ini + // TODO: From Engine ini public float KeepAliveTime { get; } = 0.2f; + + // TODO: From Engine ini + public int MaxClientRate { get; } = 100000; /// /// World this net driver is associated with diff --git a/src/Prospect.Unreal/Net/UPackageMap.cs b/src/Prospect.Unreal/Net/UPackageMap.cs index f2da8d0..46f6db8 100644 --- a/src/Prospect.Unreal/Net/UPackageMap.cs +++ b/src/Prospect.Unreal/Net/UPackageMap.cs @@ -80,4 +80,14 @@ public class UPackageMap { throw new NotImplementedException(); } + + public void ReceivedAck(int ackPacketId) + { + // TODO: Implement + } + + public void ReceivedNak(int nakPacketId) + { + // TODO: Implement + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Runtime/UGameInstance.cs b/src/Prospect.Unreal/Runtime/UGameInstance.cs new file mode 100644 index 0000000..18008be --- /dev/null +++ b/src/Prospect.Unreal/Runtime/UGameInstance.cs @@ -0,0 +1,13 @@ +using Prospect.Unreal.Core; +using Prospect.Unreal.Net.Actors; + +namespace Prospect.Unreal.Runtime; + +public class UGameInstance +{ + public AGameModeBase? CreateGameModeForURL(FUrl inUrl, UWorld inWorld) + { + // TODO: World.SpawnActor + return new AGameModeBase(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Player/UPlayer.cs b/src/Prospect.Unreal/Runtime/UPlayer.cs similarity index 71% rename from src/Prospect.Unreal/Net/Player/UPlayer.cs rename to src/Prospect.Unreal/Runtime/UPlayer.cs index ebbe585..bb8f971 100644 --- a/src/Prospect.Unreal/Net/Player/UPlayer.cs +++ b/src/Prospect.Unreal/Runtime/UPlayer.cs @@ -3,4 +3,6 @@ public class UPlayer { public APlayerController? PlayerController { get; set; } + + public int CurrentNetSpeed { get; set; } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Runtime/UWorld.cs b/src/Prospect.Unreal/Runtime/UWorld.cs index 2628641..3cf6aa5 100644 --- a/src/Prospect.Unreal/Runtime/UWorld.cs +++ b/src/Prospect.Unreal/Runtime/UWorld.cs @@ -1,6 +1,8 @@ using Prospect.Unreal.Core; +using Prospect.Unreal.Core.Names; using Prospect.Unreal.Exceptions; using Prospect.Unreal.Net; +using Prospect.Unreal.Net.Actors; using Prospect.Unreal.Net.Channels; using Prospect.Unreal.Net.Packets.Bunch; using Prospect.Unreal.Net.Packets.Control; @@ -12,9 +14,14 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable { private static readonly ILogger Logger = Log.ForContext(); + private UGameInstance? _owningGameInstance; + private AGameModeBase? _authorityGameMode; + public UWorld(FUrl url) { Url = url; + _owningGameInstance = null; + _authorityGameMode = null; } public FUrl Url { get; } @@ -28,6 +35,44 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable NetDriver.ConnectionlessHandler?.Tick(deltaTime); } } + + public void SetGameInstance(UGameInstance instance) + { + _owningGameInstance = instance; + } + + public UGameInstance GetGameInstance() + { + if (_owningGameInstance == null) + { + throw new UnrealException($"Attempted to retrieve null {nameof(UGameInstance)}"); + } + + return _owningGameInstance; + } + + public bool SetGameMode(FUrl worldUrl) + { + if (IsServer() && _authorityGameMode == null) + { + _authorityGameMode = GetGameInstance().CreateGameModeForURL(worldUrl, this); + + if (_authorityGameMode != null) + { + return true; + } + + Logger.Error("Failed to spawn GameMode actor"); + return false; + } + + return false; + } + + public AGameModeBase? GetAuthGameMode() + { + return _authorityGameMode; + } public bool Listen() { @@ -114,8 +159,12 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable { case NMT.Hello: { + const int localNetworkVersion = 0; + if (NMT_Hello.Receive(bunch, out var isLittleEndian, out var remoteNetworkVersion, out var encryptionToken)) { + Logger.Information("Client connecting with version. LocalNetworkVersion: {Local}, RemoteNetworkVersion: {Remote}", localNetworkVersion, remoteNetworkVersion); + // TODO: Version check. if (string.IsNullOrEmpty(encryptionToken)) @@ -130,9 +179,120 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable break; } + case NMT.Netspeed: + { + if (NMT_Netspeed.Receive(bunch, out var rate)) + { + connection.CurrentNetSpeed = Math.Clamp(rate, 1800, NetDriver.MaxClientRate); + Logger.Debug("Client netspeed is {Num}", connection.CurrentNetSpeed); + } + + break; + } + + case NMT.Abort: + { + break; + } + + case NMT.Skip: + { + break; + } + case NMT.Login: { - throw new NotImplementedException(); + // Admit or deny the player here. + if (NMT_Login.Receive(bunch, out var clientResponse, out var requestUrl, out var uniqueIdRepl, out var onlinePlatformName)) + { + connection.ClientResponse = clientResponse; + + // Only the options/portal for the URL should be used during join + var newRequestUrl = requestUrl; + + var oneIndex = requestUrl.IndexOf('?'); + var twoIndex = requestUrl.IndexOf('#'); + + if (oneIndex != -1 && twoIndex != -1) + { + newRequestUrl = requestUrl.Substring(Math.Min(oneIndex, twoIndex)); + } + else if (oneIndex != -1) + { + newRequestUrl = requestUrl.Substring(oneIndex); + } + else if (twoIndex != -1) + { + newRequestUrl = requestUrl.Substring(twoIndex); + } + else + { + newRequestUrl = string.Empty; + } + + Logger.Debug("Login request: {RequestUrl} userId: {UserId} platform: {Platform}", newRequestUrl, uniqueIdRepl.ToDebugString(), onlinePlatformName); + + // Compromise for passing splitscreen playercount through to gameplay login code, + // without adding a lot of extra unnecessary complexity throughout the login code. + // NOTE: This code differs from NMT_JoinSplit, by counting + 1 for SplitscreenCount + // (since this is the primary connection, not counted in Children) + // TODO: Implement proper FUrl constructor + var inUrl = new FUrl + { + Map = Url.Map + newRequestUrl + }; + + if (!inUrl.Valid) + { + requestUrl = newRequestUrl; + Logger.Error("NMT_Login: Invalid URL {Url}", requestUrl); + bunch.SetError(); + break; + } + + var splitscreenCount = Math.Min(connection.Children.Count + 1, 255); + + // Don't allow clients to specify this value + inUrl.Options.Remove("SplitscreenCount"); + inUrl.Options.Add($"SplitscreenCount={splitscreenCount}"); + + requestUrl = inUrl.ToString(); + + // skip to the first option in the URL + var tmp = requestUrl.Substring(requestUrl.IndexOf('?')); + + // keep track of net id for player associated with remote connection + connection.PlayerId = uniqueIdRepl; + + // keep track of the online platform the player associated with this connection is using. + connection.SetPlayerOnlinePlatformName(new FName(onlinePlatformName)); + + // ask the game code if this player can join + string errorMsg = null; + + var gameMode = GetAuthGameMode(); + if (gameMode != null) + { + gameMode.PreLogin(tmp, connection.LowLevelGetRemoteAddress(), connection.PlayerId, out errorMsg); + } + + if (!string.IsNullOrEmpty(errorMsg)) + { + Logger.Debug("PreLogin failure: {Error}", errorMsg); + NMT_Failure.Send(connection, errorMsg); + connection.FlushNet(true); + } + else + { + WelcomePlayer(connection); + } + } + else + { + connection.ClientResponse = string.Empty; + } + + break; } default: @@ -143,6 +303,32 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable } } + private void WelcomePlayer(UNetConnection connection) + { + // TODO: Properly fetch level name from CurrentLevel + var levelName = "/Game/ThirdPersonCPP/Maps/ThirdPersonExampleMap"; + + // TODO: Properly fetch from AuthorityGameMode + var gameName = "/Script/ThirdPersonMP.ThirdPersonMPGameMode"; + var redirectUrl = string.Empty; + + NMT_Welcome.Send(connection, levelName, gameName, redirectUrl); + + connection.FlushNet(); + // connection.QueuedBits = 0; + connection.SetClientLoginState(EClientLoginState.Welcomed); + } + + public bool IsServer() + { + if (NetDriver != null) + { + return NetDriver.IsServer(); + } + + return true; + } + public async ValueTask DisposeAsync() { if (NetDriver != null)