diff --git a/src/.gitignore b/src/.gitignore index d963265..003930f 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1,8 +1,13 @@ # Agent Prospect.Agent.dll + +# Api appsettings.Development.json appsettings.Production.json +# Unreal +Prospect.Unreal/Generated + ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. diff --git a/src/Prospect.Unreal.Generator/NetControlMessageEntry.cs b/src/Prospect.Unreal.Generator/NetControlMessageEntry.cs new file mode 100644 index 0000000..e4bd6fe --- /dev/null +++ b/src/Prospect.Unreal.Generator/NetControlMessageEntry.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Prospect.Unreal.Generator +{ + internal class NetControlMessageEntry + { + public string Name { get; set; } + + public int Index { get; set; } + + public List Params { get; set; } = new List(); + } +} diff --git a/src/Prospect.Unreal.Generator/NetControlMessageParser.cs b/src/Prospect.Unreal.Generator/NetControlMessageParser.cs new file mode 100644 index 0000000..2404089 --- /dev/null +++ b/src/Prospect.Unreal.Generator/NetControlMessageParser.cs @@ -0,0 +1,41 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Prospect.Unreal.Generator +{ + internal class NetControlMessageParser + { + internal IReadOnlyList Parse(ImmutableArray attributes) + { + var entries = new List(); + + foreach (var attribute in attributes) + { + var attributeParams = attribute.ArgumentList.Arguments; + var attributeEntry = new NetControlMessageEntry(); + + attributeEntry.Name = ((LiteralExpressionSyntax)attributeParams[0].Expression).Token.ValueText; // StringLiteralExpression + attributeEntry.Index = (int)((LiteralExpressionSyntax)attributeParams[1].Expression).Token.Value; // NumericLiteralExpression + + for (int i = 2; i < attributeParams.Count; i++) + { + var paramType = ((TypeOfExpressionSyntax)attributeParams[i].Expression); + var paramKeyword = paramType.Type; + if (paramKeyword is IdentifierNameSyntax paramIdentifierName) + { + attributeEntry.Params.Add(paramIdentifierName.Identifier.ValueText); + } + else if (paramKeyword is PredefinedTypeSyntax predefinedTypeSyntax) + { + attributeEntry.Params.Add(predefinedTypeSyntax.Keyword.ValueText); + } + } + + entries.Add(attributeEntry); + } + + return entries; + } + } +} diff --git a/src/Prospect.Unreal.Generator/NetControlMessageSourceGenerator.cs b/src/Prospect.Unreal.Generator/NetControlMessageSourceGenerator.cs new file mode 100644 index 0000000..5c84e83 --- /dev/null +++ b/src/Prospect.Unreal.Generator/NetControlMessageSourceGenerator.cs @@ -0,0 +1,182 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Prospect.Unreal.Generator.Util; +using Scriban; +using Scriban.Runtime; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Text; +using System.Threading; + +namespace Prospect.Unreal.Generator +{ + [Generator] + internal class NetControlMessageSourceIncrementalGenerator : IIncrementalGenerator + { + public const string Namespace = "Prospect.Unreal.Net.Packets.Control"; + public const string AttributeName = "NetControlMessage"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var attributes = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: IsSyntaxTargetForGeneration, + transform: GetSemanticTargetForGeneration) + .Where(static m => m is not null); + + var compilationAndAttributes = context.CompilationProvider.Combine(attributes.Collect()); + + context.RegisterSourceOutput(compilationAndAttributes, static (spc, source) => Execute(source.Left, source.Right, spc)); + } + + private static bool IsSyntaxTargetForGeneration(SyntaxNode node, CancellationToken _) => + node is AttributeSyntax attribute && attribute.Name is IdentifierNameSyntax; + + private static AttributeSyntax GetSemanticTargetForGeneration(GeneratorSyntaxContext context, CancellationToken _) + { + var attribute = (AttributeSyntax)context.Node; + var attributeName = (IdentifierNameSyntax)attribute.Name; + + if (attributeName.Identifier.ValueText != AttributeName) + { + return null; + } + + return attribute; + } + + private static void Execute(Compilation compilation, ImmutableArray attributes, SourceProductionContext context) + { + var parser = new NetControlMessageParser(); + var results = parser.Parse(attributes); + + if (results.Count == 0) + { + return; + } + + context.AddSource("NMTEnum.g.cs", GenerateEnum(results)); + + foreach (var result in results) + { + context.AddSource($"NMT_{result.Name}.g.cs", GenerateMessage(result)); + } + } + + private static string GenerateEnum(IReadOnlyList entries) + { + var templateData = EmbeddedResource.GetContent("Templates/NMTEnum.sbntxt"); + var template = Template.Parse(templateData); + + return template.Render(new + { + cnamespace = Namespace, + entries = entries + }, member => member.Name).Trim(); + } + + private static string GenerateMessage(NetControlMessageEntry entry) + { + var scriptObject = new ScriptObject(); + + scriptObject.Import(typeof(ScribanFunctions)); + scriptObject.Add("cnamespace", Namespace); + scriptObject.Add("entry", entry); + + var context = new TemplateContext(); + + context.PushGlobal(scriptObject); + + var templateData = EmbeddedResource.GetContent("Templates/NMTMessage.sbntxt"); + var template = Template.Parse(templateData); + + return template.Render(scriptObject, member => member.Name).Trim(); + } + } + + internal static class ScribanFunctions + { + [ScriptMemberIgnore] + private static readonly Dictionary TypeDefinitions = new Dictionary + { + { "byte", new ParamDef("bunch.ReadByte()", "bunch.WriteByte({0})") }, + { "int", new ParamDef("bunch.ReadInt32()", "bunch.WriteInt32({0})") }, + { "uint", new ParamDef("bunch.ReadUInt32()", "bunch.WriteUInt32({0})") }, + { "FString", new ParamDef("bunch.ReadString()", "bunch.WriteString({0})") }, + }; + + public static string SendParams(List args) + { + return MethodParameter(args); + } + + public static string ReadOutParams(List args) + { + return MethodParameter(args, "out"); + } + + public static string ReadOut(string paramType, int index) + { + var paramName = (char)('a' + index); + var paramRead = ReadType(paramType); + if (paramRead.StartsWith("throw")) { + return paramRead; + } + + return $"{paramName} = {paramRead}"; + } + + public static string SendType(string paramType, int index) + { + if (TypeDefinitions.TryGetValue(paramType, out var type)) + { + return string.Format(type.Write, (char)('a' + index)); + } + + return $"throw new NotImplementedException(\"Unsupported type {paramType}\")"; + } + + public static string ReadType(string param) + { + if (TypeDefinitions.TryGetValue(param, out var type)) + { + return type.Read; + } + + return $"throw new NotImplementedException(\"Unsupported type {param}\")"; + } + + [ScriptMemberIgnore] + private static string MethodParameter(List args, string prefix = null) + { + + var builder = new StringBuilder(); + + for (int i = 0; i < args.Count; i++) + { + if (i < args.Count) + { + builder.Append(", "); + } + + var paramName = (char)('a' + i); + var paramType = args[i]; + if (paramType == "FString") + { + paramType = "string"; + } + + if (prefix != null) + { + builder.AppendFormat("{0} {1} {2}", prefix, paramType, paramName); + } + else + { + builder.AppendFormat("{0} {1}", paramType, paramName); + } + } + + return builder.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal.Generator/NetControlMessageSyntaxReceiver.cs b/src/Prospect.Unreal.Generator/NetControlMessageSyntaxReceiver.cs new file mode 100644 index 0000000..3d5a84b --- /dev/null +++ b/src/Prospect.Unreal.Generator/NetControlMessageSyntaxReceiver.cs @@ -0,0 +1,53 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Generic; + +namespace Prospect.Unreal.Generator +{ + internal class NetControlMessageSyntaxReceiver : ISyntaxContextReceiver + { + public const string AttributeName = "NetControlMessage"; + + public List Entries { get; } = new List(); + + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is not AttributeSyntax attribute) + { + return; + } + + if (attribute.Name is not IdentifierNameSyntax identifierName) + { + return; + } + + if (identifierName.Identifier.ValueText != AttributeName) + { + return; + } + + var attributeParams = attribute.ArgumentList.Arguments; + var attributeEntry = new NetControlMessageEntry(); + + attributeEntry.Name = ((LiteralExpressionSyntax)attributeParams[0].Expression).Token.ValueText; // StringLiteralExpression + attributeEntry.Index = (int) ((LiteralExpressionSyntax)attributeParams[1].Expression).Token.Value; // NumericLiteralExpression + + for (int i = 2; i < attributeParams.Count; i++) + { + var paramType = ((TypeOfExpressionSyntax)attributeParams[i].Expression); + var paramKeyword = paramType.Type; + if (paramKeyword is IdentifierNameSyntax paramIdentifierName) + { + attributeEntry.Params.Add(paramIdentifierName.Identifier.ValueText); + } + else if (paramKeyword is PredefinedTypeSyntax predefinedTypeSyntax) + { + attributeEntry.Params.Add(predefinedTypeSyntax.Keyword.ValueText); + } + } + + Entries.Add(attributeEntry); + } + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal.Generator/Properties/launchSettings.json b/src/Prospect.Unreal.Generator/Properties/launchSettings.json new file mode 100644 index 0000000..d38378c --- /dev/null +++ b/src/Prospect.Unreal.Generator/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Roslyn": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\Prospect.Unreal\\Prospect.Unreal.csproj" + } + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal.Generator/Prospect.Unreal.Generator.csproj b/src/Prospect.Unreal.Generator/Prospect.Unreal.Generator.csproj new file mode 100644 index 0000000..531a40c --- /dev/null +++ b/src/Prospect.Unreal.Generator/Prospect.Unreal.Generator.csproj @@ -0,0 +1,36 @@ + + + + netstandard2.0 + 9.0 + true + + + + + + + + + + + + + + + + + + + + + $(GetTargetPathDependsOn);GetDependencyTargetPaths + + + + + + + + + diff --git a/src/Prospect.Unreal.Generator/Templates/NMTEnum.sbntxt b/src/Prospect.Unreal.Generator/Templates/NMTEnum.sbntxt new file mode 100644 index 0000000..5abacd9 --- /dev/null +++ b/src/Prospect.Unreal.Generator/Templates/NMTEnum.sbntxt @@ -0,0 +1,8 @@ +namespace {{ cnamespace }}; + +public enum NMT +{ +{{~ for entry in entries ~}} + {{ entry.Name }} = {{ entry.Index }}, +{{~ end ~}} +} \ No newline at end of file diff --git a/src/Prospect.Unreal.Generator/Templates/NMTMessage.sbntxt b/src/Prospect.Unreal.Generator/Templates/NMTMessage.sbntxt new file mode 100644 index 0000000..369f136 --- /dev/null +++ b/src/Prospect.Unreal.Generator/Templates/NMTMessage.sbntxt @@ -0,0 +1,36 @@ +using Prospect.Unreal.Core; +using Prospect.Unreal.Net; +using Prospect.Unreal.Net.Packets.Bunch; + +namespace {{ cnamespace }}; + +public static class NMT_{{ entry.Name }} +{ + public static void Send(UNetConnection conn{{send_params entry.Params}}) + { + if (conn.Channels[0] != null && !conn.Channels[0].Closing) + { + using var bunch = new FControlChannelOutBunch(conn.Channels[0], false); + bunch.WriteByte({{ entry.Index }}); + {{~ for param in entry.Params ~}} + {{send_type param for.index}}; + {{~ end ~}} + conn.Channels[0].SendBunch(bunch, true); + } + } + + public static bool Receive(FInBunch bunch{{read_out_params entry.Params}}) + { + {{~ for param in entry.Params ~}} + {{read_out param for.index}}; + {{~ end ~}} + return !bunch.IsError(); + } + + public static void Discard(FInBunch bunch) + { + {{~ for param in entry.Params ~}} + {{read_type param}}; + {{~ end ~}} + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal.Generator/Util/EmbeddedResource.cs b/src/Prospect.Unreal.Generator/Util/EmbeddedResource.cs new file mode 100644 index 0000000..2d052ab --- /dev/null +++ b/src/Prospect.Unreal.Generator/Util/EmbeddedResource.cs @@ -0,0 +1,27 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Prospect.Unreal.Generator.Util +{ + internal static class EmbeddedResource + { + public static string GetContent(string relativePath) + { + var baseName = Assembly.GetExecutingAssembly().GetName().Name; + var resourceName = relativePath + .TrimStart('.') + .Replace(Path.DirectorySeparatorChar, '.') + .Replace(Path.AltDirectorySeparatorChar, '.'); + + using var stream = Assembly.GetExecutingAssembly() + .GetManifestResourceStream(baseName + "." + resourceName); + + if (stream == null) + throw new NotSupportedException(); + + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + } +} diff --git a/src/Prospect.Unreal.Generator/Util/ParamDef.cs b/src/Prospect.Unreal.Generator/Util/ParamDef.cs new file mode 100644 index 0000000..b7357b6 --- /dev/null +++ b/src/Prospect.Unreal.Generator/Util/ParamDef.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Prospect.Unreal.Generator.Util +{ + internal class ParamDef + { + public ParamDef(string read, string write) + { + Read = read; + Write = write; + } + + public string Read { get; } + public string Write { get; } + } +} diff --git a/src/Prospect.Unreal/Core/FPlatformTime.cs b/src/Prospect.Unreal/Core/FPlatformTime.cs new file mode 100644 index 0000000..b214419 --- /dev/null +++ b/src/Prospect.Unreal/Core/FPlatformTime.cs @@ -0,0 +1,9 @@ +namespace Prospect.Unreal.Core; + +public static class FPlatformTime +{ + public static double Seconds() + { + return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Core/UObject.cs b/src/Prospect.Unreal/Core/UObject.cs new file mode 100644 index 0000000..2e71345 --- /dev/null +++ b/src/Prospect.Unreal/Core/UObject.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Core; + +public class UObject +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Channels/UActorChannel.cs b/src/Prospect.Unreal/Net/Channels/Actor/UActorChannel.cs similarity index 92% rename from src/Prospect.Unreal/Net/Channels/UActorChannel.cs rename to src/Prospect.Unreal/Net/Channels/Actor/UActorChannel.cs index e416f20..39b1f8d 100644 --- a/src/Prospect.Unreal/Net/Channels/UActorChannel.cs +++ b/src/Prospect.Unreal/Net/Channels/Actor/UActorChannel.cs @@ -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 { diff --git a/src/Prospect.Unreal/Net/Channels/Control/UControlChannel.cs b/src/Prospect.Unreal/Net/Channels/Control/UControlChannel.cs new file mode 100644 index 0000000..0e24e2f --- /dev/null +++ b/src/Prospect.Unreal/Net/Channels/Control/UControlChannel.cs @@ -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(); + + 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(); + } + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Channels/UChannel.cs b/src/Prospect.Unreal/Net/Channels/UChannel.cs index eba4be9..e7636d7 100644 --- a/src/Prospect.Unreal/Net/Channels/UChannel.cs +++ b/src/Prospect.Unreal/Net/Channels/UChannel.cs @@ -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 /// /// Owner connection. /// - public UNetConnection Connection { get; set; } + public UNetConnection? Connection { get; set; } /// /// 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 /// public FInBunch? InRec { get; set; } - // public FOutBunch OutRec { get; set; } + /// + /// Outgoing reliable unacked data. + /// + public FOutBunch? OutRec { get; set; } /// /// 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(); + + // 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; + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Channels/UControlChannel.cs b/src/Prospect.Unreal/Net/Channels/UControlChannel.cs deleted file mode 100644 index 4e4bd1b..0000000 --- a/src/Prospect.Unreal/Net/Channels/UControlChannel.cs +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Channels/UVoiceChannel.cs b/src/Prospect.Unreal/Net/Channels/Voice/UVoiceChannel.cs similarity index 89% rename from src/Prospect.Unreal/Net/Channels/UVoiceChannel.cs rename to src/Prospect.Unreal/Net/Channels/Voice/UVoiceChannel.cs index b2b9d3f..27d92ee 100644 --- a/src/Prospect.Unreal/Net/Channels/UVoiceChannel.cs +++ b/src/Prospect.Unreal/Net/Channels/Voice/UVoiceChannel.cs @@ -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 { diff --git a/src/Prospect.Unreal/Net/EResendAllDataState.cs b/src/Prospect.Unreal/Net/EResendAllDataState.cs new file mode 100644 index 0000000..ed645eb --- /dev/null +++ b/src/Prospect.Unreal/Net/EResendAllDataState.cs @@ -0,0 +1,8 @@ +namespace Prospect.Unreal.Net; + +public enum EResendAllDataState +{ + None, + SinceOpen, + SinceCheckpoint +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/EWriteBitsDataType.cs b/src/Prospect.Unreal/Net/EWriteBitsDataType.cs new file mode 100644 index 0000000..e8813f8 --- /dev/null +++ b/src/Prospect.Unreal/Net/EWriteBitsDataType.cs @@ -0,0 +1,8 @@ +namespace Prospect.Unreal.Net; + +public enum EWriteBitsDataType +{ + Unknown, + Bunch, + Ack +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FGuid.cs b/src/Prospect.Unreal/Net/FGuid.cs new file mode 100644 index 0000000..8d81a3d --- /dev/null +++ b/src/Prospect.Unreal/Net/FGuid.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net; + +public class FGuid +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FNetworkGUID.cs b/src/Prospect.Unreal/Net/FNetworkGUID.cs new file mode 100644 index 0000000..ad4fc3c --- /dev/null +++ b/src/Prospect.Unreal/Net/FNetworkGUID.cs @@ -0,0 +1,6 @@ +namespace Prospect.Unreal.Net; + +public class FNetworkGUID +{ + +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FNetworkNotify.cs b/src/Prospect.Unreal/Net/FNetworkNotify.cs index 2f84f00..7261e61 100644 --- a/src/Prospect.Unreal/Net/FNetworkNotify.cs +++ b/src/Prospect.Unreal/Net/FNetworkNotify.cs @@ -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()) /// - void NotifyControlMessage(UNetConnection connection, byte messageType, FInBunch bunch); + void NotifyControlMessage(UNetConnection connection, NMT messageType, FInBunch bunch); } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FOutPacketTraits.cs b/src/Prospect.Unreal/Net/FOutPacketTraits.cs index 49e2796..815a792 100644 --- a/src/Prospect.Unreal/Net/FOutPacketTraits.cs +++ b/src/Prospect.Unreal/Net/FOutPacketTraits.cs @@ -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; } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/FPacketIdRange.cs b/src/Prospect.Unreal/Net/FPacketIdRange.cs index cc01153..de66ab3 100644 --- a/src/Prospect.Unreal/Net/FPacketIdRange.cs +++ b/src/Prospect.Unreal/Net/FPacketIdRange.cs @@ -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) { diff --git a/src/Prospect.Unreal/Net/FUniqueNetIdRepl.cs b/src/Prospect.Unreal/Net/FUniqueNetIdRepl.cs new file mode 100644 index 0000000..ae85707 --- /dev/null +++ b/src/Prospect.Unreal/Net/FUniqueNetIdRepl.cs @@ -0,0 +1,5 @@ +namespace Prospect.Unreal.Net; + +public class FUniqueNetIdRepl +{ +} diff --git a/src/Prospect.Unreal/Net/HandlerComponent.cs b/src/Prospect.Unreal/Net/HandlerComponent.cs index 79b1936..5a67824 100644 --- a/src/Prospect.Unreal/Net/HandlerComponent.cs +++ b/src/Prospect.Unreal/Net/HandlerComponent.cs @@ -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; } + + /// + /// Maximum number of Outgoing packet bits supported (automatically calculated to factor in other HandlerComponent reserved bits) + /// + 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); } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/PacketHandler.cs b/src/Prospect.Unreal/Net/PacketHandler.cs index eae62eb..94d0f6b 100644 --- a/src/Prospect.Unreal/Net/PacketHandler.cs +++ b/src/Prospect.Unreal/Net/PacketHandler.cs @@ -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(); + private static readonly IPEndPoint EmptyAddress = new IPEndPoint(IPAddress.None, 0); private readonly List _handlerComponents; private bool _bConnectionlessHandler; private bool _bRawSend; + + /// + /// The maximum supported packet size (reflects UNetConnection::MaxPacket) + /// + 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(); _reliabilityComponent = null; - _outgoingPacket = new FBitWriter(0); + _outgoingPacket = new FBitWriter(0, true, false); + _outgoingPacket.AllowAppend(1); _incomingPacket = new FBitReader(Array.Empty()); - - 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(); + } + } + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Packets/Bunch/FControlChannelOutBunch.cs b/src/Prospect.Unreal/Net/Packets/Bunch/FControlChannelOutBunch.cs new file mode 100644 index 0000000..5b12ed2 --- /dev/null +++ b/src/Prospect.Unreal/Net/Packets/Bunch/FControlChannelOutBunch.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Packets/Bunch/FOutBunch.cs b/src/Prospect.Unreal/Net/Packets/Bunch/FOutBunch.cs new file mode 100644 index 0000000..7c59ec0 --- /dev/null +++ b/src/Prospect.Unreal/Net/Packets/Bunch/FOutBunch.cs @@ -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; } + + /// + /// Close, but go dormant. + /// + public bool bDormant { get; set; } + + /// + /// Replication on this channel is being paused by the server. + /// + public bool bIsReplicationPaused { get; set; } + + public bool bReliable { get; set; } + + /// + /// Not a complete bunch + /// + public bool bPartial { get; set; } + + /// + /// The first bunch of a partial bunch + /// + public bool bPartialInitial { get; set; } + + /// + /// The final bunch of a partial bunch + /// + public bool bPartialFinal { get; set; } + + /// + /// This bunch has networkGUID name/id pairs. + /// + public bool bHasPackageMapExports { get; set; } + + /// + /// This bunch has guids that must be mapped before we can process this bunch. + /// + public bool bHasMustBeMappedGUIDs { get; set; } + + public EChannelCloseReason CloseReason { get; set; } + + public List ExportNetGUIDs { get; } = new List(); + + public List NetFieldExports { get; } = new List(); +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Packets/Control/NetControlMessageAttribute.cs b/src/Prospect.Unreal/Net/Packets/Control/NetControlMessageAttribute.cs new file mode 100644 index 0000000..46333c5 --- /dev/null +++ b/src/Prospect.Unreal/Net/Packets/Control/NetControlMessageAttribute.cs @@ -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; } +} diff --git a/src/Prospect.Unreal/Net/Packets/Control/NetControlMessageDefs.cs b/src/Prospect.Unreal/Net/Packets/Control/NetControlMessageDefs.cs new file mode 100644 index 0000000..eaf5511 --- /dev/null +++ b/src/Prospect.Unreal/Net/Packets/Control/NetControlMessageDefs.cs @@ -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))] diff --git a/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs b/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs index b0bd352..ace27bb 100644 --- a/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs +++ b/src/Prospect.Unreal/Net/Packets/Header/FNetPacketNotify.cs @@ -14,7 +14,9 @@ public class FNetPacketNotify public const int MaxSequenceHistoryLength = 256; private readonly Queue _ackRecord = new Queue(); - + 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; + } + + /// + /// These methods must always write and read the exact same number of bits, that is the reason for not using WriteInt/WrittedWrappedInt + /// + 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(); + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/Packets/Header/FPackedHeader.cs b/src/Prospect.Unreal/Net/Packets/Header/FPackedHeader.cs index c1da7ce..567156d 100644 --- a/src/Prospect.Unreal/Net/Packets/Header/FPackedHeader.cs +++ b/src/Prospect.Unreal/Net/Packets/Header/FPackedHeader.cs @@ -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) { diff --git a/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs b/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs index c2b1851..e7e2f4f 100644 --- a/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs +++ b/src/Prospect.Unreal/Net/Packets/Header/Sequence/SequenceHistory.cs @@ -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]); + } + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs b/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs index cd20e2a..3dcc533 100644 --- a/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs +++ b/src/Prospect.Unreal/Net/StatelessConnectHandlerComponent.cs @@ -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) diff --git a/src/Prospect.Unreal/Net/UIpConnection.cs b/src/Prospect.Unreal/Net/UIpConnection.cs index bc3ed05..16f2175 100644 --- a/src/Prospect.Unreal/Net/UIpConnection.cs +++ b/src/Prospect.Unreal/Net/UIpConnection.cs @@ -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) diff --git a/src/Prospect.Unreal/Net/UNetConnection.cs b/src/Prospect.Unreal/Net/UNetConnection.cs index efb0683..bddfe8e 100644 --- a/src/Prospect.Unreal/Net/UNetConnection.cs +++ b/src/Prospect.Unreal/Net/UNetConnection.cs @@ -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; + /// + /// Did we write the dummy PacketInfo in the current SendBuffer + /// + private bool _bSendBufferHasDummyPacketInfo; + + /// + /// Stores the bit number where we wrote the dummy packet info in the packet header + /// + private FBitWriterMark _headerMarkForPacketInfo; + + /// + /// Timestamp of the last packet sent + /// + private double _previousPacketSendTimeInS; + + private bool _bFlushedNetThisFrame; + private bool _bAutoFlush; + public UNetConnection() { _channelsToTick = new HashSet(); @@ -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 /// /// Package map between local and remote. (negotiates net serialization) /// - public UPackageMapClient? PackageMap { get; private set; } + public UPackageMap? PackageMap { get; private set; } - public List OpenChannels { get; private set; } + public List OpenChannels { get; } /// /// Maximum packet size. @@ -115,11 +143,44 @@ public abstract class UNetConnection : UPlayer /// public IPEndPoint? RemoteAddr { get; protected set; } + /// + /// Number of bits used for the packet id in the current packet. + /// + public int NumPacketIdBits { get; private set; } + + /// + /// Number of bits used for bunches in the current packet. + /// + public int NumBunchBits { get; private set; } + + /// + /// Number of bits used for acks in the current packet. + /// + public int NumAckBits { get; private set; } + + /// + /// Number of bits used for padding in the current packet. + /// + public int NumPaddingBits { get; private set; } + + /// + /// The maximum number of bits all packet handlers will reserve. + /// + public int MaxPacketHandlerBits { get; private set; } + /// /// State this connection is in. /// public EConnectionState State { get; private set; } + /// + /// 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. + /// + public EResendAllDataState ResendAllDataState { get; set; } + /// /// PacketHandler, for managing layered handler components, which modify packets as they are sent/received /// @@ -130,7 +191,7 @@ public abstract class UNetConnection : UPlayer /// /// Used to determine what the next expected control channel msg type should be from a connecting client /// - public byte ExpectedClientLoginMsgType { get; private set; } + public NMT ExpectedClientLoginMsgType { get; private set; } /// /// Reference to the PacketHandler component, for managing stateless connection handshakes @@ -142,6 +203,20 @@ public abstract class UNetConnection : UPlayer /// public int PacketOverhead { get; private set; } + /// + /// Server-generated challenge. + /// + public string Challenge { get; private set; } + + /// + /// Queued up bits waiting to send + /// + public FBitWriter SendBuffer { get; private set; } + + public double[] OutLagTime { get; } + + public int[] OutLagPacketId { get; } + /// /// Full incoming packet index. /// @@ -164,6 +239,38 @@ public abstract class UNetConnection : UPlayer /// Related to OutAckPacketId which is tha last successfully delivered PacketId. /// public int LastNotifiedPacketId { get; private set; } + + /// + /// Keep old behavior where we send a packet with only acks even if we have no other outgoing data if we got incoming data + /// + public uint HasDirtyAcks { get; set; } + + /// + /// Most recently sent bunch start. + /// + public FBitWriterMark LastStart { get; set; } + + /// + /// Most recently sent bunch end. + /// + public FBitWriterMark LastEnd { get; set; } + + /// + /// Whether to allow merging. + /// + public bool AllowMerge { get; set; } + + /// + /// Whether contents are time-sensitive. + /// + public bool TimeSensitive { get; set; } + + /// + /// Most recent outgoing bunch. + /// + 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(); 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 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); + } + + /// + /// Returns number of bits left in current packet that can be used without causing a flush + /// + 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; diff --git a/src/Prospect.Unreal/Net/UNetDriver.cs b/src/Prospect.Unreal/Net/UNetDriver.cs index 900e357..e5782ad 100644 --- a/src/Prospect.Unreal/Net/UNetDriver.cs +++ b/src/Prospect.Unreal/Net/UNetDriver.cs @@ -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; /// /// 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(); StatelessConnectComponent.SetDriver(this); diff --git a/src/Prospect.Unreal/Net/UPackageMap.cs b/src/Prospect.Unreal/Net/UPackageMap.cs index 8615617..f2da8d0 100644 --- a/src/Prospect.Unreal/Net/UPackageMap.cs +++ b/src/Prospect.Unreal/Net/UPackageMap.cs @@ -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(); + } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Net/UPackageMapClient.cs b/src/Prospect.Unreal/Net/UPackageMapClient.cs index f3a6ac3..6695bd8 100644 --- a/src/Prospect.Unreal/Net/UPackageMapClient.cs +++ b/src/Prospect.Unreal/Net/UPackageMapClient.cs @@ -1,6 +1,6 @@ namespace Prospect.Unreal.Net; -public class UPackageMapClient +public class UPackageMapClient : UPackageMap { public void Initialize(UNetConnection connection, FNetGUIDCache guidCache) { diff --git a/src/Prospect.Unreal/Prospect.Unreal.csproj b/src/Prospect.Unreal/Prospect.Unreal.csproj index 799bfa0..2ec82ff 100644 --- a/src/Prospect.Unreal/Prospect.Unreal.csproj +++ b/src/Prospect.Unreal/Prospect.Unreal.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -7,8 +7,24 @@ true + + true + Generated + + + + + + + + + + + + + diff --git a/src/Prospect.Unreal/Runtime/UWorld.cs b/src/Prospect.Unreal/Runtime/UWorld.cs index 10633f9..2628641 100644 --- a/src/Prospect.Unreal/Runtime/UWorld.cs +++ b/src/Prospect.Unreal/Runtime/UWorld.cs @@ -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() diff --git a/src/Prospect.Unreal/Serialization/FArchive.cs b/src/Prospect.Unreal/Serialization/FArchive.cs index 71e0b01..6f26941 100644 --- a/src/Prospect.Unreal/Serialization/FArchive.cs +++ b/src/Prospect.Unreal/Serialization/FArchive.cs @@ -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 /// 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 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 } } - /// - /// - /// /// /// 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); } /// @@ -727,5 +798,10 @@ public abstract class FArchive : IDisposable _arIsFilterEditorOnly = inFilterEditorOnly; } + public virtual void Reset() + { + + } + public abstract void Dispose(); } \ No newline at end of file diff --git a/src/Prospect.Unreal/Serialization/FBitReaderMark.cs b/src/Prospect.Unreal/Serialization/FBitReaderMark.cs index de3fe9f..651ad4d 100644 --- a/src/Prospect.Unreal/Serialization/FBitReaderMark.cs +++ b/src/Prospect.Unreal/Serialization/FBitReaderMark.cs @@ -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) { diff --git a/src/Prospect.Unreal/Serialization/FBitWriter.cs b/src/Prospect.Unreal/Serialization/FBitWriter.cs index db3c97d..f17d82c 100644 --- a/src/Prospect.Unreal/Serialization/FBitWriter.cs +++ b/src/Prospect.Unreal/Serialization/FBitWriter.cs @@ -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(); private static readonly ArrayPool Pool = ArrayPool.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(); + 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(); + } + } + + 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 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); + } } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Serialization/FBitWriterMark.cs b/src/Prospect.Unreal/Serialization/FBitWriterMark.cs new file mode 100644 index 0000000..2fe774e --- /dev/null +++ b/src/Prospect.Unreal/Serialization/FBitWriterMark.cs @@ -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; + } +} \ No newline at end of file diff --git a/src/Prospect.Unreal/Serialization/FNetBitReader.cs b/src/Prospect.Unreal/Serialization/FNetBitReader.cs index 349680d..13925f4 100644 --- a/src/Prospect.Unreal/Serialization/FNetBitReader.cs +++ b/src/Prospect.Unreal/Serialization/FNetBitReader.cs @@ -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; } } \ No newline at end of file diff --git a/src/Prospect.Unreal/Serialization/FNetBitWriter.cs b/src/Prospect.Unreal/Serialization/FNetBitWriter.cs new file mode 100644 index 0000000..91d5ade --- /dev/null +++ b/src/Prospect.Unreal/Serialization/FNetBitWriter.cs @@ -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 +} \ No newline at end of file diff --git a/src/Prospect.sln b/src/Prospect.sln index 4c08147..8945549 100644 --- a/src/Prospect.sln +++ b/src/Prospect.sln @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{168B6247 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Tests", "Prospect.Unreal.Tests\Prospect.Unreal.Tests.csproj", "{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Generator", "Prospect.Unreal.Generator\Prospect.Unreal.Generator.csproj", "{71E3E262-49C5-4B6C-9670-D0741CEDB63B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -85,6 +87,14 @@ Global {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x64.Build.0 = Release|Any CPU {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x86.ActiveCfg = Release|Any CPU {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x86.Build.0 = Release|Any CPU + {71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Debug|x64.ActiveCfg = Debug|Any CPU + {71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Debug|x64.Build.0 = Debug|Any CPU + {71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Debug|x86.ActiveCfg = Debug|Any CPU + {71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Debug|x86.Build.0 = Debug|Any CPU + {71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Release|x64.ActiveCfg = Release|Any CPU + {71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Release|x64.Build.0 = Release|Any CPU + {71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Release|x86.ActiveCfg = Release|Any CPU + {71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -100,5 +110,6 @@ Global {DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834} {380DD490-8F01-4025-91D7-74C289AA5B75} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834} {14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34} = {168B6247-2D33-4942-AB3F-C5041B3BDFEA} + {71E3E262-49C5-4B6C-9670-D0741CEDB63B} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834} EndGlobalSection EndGlobal