From 0a27bdd3c0c855c2418190eb304febc1c91eb92b Mon Sep 17 00:00:00 2001 From: AeonLucid Date: Mon, 10 Jan 2022 20:20:02 +0100 Subject: [PATCH] Further parse bunches till their channel receives them --- .../Core/Names/UnrealConstants.cs | 6 + .../Net/Channels/EChannelType.cs | 16 +- .../Net/Channels/UActorChannel.cs | 21 + src/Prospect.Unreal/Net/Channels/UChannel.cs | 513 ++++++++++++++++++ .../Net/Channels/UControlChannel.cs | 18 + .../Net/Channels/UVoiceChannel.cs | 18 + src/Prospect.Unreal/Net/EAcceptConnection.cs | 11 + src/Prospect.Unreal/Net/FChannelCloseInfo.cs | 15 + src/Prospect.Unreal/Net/FChannelDefinition.cs | 6 +- src/Prospect.Unreal/Net/FNetworkNotify.cs | 28 + src/Prospect.Unreal/Net/FPacketIdRange.cs | 12 + .../Net/Packets/Bunch/FInBunch.cs | 38 +- .../Net/Packets/Header/FNetPacketNotify.cs | 32 +- .../Header/Sequence/SequenceHistory.cs | 32 +- .../Net/Player/APlayerController.cs | 6 + src/Prospect.Unreal/Net/Player/UPlayer.cs | 6 + src/Prospect.Unreal/Net/UChannel.cs | 8 - src/Prospect.Unreal/Net/UIpConnection.cs | 8 +- src/Prospect.Unreal/Net/UIpNetDriver.cs | 7 +- src/Prospect.Unreal/Net/UNetConnection.cs | 347 +++++++++++- src/Prospect.Unreal/Net/UNetDriver.cs | 44 +- src/Prospect.Unreal/Runtime/UWorld.cs | 44 +- .../Serialization/FBitReaderMark.cs | 2 +- 23 files changed, 1187 insertions(+), 51 deletions(-) create mode 100644 src/Prospect.Unreal/Core/Names/UnrealConstants.cs create mode 100644 src/Prospect.Unreal/Net/Channels/UActorChannel.cs create mode 100644 src/Prospect.Unreal/Net/Channels/UChannel.cs create mode 100644 src/Prospect.Unreal/Net/Channels/UControlChannel.cs create mode 100644 src/Prospect.Unreal/Net/Channels/UVoiceChannel.cs create mode 100644 src/Prospect.Unreal/Net/EAcceptConnection.cs create mode 100644 src/Prospect.Unreal/Net/FChannelCloseInfo.cs create mode 100644 src/Prospect.Unreal/Net/FNetworkNotify.cs create mode 100644 src/Prospect.Unreal/Net/FPacketIdRange.cs create mode 100644 src/Prospect.Unreal/Net/Player/APlayerController.cs create mode 100644 src/Prospect.Unreal/Net/Player/UPlayer.cs delete mode 100644 src/Prospect.Unreal/Net/UChannel.cs diff --git a/src/Prospect.Unreal/Core/Names/UnrealConstants.cs b/src/Prospect.Unreal/Core/Names/UnrealConstants.cs new file mode 100644 index 0000000..430d038 --- /dev/null +++ b/src/Prospect.Unreal/Core/Names/UnrealConstants.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Core.Names; + +public class UnrealConstants +{ + public const int IndexNone = -1; +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Channels/EChannelType.cs b/src/Prospect.Unreal/Net/Channels/EChannelType.cs index cc9f2a0..62bbc43 100644 --- a/src/Prospect.Unreal/Net/Channels/EChannelType.cs +++ b/src/Prospect.Unreal/Net/Channels/EChannelType.cs @@ -1,13 +1,11 @@ namespace Prospect.Unreal.Net.Channels; -public static class EChannelType +public enum 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. + CHTYPE_None = 0, // Invalid type. + CHTYPE_Control = 1, // Connection control. + CHTYPE_Actor = 2, // Actor-update channel. + CHTYPE_File = 3, // Binary file transfer. + CHTYPE_Voice = 4, // VoIP data channel + CHTYPE_MAX = 8, // Maximum. } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Channels/UActorChannel.cs b/src/Prospect.Unreal/Net/Channels/UActorChannel.cs new file mode 100644 index 0000000..e416f20 --- /dev/null +++ b/src/Prospect.Unreal/Net/Channels/UActorChannel.cs @@ -0,0 +1,21 @@ +using Prospect.Unreal.Core.Names; +using Prospect.Unreal.Net.Packets.Bunch; + +namespace Prospect.Unreal.Net.Channels; + +public class UActorChannel : UChannel +{ + public UActorChannel() + { + ChType = EChannelType.CHTYPE_Actor; + ChName = UnrealNames.FNames[UnrealNameKey.Actor]; + // bClearRecentActorRefs = true; + // bHoldQueuedExportBunchesAndGUIDs = false; + // QueuedCloseReason = EChannelCloseReason::Destroyed; + } + + protected override void ReceivedBunch(FInBunch bunch) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Channels/UChannel.cs b/src/Prospect.Unreal/Net/Channels/UChannel.cs new file mode 100644 index 0000000..eba4be9 --- /dev/null +++ b/src/Prospect.Unreal/Net/Channels/UChannel.cs @@ -0,0 +1,513 @@ +using Prospect.Unreal.Core.Names; +using Prospect.Unreal.Exceptions; +using Prospect.Unreal.Net.Packets.Bunch; +using Serilog; + +namespace Prospect.Unreal.Net.Channels; + +public abstract class UChannel +{ + private static readonly ILogger Logger = Log.ForContext(); + + private const int NetMaxConstructedPartialBunchSizeBytes = 1024 * 64; + + /// + /// Owner connection. + /// + public UNetConnection Connection { get; set; } + + /// + /// If OpenedLocally is true, this means we have acknowledged the packet we sent the bOpen bunch on. + /// Otherwise, it means we have received the bOpen bunch from the server. + /// + public bool OpenAcked { get; set; } + + /// + /// State of the channel. + /// + public bool Closing { get; set; } + + /// + /// Channel is going dormant (it will close but the client will not destroy). + /// + public bool Dormant { get; set; } + + /// + /// Replication is being paused, but channel will not be closed. + /// + public bool bIsReplicationPaused { get; set; } + + /// + /// Opened temporarily. + /// + public bool OpenTemporary { get; set; } + + /// + /// Has encountered errors and is ignoring subsequent packets. + /// + public bool Broken { get; set; } + + /// + /// Actor associated with this channel was torn off. + /// + public bool bTornOff { get; set; } + + /// + /// Channel wants to go dormant (it will check during tick if it can go dormant). + /// + public bool bPendingDormancy { get; set; } + + /// + /// Channel wants to go dormant, and is otherwise ready to become dormant, but is waiting for a timeout before doing so. + /// + public bool bIsInDormancyHysteresis { get; set; } + + /// + /// Unreliable property replication is paused until all reliables are ack'd. + /// + public bool bPausedUntilReliableACK { get; set; } + + /// + /// Set when sending closing bunch to avoid recursion in send-failure-close case. + /// + public bool SentClosingBunch { get; set; } + + /// + /// Set when placed in the actor channel pool + /// + public bool bPooled { get; set; } + + /// + /// Whether channel was opened locally or by remote. + /// + public bool OpenedLocally { get; set; } + + /// + /// Whether channel was opened by replay checkpoint recording + /// + public bool bOpenedForCheckpoint { get; set; } + + /// + /// Index of this channel. + /// + public int ChIndex { get; set; } + + /// + /// If OpenedLocally is true, this is the packet we sent the bOpen bunch on. + /// Otherwise, it's the packet we received the bOpen bunch on. + /// + public FPacketIdRange OpenPacketId { get; set; } + + /// + /// Type of this channel. + /// + public EChannelType ChType { get; set; } + + /// + /// Name of the type of this channel. + /// + public FName ChName { get; set; } + + /// + /// Number of packets in InRec. + /// + public int NumInRec { get; set; } + + /// + /// Number of packets in OutRec. + /// + public int NumOutRec { get; set; } + + /// + /// Incoming data with queued dependencies. + /// + public FInBunch? InRec { get; set; } + + // public FOutBunch OutRec { get; set; } + + /// + /// Partial bunch we are receiving (incoming partial bunches are appended to this). + /// + public FInBunch? InPartialBunch { get; set; } + + public virtual void Init(UNetConnection inConnection, int inChIndex, EChannelCreateFlags createFlags) + { + Connection = inConnection; + ChIndex = inChIndex; + OpenedLocally = (createFlags & EChannelCreateFlags.OpenedLocally) != 0; + OpenPacketId = new FPacketIdRange(); + bPausedUntilReliableACK = false; + SentClosingBunch = false; + } + + public void ReceivedRawBunch(FInBunch bunch, out bool bOutSkipAck) + { + bOutSkipAck = false; + + // Immediately consume the NetGUID portion of this bunch, regardless if it is partial or reliable. + // NOTE - For replays, we do this even earlier, to try and load this as soon as possible, in case there is an issue creating the channel + // If a replay fails to create a channel, we want to salvage as much as possible + if (bunch.bHasPackageMapExports && !Connection.IsInternalAck()) + { + throw new NotImplementedException(); + } + + if (Connection.IsInternalAck() && Broken) + { + return; + } + + if (bunch.bReliable && bunch.ChSequence != Connection.InReliable[ChIndex] + 1) + { + if (Connection.IsInternalAck()) + { + throw new UnrealNetException("Shouldn't hit this path on 100% reliable connections"); + } + + if (bunch.ChSequence <= Connection.InReliable[ChIndex]) + { + throw new UnrealNetException("Invalid bunch"); + } + + // TODO: (InRec) Queue + throw new NotImplementedException(); + } + else + { + var bDeleted = ReceivedNextBunch(bunch, out bOutSkipAck); + + if (bunch.IsError()) + { + Logger.Error("Bunch.IsError() after ReceivedNextBunch 1"); + return; + } + + if (bDeleted) + { + return; + } + + // TODO: (InRec) Dispatch waiting bunches + while (InRec != null) + { + throw new NotImplementedException(); + } + } + } + + private bool ReceivedNextBunch(FInBunch bunch, out bool bOutSkipAck) + { + bOutSkipAck = false; + + // We received the next bunch. Basically at this point: + // -We know this is in order if reliable + // -We dont know if this is partial or not + // If its not a partial bunch, of it completes a partial bunch, we can call ReceivedSequencedBunch to actually handle it + + // Note this bunch's retirement. + if (bunch.bReliable) + { + // Reliables should be ordered properly at this point + if (bunch.ChSequence != Connection.InReliable[bunch.ChIndex] + 1) + { + throw new UnrealNetException("Reliables should be ordered properly at this point"); + } + + Connection.InReliable[bunch.ChIndex] = bunch.ChSequence; + } + + var handleBunch = bunch; + + if (bunch.bPartial) + { + handleBunch = null; + + if (bunch.bPartialInitial) + { + // Create new InPartialBunch if this is the initial bunch of a new sequence. + if (InPartialBunch != null) + { + if (!InPartialBunch.bPartialFinal) + { + if (InPartialBunch.bReliable) + { + if (bunch.bReliable) + { + Logger.Warning("Reliable partial trying to destroy reliable partial 1"); + bunch.SetError(); + return false; + } + + Logger.Information("Unreliable partial trying to destroy reliable partial 1"); + bOutSkipAck = true; + return false; + } + + // We didn't complete the last partial bunch - this isn't fatal since they can be unreliable, but may want to log it. + Logger.Verbose("Incomplete partial bunch. Channel: {ChIndex} ChSequence: {ChSequence}", InPartialBunch.ChIndex, InPartialBunch.ChSequence); + } + + InPartialBunch = null; + } + + InPartialBunch = new FInBunch(bunch, false); + + if (!bunch.bHasPackageMapExports && bunch.GetBitsLeft() > 0) + { + if (bunch.GetBitsLeft() % 8 != 0) + { + Logger.Warning("Corrupt partial bunch. Initial partial bunches are expected to be byte-aligned. BitsLeft = {BitCount}", bunch.GetBitsLeft()); + bunch.SetError(); + return false; + } + + InPartialBunch.AppendDataFromChecked(bunch.GetBufferPosChecked(), bunch.GetBuffer(), bunch.GetBitsLeft()); + + Log.Verbose("Received new partial bunch"); + } + else + { + Log.Verbose("Received New partial bunch. It only contained NetGUIDs"); + } + } + else + { + // Merge in next partial bunch to InPartialBunch if: + // -We have a valid InPartialBunch + // -The current InPartialBunch wasn't already complete + // -ChSequence is next in partial sequence + // -Reliability flag matches + + var bSequenceMatches = false; + + if (InPartialBunch != null) + { + var bReliableSequencesMatches = bunch.ChSequence == InPartialBunch.ChSequence + 1; + var bUnreliableSequenceMatches = bReliableSequencesMatches || bunch.ChSequence == InPartialBunch.ChSequence; + + // Unreliable partial bunches use the packet sequence, and since we can merge multiple bunches into a single packet, + // it's perfectly legal for the ChSequence to match in this case. + // Reliable partial bunches must be in consecutive order though + bSequenceMatches = InPartialBunch.bReliable ? bReliableSequencesMatches : bUnreliableSequenceMatches; + } + + if (InPartialBunch != null && !InPartialBunch.bPartialFinal && bSequenceMatches && InPartialBunch.bReliable == bunch.bReliable) + { + // Merge. + Logger.Verbose("Merging Partial Bunch: {BytesLeft} Bytes", bunch.GetBytesLeft()); + + if (!bunch.bHasPackageMapExports && bunch.GetBitsLeft() > 0) + { + // TODO: Check if works. + InPartialBunch.AppendDataFromChecked(bunch.GetBufferPosChecked(), bunch.GetBuffer(), bunch.GetBitsLeft()); + } + + // Only the final partial bunch should ever be non byte aligned. This is enforced during partial bunch creation + // This is to ensure fast copies/appending of partial bunches. The final partial bunch may be non byte aligned. + if (!bunch.bHasPackageMapExports && !bunch.bPartialFinal && bunch.GetBitsLeft() % 8 != 0) + { + Logger.Warning("Corrupt partial bunch. Non-final partial bunches are expected to be byte-aligned. bHasPackageMapExports = {HasPackageMapExports}, bPartialFinal = {PartialFinal}, BitsLeft = {BitsLeft}", + bunch.bHasPackageMapExports ? 1 : 0, + bunch.bPartialFinal ? 1 : 0, + bunch.GetBitsLeft()); + bunch.SetError(); + return false; + } + + // Advance the sequence of the current partial bunch so we know what to expect next + InPartialBunch.ChSequence = bunch.ChSequence; + + if (bunch.bPartialFinal) + { + Logger.Verbose("Completed Partial Bunch ({BytesLeft} left)", bunch.GetBytesLeft()); + + if (bunch.bHasPackageMapExports) + { + // Shouldn't have these, they only go in initial partial export bunches + Logger.Warning("Corrupt partial bunch. Final partial bunch has package map exports"); + bunch.SetError(); + return false; + } + + handleBunch = InPartialBunch; + + InPartialBunch.bPartialFinal = true; + InPartialBunch.bClose = bunch.bClose; + InPartialBunch.bDormant = bunch.bDormant; + InPartialBunch.CloseReason = bunch.CloseReason; + InPartialBunch.bIsReplicationPaused = bunch.bIsReplicationPaused; + InPartialBunch.bHasMustBeMappedGUIDs = bunch.bHasMustBeMappedGUIDs; + } + else + { + Logger.Verbose("Received Partial Bunch"); + } + } + else + { + // Merge problem - delete InPartialBunch. + // This is mainly so that in the unlikely chance that ChSequence wraps around, we wont merge two completely separate partial bunches. + // We shouldn't hit this path on 100% reliable connections + + if (Connection.IsInternalAck()) + { + throw new UnrealNetException("We shouldn't hit this path on 100% reliable connections"); + } + + bOutSkipAck = true; // Don't ack the packet, since we didn't process the bunch + + if (InPartialBunch != null && InPartialBunch.bReliable) + { + if (bunch.bReliable) + { + Logger.Warning("Reliable partial trying to destroy reliable partial 2"); + bunch.SetError(); + return false; + } + + Logger.Warning("Unreliable partial trying to destroy reliable partial 2"); + return false; + } + + if (InPartialBunch != null) + { + InPartialBunch = null; + } + } + } + + // Fairly large number, and probably a bad idea to even have a bunch this size, but want to be safe for now and not throw out legitimate data + if (IsBunchTooLarge(Connection, InPartialBunch)) + { + Logger.Error("Received a partial bunch exceeding max allowed size. BunchSize={Size}, MaximumSize={MaxSize}", InPartialBunch!.GetNumBytes(), NetMaxConstructedPartialBunchSizeBytes); + bunch.SetError(); + return false; + } + } + + if (handleBunch != null) + { + var bBothSidesCanOpen = Connection.Driver != null && + Connection.Driver.ChannelDefinitionMap[ChName].ServerOpen && + Connection.Driver.ChannelDefinitionMap[ChName].ClientOpen; + + if (handleBunch.bOpen) + { + // Voice channels can open from both side simultaneously, so ignore this logic until we resolve this + if (!bBothSidesCanOpen) + { + // If we opened the channel, we shouldn't be receiving bOpen commands from the other side + if (OpenedLocally) + { + throw new UnrealNetException("Received channel open command for channel that was already opened locally."); + } + + if (OpenPacketId.First != UnrealConstants.IndexNone || OpenPacketId.Last != UnrealConstants.IndexNone) + { + Logger.Error("This should be the first and only assignment of the packet range (we should only receive one bOpen bunch)"); + bunch.SetError(); + return false; + } + } + + // Remember the range. + // In the case of a non partial, HandleBunch == Bunch + // In the case of a partial, HandleBunch should == InPartialBunch, and Bunch should be the last bunch. + OpenPacketId.First = handleBunch.PacketId; + OpenPacketId.Last = bunch.PacketId; + OpenAcked = true; + + Logger.Verbose("ReceivedNextBunch: Channel now fully open. ChIndex: {ChIndex}, OpenPacketId.First: {First}, OpenPacketId.Last: {Last}", ChIndex, OpenPacketId.First, OpenPacketId.Last); + } + + // Voice channels can open from both side simultaneously, so ignore this logic until we resolve this + if (!bBothSidesCanOpen) + { + // Don't process any packets until we've fully opened this channel + // (unless we opened it locally, in which case it's safe to process packets) + if (!OpenedLocally && !OpenAcked) + { + if (handleBunch.bReliable) + { + Logger.Error("ReceivedNextBunch: Reliable bunch before channel was fully open"); + bunch.SetError(); + return false; + } + + if (Connection.IsInternalAck()) + { + // Shouldn't be possible for 100% reliable connections + Broken = true; + return false; + } + + // Don't ack this packet (since we won't process all of it) + bOutSkipAck = true; + + Logger.Verbose("ReceivedNextBunch: Skipping bunch since channel isn't fully open. ChIndex: {ChIndex}", ChIndex); + return false; + } + + // At this point, we should have the open packet range + // This is because if we opened the channel locally, we set it immediately when we sent the first bOpen bunch + // If we opened it from a remote connection, then we shouldn't be processing any packets until it's fully opened (which is handled above) + if (OpenPacketId.First == -1) + { + throw new UnrealNetException("Should have open packet range."); + } + + if (OpenPacketId.Last == -1) + { + throw new UnrealNetException("Should have open packet range."); + } + } + + // Receive it in sequence. + return ReceivedSequencedBunch(handleBunch); + } + + return false; + } + + private bool ReceivedSequencedBunch(FInBunch bunch) + { + // Handle a regular bunch. + if (!Closing) + { + ReceivedBunch(bunch); + } + + // We have fully received the bunch, so process it. + if (bunch.bClose) + { + Dormant = bunch.bDormant || (bunch.CloseReason == EChannelCloseReason.Dormancy); + + if (InRec != null) + { + Logger.Warning("Close Anomaly {Seq} / {InRecSeq}", bunch.ChSequence, InRec.ChSequence); + } + + if (ChIndex == 0) + { + Logger.Debug("UChannel::ReceivedSequencedBunch: Bunch.bClose == true. ChIndex == 0. Calling ConditionalCleanUp"); + } + + ConditionalCleanUp(false, bunch.CloseReason); + return true; + } + + return false; + } + + protected abstract void ReceivedBunch(FInBunch bunch); + + public void ConditionalCleanUp(bool bForDestroy, EChannelCloseReason closeReason) + { + throw new NotImplementedException(); + } + + private static bool IsBunchTooLarge(UNetConnection connection, FInBunch? bunch) + { + return !connection.IsInternalAck() && bunch != null && bunch.GetNumBytes() > NetMaxConstructedPartialBunchSizeBytes; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Channels/UControlChannel.cs b/src/Prospect.Unreal/Net/Channels/UControlChannel.cs new file mode 100644 index 0000000..4e4bd1b --- /dev/null +++ b/src/Prospect.Unreal/Net/Channels/UControlChannel.cs @@ -0,0 +1,18 @@ +using Prospect.Unreal.Core.Names; +using Prospect.Unreal.Net.Packets.Bunch; + +namespace Prospect.Unreal.Net.Channels; + +public class UControlChannel : UChannel +{ + public UControlChannel() + { + ChType = EChannelType.CHTYPE_Control; + ChName = UnrealNames.FNames[UnrealNameKey.Control]; + } + + protected override void ReceivedBunch(FInBunch bunch) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Channels/UVoiceChannel.cs b/src/Prospect.Unreal/Net/Channels/UVoiceChannel.cs new file mode 100644 index 0000000..b2b9d3f --- /dev/null +++ b/src/Prospect.Unreal/Net/Channels/UVoiceChannel.cs @@ -0,0 +1,18 @@ +using Prospect.Unreal.Core.Names; +using Prospect.Unreal.Net.Packets.Bunch; + +namespace Prospect.Unreal.Net.Channels; + +public class UVoiceChannel : UChannel +{ + public UVoiceChannel() + { + ChType = EChannelType.CHTYPE_Voice; + ChName = UnrealNames.FNames[UnrealNameKey.Control]; + } + + protected override void ReceivedBunch(FInBunch bunch) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/EAcceptConnection.cs b/src/Prospect.Unreal/Net/EAcceptConnection.cs new file mode 100644 index 0000000..8f59309 --- /dev/null +++ b/src/Prospect.Unreal/Net/EAcceptConnection.cs @@ -0,0 +1,11 @@ +namespace Prospect.Unreal.Net; + +public enum EAcceptConnection +{ + /** Reject the connection */ + Reject, + /** Accept the connection */ + Accept, + /** Ignore the connection, sending no reply, while server traveling */ + Ignore +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FChannelCloseInfo.cs b/src/Prospect.Unreal/Net/FChannelCloseInfo.cs new file mode 100644 index 0000000..3682c44 --- /dev/null +++ b/src/Prospect.Unreal/Net/FChannelCloseInfo.cs @@ -0,0 +1,15 @@ +using Prospect.Unreal.Net.Channels; + +namespace Prospect.Unreal.Net; + +public readonly struct FChannelCloseInfo +{ + public FChannelCloseInfo(uint id, EChannelCloseReason closeReason) + { + Id = id; + CloseReason = closeReason; + } + + public uint Id { get; } + public EChannelCloseReason CloseReason { get; } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FChannelDefinition.cs b/src/Prospect.Unreal/Net/FChannelDefinition.cs index d3b35ef..b892fe8 100644 --- a/src/Prospect.Unreal/Net/FChannelDefinition.cs +++ b/src/Prospect.Unreal/Net/FChannelDefinition.cs @@ -1,3 +1,5 @@ -namespace Prospect.Unreal.Net; +using Prospect.Unreal.Core.Names; -public record FChannelDefinition(string Name, Type Class, int StaticChannelIndex, bool TickOnCreate, bool ServerOpen, bool ClientOpen, bool InitialServer, bool InitialClient); \ No newline at end of file +namespace Prospect.Unreal.Net; + +public record FChannelDefinition(FName Name, Type Class, int StaticChannelIndex, bool TickOnCreate, bool ServerOpen, bool ClientOpen, bool InitialServer, bool InitialClient); \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FNetworkNotify.cs b/src/Prospect.Unreal/Net/FNetworkNotify.cs new file mode 100644 index 0000000..2f84f00 --- /dev/null +++ b/src/Prospect.Unreal/Net/FNetworkNotify.cs @@ -0,0 +1,28 @@ +using Prospect.Unreal.Net.Channels; +using Prospect.Unreal.Net.Packets.Bunch; + +namespace Prospect.Unreal.Net; + +public interface FNetworkNotify +{ + EAcceptConnection NotifyAcceptingConnection(); + + /// + /// Notification that a new connection has been created/established as a result of a + /// remote request, previously approved by NotifyAcceptingConnection + /// + void NotifyAcceptedConnection(UNetConnection connection); + + /// + /// Notification that a new channel is being created/opened as a result of a remote request (Actor creation, etc) + /// + bool NotifyAcceptingChannel(UChannel channel); + + /// + /// Handler for messages sent through a remote connection's control channel not required to handle the message, + /// but if it reads any data from Bunch, it MUST read the ENTIRE data stream for that message + /// + /// (i.e. use FNetControlMessage::Receive()) + /// + void NotifyControlMessage(UNetConnection connection, byte messageType, FInBunch bunch); +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FPacketIdRange.cs b/src/Prospect.Unreal/Net/FPacketIdRange.cs new file mode 100644 index 0000000..cc01153 --- /dev/null +++ b/src/Prospect.Unreal/Net/FPacketIdRange.cs @@ -0,0 +1,12 @@ +namespace Prospect.Unreal.Net; + +public class FPacketIdRange +{ + public int First = -1; + public int Last = -1; + + public bool InRange(int packetId) + { + return First <= packetId && packetId <= Last; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Packets/Bunch/FInBunch.cs b/src/Prospect.Unreal/Net/Packets/Bunch/FInBunch.cs index 6a3ee3d..db0bfbc 100644 --- a/src/Prospect.Unreal/Net/Packets/Bunch/FInBunch.cs +++ b/src/Prospect.Unreal/Net/Packets/Bunch/FInBunch.cs @@ -6,14 +6,46 @@ namespace Prospect.Unreal.Net.Packets.Bunch { public class FInBunch : FNetBitReader { + public FInBunch(FInBunch inBunch, bool copyBuffer) : base(inBunch.PackageMap, null, 0) + { + PacketId = inBunch.PacketId; + Next = inBunch.Next; + Connection = inBunch.Connection; + ChIndex = inBunch.ChIndex; + ChType = inBunch.ChType; + ChName = inBunch.ChName; + ChSequence = inBunch.ChSequence; + bOpen = inBunch.bOpen; + bClose = inBunch.bClose; + bDormant = inBunch.bDormant; + bIsReplicationPaused = inBunch.bIsReplicationPaused; + bReliable = inBunch.bReliable; + bPartial = inBunch.bPartial; + bPartialInitial = inBunch.bPartialInitial; + bPartialFinal = inBunch.bPartialFinal; + bHasPackageMapExports = inBunch.bHasPackageMapExports; + bHasMustBeMappedGUIDs = inBunch.bHasMustBeMappedGUIDs; + bIgnoreRPCs = inBunch.bIgnoreRPCs; + CloseReason = inBunch.CloseReason; + + // Copy network version info + SetEngineNetVer(inBunch.EngineNetVer()); + SetGameNetVer(inBunch.GameNetVer()); + + if (copyBuffer) + { + throw new NotImplementedException(); + } + } + 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 + ChType = EChannelType.CHTYPE_None; + ChName = UnrealNames.FNames[UnrealNameKey.None]; ChSequence = 0; bOpen = false; bClose = false; @@ -41,7 +73,7 @@ namespace Prospect.Unreal.Net.Packets.Bunch public int ChIndex { get; set; } - public int ChType { get; set; } + public EChannelType ChType { get; set; } public FName ChName { get; set; } diff --git a/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs b/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs index 0dfe9cd..b0bd352 100644 --- a/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs +++ b/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs @@ -71,7 +71,7 @@ public class FNetPacketNotify var inSeqDelta = GetSequenceDelta(notificationData); if (inSeqDelta > 0) { - Logger.Verbose("FNetPacketNotify::Update - Seq {Seq}, InSeq {InSeq}", notificationData.Seq.Value, _inSeq.Value); + Logger.Verbose("Update - Seq {Seq}, InSeq {InSeq}", notificationData.Seq.Value, _inSeq.Value); ProcessReceivedAcks(notificationData, func); @@ -143,4 +143,34 @@ public class FNetPacketNotify return new SequenceNumber((ushort)(ackedSeq.Value - MaxSequenceHistoryLength)); } + + /// + /// Mark Seq as received and update current InSeq, missing sequence numbers will be marked as lost + /// + public void AckSeq(SequenceNumber seq) + { + AckSeq(seq, true); + } + + /// + /// Explicitly mark Seq as not received and update current InSeq, additional missing sequence numbers will be marked as lost + /// + public void NakSeq(SequenceNumber seq) + { + AckSeq(seq, false); + } + + private void AckSeq(SequenceNumber ackedSeq, bool isAck) + { + while (ackedSeq.Greater(_inAckSeq)) + { + _inAckSeq.IncrementAndGet(); + + var bReportAcked = _inAckSeq.Equals(ackedSeq) ? isAck : false; + + Logger.Verbose("AckSeq - AckedSeq: {Seq}, IsAck {IsAck}", _inAckSeq.Value, bReportAcked); + + _inSeqHistory.AddDeliveryStatus(bReportAcked); + } + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs b/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs index 517dee5..c2b1851 100644 --- a/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs +++ b/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs @@ -18,15 +18,6 @@ public readonly struct 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() { @@ -36,6 +27,20 @@ public readonly struct SequenceHistory } } + public void AddDeliveryStatus(bool delivered) + { + var carry = delivered ? 1u : 0u; + var valueMask = 1u << (int)(BitsPerWord - 1); + + for (var i = 0; i < WordCount; i++) + { + var oldValue = carry; + + carry = (_storage[i] & valueMask) >> (int)(BitsPerWord - 1); + _storage[i] = (_storage[i] << 1) | oldValue; + } + } + public bool IsDelivered(int index) { var wordIndex = (int)(index / BitsPerWord); @@ -43,4 +48,13 @@ public readonly struct SequenceHistory return (_storage[wordIndex] & wordMask) != 0; } + + public void Read(FBitReader reader, uint numWords) + { + numWords = Math.Min(numWords, WordCount); + for (var i = 0; i < numWords; i++) + { + _storage[i] = reader.ReadUInt32(); + } + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Player/APlayerController.cs b/src/Prospect.Unreal/Net/Player/APlayerController.cs new file mode 100644 index 0000000..2ca7ea2 --- /dev/null +++ b/src/Prospect.Unreal/Net/Player/APlayerController.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net.Player; + +public class APlayerController +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Player/UPlayer.cs b/src/Prospect.Unreal/Net/Player/UPlayer.cs new file mode 100644 index 0000000..ebbe585 --- /dev/null +++ b/src/Prospect.Unreal/Net/Player/UPlayer.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net.Player; + +public class UPlayer +{ + public APlayerController? PlayerController { get; set; } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UChannel.cs b/src/Prospect.Unreal/Net/UChannel.cs deleted file mode 100644 index 6835a06..0000000 --- a/src/Prospect.Unreal/Net/UChannel.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Prospect.Unreal.Core.Names; - -namespace Prospect.Unreal.Net; - -public class UChannel -{ - public FName ChName { get; } -} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UIpConnection.cs b/src/Prospect.Unreal/Net/UIpConnection.cs index cfa16ea..bc3ed05 100644 --- a/src/Prospect.Unreal/Net/UIpConnection.cs +++ b/src/Prospect.Unreal/Net/UIpConnection.cs @@ -55,7 +55,13 @@ public class UIpConnection : UNetConnection public override string LowLevelGetRemoteAddress(bool bAppendPort = false) { - throw new NotImplementedException(); + if (RemoteAddr != null) + { + // TODO: Remove port + return RemoteAddr.ToString(); + } + + return string.Empty; } public override string LowLevelDescribe() diff --git a/src/Prospect.Unreal/Net/UIpNetDriver.cs b/src/Prospect.Unreal/Net/UIpNetDriver.cs index 1549330..99c2f85 100644 --- a/src/Prospect.Unreal/Net/UIpNetDriver.cs +++ b/src/Prospect.Unreal/Net/UIpNetDriver.cs @@ -22,8 +22,13 @@ public class UIpNetDriver : UNetDriver public UdpClient Socket { get; } public FReceiveThreadRunnable ReceiveThread { get; } - public override bool Init() + public override bool Init(FNetworkNotify notify) { + if (!base.Init(notify)) + { + return false; + } + // Initialize connectionless packet handler. InitConnectionlessHandler(); diff --git a/src/Prospect.Unreal/Net/UNetConnection.cs b/src/Prospect.Unreal/Net/UNetConnection.cs index 0ce4b67..efb0683 100644 --- a/src/Prospect.Unreal/Net/UNetConnection.cs +++ b/src/Prospect.Unreal/Net/UNetConnection.cs @@ -1,4 +1,5 @@ -using System.Net; +using System.Diagnostics.CodeAnalysis; +using System.Net; using System.Net.Sockets; using Prospect.Unreal.Core; using Prospect.Unreal.Core.Names; @@ -7,12 +8,13 @@ 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.Net.Player; using Prospect.Unreal.Serialization; using Serilog; namespace Prospect.Unreal.Net; -public abstract class UNetConnection +public abstract class UNetConnection : UPlayer { private static readonly ILogger Logger = Log.ForContext(); @@ -30,6 +32,7 @@ public abstract class UNetConnection public const EEngineNetworkVersionHistory DefaultEngineNetworkProtocolVersion = EEngineNetworkVersionHistory.HISTORY_ENGINENETVERSION_LATEST; public const uint DefaultGameNetworkProtocolVersion = 0; + private HashSet _channelsToTick; private List? _packetOrderCache; private int _packetOrderCacheStartIdx; private int _packetOrderCacheCount; @@ -38,8 +41,17 @@ public abstract class UNetConnection private bool _bInternalAck; private bool _bReplay; + private double _statUpdateTime; + private double _lastReceiveTime; + private double _lastReceiveRealTime; + private double _lastGoodPacketRealtime; + private double _lastTime; + private double _lastSendTime; + private double _lastTickTime; + public UNetConnection() { + _channelsToTick = new HashSet(); _packetOrderCache = null; _packetOrderCacheStartIdx = 0; _packetOrderCacheCount = 0; @@ -49,6 +61,7 @@ public abstract class UNetConnection Driver = null; PackageMap = null; + OpenChannels = new List(); MaxPacket = 0; Url = new FUrl(); RemoteAddr = new IPEndPoint(IPAddress.None, 0); @@ -85,6 +98,8 @@ public abstract class UNetConnection /// public UPackageMapClient? PackageMap { get; private set; } + public List OpenChannels { get; private set; } + /// /// Maximum packet size. /// @@ -168,7 +183,17 @@ public abstract class UNetConnection Driver = inDriver; // TODO: ConnectionId + + var driverElapsedTime = Driver.GetElapsedTime(); + _statUpdateTime = driverElapsedTime; + _lastReceiveTime = driverElapsedTime; + _lastReceiveRealTime = 0; + _lastGoodPacketRealtime = 0; + _lastTime = 0; + _lastSendTime = driverElapsedTime; + _lastTickTime = driverElapsedTime; + State = inState; Url.Protocol = inURL.Protocol; @@ -263,7 +288,8 @@ public abstract class UNetConnection } var resetReaderMark = new FBitReaderMark(reader); - + var channelsToClose = new List(); + if (_bInternalAck) { ++InPacketId; @@ -369,7 +395,7 @@ public abstract class UNetConnection } var bIgnoreRPCS = Driver!.ShouldIgnoreRPCs(); - var bSickAcp = false; + var bSkipAck = false; // Track channels that were rejected while processing this packet - used to avoid sending multiple close-channel bunches, // which would cause a disconnect serverside @@ -471,7 +497,7 @@ public abstract class UNetConnection if (bunch.EngineNetVer() < EEngineNetworkVersionHistory.HISTORY_CHANNEL_NAMES) { - bunch.ChType = (bunch.bReliable || bunch.bOpen) ? (int) reader.ReadInt(EChannelType.CHTYPE_MAX) : EChannelType.CHTYPE_None; + bunch.ChType = ((bunch.bReliable || bunch.bOpen) ? (EChannelType) reader.ReadInt((int) EChannelType.CHTYPE_MAX) : EChannelType.CHTYPE_None); switch (bunch.ChType) { @@ -533,10 +559,225 @@ public abstract class UNetConnection // TODO: Close(); return; } + + var bunchDataBits = reader.ReadInt((uint)(MaxPacket * 8)); + var headerPos = reader.GetPosBits(); + if (reader.IsError()) + { + Logger.Error("Bunch header overflow"); + // TODO: Close(); + return; + } + + bunch.SetData(reader, bunchDataBits); + + if (reader.IsError()) + { + Logger.Fatal("Bunch data overflowed ({IncomingStartPos} {HeaderPos}+{BunchDataBits}/{NumBits})", incomingStartPos, headerPos, bunchDataBits, reader.GetNumBits()); + // TOOD: Close(); + return; + } + + if (bunch.bHasPackageMapExports) + { + throw new NotImplementedException(); + } + + 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); + } + else + { + 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); + } + + if (Channels[bunch.ChIndex] == null && (bunch.ChIndex != 0 || bunch.ChName != UnrealNames.FNames[UnrealNameKey.Control])) + { + if (Channels[0] == null) + { + Logger.Fatal(" Received non-control bunch before control channel was created. ChIndex: {Ch}, ChName: {Name}", bunch.ChIndex, bunch.ChName); + // TODO: Close(); + return; + } + else if (PlayerController == null && Driver.ClientConnections.Contains(this)) + { + Logger.Fatal(" Received non-control bunch before player controller was assigned. ChIndex: {Ch}, ChName: {Name}", bunch.ChIndex, bunch.ChName); + // TODO: Close(); + return; + } + } + + // ignore control channel close if it hasn't been opened yet + if (bunch.ChIndex == 0 && Channels[0] == null && bunch.bClose && bunch.ChName == UnrealNames.FNames[UnrealNameKey.Control]) + { + Logger.Fatal("Received control channel close before open"); + // Close(); + return; + } + + // We're on a 100% reliable connection and we are rolling back some data. + // In that case, we can generally ignore these bunches. + if (_bInternalAck /* && _bAllowExistingChannelIndex */) + { + throw new NotImplementedException(); + } + + // Ignore if reliable packet has already been processed. + if (bunch.bReliable && bunch.ChSequence <= InReliable[bunch.ChIndex]) + { + Logger.Warning("Received outdated bunch (Channel {Ch} Current Sequence {Seq})", bunch.ChIndex, InReliable[bunch.ChIndex]); + continue; + } + + // If opening the channel with an unreliable packet, check that it is "bNetTemporary", otherwise discard it + if (channel == null && !bunch.bReliable) + { + // Unreliable bunches that open channels should be bOpen && (bClose || bPartial) + // NetTemporary usually means one bunch that is unreliable (bOpen and bClose): 1(bOpen, bClose) + // But if that bunch export NetGUIDs, it will get split into 2 bunches: 1(bOpen, bPartial) - 2(bClose). + // (the initial actor bunch itself could also be split into multiple bunches. So bPartial is the right check here) + + var validUnreliableOpen = bunch.bOpen && (bunch.bClose || bunch.bPartial); + if (!validUnreliableOpen) + { + if (_bInternalAck) + { + // Should be impossible with 100% reliable connections + Logger.Error("Received unreliable bunch before open with reliable connection (Channel {Ch} Current Sequence {Seq})", bunch.ChIndex, InReliable[bunch.ChIndex]); + } + else + { + // Simply a log (not a warning, since this can happen under normal conditions, like from a re-join, etc) + Logger.Information("Received unreliable bunch before open (Channel {Ch} Current Sequence {Seq})", bunch.ChIndex, InReliable[bunch.ChIndex]); + } + + // Since we won't be processing this packet, don't ack it + // We don't want the sender to think this bunch was processed when it really wasn't + bSkipAck = true; + continue; + } + } + + // Create channel if necessary. + if (channel == null) + { + if (rejectedChans.Contains(bunch.ChIndex)) + { + Logger.Warning("Ignoring Bunch for ChIndex {Ch}, as the channel was already rejected while processing this packet", bunch.ChIndex); + continue; + } + + // Validate channel type. + if (!Driver.IsKnownChannelName(bunch.ChName)) + { + // Unknown type. + Logger.Fatal("Connection unknown channel type ({Name})", bunch.ChName); + // TODO: Close() + return; + } + + // Ignore incoming data on channel types that the client are not allowed to create. This can occur if we have in-flight data when server is closing a channel + if (Driver.IsServer() && (Driver.ChannelDefinitionMap[bunch.ChName].ClientOpen == false)) + { + Logger.Warning("Ignoring Bunch Create received from client since only server is allowed to create this type of channel: Bunch {Ch}: ChName {Name}, ChSequence: {Seq}", bunch.ChIndex, bunch.ChName, bunch.ChSequence); + rejectedChans.Add(bunch.ChIndex); + continue; + } + + // peek for guid + if (_bInternalAck /* && bIgnoreActorBunches */) + { + throw new NotImplementedException(); + } + + // Reliable (either open or later), so create new channel. + Logger.Information(" Bunch Create {ChIndex}: ChName {ChName}, ChSequence: {ChSequence}, bReliable: {Reliable}, bPartial: {Partial}, bPartialInitial: {PartInit}, bPartialFinal: {PartFin}", + bunch.ChIndex, + bunch.ChName, + bunch.ChSequence, + bunch.bReliable, + bunch.bPartial, + bunch.bPartialInitial, + bunch.bPartialFinal); + + channel = CreateChannelByName(bunch.ChName, EChannelCreateFlags.None, bunch.ChIndex); + + // Notify the server of the new channel. + if (!Driver.Notify.NotifyAcceptingChannel(channel)) + { + // Channel refused, so close it, flush it, and delete it. + Logger.Verbose("NotifyAcceptingChannel Failed! Channel: {Channel}", channel); + rejectedChans.Add(bunch.ChIndex); + + // TODO: FOutBunch + continue; + } + } + + bunch.bIgnoreRPCs = bIgnoreRPCS; + + // Dispatch the raw, unsequenced bunch to the channel. + channel.ReceivedRawBunch(bunch, out var bLocalSkipAck); + + if (bLocalSkipAck) + { + bSkipAck = true; + } + + // Disconnect if we received a corrupted packet from the client (eg server crash attempt). + if (Driver.IsServer() && (bunch.IsCriticalError() || bunch.IsError())) + { + Logger.Error("Received corrupted packet data from client {RemoteAddress}. Disconnecting", LowLevelGetRemoteAddress()); + // TODO: Close() + return; + } } } - throw new NotImplementedException(); + // Close/clean-up channels pending close due to received acks. + foreach (var info in channelsToClose) + { + var channel = Channels[info.Id]; + if (channel != null) + { + channel.ConditionalCleanUp(false, info.CloseReason); + } + } + + // TODO: ValidateSendBuffer(); + + if (!bSkipAck) + { + _lastGoodPacketRealtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } + + if (!_bInternalAck) + { + if (bSkipAck) + { + PacketNotify.NakSeq(new SequenceNumber((ushort)InPacketId)); + } + else + { + PacketNotify.AckSeq(new SequenceNumber((ushort)InPacketId)); + + // TODO: Increment things + // ++OutTotalAcks; + // ++Driver->OutTotalAcks; + } + + // We do want to let the other side know about the ack, so even if there are no other outgoing data when we tick the connection we will send an ackpacket. + // TimeSensitive = 1; + // ++HasDirtyAcks; + + // TODO: FlushNet if HasDirtyAcks + } } private bool ReadPacketInfo(FBitReader reader, bool bHasPacketInfoPayload) @@ -641,10 +882,93 @@ public abstract class UNetConnection Handler.InitializeComponents(); } - public void CreateChannelByName(string chName, EChannelCreateFlags createFlags, int channelIndex) + public UChannel CreateChannelByName(FName chName, EChannelCreateFlags createFlags, int chIndex) { - // TODO: Implement - Logger.Verbose("Creating channel {Name} with index {Index}", chName, channelIndex); + if (!Driver!.IsKnownChannelName(chName)) + { + throw new UnrealNetException("Unknown channel name was specified."); + } + + if (chIndex == UnrealConstants.IndexNone) + { + chIndex = GetFreeChannelIndex(chName); + + if (chIndex == UnrealConstants.IndexNone) + { + Logger.Warning("No free channel could be found in the channel list (current limit is {Max} channels)", MaxChannelSize); + throw new UnrealNetException("Exhausted channels"); + } + } + + // Make sure channel is valid. + if (chIndex >= Channels.Length) + { + throw new UnrealNetException("Channel index is too high."); + } + + if (Channels[chIndex] != null) + { + throw new UnrealNetException("Trying to replace an existing channel."); + } + + // Create channel. + var channel = Driver.GetOrCreateChannelByName(chName); + if (channel == null) + { + throw new UnrealNetException("Failed to create channel."); + } + + channel.Init(this, chIndex, createFlags); + + Channels[chIndex] = channel; + OpenChannels.Add(channel); + + if (Driver.ChannelDefinitionMap[chName].TickOnCreate) + { + StartTickingChannel(channel); + } + + Logger.Verbose("Created channel {Ch} of type {Name}", chIndex, chName); + + return channel; + } + + private int GetFreeChannelIndex(FName chName) + { + int chIndex; + var firstChannel = 1; + + var staticChannelIndex = Driver!.ChannelDefinitionMap[chName].StaticChannelIndex; + if (staticChannelIndex != -1) + { + firstChannel = staticChannelIndex; + } + + // Search the channel array for an available location + for (chIndex = firstChannel; chIndex < Channels.Length; chIndex++) + { + if (Channels[chIndex] == null) + { + break; + } + } + + if (chIndex == Channels.Length) + { + chIndex = UnrealConstants.IndexNone; + } + + return chIndex; + } + + private void StartTickingChannel(UChannel channel) + { + _channelsToTick.Add(channel); + } + + private void StopTickingChannel(UChannel channel) + { + _channelsToTick.Remove(channel); } private protected void SetClientLoginState(EClientLoginState newState) @@ -673,6 +997,11 @@ public abstract class UNetConnection ExpectedClientLoginMsgType = newType; } + public bool IsInternalAck() + { + return _bInternalAck; + } + private static int BestSignedDifference(int value, int reference, int max) { return ((value - reference + max / 2) & (max - 1)) - max / 2; diff --git a/src/Prospect.Unreal/Net/UNetDriver.cs b/src/Prospect.Unreal/Net/UNetDriver.cs index 5569df7..900e357 100644 --- a/src/Prospect.Unreal/Net/UNetDriver.cs +++ b/src/Prospect.Unreal/Net/UNetDriver.cs @@ -1,5 +1,7 @@ using System.Collections.Concurrent; using System.Net; +using Prospect.Unreal.Core.Names; +using Prospect.Unreal.Exceptions; using Prospect.Unreal.Net.Channels; using Prospect.Unreal.Runtime; @@ -13,14 +15,20 @@ public abstract class UNetDriver : IAsyncDisposable { GuidCache = new FNetGUIDCache(this); // TODO: Load from Engine ini + ChannelDefinitionMap = new Dictionary(); ChannelDefinitions = new List { - new FChannelDefinition("Control", typeof(string), 0, true, false, true, false, true), - new FChannelDefinition("Voice", typeof(string), 1, true, true, true, true, true), - new FChannelDefinition("Actor", typeof(string), -1, false, true, false, false, false) + new FChannelDefinition(UnrealNames.FNames[UnrealNameKey.Control], typeof(string), 0, true, false, true, false, true), + new FChannelDefinition(UnrealNames.FNames[UnrealNameKey.Voice], typeof(string), 1, true, true, true, true, true), + new FChannelDefinition(UnrealNames.FNames[UnrealNameKey.Actor], typeof(string), -1, false, true, false, false, false) }; ClientConnections = new List(); MappedClientConnections = new ConcurrentDictionary(); + + foreach (var channel in ChannelDefinitions) + { + ChannelDefinitionMap[channel.Name] = channel; + } } /// @@ -28,12 +36,19 @@ public abstract class UNetDriver : IAsyncDisposable /// public UWorld? World { get; private set; } + public FNetworkNotify Notify { get; private set; } + public FNetGUIDCache GuidCache { get; } /// /// Used to specify available channel types and their associated UClass /// public List ChannelDefinitions { get; } + + /// + /// Used for faster lookup of channel definitions by name. + /// + public Dictionary ChannelDefinitionMap { get; } /// /// Array of connections to clients (this net driver is a host) - unsorted, and ordering changes depending on actor replication @@ -66,9 +81,10 @@ public abstract class UNetDriver : IAsyncDisposable ConnectionlessHandler.InitializeComponents(); } - public virtual bool Init() + public virtual bool Init(FNetworkNotify notify) { - throw new NotImplementedException(); + Notify = notify; + return true; } public virtual void TickDispatch(float deltaTime) @@ -103,6 +119,11 @@ public abstract class UNetDriver : IAsyncDisposable return true; } + public bool IsKnownChannelName(FName name) + { + return ChannelDefinitionMap.ContainsKey(name); + } + public virtual bool ShouldIgnoreRPCs() { return false; @@ -145,6 +166,19 @@ public abstract class UNetDriver : IAsyncDisposable } } + public UChannel GetOrCreateChannelByName(FName chName) + { + // TODO: Pool actor channels (?) + + return (UnrealNameKey)chName.Number switch + { + UnrealNameKey.Actor => new UActorChannel(), + UnrealNameKey.Control => new UControlChannel(), + UnrealNameKey.Voice => new UVoiceChannel(), + _ => throw new UnrealNetException($"Attempted to create unknown channel {chName}") + }; + } + public virtual ValueTask DisposeAsync() { return ValueTask.CompletedTask; diff --git a/src/Prospect.Unreal/Runtime/UWorld.cs b/src/Prospect.Unreal/Runtime/UWorld.cs index e494903..10633f9 100644 --- a/src/Prospect.Unreal/Runtime/UWorld.cs +++ b/src/Prospect.Unreal/Runtime/UWorld.cs @@ -1,10 +1,12 @@ using Prospect.Unreal.Core; using Prospect.Unreal.Net; +using Prospect.Unreal.Net.Channels; +using Prospect.Unreal.Net.Packets.Bunch; using Serilog; namespace Prospect.Unreal.Runtime; -public abstract class UWorld : IAsyncDisposable +public abstract class UWorld : FNetworkNotify, IAsyncDisposable { private static readonly ILogger Logger = Log.ForContext(); @@ -36,7 +38,7 @@ public abstract class UWorld : IAsyncDisposable NetDriver = new UIpNetDriver(Url.Host, Url.Port); NetDriver.SetWorld(this); - if (!NetDriver.Init()) + if (!NetDriver.Init(this)) { Logger.Error("Failed to listen"); NetDriver = null; @@ -46,6 +48,44 @@ public abstract class UWorld : IAsyncDisposable return true; } + public EAcceptConnection NotifyAcceptingConnection() + { + return EAcceptConnection.Accept; + } + + public void NotifyAcceptedConnection(UNetConnection connection) + { + + } + + public bool NotifyAcceptingChannel(UChannel channel) + { + var driver = channel.Connection.Driver!; + if (!driver.IsServer()) + { + throw new NotSupportedException("Client code"); + } + else + { + // We are the server. + if (driver.ChannelDefinitionMap[channel.ChName].ClientOpen) + { + // The client has opened initial channel. + Logger.Verbose("NotifyAcceptingChannel {ChName} {ChIndex} server {FullName}: Accepted", channel.ChName, channel.ChIndex, typeof(UWorld).FullName); + return true; + } + + // Client can't open any other kinds of channels. + Logger.Verbose("NotifyAcceptingChannel {ChName} {ChIndex} server {FullName}: Refused", channel.ChName, channel.ChIndex, typeof(UWorld).FullName); + return false; + } + } + + public void NotifyControlMessage(UNetConnection connection, byte messageType, FInBunch bunch) + { + + } + public async ValueTask DisposeAsync() { if (NetDriver != null) diff --git a/src/Prospect.Unreal/Serialization/FBitReaderMark.cs b/src/Prospect.Unreal/Serialization/FBitReaderMark.cs index bff3e44..de3fe9f 100644 --- a/src/Prospect.Unreal/Serialization/FBitReaderMark.cs +++ b/src/Prospect.Unreal/Serialization/FBitReaderMark.cs @@ -1,6 +1,6 @@ namespace Prospect.Unreal.Serialization; -public class FBitReaderMark +public struct FBitReaderMark { private long _pos;