Further parse bunches till their channel receives them
This commit is contained in:
@@ -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.
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<UChannel>();
|
||||
|
||||
private const int NetMaxConstructedPartialBunchSizeBytes = 1024 * 64;
|
||||
|
||||
/// <summary>
|
||||
/// Owner connection.
|
||||
/// </summary>
|
||||
public UNetConnection Connection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool OpenAcked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// State of the channel.
|
||||
/// </summary>
|
||||
public bool Closing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Channel is going dormant (it will close but the client will not destroy).
|
||||
/// </summary>
|
||||
public bool Dormant { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Replication is being paused, but channel will not be closed.
|
||||
/// </summary>
|
||||
public bool bIsReplicationPaused { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Opened temporarily.
|
||||
/// </summary>
|
||||
public bool OpenTemporary { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Has encountered errors and is ignoring subsequent packets.
|
||||
/// </summary>
|
||||
public bool Broken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Actor associated with this channel was torn off.
|
||||
/// </summary>
|
||||
public bool bTornOff { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Channel wants to go dormant (it will check during tick if it can go dormant).
|
||||
/// </summary>
|
||||
public bool bPendingDormancy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Channel wants to go dormant, and is otherwise ready to become dormant, but is waiting for a timeout before doing so.
|
||||
/// </summary>
|
||||
public bool bIsInDormancyHysteresis { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unreliable property replication is paused until all reliables are ack'd.
|
||||
/// </summary>
|
||||
public bool bPausedUntilReliableACK { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set when sending closing bunch to avoid recursion in send-failure-close case.
|
||||
/// </summary>
|
||||
public bool SentClosingBunch { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set when placed in the actor channel pool
|
||||
/// </summary>
|
||||
public bool bPooled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether channel was opened locally or by remote.
|
||||
/// </summary>
|
||||
public bool OpenedLocally { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether channel was opened by replay checkpoint recording
|
||||
/// </summary>
|
||||
public bool bOpenedForCheckpoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Index of this channel.
|
||||
/// </summary>
|
||||
public int ChIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public FPacketIdRange OpenPacketId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of this channel.
|
||||
/// </summary>
|
||||
public EChannelType ChType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the type of this channel.
|
||||
/// </summary>
|
||||
public FName ChName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of packets in InRec.
|
||||
/// </summary>
|
||||
public int NumInRec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of packets in OutRec.
|
||||
/// </summary>
|
||||
public int NumOutRec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Incoming data with queued dependencies.
|
||||
/// </summary>
|
||||
public FInBunch? InRec { get; set; }
|
||||
|
||||
// public FOutBunch OutRec { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Partial bunch we are receiving (incoming partial bunches are appended to this).
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
namespace Prospect.Unreal.Net;
|
||||
|
||||
public record FChannelDefinition(FName Name, Type Class, int StaticChannelIndex, bool TickOnCreate, bool ServerOpen, bool ClientOpen, bool InitialServer, bool InitialClient);
|
||||
@@ -0,0 +1,28 @@
|
||||
using Prospect.Unreal.Net.Channels;
|
||||
using Prospect.Unreal.Net.Packets.Bunch;
|
||||
|
||||
namespace Prospect.Unreal.Net;
|
||||
|
||||
public interface FNetworkNotify
|
||||
{
|
||||
EAcceptConnection NotifyAcceptingConnection();
|
||||
|
||||
/// <summary>
|
||||
/// Notification that a new connection has been created/established as a result of a
|
||||
/// remote request, previously approved by NotifyAcceptingConnection
|
||||
/// </summary>
|
||||
void NotifyAcceptedConnection(UNetConnection connection);
|
||||
|
||||
/// <summary>
|
||||
/// Notification that a new channel is being created/opened as a result of a remote request (Actor creation, etc)
|
||||
/// </summary>
|
||||
bool NotifyAcceptingChannel(UChannel channel);
|
||||
|
||||
/// <summary>
|
||||
/// 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())
|
||||
/// </summary>
|
||||
void NotifyControlMessage(UNetConnection connection, byte messageType, FInBunch bunch);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark Seq as received and update current InSeq, missing sequence numbers will be marked as lost
|
||||
/// </summary>
|
||||
public void AckSeq(SequenceNumber seq)
|
||||
{
|
||||
AckSeq(seq, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly mark Seq as not received and update current InSeq, additional missing sequence numbers will be marked as lost
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Prospect.Unreal.Net.Player;
|
||||
|
||||
public class APlayerController
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Prospect.Unreal.Net.Player;
|
||||
|
||||
public class UPlayer
|
||||
{
|
||||
public APlayerController? PlayerController { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Prospect.Unreal.Core.Names;
|
||||
|
||||
namespace Prospect.Unreal.Net;
|
||||
|
||||
public class UChannel
|
||||
{
|
||||
public FName ChName { get; }
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<UNetConnection>();
|
||||
|
||||
@@ -30,6 +32,7 @@ public abstract class UNetConnection
|
||||
public const EEngineNetworkVersionHistory DefaultEngineNetworkProtocolVersion = EEngineNetworkVersionHistory.HISTORY_ENGINENETVERSION_LATEST;
|
||||
public const uint DefaultGameNetworkProtocolVersion = 0;
|
||||
|
||||
private HashSet<UChannel> _channelsToTick;
|
||||
private List<FBitReader>? _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<UChannel>();
|
||||
_packetOrderCache = null;
|
||||
_packetOrderCacheStartIdx = 0;
|
||||
_packetOrderCacheCount = 0;
|
||||
@@ -49,6 +61,7 @@ public abstract class UNetConnection
|
||||
|
||||
Driver = null;
|
||||
PackageMap = null;
|
||||
OpenChannels = new List<UChannel>();
|
||||
MaxPacket = 0;
|
||||
Url = new FUrl();
|
||||
RemoteAddr = new IPEndPoint(IPAddress.None, 0);
|
||||
@@ -85,6 +98,8 @@ public abstract class UNetConnection
|
||||
/// </summary>
|
||||
public UPackageMapClient? PackageMap { get; private set; }
|
||||
|
||||
public List<UChannel> OpenChannels { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum packet size.
|
||||
/// </summary>
|
||||
@@ -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<FChannelCloseInfo>();
|
||||
|
||||
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;
|
||||
|
||||
@@ -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<FName, FChannelDefinition>();
|
||||
ChannelDefinitions = new List<FChannelDefinition>
|
||||
{
|
||||
new FChannelDefinition("Control", typeof(string), 0, true, false, true, false, true),
|
||||
new FChannelDefinition("Voice", typeof(string), 1, true, true, true, true, true),
|
||||
new FChannelDefinition("Actor", typeof(string), -1, false, true, false, false, false)
|
||||
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<UNetConnection>();
|
||||
MappedClientConnections = new ConcurrentDictionary<IPEndPoint, UNetConnection>();
|
||||
|
||||
foreach (var channel in ChannelDefinitions)
|
||||
{
|
||||
ChannelDefinitionMap[channel.Name] = channel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -28,12 +36,19 @@ public abstract class UNetDriver : IAsyncDisposable
|
||||
/// </summary>
|
||||
public UWorld? World { get; private set; }
|
||||
|
||||
public FNetworkNotify Notify { get; private set; }
|
||||
|
||||
public FNetGUIDCache GuidCache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to specify available channel types and their associated UClass
|
||||
/// </summary>
|
||||
public List<FChannelDefinition> ChannelDefinitions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Used for faster lookup of channel definitions by name.
|
||||
/// </summary>
|
||||
public Dictionary<FName, FChannelDefinition> ChannelDefinitionMap { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
Reference in New Issue
Block a user