Implement handshake Hello NetControlMessage, recv and send

This commit is contained in:
AeonLucid
2022-01-12 07:26:25 +01:00
parent 0a27bdd3c0
commit 65a28b32ef
50 changed files with 2768 additions and 156 deletions
@@ -0,0 +1,9 @@
namespace Prospect.Unreal.Core;
public static class FPlatformTime
{
public static double Seconds()
{
return DateTimeOffset.UtcNow.ToUnixTimeSeconds();
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace Prospect.Unreal.Core;
public class UObject
{
}
@@ -1,7 +1,7 @@
using Prospect.Unreal.Core.Names;
using Prospect.Unreal.Net.Packets.Bunch;
namespace Prospect.Unreal.Net.Channels;
namespace Prospect.Unreal.Net.Channels.Actor;
public class UActorChannel : UChannel
{
@@ -0,0 +1,173 @@
using Prospect.Unreal.Core.Names;
using Prospect.Unreal.Net.Packets.Bunch;
using Prospect.Unreal.Net.Packets.Control;
using Serilog;
namespace Prospect.Unreal.Net.Channels.Control;
public class UControlChannel : UChannel
{
private static readonly ILogger Logger = Log.ForContext<UControlChannel>();
public UControlChannel()
{
ChType = EChannelType.CHTYPE_Control;
ChName = UnrealNames.FNames[UnrealNameKey.Control];
}
protected override void ReceivedBunch(FInBunch bunch)
{
// if (Connection != null && bNeedsEndianInspection && !CheckEndianess(bunch))
// {
// // Send close bunch and shutdown this connection
// }
// bunch.()
while (!bunch.AtEnd() && Connection != null && Connection.State != EConnectionState.USOCK_Closed)
{
var messageType = (NMT) bunch.ReadByte();
if (bunch.IsError())
{
break;
}
var pos = bunch.GetPosBits();
if (messageType == NMT.ActorChannelFailure)
{
throw new NotImplementedException();
}
else if (messageType == NMT.GameSpecific)
{
// the most common Notify handlers do not support subclasses by default and
// so we redirect the game specific messaging to the GameInstance instead
throw new NotImplementedException();
}
else if (messageType == NMT.SecurityViolation)
{
throw new NotImplementedException();
}
else if (messageType == NMT.DestructionInfo)
{
throw new NotImplementedException();
}
else
{
// Process control message on client/server connection
Connection.Driver!.Notify.NotifyControlMessage(Connection, messageType, bunch);
}
// if the message was not handled, eat it ourselves
if (pos == bunch.GetPosBits() && !bunch.IsError())
{
switch (messageType)
{
case NMT.Hello:
NMT_Hello.Discard(bunch);
break;
case NMT.Welcome:
NMT_Welcome.Discard(bunch);
break;
case NMT.Upgrade:
NMT_Upgrade.Discard(bunch);
break;
case NMT.Challenge:
NMT_Challenge.Discard(bunch);
break;
case NMT.Netspeed:
NMT_Netspeed.Discard(bunch);
break;
case NMT.Login:
NMT_Login.Discard(bunch);
break;
case NMT.Failure:
NMT_Failure.Discard(bunch);
break;
case NMT.Join:
NMT_Join.Discard(bunch);
break;
case NMT.JoinSplit:
NMT_JoinSplit.Discard(bunch);
break;
case NMT.Skip:
NMT_Skip.Discard(bunch);
break;
case NMT.Abort:
NMT_Abort.Discard(bunch);
break;
case NMT.PCSwap:
NMT_PCSwap.Discard(bunch);
break;
case NMT.ActorChannelFailure:
NMT_ActorChannelFailure.Discard(bunch);
break;
case NMT.DebugText:
NMT_DebugText.Discard(bunch);
break;
case NMT.NetGUIDAssign:
NMT_NetGUIDAssign.Discard(bunch);
break;
case NMT.EncryptionAck:
NMT_EncryptionAck.Discard(bunch);
break;
case NMT.BeaconWelcome:
NMT_BeaconWelcome.Discard(bunch);
break;
case NMT.BeaconJoin:
NMT_BeaconJoin.Discard(bunch);
break;
case NMT.BeaconAssignGUID:
NMT_BeaconAssignGUID.Discard(bunch);
break;
case NMT.BeaconNetGUIDAck:
NMT_BeaconNetGUIDAck.Discard(bunch);
break;
default:
// if this fails, a case is missing above for an implemented message type
// or the connection is being sent potentially malformed packets
// @PotentialDOSAttackDetection
Logger.Error("Received unknown control channel message {MessageType}. Closing connection", (int)messageType);
Connection.Close();
return;
}
}
if (bunch.IsError())
{
Logger.Error("Failed to read control channel message '{MessageType}'", messageType);
break;
}
}
if (bunch.IsError())
{
Logger.Error("Failed to read control channel message");
if (Connection != null)
{
Connection.Close();
}
}
// throw new NotImplementedException();
}
public override FPacketIdRange SendBunch(FOutBunch bunch, bool merge)
{
// TODO: Queue.
if (!bunch.IsError())
{
return base.SendBunch(bunch, merge);
}
else
{
// an error here most likely indicates an unfixable error, such as the text using more than the maximum packet size
// so there is no point in queueing it as it will just fail again
Logger.Error("Control channel bunch overflowed");
Connection!.Close();
return new FPacketIdRange();
}
}
}
+315 -4
View File
@@ -1,6 +1,7 @@
using Prospect.Unreal.Core.Names;
using Prospect.Unreal.Exceptions;
using Prospect.Unreal.Net.Packets.Bunch;
using Prospect.Unreal.Serialization;
using Serilog;
namespace Prospect.Unreal.Net.Channels;
@@ -14,7 +15,7 @@ public abstract class UChannel
/// <summary>
/// Owner connection.
/// </summary>
public UNetConnection Connection { get; set; }
public UNetConnection? Connection { get; set; }
/// <summary>
/// If OpenedLocally is true, this means we have acknowledged the packet we sent the bOpen bunch on.
@@ -123,7 +124,10 @@ public abstract class UChannel
/// </summary>
public FInBunch? InRec { get; set; }
// public FOutBunch OutRec { get; set; }
/// <summary>
/// Outgoing reliable unacked data.
/// </summary>
public FOutBunch? OutRec { get; set; }
/// <summary>
/// Partial bunch we are receiving (incoming partial bunches are appended to this).
@@ -413,8 +417,7 @@ public abstract class UChannel
// 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;
OpenPacketId = new FPacketIdRange(handleBunch.PacketId, bunch.PacketId);
OpenAcked = true;
Logger.Verbose("ReceivedNextBunch: Channel now fully open. ChIndex: {ChIndex}, OpenPacketId.First: {First}, OpenPacketId.Last: {Last}", ChIndex, OpenPacketId.First, OpenPacketId.Last);
@@ -501,6 +504,309 @@ public abstract class UChannel
protected abstract void ReceivedBunch(FInBunch bunch);
public virtual FPacketIdRange SendBunch(FOutBunch bunch, bool merge)
{
if (Connection == null || Connection.Driver == null)
{
throw new UnrealNetException();
}
if (ChIndex == -1)
{
return new FPacketIdRange(UnrealConstants.IndexNone);
}
if (IsBunchTooLarge(Connection!, bunch))
{
Logger.Error("Attempted to send bunch exceeding max allowed size. BunchSize={Size}, MaximumSize={MaxSize}", bunch.GetNumBytes(), NetMaxConstructedPartialBunchSizeBytes);
bunch.SetError();
return new FPacketIdRange(UnrealConstants.IndexNone);
}
if (Closing || Connection.Channels[ChIndex] != this || bunch.IsError() || bunch.bHasPackageMapExports)
{
throw new UnrealNetException();
}
// Set bunch flags.
var bDormancyClose = bunch.bClose && (bunch.CloseReason == EChannelCloseReason.Dormancy);
if (OpenedLocally && ((OpenPacketId.First == UnrealConstants.IndexNone) || ((Connection.ResendAllDataState == EResendAllDataState.None) && !bDormancyClose)))
{
var bOpenBunch = true;
if (Connection.ResendAllDataState == EResendAllDataState.SinceCheckpoint)
{
bOpenBunch = !bOpenedForCheckpoint;
bOpenedForCheckpoint = true;
}
if (bOpenBunch)
{
bunch.bOpen = true;
OpenTemporary = !bunch.bReliable;
}
}
if (OpenTemporary && bunch.bReliable)
{
throw new UnrealNetException("Channel was opened temporarily, we are never allowed to send reliable packets on it");
}
// This is the max number of bits we can have in a single bunch
var MAX_SINGLE_BUNCH_SIZE_BITS = Connection.GetMaxSingleBunchSizeBits();
// Max bytes we'll put in a partial bunch
var MAX_SINGLE_BUNCH_SIZE_BYTES = MAX_SINGLE_BUNCH_SIZE_BITS / 8;
// Max bits will put in a partial bunch (byte aligned, we dont want to deal with partial bytes in the partial bunches)
var MAX_PARTIAL_BUNCH_SIZE_BITS = MAX_SINGLE_BUNCH_SIZE_BYTES * 8;
var outgoingBunches = new List<FOutBunch>();
// Add any export bunches
// Replay connections will manage export bunches separately.
if (!Connection.IsInternalAck())
{
// TODO: AppendExportBunches
}
if (outgoingBunches.Count != 0)
{
// Don't merge if we are exporting guid's
// We can't be for sure if the last bunch has exported guids as well, so this just simplifies things
merge = false;
}
if (Connection.Driver.IsServer())
{
// Append any "must be mapped" guids to front of bunch from the packagemap
// TODO: AppendMustBeMappedGuids
if (bunch.bHasMustBeMappedGUIDs)
{
merge = false;
}
}
//-----------------------------------------------------
// Contemplate merging.
//-----------------------------------------------------
// TODO: Merge
// var preExistingBits = 0;
FOutBunch? outBunch = null;
// if (merge
// && Connection.LastOut.ChIndex == bunch.ChIndex
// && Connection.LastOut.bReliable == bunch.bReliable
// && Connection.AllowMerge
// && Connection.LastEnd.GetNumBits() != 0
// && Connection.LastEnd.GetNumBits() == Connection.SendBuffer.GetNumBits()
// && Connection.LastOut.GetNumBits() + bunch.GetNumBits() <= MAX_SINGLE_BUNCH_SIZE_BITS)
// {
//
// }
//-----------------------------------------------------
// Possibly split large bunch into list of smaller partial bunches
//-----------------------------------------------------
if (bunch.GetNumBits() > MAX_SINGLE_BUNCH_SIZE_BITS)
{
var data = bunch.GetData().AsSpan();
var bitsLeft = bunch.GetNumBits();
merge = false;
while (bitsLeft > 0)
{
var partialBunch = new FOutBunch(this, false);
var bitsThisBunch = (long) Math.Min(bitsLeft, MAX_PARTIAL_BUNCH_SIZE_BITS);
partialBunch.SerializeBits(data, bitsThisBunch);
outgoingBunches.Add(partialBunch);
bitsLeft -= bitsThisBunch;
data = data.Slice((int)(bitsThisBunch >> 3));
Logger.Debug("Making partial bunch from content bunch. bitsThisBunch: {Bits} bitsLeft: {Left}", bitsThisBunch, bitsLeft);
}
}
else
{
outgoingBunches.Add(bunch);
}
//-----------------------------------------------------
// Send all the bunches we need to
// Note: this is done all at once. We could queue this up somewhere else before sending to Out.
//-----------------------------------------------------
var packetIdRange = new FPacketIdRange();
var bOverflowsReliable = (NumOutRec + outgoingBunches.Count >= UNetConnection.ReliableBuffer + (bunch.bClose ? 1 : 0));
if (bunch.bReliable && bOverflowsReliable)
{
// TODO: Send NMT_Failure
// TODO: FlushNet(true);
Connection.Close();
throw new NotImplementedException();
return packetIdRange;
}
if (outgoingBunches.Count > 1)
{
Logger.Debug("Sending {Count} bunches. Channel: {ChIndex} {Channel}", outgoingBunches.Count, bunch.ChIndex, this);
}
for (var partialNum = 0; partialNum < outgoingBunches.Count; partialNum++)
{
var nextBunch = outgoingBunches[partialNum];
nextBunch.bReliable = bunch.bReliable;
nextBunch.bOpen = bunch.bOpen;
nextBunch.bClose = bunch.bClose;
nextBunch.bDormant = bunch.bDormant;
nextBunch.CloseReason = bunch.CloseReason;
nextBunch.bIsReplicationPaused = bunch.bIsReplicationPaused;
nextBunch.ChIndex = bunch.ChIndex;
nextBunch.ChType = bunch.ChType;
nextBunch.ChName = bunch.ChName;
if (!nextBunch.bHasPackageMapExports)
{
nextBunch.bHasMustBeMappedGUIDs |= bunch.bHasMustBeMappedGUIDs;
}
if (outgoingBunches.Count > 1)
{
nextBunch.bPartial = true;
nextBunch.bPartialInitial = partialNum == 0;
nextBunch.bPartialFinal = partialNum == outgoingBunches.Count - 1;
nextBunch.bOpen &= partialNum == 0; // Only the first bunch should have the bOpen bit set
nextBunch.bClose = (bunch.bClose && (outgoingBunches.Count - 1 == partialNum)); // Only last bunch should have bClose bit set
}
var thisOutBunch = PrepBunch(nextBunch, ref outBunch, merge);
// Update Packet Range
var packetId = SendRawBunch(thisOutBunch, merge);
if (partialNum == 0)
{
packetIdRange = new FPacketIdRange(packetId);
}
else
{
packetIdRange = new FPacketIdRange(packetIdRange.First, packetId);
}
// Update channel sequence count.
Connection.LastOut = thisOutBunch;
Connection.LastEnd = new FBitWriterMark(Connection.SendBuffer);
}
// Update open range if necessary
if (bunch.bOpen && (Connection.ResendAllDataState == EResendAllDataState.None))
{
OpenPacketId = packetIdRange;
}
// Destroy outgoing bunches now that they are sent, except the one that was passed into ::SendBunch
// This is because the one passed in ::SendBunch is the responsibility of the caller, the other bunches in OutgoingBunches
// were either allocated in this function for partial bunches, or taken from the package map, which expects us to destroy them.
foreach (var deleteBunch in outgoingBunches)
{
if (deleteBunch != bunch)
{
deleteBunch.Dispose();
}
}
return packetIdRange;
}
private int SendRawBunch(FOutBunch outBunch, bool merge)
{
// Send the raw bunch.
outBunch.ReceivedAck = false;
var packetId = Connection!.SendRawBunch(outBunch, merge);
if (OpenPacketId.First == UnrealConstants.IndexNone && OpenedLocally)
{
OpenPacketId = new FPacketIdRange(packetId);
}
if (outBunch.bClose)
{
SetClosingFlag();
}
return packetId;
}
private FOutBunch PrepBunch(FOutBunch bunch, ref FOutBunch? outBunch, bool merge)
{
if (Connection!.ResendAllDataState != EResendAllDataState.None)
{
return bunch;
}
// Find outgoing bunch index.
if (bunch.bReliable)
{
// Find spot, which was guaranteed available by FOutBunch constructor.
if (outBunch == null)
{
if (!(NumOutRec < UNetConnection.ReliableBuffer - 1 + (bunch.bClose ? 1 : 0)))
{
Logger.Warning("PrepBunch: Reliable buffer overflow! {Channel}", this);
}
bunch.Next = null;
bunch.ChSequence = ++Connection.OutReliable[ChIndex];
NumOutRec++;
// TODO: Verify below works as expected
outBunch = new FOutBunch(bunch);
var outLink = OutRec;
while (outLink != null)
{
outLink = outLink.Next;
}
if (outLink == null)
{
OutRec = outBunch;
}
else
{
outLink.Next = outBunch;
}
}
else
{
bunch.Next = outBunch.Next;
outBunch = bunch;
}
Connection.LastOutBunch = outBunch;
}
else
{
outBunch = bunch;
Connection.LastOutBunch = null;
}
return outBunch;
}
private void SetClosingFlag()
{
Closing = true;
}
public void ConditionalCleanUp(bool bForDestroy, EChannelCloseReason closeReason)
{
throw new NotImplementedException();
@@ -510,4 +816,9 @@ public abstract class UChannel
{
return !connection.IsInternalAck() && bunch != null && bunch.GetNumBytes() > NetMaxConstructedPartialBunchSizeBytes;
}
private static bool IsBunchTooLarge(UNetConnection connection, FOutBunch? bunch)
{
return !connection.IsInternalAck() && bunch != null && bunch.GetNumBytes() > NetMaxConstructedPartialBunchSizeBytes;
}
}
@@ -1,18 +0,0 @@
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();
}
}
@@ -1,7 +1,7 @@
using Prospect.Unreal.Core.Names;
using Prospect.Unreal.Net.Packets.Bunch;
namespace Prospect.Unreal.Net.Channels;
namespace Prospect.Unreal.Net.Channels.Voice;
public class UVoiceChannel : UChannel
{
@@ -0,0 +1,8 @@
namespace Prospect.Unreal.Net;
public enum EResendAllDataState
{
None,
SinceOpen,
SinceCheckpoint
}
@@ -0,0 +1,8 @@
namespace Prospect.Unreal.Net;
public enum EWriteBitsDataType
{
Unknown,
Bunch,
Ack
}
+6
View File
@@ -0,0 +1,6 @@
namespace Prospect.Unreal.Net;
public class FGuid
{
}
+6
View File
@@ -0,0 +1,6 @@
namespace Prospect.Unreal.Net;
public class FNetworkGUID
{
}
+2 -1
View File
@@ -1,5 +1,6 @@
using Prospect.Unreal.Net.Channels;
using Prospect.Unreal.Net.Packets.Bunch;
using Prospect.Unreal.Net.Packets.Control;
namespace Prospect.Unreal.Net;
@@ -24,5 +25,5 @@ public interface FNetworkNotify
///
/// (i.e. use FNetControlMessage::Receive())
/// </summary>
void NotifyControlMessage(UNetConnection connection, byte messageType, FInBunch bunch);
void NotifyControlMessage(UNetConnection connection, NMT messageType, FInBunch bunch);
}
+5 -1
View File
@@ -2,5 +2,9 @@
public class FOutPacketTraits
{
public bool AllowCompression { get; set; } = true;
public uint NumAckBits { get; set; }
public uint NumBunchBits { get; set; }
public bool IsKeepAlive { get; set; }
public bool IsCompressed { get; set; }
}
+24 -4
View File
@@ -1,9 +1,29 @@
namespace Prospect.Unreal.Net;
using Prospect.Unreal.Core.Names;
public class FPacketIdRange
namespace Prospect.Unreal.Net;
public readonly struct FPacketIdRange
{
public int First = -1;
public int Last = -1;
public FPacketIdRange()
{
First = UnrealConstants.IndexNone;
Last = UnrealConstants.IndexNone;
}
public FPacketIdRange(int packetId)
{
First = packetId;
Last = packetId;
}
public FPacketIdRange(int first, int last)
{
First = first;
Last = last;
}
public int First { get; }
public int Last { get; }
public bool InRange(int packetId)
{
@@ -0,0 +1,5 @@
namespace Prospect.Unreal.Net;
public class FUniqueNetIdRepl
{
}
+12 -15
View File
@@ -5,8 +5,6 @@ namespace Prospect.Unreal.Net;
public abstract class HandlerComponent
{
private bool _bRequiresHandshake;
private bool _bRequiresReliability;
private bool _bActive;
private bool _bInitialized;
private string _name;
@@ -14,8 +12,8 @@ public abstract class HandlerComponent
protected HandlerComponent(PacketHandler handler, string name)
{
_name = name;
_bRequiresHandshake = false;
_bRequiresReliability = false;
RequiresHandshake = false;
RequiresReliability = false;
_bActive = false;
_bInitialized = false;
@@ -26,6 +24,14 @@ public abstract class HandlerComponent
protected PacketHandler Handler { get; }
protected HandlerComponentState State { get; private set; }
public bool RequiresHandshake { get; protected set; }
public bool RequiresReliability { get; protected set; }
/// <summary>
/// Maximum number of Outgoing packet bits supported (automatically calculated to factor in other HandlerComponent reserved bits)
/// </summary>
public uint MaxOutgoingBits { get; set; }
public virtual bool IsActive()
{
return _bActive;
@@ -41,21 +47,11 @@ public abstract class HandlerComponent
return _bInitialized;
}
public bool RequiresHandshake()
{
return _bRequiresHandshake;
}
public bool RequiresReliability()
{
return _bRequiresReliability;
}
public virtual void Incoming(FBitReader packet)
{
}
public virtual void Outgoing(FBitWriter packet, FOutPacketTraits traits)
public virtual void Outgoing(ref FBitWriter packet, FOutPacketTraits traits)
{
}
@@ -104,5 +100,6 @@ public abstract class HandlerComponent
protected void Initialized()
{
_bInitialized = true;
Handler.HandlerComponentInitialized(this);
}
}
+163 -11
View File
@@ -1,4 +1,5 @@
using System.Net;
using Prospect.Unreal.Exceptions;
using Prospect.Unreal.Serialization;
using Serilog;
@@ -11,30 +12,38 @@ public record ProcessedPacket(byte[] Data, int CountBits, bool Error = false);
public class PacketHandler
{
private static readonly ILogger Logger = Log.ForContext<PacketHandler>();
private static readonly IPEndPoint EmptyAddress = new IPEndPoint(IPAddress.None, 0);
private readonly List<HandlerComponent> _handlerComponents;
private bool _bConnectionlessHandler;
private bool _bRawSend;
/// <summary>
/// The maximum supported packet size (reflects UNetConnection::MaxPacket)
/// </summary>
private uint _maxPacketBits;
private HandlerState _state;
private ReliabilityHandlerComponent? _reliabilityComponent;
private FBitWriter _outgoingPacket;
private FBitReader _incomingPacket;
private bool _bBeganHandshaking;
public PacketHandler()
{
_bConnectionlessHandler = false;
_state = HandlerState.Uninitialized;
_handlerComponents = new List<HandlerComponent>();
_reliabilityComponent = null;
_outgoingPacket = new FBitWriter(0);
_outgoingPacket = new FBitWriter(0, true, false);
_outgoingPacket.AllowAppend(1);
_incomingPacket = new FBitReader(Array.Empty<byte>());
Mode = HandlerMode.Server;
}
public HandlerMode Mode { get; }
public HandlerMode Mode { get; private set; }
public void Tick(float deltaTime)
{
@@ -44,8 +53,10 @@ public class PacketHandler
}
}
public void Initialize(bool bConnectionlessOnly)
public void Initialize(HandlerMode mode, uint inMaxPacketBits, bool bConnectionlessOnly)
{
Mode = mode;
_maxPacketBits = inMaxPacketBits;
_bConnectionlessHandler = bConnectionlessOnly;
if (!_bConnectionlessHandler)
@@ -83,6 +94,9 @@ public class PacketHandler
component.Initialize();
}
}
// Called early, to ensure that all handlers report a valid reserved packet bits value (triggers an assert if not)
GetTotalReservedPacketBits();
}
public bool IncomingConnectionless(FReceivedPacketView packetView)
@@ -97,13 +111,18 @@ public class PacketHandler
return Outgoing_Internal(packet, countBits, traits, true, address);
}
public ProcessedPacket Outgoing(byte[] packet, int countBits, FOutPacketTraits traits)
{
return Outgoing_Internal(packet, countBits, traits, false, EmptyAddress);
}
public void BeginHandshaking()
{
// bBeganHandshaking = true;
foreach (var component in _handlerComponents)
{
if (component.RequiresHandshake() && !component.IsInitialized())
if (component.RequiresHandshake && !component.IsInitialized())
{
component.NotifiyHandshakeBegin();
}
@@ -129,6 +148,11 @@ public class PacketHandler
// NO-OP
}
public void OutgoingHigh(FBitWriter writer)
{
// NO-OP
}
private bool Incoming_Internal(FReceivedPacketView packetView)
{
var returnVal = true;
@@ -201,12 +225,68 @@ public class PacketHandler
{
if (!_bRawSend)
{
throw new NotImplementedException();
}
else
{
return new ProcessedPacket(packet, countBits);
_outgoingPacket.Reset();
if (_state == HandlerState.Uninitialized)
{
UpdateInitialState();
}
if (_state == HandlerState.Initialized)
{
_outgoingPacket.SerializeBits(packet, countBits);
foreach (var component in _handlerComponents)
{
if (component.IsActive())
{
if (_outgoingPacket.GetNumBits() <= component.MaxOutgoingBits)
{
if (bConnectionLess)
{
component.OutgoingConnectionless(address, _outgoingPacket, traits);
}
else
{
component.Outgoing(ref _outgoingPacket, traits);
}
}
else
{
_outgoingPacket.SetError();
Logger.Error("Packet exceeded HandlerComponents 'MaxOutgoingBits' value: {A} vs {B}", _outgoingPacket.GetNumBits(), component.MaxOutgoingBits);
break;
}
}
}
// Add a termination bit, the same as the UNetConnection code does, if appropriate
if (_handlerComponents.Count > 0 && _outgoingPacket.GetNumBits() > 0)
{
_outgoingPacket.WriteBit(true);
}
if (!bConnectionLess && _reliabilityComponent != null && _outgoingPacket.GetNumBits() > 0)
{
// Let the reliability handler know about all processed packets, so it can record them for resending if needed
throw new NotImplementedException();
}
}
// Buffer any packets being sent from game code until processors are initialized
else if (_state == HandlerState.InitializingComponents && countBits > 0)
{
throw new NotImplementedException();
}
if (!_outgoingPacket.IsError())
{
return new ProcessedPacket(_outgoingPacket.GetData(), (int)_outgoingPacket.GetNumBits());
}
return new ProcessedPacket(packet, countBits, true);
}
return new ProcessedPacket(packet, countBits);
}
private void SetState(HandlerState state)
@@ -289,4 +369,76 @@ public class PacketHandler
{
return _bRawSend;
}
public int GetTotalReservedPacketBits()
{
var returnVal = 0;
var curMaxOutgoingBits = _maxPacketBits;
for (int i = _handlerComponents.Count - 1; i >= 0; i--)
{
var curComponent = _handlerComponents[i];
var curReservedBits = curComponent.GetReservedPacketBits();
// Specifying the reserved packet bits is mandatory, even if zero (as accidentally forgetting, leads to hard to trace issues).
if (curReservedBits == -1)
{
throw new UnrealNetException("Handler returned invalid 'GetReservedPacketBits' value");
}
curComponent.MaxOutgoingBits = curMaxOutgoingBits;
curMaxOutgoingBits -= (uint) curReservedBits;
returnVal += curReservedBits;
}
// Reserve space for the termination bit
if (_handlerComponents.Count > 0)
{
returnVal++;
}
return returnVal;
}
public void HandlerComponentInitialized(HandlerComponent inComponent)
{
// Check if all handlers are initialized
if (_state != HandlerState.Initialized)
{
var bAllInitialized = true;
var bEncounteredComponent = false;
var bPassedHandshakeNotify = false;
for (int i = _handlerComponents.Count - 1; i >= 0; --i)
{
var curComponent = _handlerComponents[i];
if (!curComponent.IsInitialized())
{
bAllInitialized = false;
}
if (bEncounteredComponent)
{
// If the initialized component required a handshake, pass on notification to the next handshaking component
// (components closer to the Socket, perform their handshake first)
if (_bBeganHandshaking && !curComponent.IsInitialized() && inComponent.RequiresHandshake && !bPassedHandshakeNotify && curComponent.RequiresHandshake)
{
curComponent.NotifiyHandshakeBegin();
bPassedHandshakeNotify = true;
}
}
else
{
bEncounteredComponent = curComponent == inComponent;
}
}
if (bAllInitialized)
{
HandlerInitialized();
}
}
}
}
@@ -0,0 +1,11 @@
using Prospect.Unreal.Net.Channels;
namespace Prospect.Unreal.Net.Packets.Bunch;
public class FControlChannelOutBunch : FOutBunch
{
public FControlChannelOutBunch(UChannel inChannel, bool bClose) : base(inChannel, bClose)
{
bReliable = true;
}
}
@@ -0,0 +1,161 @@
using Prospect.Unreal.Core.Names;
using Prospect.Unreal.Net.Channels;
using Prospect.Unreal.Serialization;
namespace Prospect.Unreal.Net.Packets.Bunch;
public class FOutBunch : FNetBitWriter
{
public FOutBunch() : base(0)
{
ChName = UnrealNames.FNames[UnrealNameKey.None];
}
public FOutBunch(UChannel inChannel, bool bInClose) : base(
inChannel.Connection!.PackageMap!,
inChannel.Connection.GetMaxSingleBunchSizeBits())
{
Next = null;
Channel = inChannel;
Time = 0;
ChIndex = inChannel.ChIndex;
ChType = inChannel.ChType;
ChName = inChannel.ChName;
ChSequence = 0;
PacketId = 0;
ReceivedAck = false;
bOpen = false;
bClose = bInClose;
bDormant = false;
bIsReplicationPaused = false;
bReliable = false;
bPartial = false;
bPartialInitial = false;
bPartialFinal = false;
bHasPackageMapExports = false;
bHasMustBeMappedGUIDs = false;
CloseReason = EChannelCloseReason.Destroyed;
// Match the byte swapping settings of the connection
// TODO: SetByteSwapping(Channel->Connection->bNeedsByteSwapping);
// Reserve channel and set bunch info.
if (Channel.NumOutRec >= UNetConnection.ReliableBuffer - 1 + (bClose ? 1 : 0))
{
SetOverflowed(-1);
}
}
public FOutBunch(UPackageMap inPackageMap, long maxBits) : base(inPackageMap, maxBits)
{
Next = null;
Channel = null;
Time = 0;
ChIndex = 0;
ChType = EChannelType.CHTYPE_None;
ChName = UnrealNames.FNames[UnrealNameKey.None];
ChSequence = 0;
PacketId = 0;
ReceivedAck = false;
bOpen = false;
bClose = false;
bDormant = false;
bIsReplicationPaused = false;
bReliable = false;
bPartial = false;
bPartialInitial = false;
bPartialFinal = false;
bHasPackageMapExports = false;
bHasMustBeMappedGUIDs = false;
CloseReason = EChannelCloseReason.Destroyed;
}
public FOutBunch(FOutBunch bunch) : base(bunch)
{
Next = bunch.Next;
Channel = bunch.Channel;
Time = bunch.Time;
ChIndex = bunch.ChIndex;
ChType = bunch.ChType;
ChName = bunch.ChName;
ChSequence = bunch.ChSequence;
PacketId = bunch.PacketId;
ReceivedAck = bunch.ReceivedAck;
bOpen = bunch.bOpen;
bClose = bunch.bClose;
bDormant = bunch.bDormant;
bIsReplicationPaused = bunch.bIsReplicationPaused;
bReliable = bunch.bReliable;
bPartial = bunch.bPartial;
bPartialInitial = bunch.bPartialInitial;
bPartialFinal = bunch.bPartialFinal;
bHasPackageMapExports = bunch.bHasPackageMapExports;
bHasMustBeMappedGUIDs = bunch.bHasMustBeMappedGUIDs;
CloseReason = bunch.CloseReason;
}
public FOutBunch? Next { get; set; }
public UChannel? Channel { get; set; }
public double Time { get; set; }
public int ChIndex { get; set; }
public EChannelType ChType { get; set; }
public FName ChName { get; set; }
public int ChSequence { get; set; }
public int PacketId { get; set; }
public bool ReceivedAck { 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 EChannelCloseReason CloseReason { get; set; }
public List<FNetworkGUID> ExportNetGUIDs { get; } = new List<FNetworkGUID>();
public List<ulong> NetFieldExports { get; } = new List<ulong>();
}
@@ -0,0 +1,16 @@
namespace Prospect.Unreal.Net.Packets.Control;
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
internal class NetControlMessageAttribute : Attribute
{
public NetControlMessageAttribute(string name, int index, params Type[] args)
{
Name = name;
Index = index;
Args = args;
}
public string Name { get; }
public int Index { get; }
public Type[] Args { get; }
}
@@ -0,0 +1,28 @@
using Prospect.Unreal.Core;
using Prospect.Unreal.Net;
using Prospect.Unreal.Net.Packets.Control;
[assembly: NetControlMessage("Hello", 0, typeof(byte), typeof(uint), typeof(FString))]
[assembly: NetControlMessage("Welcome", 1, typeof(FString), typeof(FString), typeof(FString))]
[assembly: NetControlMessage("Upgrade", 2, typeof(uint))]
[assembly: NetControlMessage("Challenge", 3, typeof(FString))]
[assembly: NetControlMessage("Netspeed", 4, typeof(int))]
[assembly: NetControlMessage("Login", 5, typeof(FString), typeof(FString), typeof(FUniqueNetIdRepl), typeof(FString))]
[assembly: NetControlMessage("Failure", 6, typeof(FString))]
[assembly: NetControlMessage("Join", 9)]
[assembly: NetControlMessage("JoinSplit", 10, typeof(FString), typeof(FUniqueNetIdRepl))]
[assembly: NetControlMessage("Skip", 12, typeof(FGuid))]
[assembly: NetControlMessage("Abort", 13, typeof(FGuid))]
[assembly: NetControlMessage("PCSwap", 15, typeof(int))]
[assembly: NetControlMessage("ActorChannelFailure", 16, typeof(int))]
[assembly: NetControlMessage("DebugText", 17, typeof(FString))]
[assembly: NetControlMessage("NetGUIDAssign", 18, typeof(FNetworkGUID), typeof(FString))]
[assembly: NetControlMessage("SecurityViolation", 19, typeof(FString))]
[assembly: NetControlMessage("GameSpecific", 20, typeof(byte), typeof(FString))]
[assembly: NetControlMessage("EncryptionAck", 21)]
[assembly: NetControlMessage("DestructionInfo", 22)]
[assembly: NetControlMessage("BeaconWelcome", 25)]
[assembly: NetControlMessage("BeaconJoin", 26, typeof(FString), typeof(FUniqueNetIdRepl))]
[assembly: NetControlMessage("BeaconAssignGUID", 27, typeof(FNetworkGUID))]
[assembly: NetControlMessage("BeaconNetGUIDAck", 28, typeof(FString))]
@@ -14,7 +14,9 @@ public class FNetPacketNotify
public const int MaxSequenceHistoryLength = 256;
private readonly Queue<FSentAckData> _ackRecord = new Queue<FSentAckData>();
private uint _writtenHistoryWordCount;
private SequenceNumber _writtenInAckSeq;
private readonly SequenceHistory _inSeqHistory = new SequenceHistory();
private SequenceNumber _inSeq;
private SequenceNumber _inAckSeq;
@@ -33,6 +35,71 @@ public class FNetPacketNotify
_outAckSeq = new SequenceNumber((ushort)(initialOutSeq.Value - 1));
}
public SequenceNumber GetInSeq()
{
return _inSeq;
}
public SequenceNumber GetInAckSeq()
{
return _inAckSeq;
}
public SequenceNumber GetOutSeq()
{
return _outSeq;
}
public SequenceNumber GetOutAckSeq()
{
return _outAckSeq;
}
/// <summary>
/// These methods must always write and read the exact same number of bits, that is the reason for not using WriteInt/WrittedWrappedInt
/// </summary>
public bool WriteHeader(FBitWriter writer, bool bRefresh)
{
// we always write at least 1 word
var currentHistoryWorkCount = Math.Clamp((GetCurrentSequenceHistoryLength() + SequenceHistory.BitsPerWord - 1) / SequenceHistory.BitsPerWord, 1, SequenceHistory.WordCount);
// We can only do a refresh if we do not need more space for the history
if (bRefresh && (currentHistoryWorkCount > _writtenHistoryWordCount))
{
return false;
}
// How many words of ack data should we write? If this is a refresh we must write the same size as the original header
_writtenHistoryWordCount = bRefresh ? _writtenHistoryWordCount : currentHistoryWorkCount;
// This is the last InAck we have acknowledged at this time
_writtenInAckSeq = _inAckSeq;
var seq = _outSeq;
var ackedSeq = _inAckSeq;
// Pack data into a uint
var packedHeader = FPackedHeader.Pack(seq, ackedSeq, _writtenHistoryWordCount - 1);
// Write packed header
writer.WriteUInt32(packedHeader);
// Write ack history
_inSeqHistory.Write(writer, _writtenHistoryWordCount);
return true;
}
private uint GetCurrentSequenceHistoryLength()
{
if (_inAckSeq.GreaterEq(_inAckSeqAck))
{
return (uint) SequenceNumber.Diff(_inAckSeq, _inAckSeqAck);
}
return SequenceHistory.Size;
}
public bool ReadHeader(ref FNotificationHeader data, FBitReader reader)
{
var packedHeader = reader.ReadUInt32();
@@ -173,4 +240,13 @@ public class FNetPacketNotify
_inSeqHistory.AddDeliveryStatus(bReportAcked);
}
}
public SequenceNumber CommitAndIncrementOutSeq()
{
// 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;
return _outSeq.IncrementAndGet();
}
}
@@ -9,6 +9,17 @@ public class FPackedHeader
public const int HistoryWordCountMask = (1 << HistoryWordCountBits) - 1;
public const int AckSeqShift = HistoryWordCountBits;
public const int SeqShift = AckSeqShift + FNetPacketNotify.SequenceNumberBits;
public static uint Pack(SequenceNumber seq, SequenceNumber ackedSeq, uint historyWordCount)
{
uint packed = 0;
packed |= (uint)(seq.Value << SeqShift);
packed |= (uint)(ackedSeq.Value << AckSeqShift);
packed |= (uint)(historyWordCount & HistoryWordCountMask);
return packed;
}
public static SequenceNumber GetSeq(uint packed)
{
@@ -57,4 +57,13 @@ public readonly struct SequenceHistory
_storage[i] = reader.ReadUInt32();
}
}
public void Write(FBitWriter writer, uint numWords)
{
numWords = Math.Min(numWords, WordCount);
for (var i = 0; i < numWords; i++)
{
writer.WriteUInt32(_storage[i]);
}
}
}
@@ -76,6 +76,10 @@ public class StatelessConnectHandlerComponent : HandlerComponent
public StatelessConnectHandlerComponent(PacketHandler handler) : base(handler, nameof(StatelessConnectHandlerComponent))
{
SetActive(true);
RequiresHandshake = true;
_handshakeSecret = new byte[2][];
_activeSecret = byte.MaxValue;
_lastChallengeSuccessAddress = null;
@@ -202,9 +206,21 @@ public class StatelessConnectHandlerComponent : HandlerComponent
}
}
public override void Outgoing(FBitWriter packet, FOutPacketTraits traits)
public override void Outgoing(ref FBitWriter packet, FOutPacketTraits traits)
{
base.Outgoing(packet, traits);
const bool bHandshakePacket = false;
var newPacket = new FBitWriter(GetAdjustedSizeBits((int)packet.GetNumBits()) + 1, true, false);
if (_magicHeader.Length > 0)
{
newPacket.SerializeBits(_magicHeader, _magicHeader.Length);
}
newPacket.WriteBit(bHandshakePacket);
newPacket.SerializeBits(packet.GetData(), packet.GetNumBits());
packet = newPacket;
}
public override void IncomingConnectionless(FIncomingPacketRef packetRef)
+39 -2
View File
@@ -1,6 +1,8 @@
using System.Net;
using System.Net.Sockets;
using Prospect.Unreal.Core;
using Prospect.Unreal.Exceptions;
using Prospect.Unreal.Net.Packets.Control;
namespace Prospect.Unreal.Net;
@@ -39,8 +41,12 @@ public class UIpConnection : UNetConnection
RemoteAddr = inRemoteAddr;
Url.Host = RemoteAddr.Address;
// Initialize our send bunch
InitSendBuffer();
// This is for a client that needs to log in, setup ClientLoginState and ExpectedClientLoginMsgType to reflect that
SetClientLoginState(EClientLoginState.LoggingIn);
SetExpectedClientLoginMsgType(0); // TODO: NMT_HELLO
SetExpectedClientLoginMsgType(NMT.Hello); // TODO: NMT_HELLO
}
public override void InitLocalConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0)
@@ -50,7 +56,38 @@ public class UIpConnection : UNetConnection
public override void LowLevelSend(byte[] data, int countBits, FOutPacketTraits traits)
{
throw new NotImplementedException();
// TODO: Improve according to UE4
var dataToSend = data;
// 243
// 244
// Process any packet modifiers
if (Handler != null && !Handler.GetRawSend())
{
var processedData = Handler.Outgoing(data, countBits, traits);
if (!processedData.Error)
{
dataToSend = processedData.Data;
countBits = processedData.CountBits;
}
else
{
countBits = 0;
}
}
var countBytes = FMath.DivideAndRoundUp(countBits, 8);
if (countBits > 0)
{
if (Socket == null)
{
throw new UnrealNetException();
}
Socket.Send(dataToSend, countBytes, RemoteAddr);
}
}
public override string LowLevelGetRemoteAddress(bool bAppendPort = false)
+626 -16
View File
@@ -6,6 +6,7 @@ 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.Control;
using Prospect.Unreal.Net.Packets.Header;
using Prospect.Unreal.Net.Packets.Header.Sequence;
using Prospect.Unreal.Net.Player;
@@ -23,11 +24,16 @@ public abstract class UNetConnection : UPlayer
public const int MaxChSequence = 1024;
public const int MaxBunchHeaderBits = 256;
public const int MaxPacketSize = 1024;
public const int MaxPacketReliableSequenceHeaderBits = 32 + FNetPacketNotify.MaxSequenceHistoryLength;
public const int MaxPacketInfoHeaderBits = 1 /* bHasPacketInfo */ + NumBitsForJitterClockTimeInHeader + 1 /* bHasServerFrameTime */ + 8 /* ServerFrameTime */;
public const int MaxPacketHeaderBits = MaxPacketReliableSequenceHeaderBits + MaxPacketInfoHeaderBits;
public const int MaxPacketTrailerBits = 1;
public const int DefaultMaxChannelSize = 32767;
public const int MaxJitterClockTimeValue = 1023;
public const int NumBitsForJitterClockTimeInHeader = 10;
public const int MaxJitterClockTimeValue = (1 << NumBitsForJitterClockTimeInHeader) - 1;
public const int MaxJitterPrecisionInMS = 1000;
public const EEngineNetworkVersionHistory DefaultEngineNetworkProtocolVersion = EEngineNetworkVersionHistory.HISTORY_ENGINENETVERSION_LATEST;
public const uint DefaultGameNetworkProtocolVersion = 0;
@@ -49,6 +55,24 @@ public abstract class UNetConnection : UPlayer
private double _lastSendTime;
private double _lastTickTime;
/// <summary>
/// Did we write the dummy PacketInfo in the current SendBuffer
/// </summary>
private bool _bSendBufferHasDummyPacketInfo;
/// <summary>
/// Stores the bit number where we wrote the dummy packet info in the packet header
/// </summary>
private FBitWriterMark _headerMarkForPacketInfo;
/// <summary>
/// Timestamp of the last packet sent
/// </summary>
private double _previousPacketSendTimeInS;
private bool _bFlushedNetThisFrame;
private bool _bAutoFlush;
public UNetConnection()
{
_channelsToTick = new HashSet<UChannel>();
@@ -70,6 +94,10 @@ public abstract class UNetConnection : UPlayer
ClientLoginState = EClientLoginState.Invalid;
ExpectedClientLoginMsgType = 0;
PacketOverhead = 0;
Challenge = string.Empty;
SendBuffer = new FBitWriter(0);
OutLagTime = new double[256];
OutLagPacketId = new int[256];
InPacketId = -1;
OutPacketId = 0;
OutAckPacketId = -1;
@@ -96,9 +124,9 @@ public abstract class UNetConnection : UPlayer
/// <summary>
/// Package map between local and remote. (negotiates net serialization)
/// </summary>
public UPackageMapClient? PackageMap { get; private set; }
public UPackageMap? PackageMap { get; private set; }
public List<UChannel> OpenChannels { get; private set; }
public List<UChannel> OpenChannels { get; }
/// <summary>
/// Maximum packet size.
@@ -115,11 +143,44 @@ public abstract class UNetConnection : UPlayer
/// </summary>
public IPEndPoint? RemoteAddr { get; protected set; }
/// <summary>
/// Number of bits used for the packet id in the current packet.
/// </summary>
public int NumPacketIdBits { get; private set; }
/// <summary>
/// Number of bits used for bunches in the current packet.
/// </summary>
public int NumBunchBits { get; private set; }
/// <summary>
/// Number of bits used for acks in the current packet.
/// </summary>
public int NumAckBits { get; private set; }
/// <summary>
/// Number of bits used for padding in the current packet.
/// </summary>
public int NumPaddingBits { get; private set; }
/// <summary>
/// The maximum number of bits all packet handlers will reserve.
/// </summary>
public int MaxPacketHandlerBits { get; private set; }
/// <summary>
/// State this connection is in.
/// </summary>
public EConnectionState State { get; private set; }
/// <summary>
/// This functionality is used during replay checkpoints for example, so we can re-use the existing connection and channels to record
/// a version of each actor and capture all properties that have changed since the actor has been alive...
/// This will also act as if it needs to re-open all the channels, etc.
/// NOTE - This doesn't force all exports to happen again though, it will only export new stuff, so keep that in mind.
/// </summary>
public EResendAllDataState ResendAllDataState { get; set; }
/// <summary>
/// PacketHandler, for managing layered handler components, which modify packets as they are sent/received
/// </summary>
@@ -130,7 +191,7 @@ public abstract class UNetConnection : UPlayer
/// <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; }
public NMT ExpectedClientLoginMsgType { get; private set; }
/// <summary>
/// Reference to the PacketHandler component, for managing stateless connection handshakes
@@ -142,6 +203,20 @@ public abstract class UNetConnection : UPlayer
/// </summary>
public int PacketOverhead { get; private set; }
/// <summary>
/// Server-generated challenge.
/// </summary>
public string Challenge { get; private set; }
/// <summary>
/// Queued up bits waiting to send
/// </summary>
public FBitWriter SendBuffer { get; private set; }
public double[] OutLagTime { get; }
public int[] OutLagPacketId { get; }
/// <summary>
/// Full incoming packet index.
/// </summary>
@@ -164,6 +239,38 @@ public abstract class UNetConnection : UPlayer
/// Related to OutAckPacketId which is tha last successfully delivered PacketId.
/// </summary>
public int LastNotifiedPacketId { get; private set; }
/// <summary>
/// Keep old behavior where we send a packet with only acks even if we have no other outgoing data if we got incoming data
/// </summary>
public uint HasDirtyAcks { get; set; }
/// <summary>
/// Most recently sent bunch start.
/// </summary>
public FBitWriterMark LastStart { get; set; }
/// <summary>
/// Most recently sent bunch end.
/// </summary>
public FBitWriterMark LastEnd { get; set; }
/// <summary>
/// Whether to allow merging.
/// </summary>
public bool AllowMerge { get; set; }
/// <summary>
/// Whether contents are time-sensitive.
/// </summary>
public bool TimeSensitive { get; set; }
/// <summary>
/// Most recent outgoing bunch.
/// </summary>
public FOutBunch? LastOutBunch { get; set; }
public FOutBunch LastOut { get; set; }
public int MaxChannelSize { get; }
public UChannel?[] Channels { get; }
@@ -209,8 +316,9 @@ public abstract class UNetConnection : UPlayer
InitHandler();
PackageMap = new UPackageMapClient();
PackageMap.Initialize(this, Driver.GuidCache);
var packageMapClient = new UPackageMapClient();
packageMapClient.Initialize(this, Driver.GuidCache);
PackageMap = packageMapClient;
}
public abstract void InitRemoteConnection(UNetDriver inDriver, UdpClient inSocket, FUrl inURL, IPEndPoint inRemoteAddr, EConnectionState inState, int inMaxPacket = 0, int inPacketOverhead = 0);
@@ -235,10 +343,13 @@ public abstract class UNetConnection : UPlayer
}
else
{
// TODO: Close connection, malformed packet.
Logger.Fatal("Connection should be closed here");
Logger.Fatal("Packet failed PacketHandler processing");
Close();
return;
}
// See if we receive a packet that wasn't fully consumed by the handler before the handler is initialized.
// TODO: Handler.IsFullyInitialized
}
// Handle an incoming raw packet from the driver.
@@ -273,7 +384,7 @@ public abstract class UNetConnection : UPlayer
{
ReceivedPacket(reader);
// TODO: Flush out of order cache
// TODO: FlushPacketOrderCache
}
}
}
@@ -301,8 +412,8 @@ public abstract class UNetConnection : UPlayer
if (!PacketNotify.ReadHeader(ref header, reader))
{
// TODO: Close connection, malformed packet.
Logger.Fatal("Failed to read PacketHeader");
Close();
return;
}
@@ -516,7 +627,9 @@ public abstract class UNetConnection : UPlayer
{
if (bunch.bReliable || bunch.bOpen)
{
if (!UPackageMap.StaticSerializeName(reader, out var chName) || reader.IsError())
FName? chName = null;
if (!UPackageMap.StaticSerializeName(reader, ref chName) || reader.IsError())
{
// TODO: Close connection
Logger.Fatal("Channel name serialization failed");
@@ -773,10 +886,19 @@ public abstract class UNetConnection : UPlayer
}
// 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;
TimeSensitive = true;
++HasDirtyAcks;
// TODO: FlushNet if HasDirtyAcks
if (HasDirtyAcks >= FNetPacketNotify.MaxSequenceHistoryLength)
{
Logger.Warning("ReceivedPacket - Too many received packets to ack ({Acks}) since last sent packet. InSeq: {Seq} {Conn} NextOutGoingSeq: {OutSeq}", HasDirtyAcks, PacketNotify.GetInSeq().Value, this, PacketNotify.GetOutSeq().Value);
FlushNet();
if (HasDirtyAcks != 0)
{
FlushNet();
}
}
}
}
@@ -874,12 +996,24 @@ public abstract class UNetConnection : UPlayer
}
Handler = new PacketHandler();
Handler.Initialize(false);
Handler.Initialize(HandlerMode.Server /* Mode */, UNetConnection.MaxPacketSize * 8 ,false);
StatelessConnectComponent = (StatelessConnectHandlerComponent) Handler.AddHandler<StatelessConnectHandlerComponent>();
StatelessConnectComponent.SetDriver(Driver);
Handler.InitializeComponents();
MaxPacketHandlerBits = Handler.GetTotalReservedPacketBits();
}
public void Close()
{
// TODO: Implement
}
public long GetMaxSingleBunchSizeBits()
{
return (MaxPacket * 8) - MaxBunchHeaderBits - MaxPacketTrailerBits - MaxPacketHeaderBits - MaxPacketHandlerBits;
}
public UChannel CreateChannelByName(FName chName, EChannelCreateFlags createFlags, int chIndex)
@@ -933,6 +1067,455 @@ public abstract class UNetConnection : UPlayer
return channel;
}
public void SendChallengeControlMessage()
{
if (State != EConnectionState.USOCK_Invalid && State != EConnectionState.USOCK_Closed && Driver != null)
{
Challenge = "E660A966";
SetExpectedClientLoginMsgType(NMT.Login);
NMT_Challenge.Send(this, Challenge);
FlushNet();
}
}
public int SendRawBunch(FOutBunch bunch, bool inAllowMerge)
{
ValidateSendBuffer();
if (Driver == null || bunch.ReceivedAck || bunch.IsError())
{
throw new UnrealNetException();
}
// TODO: Increment things
TimeSensitive = true;
using var sendBunchHeader = new FBitWriter(MaxBunchHeaderBits);
var bIsOpenOrClose = bunch.bOpen || bunch.bClose;
var bIsOpenOrReliable = bunch.bOpen || bunch.bReliable;
sendBunchHeader.WriteBit(bIsOpenOrClose);
if (bIsOpenOrClose)
{
sendBunchHeader.WriteBit(bunch.bOpen);
sendBunchHeader.WriteBit(bunch.bClose);
if (bunch.bClose)
{
sendBunchHeader.SerializeInt((uint)bunch.CloseReason, (uint)EChannelCloseReason.MAX);
}
}
sendBunchHeader.WriteBit(bunch.bIsReplicationPaused);
sendBunchHeader.WriteBit(bunch.bReliable);
sendBunchHeader.SerializeIntPacked((uint)bunch.ChIndex);
sendBunchHeader.WriteBit(bunch.bHasPackageMapExports);
sendBunchHeader.WriteBit(bunch.bHasMustBeMappedGUIDs);
sendBunchHeader.WriteBit(bunch.bPartial);
if (bunch.bReliable && !IsInternalAck())
{
// 14 > 24
sendBunchHeader.WriteIntWrapped((uint)bunch.ChSequence, MaxChSequence);
}
if (bunch.bPartial)
{
sendBunchHeader.WriteBit(bunch.bPartialInitial);
sendBunchHeader.WriteBit(bunch.bPartialFinal);
}
if (bIsOpenOrReliable)
{
var name = bunch.ChName;
UPackageMap.StaticSerializeName(sendBunchHeader, ref name);
}
sendBunchHeader.WriteIntWrapped((uint)bunch.GetNumBits(), (uint)(MaxPacket * 8));
if (sendBunchHeader.IsError())
{
Logger.Fatal("SendBunchHeader Error: Bunch = {Bunch}", bunch);
}
// Remember start position.
AllowMerge = inAllowMerge;
bunch.Time = Driver.GetElapsedTime();
var bunchHeaderBits = sendBunchHeader.GetNumBits();
var bunchBits = bunch.GetNumBits();
// If the bunch does not fit in the current packet,
// flush packet now so that we can report collected stats in the correct scope
PrepareWriteBitsToSendBuffer(bunchHeaderBits, bunchBits);
// Write the bits to the buffer and remember the packet id used
bunch.PacketId = WriteBitsToSendBufferInternal(sendBunchHeader.GetData(), (int)bunchHeaderBits, bunch.GetData(), (int)bunchBits, EWriteBitsDataType.Bunch);
if (PackageMap != null && bunch.bHasPackageMapExports)
{
PackageMap.NotifyBunchCommit(bunch.PacketId, bunch);
}
if (bunch.bHasPackageMapExports)
{
// TOOD: Increment things
}
FlushNet();
return bunch.PacketId;
}
private void PrepareWriteBitsToSendBuffer(long sizeInBits, long extraSizeInBits)
{
ValidateSendBuffer();
var totalSizeInBits = sizeInBits + extraSizeInBits;
// Flush if we can't add to current buffer
if (totalSizeInBits > GetFreeSendBufferBits())
{
FlushNet();
}
// If this is the start of the queue, make sure to add the packet id
if (SendBuffer.GetNumBits() == 0 && !IsInternalAck())
{
// Write Packet Header, before sending the packet we will go back and rewrite the data
WritePacketHeader(SendBuffer);
// Pre-write the bits for the packet info
WriteDummyPacketInfo(SendBuffer);
// We do not allow the first bunch to merge with the ack data as this will "revert" the ack data.
AllowMerge = false;
// Update stats for PacketIdBits and ackdata (also including the data used for packet RTT and saturation calculations)
var bitsWritten = (int)SendBuffer.GetNumBits();
NumPacketIdBits += FNetPacketNotify.SequenceNumberBits;
NumAckBits += bitsWritten - FNetPacketNotify.SequenceNumberBits;
ValidateSendBuffer();
}
}
private void WritePacketHeader(FBitWriter writer)
{
// If this is a header refresh, we only serialize the updated serial number information
var bIsHeaderUpdate = writer.GetNumBits() > 0;
// Header is always written first in the packet
var restore = new FBitWriterMark(writer);
writer.Num = 0;
// Write notification header or refresh the header if used space is the same.
var bWroteHeader = PacketNotify.WriteHeader(writer, bIsHeaderUpdate);
// Jump back to where we came from.
if (bIsHeaderUpdate)
{
restore.PopWithoutClear(writer);
// if we wrote the header and successfully refreshed the header status we no longer has any dirty acks
if (bWroteHeader)
{
HasDirtyAcks = 0;
}
}
}
private void WriteFinalPacketInfo(FBitWriter writer, double packetSentTimeInS)
{
if (!_bSendBufferHasDummyPacketInfo)
{
// PacketInfo payload is not included in this SendBuffer; nothing to rewrite
return;
}
var currentMark = new FBitWriterMark(writer);
// Go back to write over the dummy bits
_headerMarkForPacketInfo.PopWithoutClear(writer);
// Write Jitter clock time
{
var deltaSendTimeInMS = (packetSentTimeInS - _previousPacketSendTimeInS) * 1000.0;
var clockTimeMilliseconds = 0;
// If the delta is over our max precision, we send MAX value and jitter will be ignored by the receiver.
if (deltaSendTimeInMS >= MaxJitterPrecisionInMS)
{
clockTimeMilliseconds = MaxJitterClockTimeValue;
}
else
{
// TODO: Proper
// Get the fractional part (milliseconds) of the clock time
clockTimeMilliseconds = 0;
// Ensure we don't overflow
clockTimeMilliseconds &= MaxJitterClockTimeValue;
}
writer.SerializeInt((uint)clockTimeMilliseconds, MaxJitterClockTimeValue + 1);
_previousPacketSendTimeInS = packetSentTimeInS;
}
// Write server frame time
{
var bHasServerFrameTime = LastHasServerFrameTime;
writer.WriteBit(bHasServerFrameTime);
if (bHasServerFrameTime && Driver!.IsServer())
{
// Write data used to calculate link latency
// TODO: Proper
const int FrameTime = 123;
var frameTimeByte = (byte) Math.Min(Math.Floor((double)(FrameTime * 1000)), 255);
writer.WriteByte(frameTimeByte);
}
}
_headerMarkForPacketInfo.Reset();
// Revert to the correct bit writing place
currentMark.PopWithoutClear(writer);
}
private void WriteDummyPacketInfo(FBitWriter writer)
{
var bHasPacketInfoPayload = _bFlushedNetThisFrame == false;
writer.WriteBit(bHasPacketInfoPayload);
if (bHasPacketInfoPayload)
{
// Pre-insert the bits since the final time values will be calculated and inserted right before LowLevelSend
_headerMarkForPacketInfo.Init(writer);
Span<byte> dummyJitterClockTime = stackalloc byte[4];
writer.SerializeBits(dummyJitterClockTime, NumBitsForJitterClockTimeInHeader);
var bHasServerFrameTime = LastHasServerFrameTime;
writer.WriteBit(bHasServerFrameTime);
if (bHasServerFrameTime && Driver!.IsServer()) // false
{
const byte dummyFrameTimeByte = 0;
writer.WriteByte(dummyFrameTimeByte);
}
}
_bSendBufferHasDummyPacketInfo = bHasPacketInfoPayload;
}
private int WriteBitsToSendBufferInternal(byte[]? bits, int sizeInBits, byte[]? extraBits, int extraSizeInBits, EWriteBitsDataType dataType)
{
// Remember start position in case we want to undo this write, no meaning to undo the header write as this is only used to pop bunches and the header should not count towards the bunch
// Store this after the possible flush above so we have the correct start position in the case that we do flush
LastStart = new FBitWriterMark(SendBuffer);
// Add the bits to the queue
if (sizeInBits != 0)
{
if (bits == null)
{
throw new UnrealNetException("bits should not be null if a size is set");
}
SendBuffer.SerializeBits(bits, sizeInBits);
ValidateSendBuffer();
}
// Add any extra bits
if (extraSizeInBits != 0)
{
if (extraBits == null)
{
throw new UnrealNetException("extraBits should not be null if a size is set");
}
SendBuffer.SerializeBits(extraBits, extraSizeInBits);
ValidateSendBuffer();
}
// 242 after
var rememberedPacketId = OutPacketId;
if (dataType == EWriteBitsDataType.Bunch)
{
NumBunchBits += sizeInBits + extraSizeInBits;
}
// Flush now if we are full
if (GetFreeSendBufferBits() == 0)
{
FlushNet();
}
return rememberedPacketId;
}
private int WriteBitsToSendBuffer(byte[]? bits, int sizeInBits, byte[]? extraBits = null, int extraSizeInBits = 0, EWriteBitsDataType dataType = EWriteBitsDataType.Unknown)
{
// Flush packet as needed and begin new packet
PrepareWriteBitsToSendBuffer(sizeInBits, extraSizeInBits);
// Write the data and flush if the packet is full, return value is the packetId into which the data was written
return WriteBitsToSendBufferInternal(bits, sizeInBits, extraBits, extraSizeInBits, dataType);
}
/// <summary>
/// Returns number of bits left in current packet that can be used without causing a flush
/// </summary>
private long GetFreeSendBufferBits()
{
// If we haven't sent anything yet, make sure to account for the packet header + trailer size
// Otherwise, we only need to account for trailer size
var extraBits = (SendBuffer.GetNumBits() > 0) ? MaxPacketTrailerBits : MaxPacketHeaderBits + MaxPacketTrailerBits;
var numberOfFreeBits = SendBuffer.GetMaxBits() - (SendBuffer.GetNumBits() + extraBits);
return numberOfFreeBits;
}
private void ValidateSendBuffer()
{
if (SendBuffer.IsError())
{
Logger.Fatal("ValidateSendBuffer: Out.IsError() == true. NumBits: {A}, NumBytes: {B}, MaxBits: {C}", SendBuffer.GetNumBits(), SendBuffer.GetNumBytes(), SendBuffer.GetMaxBits());
}
}
private protected void InitSendBuffer()
{
var finalBufferSize = (MaxPacket * 8) - MaxPacketHandlerBits;
if (finalBufferSize == SendBuffer.GetMaxBits())
{
SendBuffer.Reset();
}
else
{
SendBuffer = new FBitWriter(finalBufferSize);
}
_headerMarkForPacketInfo.Reset();
ResetPacketBitCounts();
ValidateSendBuffer();
}
private void ResetPacketBitCounts()
{
NumPacketIdBits = 0;
NumBunchBits = 0;
NumAckBits = 0;
NumPaddingBits = 0;
}
private void FlushNet()
{
if (Driver == null)
{
throw new UnrealNetException();
}
// Update info.
ValidateSendBuffer();
LastEnd = new FBitWriterMark();
TimeSensitive = false;
// If there is any pending data to send, send it.
if (SendBuffer.GetNumBits() != 0 || HasDirtyAcks != 0 || (Driver.GetElapsedTime() - _lastSendTime > Driver.KeepAliveTime && !IsInternalAck() && State != EConnectionState.USOCK_Closed))
{
// Due to the PacketHandler handshake code, servers must never send the client data,
// before first receiving a client control packet (which is taken as an indication of a complete handshake).
if (!HasReceivedClientPacket())
{
Logger.Debug("Attempting to send data before handshake is complete. {Channel}", this);
Close();
InitSendBuffer();
return;
}
var traits = new FOutPacketTraits();
// If sending keepalive packet or just acks, still write the packet header
if (SendBuffer.GetNumBits() == 0)
{
WriteBitsToSendBuffer(null, 0); // This will force the packet header to be written
traits.IsKeepAlive = true;
}
if (Handler != null)
{
Handler.OutgoingHigh(SendBuffer);
}
var packetSentTimeInS = FPlatformTime.Seconds();
// Write the UNetConnection-level termination bit
SendBuffer.WriteBit(true);
// Refresh outgoing header with latest data
if (!IsInternalAck())
{
// if we update ack, we also update received ack associated with outgoing seq
// so we know how many ack bits we need to write (which is updated in received packet)
WritePacketHeader(SendBuffer);
WriteFinalPacketInfo(SendBuffer, packetSentTimeInS);
}
ValidateSendBuffer();
var numStrayBits = SendBuffer.GetNumBits();
traits.NumAckBits = (uint)NumAckBits;
traits.NumBunchBits = (uint)NumBunchBits;
// Removed packet emulation
if (Driver.IsNetResourceValid())
{
LowLevelSend(SendBuffer.GetData(), (int)SendBuffer.GetNumBits(), traits);
}
// Update stuff.
var index = OutPacketId & (OutLagPacketId.Length - 1);
// Remember the actual time this packet was sent out, so we can compute ping when the ack comes back
OutLagPacketId[index] = OutPacketId;
OutLagTime[index] = packetSentTimeInS;
// Increase outgoing sequence number
if (!IsInternalAck())
{
PacketNotify.CommitAndIncrementOutSeq();
}
// TODO: Make sure that we always push an ChannelRecordEntry for each transmitted packet even if it is empty
++OutPacketId;
// TODO: Increment things
_lastSendTime = Driver.GetElapsedTime();
_bFlushedNetThisFrame = true;
InitSendBuffer();
}
}
private int GetFreeChannelIndex(FName chName)
{
int chIndex;
@@ -984,7 +1567,7 @@ public abstract class UNetConnection : UPlayer
ClientLoginState = newState;
}
private protected void SetExpectedClientLoginMsgType(byte newType)
private protected void SetExpectedClientLoginMsgType(NMT newType)
{
if (ExpectedClientLoginMsgType == newType)
{
@@ -997,6 +1580,33 @@ public abstract class UNetConnection : UPlayer
ExpectedClientLoginMsgType = newType;
}
public bool IsClientMsgTypeValid(NMT clientMsgType)
{
if (ClientLoginState == EClientLoginState.LoggingIn)
{
if (clientMsgType != ExpectedClientLoginMsgType)
{
Logger.Debug("IsClientMsgTypeValid FAILED: (ClientMsgType != ExpectedClientLoginMsgType) Remote Address={Address}", LowLevelGetRemoteAddress());
return false;
}
}
else
{
if (clientMsgType == NMT.Hello || clientMsgType == NMT.Login)
{
Logger.Debug("IsClientMsgTypeValid FAILED: Invalid msg after being logged in - Remote Address={Address}", LowLevelGetRemoteAddress());
return false;
}
}
return true;
}
private bool HasReceivedClientPacket()
{
return IsInternalAck() || !Driver!.IsServer() || InReliable[0] != InitInReliable;
}
public bool IsInternalAck()
{
return _bInternalAck;
+7 -1
View File
@@ -3,6 +3,9 @@ using System.Net;
using Prospect.Unreal.Core.Names;
using Prospect.Unreal.Exceptions;
using Prospect.Unreal.Net.Channels;
using Prospect.Unreal.Net.Channels.Actor;
using Prospect.Unreal.Net.Channels.Control;
using Prospect.Unreal.Net.Channels.Voice;
using Prospect.Unreal.Runtime;
namespace Prospect.Unreal.Net;
@@ -30,6 +33,9 @@ public abstract class UNetDriver : IAsyncDisposable
ChannelDefinitionMap[channel.Name] = channel;
}
}
// From Engine ini
public float KeepAliveTime { get; } = 0.2f;
/// <summary>
/// World this net driver is associated with
@@ -73,7 +79,7 @@ public abstract class UNetDriver : IAsyncDisposable
public void InitConnectionlessHandler()
{
ConnectionlessHandler = new PacketHandler();
ConnectionlessHandler.Initialize(true);
ConnectionlessHandler.Initialize(HandlerMode.Server, UNetConnection.MaxPacketSize, true);
StatelessConnectComponent = (StatelessConnectHandlerComponent) ConnectionlessHandler.AddHandler<StatelessConnectHandlerComponent>();
StatelessConnectComponent.SetDriver(this);
+57 -30
View File
@@ -1,56 +1,83 @@
using System.Diagnostics.CodeAnalysis;
using Prospect.Unreal.Core;
using Prospect.Unreal.Core.Names;
using Prospect.Unreal.Net.Packets.Bunch;
using Prospect.Unreal.Serialization;
namespace Prospect.Unreal.Net;
public class UPackageMap
{
public static unsafe bool StaticSerializeName(FBitReader ar, [MaybeNullWhen(false)] out FName name)
public static unsafe bool StaticSerializeName(FArchive ar, [MaybeNullWhen(false)] ref FName name)
{
if (!ar.IsLoading())
if (ar.IsLoading())
{
throw new NotImplementedException();
}
name = null;
name = null;
bool bHardcoded;
ar.SerializeBits(&bHardcoded, 1);
bool bHardcoded;
ar.SerializeBits(&bHardcoded, 1);
if (bHardcoded)
{
// replicated by hardcoded index
uint nameIndex;
if (ar.EngineNetVer() < EEngineNetworkVersionHistory.HISTORY_CHANNEL_NAMES)
if (bHardcoded)
{
ar.SerializeInt(&nameIndex, UnrealNames.MaxNetworkedHardcodedName);
// 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
{
ar.SerializeIntPacked(&nameIndex);
}
if (nameIndex < UnrealNames.MaxHardcodedNameIndex)
{
// hardcoded names never have a Number
name = UnrealNames.FNames[(UnrealNameKey) nameIndex];
}
else
{
ar.SetError();
// replicated by string
var inString = FString.Deserialize(ar);
var inNumber = ar.ReadInt32();
name = new FName(inString, inNumber);
}
}
else
{
// replicated by string
var inString = FString.Deserialize(ar);
var inNumber = ar.ReadInt32();
name = new FName(inString, inNumber);
var bHardcoded = (byte)(ShouldReplicateAsInteger(name) ? 1 : 0);
ar.SerializeBits(&bHardcoded, 1); // 25
if (bHardcoded != 0)
{
ar.SerializeIntPacked((uint)name.Number);
}
else
{
// send by string
var outString = name.Str;
var outNumber = name.Number;
ar.WriteString(outString);
ar.WriteInt32(outNumber);
}
}
return true;
}
private static bool ShouldReplicateAsInteger(FName name)
{
return name.Number <= UnrealNames.MaxNetworkedHardcodedName;
}
public void NotifyBunchCommit(int bunchPacketId, FOutBunch bunch)
{
throw new NotImplementedException();
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Prospect.Unreal.Net;
public class UPackageMapClient
public class UPackageMapClient : UPackageMap
{
public void Initialize(UNetConnection connection, FNetGUIDCache guidCache)
{
+17 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
@@ -7,8 +7,24 @@
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<!--Don't include the output from a previous source generator execution into future runs; the */** trick here ensures that there's -->
<!--at least one subdirectory, which is our key that it's coming from a source generator as opposed to something that is coming from -->
<!--some other tool. -->
<Compile Remove="$(CompilerGeneratedFilesOutputPath)/*/**/*.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Prospect.Unreal.Generator\Prospect.Unreal.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
+59 -2
View File
@@ -1,7 +1,9 @@
using Prospect.Unreal.Core;
using Prospect.Unreal.Exceptions;
using Prospect.Unreal.Net;
using Prospect.Unreal.Net.Channels;
using Prospect.Unreal.Net.Packets.Bunch;
using Prospect.Unreal.Net.Packets.Control;
using Serilog;
namespace Prospect.Unreal.Runtime;
@@ -60,7 +62,12 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable
public bool NotifyAcceptingChannel(UChannel channel)
{
var driver = channel.Connection.Driver!;
if (channel.Connection?.Driver == null)
{
throw new UnrealNetException();
}
var driver = channel.Connection.Driver;
if (!driver.IsServer())
{
throw new NotSupportedException("Client code");
@@ -81,9 +88,59 @@ public abstract class UWorld : FNetworkNotify, IAsyncDisposable
}
}
public void NotifyControlMessage(UNetConnection connection, byte messageType, FInBunch bunch)
public void NotifyControlMessage(UNetConnection connection, NMT messageType, FInBunch bunch)
{
if (NetDriver == null)
{
throw new UnrealNetException();
}
if (!NetDriver.IsServer())
{
throw new NotSupportedException("Client code");
}
else
{
Logger.Verbose("Level server received: {MessageType}", messageType);
if (!connection.IsClientMsgTypeValid(messageType))
{
Logger.Error("IsClientMsgTypeValid FAILED ({MessageType}): Remote Address = {Address}", (int)messageType, connection.LowLevelGetRemoteAddress());
bunch.SetError();
return;
}
switch (messageType)
{
case NMT.Hello:
{
if (NMT_Hello.Receive(bunch, out var isLittleEndian, out var remoteNetworkVersion, out var encryptionToken))
{
// TODO: Version check.
if (string.IsNullOrEmpty(encryptionToken))
{
connection.SendChallengeControlMessage();
}
else
{
throw new NotImplementedException("Encryption");
}
}
break;
}
case NMT.Login:
{
throw new NotImplementedException();
}
default:
{
throw new NotImplementedException($"Unhandled control message {messageType}");
}
}
}
}
public async ValueTask DisposeAsync()
+82 -6
View File
@@ -1,4 +1,5 @@
using Prospect.Unreal.Core;
using System.Runtime.CompilerServices;
using Prospect.Unreal.Core;
namespace Prospect.Unreal.Serialization;
@@ -179,6 +180,50 @@ public abstract class FArchive : IDisposable
/// </summary>
protected uint _arGameNetVer;
public FArchive()
{
}
public FArchive(FArchive archive)
{
_arIsLoading = archive._arIsLoading;
_arIsSaving = archive._arIsSaving;
_arIsTransacting = archive._arIsTransacting;
_arIsTextFormat = archive._arIsTextFormat;
_arWantBinaryPropertySerialization = archive._arWantBinaryPropertySerialization;
_arForceUnicode = archive._arForceUnicode;
_arIsPersistent = archive._arIsPersistent;
_arIsError = archive._arIsError;
_arIsCriticalError = archive._arIsCriticalError;
_arContainsCode = archive._arContainsCode;
_arContainsMap = archive._arContainsMap;
_arRequiresLocalizationGather = archive._arRequiresLocalizationGather;
_arForceByteSwapping = archive._arForceByteSwapping;
_arIgnoreArchetypeRef = archive._arIgnoreArchetypeRef;
_arNoDelta = archive._arNoDelta;
_arIgnoreOuterRef = archive._arIgnoreOuterRef;
_arIgnoreClassGeneratedByRef = archive._arIgnoreClassGeneratedByRef;
_arIgnoreClassRef = archive._arIgnoreClassRef;
_arAllowLazyLoading = archive._arAllowLazyLoading;
_arIsObjectReferenceCollector = archive._arIsObjectReferenceCollector;
_arIsModifyingWeakAndStrongReferences = archive._arIsModifyingWeakAndStrongReferences;
_arIsCountingMemory = archive._arIsCountingMemory;
_arShouldSkipBulkData = archive._arShouldSkipBulkData;
_arIsFilterEditorOnly = archive._arIsFilterEditorOnly;
_arIsSaveGame = archive._arIsSaveGame;
_arIsNetArchive = archive._arIsNetArchive;
_arUseCustomPropertyList = archive._arUseCustomPropertyList;
_arSerializingDefaults = archive._arSerializingDefaults;
_arPortFlags = archive._arPortFlags;
_arMaxSerializeSize = archive._arMaxSerializeSize;
_arUE4Ver = archive._arUE4Ver;
_arLicenseeUE4Ver = archive._arLicenseeUE4Ver;
_arEngineVer = archive._arEngineVer;
_arEngineNetVer = archive._arEngineNetVer;
_arGameNetVer = archive._arGameNetVer;
}
public virtual bool ReadBit()
{
throw new NotImplementedException();
@@ -190,6 +235,11 @@ public abstract class FArchive : IDisposable
Serialize(&value, 1);
return value;
}
public virtual unsafe void WriteByte(byte value)
{
Serialize(&value, 1);
}
public virtual unsafe byte[] ReadBytes(long amount)
{
@@ -224,6 +274,11 @@ public abstract class FArchive : IDisposable
return value;
}
public virtual unsafe void WriteUInt32(uint value)
{
ByteOrderSerialize(&value, sizeof(uint));
}
public virtual unsafe int ReadInt32()
{
int value;
@@ -312,6 +367,14 @@ public abstract class FArchive : IDisposable
throw new NotImplementedException();
}
public unsafe void SerializeBits(Span<byte> value, long lengthBits)
{
fixed (byte* pBuffer = value)
{
SerializeBits(pBuffer, lengthBits);
}
}
public unsafe void SerializeBits(byte[] value, long lengthBits)
{
fixed (byte* pBuffer = value)
@@ -320,9 +383,6 @@ public abstract class FArchive : IDisposable
}
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <param name="lengthBits"></param>
public virtual unsafe void SerializeBits(void* value, long lengthBits)
@@ -335,10 +395,21 @@ public abstract class FArchive : IDisposable
}
}
public virtual unsafe void SerializeInt(uint* value, uint max)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void SerializeInt(uint value, uint valueMax)
{
SerializeInt(&value, valueMax);
}
public virtual unsafe void SerializeInt(uint* value, uint valueMax)
{
throw new NotImplementedException();
ByteOrderSerialize(&value, sizeof(uint));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void SerializeIntPacked(uint value)
{
SerializeIntPacked(&value);
}
/// <summary>
@@ -727,5 +798,10 @@ public abstract class FArchive : IDisposable
_arIsFilterEditorOnly = inFilterEditorOnly;
}
public virtual void Reset()
{
}
public abstract void Dispose();
}
@@ -1,8 +1,8 @@
namespace Prospect.Unreal.Serialization;
public struct FBitReaderMark
public readonly struct FBitReaderMark
{
private long _pos;
private readonly long _pos;
public FBitReaderMark(FBitReader reader)
{
+273 -34
View File
@@ -1,6 +1,5 @@
using System.Buffers;
using System.Collections;
using Prospect.Unreal.Core;
using Serilog;
namespace Prospect.Unreal.Serialization;
@@ -9,28 +8,103 @@ public class FBitWriter : FArchive
{
private static readonly ILogger Logger = Log.ForContext<FBitWriter>();
private static readonly ArrayPool<byte> Pool = ArrayPool<byte>.Create();
private readonly bool _usesPool;
public FBitWriter(int inMaxBits, bool inAllowResize = false)
public FBitWriter()
{
Num = 0;
Max = inMaxBits;
AllowResize = inAllowResize;
Buffer = Pool.Rent((inMaxBits + 7) >> 3);
Max = 0;
Data = Array.Empty<byte>();
AllowResize = false;
AllowOverflow = false;
_usesPool = false;
_arIsSaving = true;
_arIsPersistent = true;
_arIsNetArchive = true;
}
private byte[] Buffer { get; set; }
private long Num { get; set; }
private long Max { get; set; }
private bool AllowResize { get; }
private bool AllowOverflow { get; }
public override void Dispose()
public FBitWriter(long inMaxBits, bool inAllowResize = false, bool usePool = true)
{
Pool.Return(Buffer, true);
Num = 0;
Max = inMaxBits;
AllowResize = inAllowResize;
var byteCount = (int)((inMaxBits + 7) >> 3);
if (usePool)
{
Data = Pool.Rent(byteCount);
_usesPool = true;
}
else
{
Data = new byte[byteCount];
_usesPool = false;
}
_arIsSaving = true;
_arIsPersistent = true;
_arIsNetArchive = true;
}
public FBitWriter(FBitWriter writer) : base(writer)
{
Num = writer.Num;
Max = writer.Max;
AllowOverflow = writer.AllowOverflow;
AllowResize = writer.AllowResize;
if (writer.Data.Length > 0)
{
_usesPool = writer._usesPool;
if (_usesPool)
{
Data = Pool.Rent(writer.Data.Length);
}
else
{
Data = new byte[writer.Data.Length];
}
Buffer.BlockCopy(writer.Data, 0, Data, 0, writer.Data.Length);
}
else
{
Data = Array.Empty<byte>();
}
}
private byte[] Data { get; set; }
internal long Num { get; set; }
private long Max { get; set; }
private bool AllowResize { get; set; }
private bool AllowOverflow { get; }
public byte[] GetData()
{
if (IsError())
{
Logger.Error("Retrieved data from a BitWriter that had an error");
}
return Data;
}
public long GetNumBytes()
{
return (Num + 7) >> 3;
}
public long GetNumBits()
{
return Num;
}
public long GetMaxBits()
{
return Max;
}
public override unsafe void Serialize(void* src, long lengthBytes)
@@ -38,7 +112,7 @@ public class FBitWriter : FArchive
var lengthBits = lengthBytes * 8;
if (AllowAppend(lengthBits))
{
fixed (byte* pBuffer = Buffer)
fixed (byte* pBuffer = Data)
{
FBitUtil.AppBitsCpy(pBuffer, (int)Num, (byte*)src, 0, (int)lengthBits);
}
@@ -51,6 +125,34 @@ public class FBitWriter : FArchive
}
}
public override unsafe void SerializeBits(void* value, long lengthBits)
{
if (AllowAppend(lengthBits))
{
if (lengthBits == 1)
{
if ((((byte*)value)[0] & 0x01) != 0)
{
Data[Num >> 3] |= FBitUtil.GShift[Num & 7];
}
Num++;
}
else
{
fixed (byte* pBuffer = Data)
{
FBitUtil.AppBitsCpy(pBuffer, (int)Num, (byte*)value, 0, (int)lengthBits);
Num += lengthBits;
}
}
}
else
{
SetOverflowed(lengthBits);
}
}
public void SerializeBits(BitArray bits, int lengthBits)
{
if (AllowAppend(lengthBits))
@@ -66,13 +168,134 @@ public class FBitWriter : FArchive
}
}
public override unsafe void SerializeInt(uint* value, uint valueMax)
{
if (valueMax < 2)
{
throw new NotSupportedException();
}
var lengthBits = (int) Math.Ceiling(Math.Log2(valueMax));
var writeValue = *value;
if (writeValue >= valueMax)
{
Logger.Error("SerializeInt(): Value out of bounds (Value: {Value}, ValueMax: {ValueMax})", writeValue, valueMax);
writeValue = valueMax - 1;
}
if (AllowAppend(lengthBits))
{
uint newValue = 0;
var localNum = Num;
for (uint mask = 1; (newValue + mask) < valueMax && (mask != 0); mask *= 2, localNum++)
{
if ((writeValue & mask) != 0)
{
Data[localNum >> 3] += FBitUtil.GShift[localNum & 7];
newValue += mask;
}
}
Num = localNum;
}
else
{
SetOverflowed(lengthBits);
}
}
public override unsafe void SerializeIntPacked(uint* inValue)
{
uint value = *inValue;
Span<uint> bytesAsWords = stackalloc uint[5];
uint byteCount = 0;
for (uint It = 0; (It == 0) | (value != 0); ++It, value = value >> 7)
{
if ((value & ~0x7F) != 0)
{
bytesAsWords[(int)byteCount++] = ((value & 0x7FU) << 1) | 1;
}
else
{
bytesAsWords[(int)byteCount++] = ((value & 0x7FU) << 1);
}
}
var lengthBits = byteCount * 8;
if (!AllowAppend(lengthBits))
{
SetOverflowed(lengthBits);
return;
}
int BitCountUsedInByte = (int)(Num & 7);
int BitCountLeftInByte = (int)(8 - (Num & 7));
byte DestMaskByte0 = (byte)((1U << BitCountUsedInByte) - 1U);
byte DestMaskByte1 = (byte)(0xFF ^ DestMaskByte0);
bool bStraddlesTwoBytes = (BitCountUsedInByte != 0);
fixed (byte* pData = Data)
{
var Dest = pData + (Num >> 3);
Num += lengthBits;
for (var ByteIt = 0; ByteIt != byteCount; ++ByteIt)
{
uint ByteAsWord = bytesAsWords[ByteIt];
*Dest = (byte)((*Dest & DestMaskByte0) | (byte)(ByteAsWord << BitCountUsedInByte));
++Dest;
if (bStraddlesTwoBytes)
*Dest = (byte)((*Dest & DestMaskByte1) | (byte)(ByteAsWord >> BitCountLeftInByte));
}
}
}
public void WriteIntWrapped(uint value, uint valueMax)
{
var lengthBits = (int) Math.Ceiling(Math.Log2(valueMax));
if (AllowAppend(lengthBits))
{
uint newValue = 0;
for (uint mask = 1; newValue + mask < valueMax && (mask != 0); mask *= 2, Num++)
{
if ((value & mask) != 0)
{
Data[Num >> 3] += FBitUtil.GShift[Num & 7];
newValue += mask;
}
}
}
else
{
SetOverflowed(lengthBits);
}
}
public void WriteBit(bool value)
{
if (value)
{
WriteBit(1);
}
else
{
WriteBit(0);
}
}
public void WriteBit(byte value)
{
if (AllowAppend(1))
{
if (value != 0)
{
Buffer[Num >> 3] |= FBitUtil.GShift[Num & 7];
Data[Num >> 3] |= FBitUtil.GShift[Num & 7];
}
Num++;
@@ -83,7 +306,7 @@ public class FBitWriter : FArchive
}
}
private void SetOverflowed(long lengthBits)
protected void SetOverflowed(long lengthBits)
{
if (!AllowOverflow)
{
@@ -93,13 +316,31 @@ public class FBitWriter : FArchive
SetError();
}
private bool AllowAppend(long lengthBits)
public bool AllowAppend(long lengthBits)
{
if (Num + lengthBits > Max)
{
if (AllowResize)
{
throw new NotImplementedException();
// Resize our buffer. The common case for resizing bitwriters is hitting the max and continuing to add a lot of small segments of data
// Though we could just allow the TArray buffer to handle the slack and resizing, we would still constantly hit the FBitWriter's max
// and cause this block to be executed, as well as constantly zeroing out memory inside AddZeroes (though the memory would be allocated
// in chunks).
Max = Math.Max(Max << 1, Num + lengthBits);
var byteMax = (Max + 7) >> 3;
if (!_usesPool)
{
var dataTemp = Data;
Array.Resize(ref dataTemp, (int) byteMax);
Data = dataTemp;
}
else
{
throw new NotImplementedException();
}
return true;
}
else
{
@@ -110,28 +351,26 @@ public class FBitWriter : FArchive
return true;
}
public byte[] GetData()
public void SetAllowResize(bool newResize)
{
if (IsError())
{
Logger.Error("Retrieved data from a BitWriter that had an error");
}
return Buffer;
AllowResize = true;
}
public long GetNumBytes()
public override void Reset()
{
return (Num + 7) >> 3;
base.Reset();
Num = 0;
Array.Clear(Data);
_arIsSaving = true;
_arIsPersistent = true;
_arIsNetArchive = true;
}
public long GetNumBits()
public override void Dispose()
{
return Num;
}
public long GetMaxBits()
{
return Max;
if (_usesPool)
{
Pool.Return(Data, true);
}
}
}
@@ -0,0 +1,46 @@
namespace Prospect.Unreal.Serialization;
public struct FBitWriterMark
{
private bool _overflowed;
private long _num;
public FBitWriterMark()
{
_overflowed = false;
_num = 0;
}
public FBitWriterMark(FBitWriter writer)
{
_overflowed = writer.IsError();
_num = writer.GetNumBits();
}
public long GetPos()
{
return _num;
}
public void Pop(FBitReader reader)
{
reader.Pos = _num;
}
public void Init(FBitWriter writer)
{
_num = writer.Num;
_overflowed = writer.IsError();
}
public void Reset()
{
_overflowed = false;
_num = 0;
}
public void PopWithoutClear(FBitWriter writer)
{
writer.Num = _num;
}
}
@@ -9,10 +9,10 @@ public class FNetBitReader : FBitReader
throw new NotSupportedException();
}
public FNetBitReader(UPackageMapClient? inPackageMap, byte[]? src, int num) : base(src, num)
public FNetBitReader(UPackageMap? inPackageMap, byte[]? src, int num) : base(src, num)
{
PackageMap = inPackageMap;
}
public UPackageMapClient? PackageMap { get; }
public UPackageMap? PackageMap { get; }
}
@@ -0,0 +1,44 @@
using Prospect.Unreal.Core;
using Prospect.Unreal.Core.Names;
using Prospect.Unreal.Net;
namespace Prospect.Unreal.Serialization;
public class FNetBitWriter : FBitWriter
{
public FNetBitWriter() : base(0)
{
PackageMap = null;
}
public FNetBitWriter(long inMaxBits) : base(inMaxBits, true)
{
PackageMap = null;
}
public FNetBitWriter(UPackageMap inPackageMap, long inMaxBits) : base(inMaxBits, true)
{
PackageMap = inPackageMap;
}
public FNetBitWriter(FNetBitWriter writer) : base(writer)
{
PackageMap = writer.PackageMap;
}
public UPackageMap? PackageMap { get; }
public virtual void WriteFName(FName name)
{
throw new NotImplementedException();
}
public virtual void WriteUObject(UObject obj)
{
throw new NotImplementedException();
}
// TODO: FSoftObjectPath
// TODO: FSoftObjectPtr
// TODO: FWeakObjectPtr
}