diff --git a/Aero/Aero.Gen/AGUtils.cs b/Aero/Aero.Gen/AGUtils.cs index 2c63163..b74eb0f 100644 --- a/Aero/Aero.Gen/AGUtils.cs +++ b/Aero/Aero.Gen/AGUtils.cs @@ -274,6 +274,13 @@ public static bool IsViewClass(ClassDeclarationSyntax cd, SemanticModel sm) return false; } + public static bool IsEncounterClass(ClassDeclarationSyntax cd, SemanticModel sm) + { + var aeroAttr = NodeWithName(cd, AeroEncounterAttribute.Name); + + return aeroAttr != null; + } + /*{ return NodeWithName(fd, name); fd.DescendantNodes().OfType() diff --git a/Aero/Aero.Gen/AeroGenrator.cs b/Aero/Aero.Gen/AeroGenrator.cs index 75710d0..d28f5a9 100644 --- a/Aero/Aero.Gen/AeroGenrator.cs +++ b/Aero/Aero.Gen/AeroGenrator.cs @@ -70,6 +70,20 @@ public class AeroGenerator : ISourceGenerator DiagnosticSeverity.Error, isEnabledByDefault: true); + public static readonly DiagnosticDescriptor InvalidTypeInEncounterView = new DiagnosticDescriptor(id: "Aero7", + title: "Invalid type in encounter view", + messageFormat: "Field '{0}' uses invalid type '{1}', use byte/bool/ushort/uint/float/ulong/Timer/EntityId or fixed size array of any of these types", + category: "Aero.Gen", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public static readonly DiagnosticDescriptor InvalidArrayModeInEncounterView = new DiagnosticDescriptor(id: "Aero8", + title: "Invalid array mode in encounter view", + messageFormat: "Field '{0}' uses array mode '{1}', but only fixed size arrays are supported in encounter views", + category: "Aero.Gen", + DiagnosticSeverity.Error, + isEnabledByDefault: true); + #endregion public static FieldDeclarationSyntax LastCheckedField; @@ -111,6 +125,9 @@ public void Execute(GeneratorExecutionContext context) var routing = CreateRouting(snRecv, config); Debug.WriteLine(routing); context.AddSource("AeroRouting.cs", SourceText.From(routing, Encoding.UTF8)); + + var encounters = CreateEncounters(snRecv); + context.AddSource("AeroEncounters.cs", SourceText.From(encounters, Encoding.UTF8)); } } catch (Exception e) { @@ -243,5 +260,46 @@ void AddPacketSwitch(IEnumerable msgs) } } } + + private string CreateEncounters(AeroSyntaxReceiver snRecv) + { + var sb = new StringBuilder(); + + AddLine(sb, "using System;"); + AddLine(sb, "using Aero.Gen;"); + + AddLine(sb, $"public static class AeroEncounters"); + AddLine(sb, "{"); + + Indent(); + { + AddLine(sb, "public static IAeroEncounter GetEncounterClass(string encounterType)"); + AddLineAndIndent(sb, "{"); + { + AddLineAndIndent(sb, "IAeroEncounter encounter = encounterType switch {"); + { + foreach (var encounter in snRecv.AeroEncounterClasses) + { + var attr = AgUtils.NodeWithName(encounter, AeroEncounterAttribute.Name); + + var type = attr.ArgumentList.Arguments[0].Expression.ToString(); + + AddLine(sb, $"{type} => new {encounter.GetFullName()}(),"); + } + + AddLine(sb, "_ => null,"); + } + + UnIndentAndAddLine(sb, "};"); + AddLine(sb, ""); + AddLine(sb, "return encounter;"); + } + + UnIndentAndAddLine(sb, "}"); + UnIndentAndAddLine(sb, "}"); + + return sb.ToString(); + } + } } -} \ No newline at end of file +} diff --git a/Aero/Aero.Gen/AeroSyntaxReceiver.cs b/Aero/Aero.Gen/AeroSyntaxReceiver.cs index 51cc2aa..2b229c4 100644 --- a/Aero/Aero.Gen/AeroSyntaxReceiver.cs +++ b/Aero/Aero.Gen/AeroSyntaxReceiver.cs @@ -13,9 +13,10 @@ public class AeroSyntaxReceiver : ISyntaxReceiver public GeneratorExecutionContext Context; public List ClassesToAugment { get; private set; } = new(); - public List AeroClasses { get; private set; } = new(); - public Dictionary AeroBlockLookup { get; private set; } = new(); - public Dictionary AeroMessageIds { get; private set; } = new(); + public List AeroClasses { get; private set; } = new(); + public List AeroEncounterClasses { get; private set; } = new(); + public Dictionary AeroBlockLookup { get; private set; } = new(); + public Dictionary AeroMessageIds { get; private set; } = new(); public static bool HasAttribute(ClassDeclarationSyntax cds, string attributeName) => cds.AttributeLists.Any(x => x.Attributes.Any(y => (y.Name is IdentifierNameSyntax ins && ins.Identifier.Text == attributeName) || (y.Name is QualifiedNameSyntax qns && qns.ToString() == attributeName))); @@ -44,6 +45,11 @@ public void OnVisitSyntaxNode(SyntaxNode syntaxNode) } } } + + if (HasAttribute(cds, "AeroEncounter")) + { + AeroEncounterClasses.Add(cds); + } } if (syntaxNode is StructDeclarationSyntax sds && sds.AttributeLists.Count > 0 && HasAttribute(sds, AeroBlockAttribute.Name)) { diff --git a/Aero/Aero.Gen/Attributes/AeroEncounterAttribute.cs b/Aero/Aero.Gen/Attributes/AeroEncounterAttribute.cs new file mode 100644 index 0000000..30d19d6 --- /dev/null +++ b/Aero/Aero.Gen/Attributes/AeroEncounterAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Aero.Gen.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class AeroEncounterAttribute : Attribute + { + public static string Name = "AeroEncounter"; + + public string EncounterType; + + public AeroEncounterAttribute(string encounterType) + { + EncounterType = encounterType; + } + } +} diff --git a/Aero/Aero.Gen/GenV2.Encounters.cs b/Aero/Aero.Gen/GenV2.Encounters.cs new file mode 100644 index 0000000..3470266 --- /dev/null +++ b/Aero/Aero.Gen/GenV2.Encounters.cs @@ -0,0 +1,133 @@ +using System; +using System.Text; +using Aero.Gen.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Aero.Gen +{ + public partial class Genv2 + { + private void GenerateEncounterFunctions(ClassDeclarationSyntax cd, SemanticModel sm) + { + GenerateEncounterHeader(cd); + + GenerateViewUpdateUnpacker(cd, sm); + GenerateViewUpdatePacker(cd, sm); + GenerateClearViewChanges(cd); + GenerateGetPackedChangesSize(cd, sm); + } + + private void GenerateEncounterHeader(ClassDeclarationSyntax cd) + { + var encounterAttr = AgUtils.NodeWithName(cd, AeroEncounterAttribute.Name); + + var encounterType = encounterAttr.ArgumentList.Arguments[0].Expression.ToString().Trim('"'); + + string NameToHex(string name) + { + var bytes = Encoding.UTF8.GetBytes(name + '\0'); + + var hex = new StringBuilder(); + + foreach (var b in bytes) + { + hex.AppendFormat("0x{0:X2}, ", b); + } + + return hex.ToString().Trim(); + } + + var rootNode = AeroSourceGraphGen.BuildTree(SyntaxReceiver, cd); + byte fieldIdx = 0; + + AddLine("public static readonly byte[] Header = {"); + Indent(); + + AddLine($"{NameToHex(encounterType)} // {encounterType}"); + + AeroSourceGraphGen.WalkTree(rootNode, node => + { + if (node.Depth != 0) + { + return; + } + + var name = node.Name; + byte count = 1; + + if (node is AeroArrayNode arr) + { + if (arr.Mode != AeroArrayNode.Modes.Fixed) + { + Context.ReportDiagnostic( + Diagnostic.Create(AeroGenerator.InvalidArrayModeInEncounterView, cd.GetLocation(), name, arr.Mode) + ); + } + + name = arr.Nodes[0].Name; + count = (byte)arr.Length; + } + + var typeStr = node.TypeStr.ToLower(); + + if (node is AeroFieldNode { IsEnum: true } fieldNode) + { + typeStr = TypeAlias(fieldNode.EnumStr).ToLower(); + } + + byte byteType = typeStr switch + { + "uint" => 0, + "float" => 1, + string t when t.EndsWith("entityid") => 2, + "ulong" => 3, + "byte" => 4, + // there's no type 5 + "ushort" => 6, + string t when t.EndsWith("timer") => 7, + "bool" => 8, + + "uint[]" => 128, + "float[]" => 129, + string t when t.EndsWith("entityid[]") => 130, + "ulong[]" => 131, + "byte[]" => 132, + // there's no type 133 either + "ushort[]" => 134, + string t when t.EndsWith("timer[]") => 135, + "bool[]" => 136, + + _ => 255, + }; + + if (byteType == 255) + { + Context.ReportDiagnostic( + Diagnostic.Create(AeroGenerator.InvalidTypeInEncounterView, cd.GetLocation(), name, typeStr) + ); + } + + var hexIdx = BitConverter.ToString(new[]{ fieldIdx }); + var hexType = BitConverter.ToString(new[]{ byteType }); + var hexCount = BitConverter.ToString(new[]{ count }); + + AddLine( + $"0x{hexIdx}, 0x{hexType}, 0x{hexCount}, // idx: {fieldIdx}, type: {typeStr}, count: {count}, name: {name}"); + AddLine(NameToHex(name)); + + fieldIdx++; + }); + + UnIndent(); + AddLine("};"); // end static Header + + using (Function("public byte[] GetHeader()")) + { + AddLine("return Header;"); + } + + AddLine(); + } + } +} diff --git a/Aero/Aero.Gen/GenV2.Views.cs b/Aero/Aero.Gen/GenV2.Views.cs index ae20215..c4c3f6e 100644 --- a/Aero/Aero.Gen/GenV2.Views.cs +++ b/Aero/Aero.Gen/GenV2.Views.cs @@ -166,6 +166,7 @@ private void GenerateViewUpdateUnpacker(ClassDeclarationSyntax cd, SemanticModel AddLine(); var rootNode = AeroSourceGraphGen.BuildTree(SyntaxReceiver, cd); + var isEncounterClass = AgUtils.IsEncounterClass(cd, sm); using (DoWhile("offset < data.Length")) { AddLine("var id = data[offset++];"); @@ -187,7 +188,7 @@ private void GenerateViewUpdateUnpacker(ClassDeclarationSyntax cd, SemanticModel using (Block($"case {shadowFieldIdx}: // {node.GetFullName()}")) { CreateLogicFlow(node, CreateUnpackerPreNode, - node => { CreateUnpackerOnNode(false, node, ref nullableIdx); }); + node => { CreateUnpackerOnNode(false, node, ref nullableIdx, isEncounterClass); }); AddLine("break;"); } @@ -222,6 +223,7 @@ private void GenerateViewUpdatePacker(ClassDeclarationSyntax cd, SemanticModel s AddLine("int offset = 0;"); AddLine(); var rootNode = AeroSourceGraphGen.BuildTree(SyntaxReceiver, cd); + var isEncounterClass = AgUtils.IsEncounterClass(cd, sm); var fieldIdx = 0; AeroSourceGraphGen.WalkTree(rootNode, node => { @@ -230,7 +232,7 @@ private void GenerateViewUpdatePacker(ClassDeclarationSyntax cd, SemanticModel s using (If($"{GenerateViewFieldIdx(fieldIdx, DIRTY_FIELD_BASE_NAME)}")) { if (node.IsNullable) { using (If($"{node.GetFullName()}Prop.HasValue")) { // TODO: change to use nullable bits - CreatePacker(fieldIdx, node, true); + CreatePacker(fieldIdx, node, isEncounterClass); } using (Else()) { @@ -238,7 +240,7 @@ private void GenerateViewUpdatePacker(ClassDeclarationSyntax cd, SemanticModel s } } else { - CreatePacker(fieldIdx, node, false); + CreatePacker(fieldIdx, node, isEncounterClass); } } @@ -253,12 +255,22 @@ private void GenerateViewUpdatePacker(ClassDeclarationSyntax cd, SemanticModel s AddLine($"return offset;"); } - void CreatePacker(int fieldIdx, AeroNode node, bool noNullableCheck) + void CreatePacker(int fieldIdx, AeroNode node, bool isEncounter) { - AddLine($"buffer[offset++] = {fieldIdx};"); - CreateLogicFlow(node, - (node) => CreatePackerPreNode(node), - (node) => CreatePackerOnNode(node, false)); + // For arrays in encounters we add idx inside the for loop in PackerOnNode + if (isEncounter && node is AeroArrayNode) + { + CreateLogicFlow(node, + (node) => CreatePackerPreNode(node), + (node) => CreatePackerOnNode(node, false, true, fieldIdx)); + } + else + { + AddLine($"buffer[offset++] = {fieldIdx};"); + CreateLogicFlow(node, + (node) => CreatePackerPreNode(node), + (node) => CreatePackerOnNode(node, false)); + } } } @@ -269,6 +281,7 @@ private void GenerateGetPackedChangesSize(ClassDeclarationSyntax cd, SemanticMod AddLine("int offset = 0;"); AddLine(); var rootNode = AeroSourceGraphGen.BuildTree(SyntaxReceiver, cd); + var isEncounter = AgUtils.IsEncounterClass(cd, SyntaxReceiver.Context.Compilation.GetSemanticModel(cd.SyntaxTree)); var fieldIdx = 0; AeroSourceGraphGen.WalkTree(rootNode, node => { @@ -278,7 +291,7 @@ private void GenerateGetPackedChangesSize(ClassDeclarationSyntax cd, SemanticMod AddLine("offset++;"); CreateLogicFlow(node, preNode: GetPackedSizePreNode, - onNode: node => { GetPackedSizeOnNode(true, node); }); + onNode: node => { GetPackedSizeOnNode(true, node, isEncounter); }); } fieldIdx++; diff --git a/Aero/Aero.Gen/Genv2.cs b/Aero/Aero.Gen/Genv2.cs index 2949d40..899bfd4 100644 --- a/Aero/Aero.Gen/Genv2.cs +++ b/Aero/Aero.Gen/Genv2.cs @@ -63,6 +63,14 @@ public class AeroTypeHandler Writer = (name, typeCast) => $"buffer[offset] = {typeCast}((byte){name});", } }, + { + "bool", new AeroTypeHandler + { + Size = 1, + Reader = (name, typeCast) => $"{name} = {typeCast}(data[offset] == 1);", + Writer = (name, typeCast) => $"buffer[offset] = {typeCast}((byte)({name} ? 1 : 0));", + } + }, { "char", new AeroTypeHandler { @@ -376,14 +384,26 @@ public void EndScope(bool noTrailingNewLine = false) var fileName = $"{ns}.{cn}.Aero.cs"; var sm = SyntaxReceiver.Context.Compilation.GetSemanticModel(cd.SyntaxTree); var isViewClass = AgUtils.IsViewClass(cd, sm); + var isEncounterClass = AgUtils.IsEncounterClass(cd, sm); AddLines( $"// Aero Generated file, not a not a good idea to edit :>", $"// {DateTime.Now.ToLongDateString()} {DateTime.Now.ToLongTimeString()}"); AddUsings(); AddLine(); - using (Namespace(ns)) { - using (Class(cn, isViewClass ? " : Aero.Gen.IAeroViewInterface" : " : Aero.Gen.IAero")) { + using (Namespace(ns)) + { + var iface = " : Aero.Gen.IAero"; + if (isViewClass) + { + iface = " : Aero.Gen.IAeroViewInterface"; + + if (isEncounterClass) + { + iface = " : Aero.Gen.IAeroEncounter"; + } + } + using (Class(cn, iface)) { if (Config.DiagLogging) AddDiagBoilerplate(); #if DEBUG @@ -400,17 +420,27 @@ public void EndScope(bool noTrailingNewLine = false) AddLine(); - CreateReaderV2(cd); - AddLine(); + if (!isEncounterClass) + { + CreateReaderV2(cd); + AddLine(); - CreateGetPackedSizeV2(cd); - AddLine(); + CreateGetPackedSizeV2(cd); + AddLine(); - CreatePackerV2(cd); - AddLine(); + CreatePackerV2(cd); + AddLine(); + } if (isViewClass) { - GenerateViewFunctions(cd, sm); + if (isEncounterClass) + { + GenerateEncounterFunctions(cd, sm); + } + else + { + GenerateViewFunctions(cd, sm); + } } AddLine($"public System.Collections.Generic.List GetDiagReadLogs() => {(Config.DiagLogging ? "ReadLogs" : "null")};"); @@ -483,7 +513,7 @@ public virtual void CreateReaderV2(ClassDeclarationSyntax cd) } } - private int CreateUnpackerOnNode(bool isView, AeroNode node, ref int nullableIdx) + private int CreateUnpackerOnNode(bool isView, AeroNode node, ref int nullableIdx, bool isEncounter = false) { if (node.IsNullable) { if (isView) { @@ -503,6 +533,20 @@ private int CreateUnpackerOnNode(bool isView, AeroNode node, ref int nullableIdx //AddLine("offsetBefore = offset;"); } + if (isEncounter && node.Parent is AeroArrayNode) + { + AddLines("// Encounter arrays contain both field index and array index before values,", + "// index of the first is element is handled at the beginning of the 'do while' loop"); + using (If($"idx{node.Parent.Depth} == 0")) + { + AddLine("offset++;"); + } + using (Else()) + { + AddLine("offset += 2;"); + } + } + if (node is AeroFieldNode fieldNode) { var name = fieldNode.GetFullName(); if (node.Parent?.Parent is {IsNullable: true, IsRoot: false}) { @@ -617,7 +661,7 @@ public virtual void CreateGetPackedSizeV2(ClassDeclarationSyntax cd) } } - private void GetPackedSizeOnNode(bool isView, AeroNode node) + private void GetPackedSizeOnNode(bool isView, AeroNode node, bool isEncounter = false) { if (isView && node.IsNullable) { AddLine($"if ({node.GetFullName()}Prop.HasValue)"); // TODO: replace with bit field check instead @@ -666,8 +710,16 @@ private void GetPackedSizeOnNode(bool isView, AeroNode node) if (arrayNode.Mode == AeroArrayNode.Modes.Fixed) { // If the array always has a Fixed length, then we get the correct size directly from the array node - AddLine( - $"offset += ({prefixLen}) + ({arrayNode.GetSize()}); // array fixed {node.Name}"); + if (isEncounter) + { + AddLine("// in encounters every array element is prefixed with 2 bytes: field idx and array idx"); + AddLine("// We subtract one (field idx for first element) because its added in the line above this one"); + AddLine($"offset += ({prefixLen}) + ({arrayNode.Length * 2 - 1}) + ({arrayNode.GetSize()}); // array fixed {node.Name}"); + } + else + { + AddLine($"offset += ({prefixLen}) + ({arrayNode.GetSize()}); // array fixed {node.Name}"); + } } else { // Otherwise we gotta multiply by length @@ -719,7 +771,8 @@ public virtual void CreatePackerV2(ClassDeclarationSyntax cd) AddLine(); var isView = AgUtils.IsViewClass(cd, SyntaxReceiver.Context.Compilation.GetSemanticModel(cd.SyntaxTree)); - if (isView) { + var isEncounter = AgUtils.IsEncounterClass(cd, SyntaxReceiver.Context.Compilation.GetSemanticModel(cd.SyntaxTree)); + if (isView && !isEncounter) { AddLine("// Nullable bitfields fields"); //AddLine("UpdateNullableBitFields();"); @@ -743,7 +796,7 @@ private void CreatePackerPreNode(AeroNode node) } } - private void CreatePackerOnNode(AeroNode node, bool noNullableCheck = false) + private void CreatePackerOnNode(AeroNode node, bool noNullableCheck = false, bool isEncounter = false, int fieldIdx = -1) { if (node.IsNullable && noNullableCheck) { AddLine($"if ({node.GetFullName()}Prop.HasValue)"); // TODO: use bitfield @@ -751,6 +804,11 @@ private void CreatePackerOnNode(AeroNode node, bool noNullableCheck = false) Indent(); } + if (isEncounter && node.Parent is AeroArrayNode { Mode: AeroArrayNode.Modes.Fixed}) + { + AddLine($"buffer[offset++] = {fieldIdx};"); + AddLine($"buffer[offset++] = (byte)idx{node.Parent.Depth};"); + } if (node is AeroFieldNode fieldNode) { var name = fieldNode.IsNullable ? $"{fieldNode.GetFullName()}" : fieldNode.GetFullName(); diff --git a/Aero/Aero.Gen/IAeroEncounterInterface.cs b/Aero/Aero.Gen/IAeroEncounterInterface.cs new file mode 100644 index 0000000..05a5ade --- /dev/null +++ b/Aero/Aero.Gen/IAeroEncounterInterface.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Aero.Gen +{ + public interface IAeroEncounter + { + // Gets static encounter header created from indexes, types, element count + // and names for each encounter view field + public byte[] GetHeader(); + + // Get read logs, has the field name, offset and length + // should only have data in debug builds + public List GetDiagReadLogs(); + + // Clear the read log list + public void ClearReadLogs(); + + // Unpacks a view update to this class, returns how many bytes were read + public int UnpackChanges(ReadOnlySpan data); + + // Gets the number of bytes needed to pack all the changes. + public int GetPackedChangesSize(); + + // Packs what has changed into the buffer and returns the number of bytes written + // ClearViewChanges should be called after you pack the if clearDirtyAfterSend wasn't set, this reset the change field tracking + public int PackChanges(Span buffer, bool clearDirtyAfterSend = true); + + // Will clear the internal data tracking if a field has been changed + public void ClearViewChanges(); + } +} \ No newline at end of file diff --git a/Aero/Aero.Gen/Properties/launchSettings.json b/Aero/Aero.Gen/Properties/launchSettings.json new file mode 100644 index 0000000..1984bf7 --- /dev/null +++ b/Aero/Aero.Gen/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Generators": { + "commandName": "DebugRoslynComponent", + "targetProject": "../Aero.UnitTests/Aero.UnitTests.csproj" + } + } +} diff --git a/Aero/Aero.UnitTests/EncounterTests.cs b/Aero/Aero.UnitTests/EncounterTests.cs new file mode 100644 index 0000000..fe01378 --- /dev/null +++ b/Aero/Aero.UnitTests/EncounterTests.cs @@ -0,0 +1,331 @@ +using System; +using System.Linq; +using Aero.Gen; +using Aero.Gen.Attributes; +using NUnit.Framework; + +namespace Aero.UnitTests +{ + [Aero(AeroGenTypes.View)] + [AeroEncounter("arc")] + public partial class ArcView + { + private uint arc_name; + + private uint activity_string; + + private ushort activity_visible; + + [AeroNullable] + private EntityId healthbar_1; + + private ushort healthbar_1_visible; + } + + [Aero(AeroGenTypes.View)] + [AeroEncounter("AirTrafficControl")] + public partial class AirTrafficControlView + { + // has no fields + } + + [Aero(AeroGenTypes.View)] + [AeroEncounter("MoreTypes")] + public partial class MoreTypesView + { + private uint name; + + [AeroArray(2)] + private uint[] localizedTexts; + + private ulong eta; + + [AeroNullable] + private EntityId healthbar_1; + + [AeroArray(3)] + private bool[] booleans; + } + + [Aero(AeroGenTypes.View)] + [AeroEncounter("default")] + public partial class HudTimerView + { + private Timer hudtimer_timer; + + private uint hudtimer_label; + } + + [AeroBlock] + public struct EntityId + { + public ulong Backing; + } + + [Flags] + public enum TimerState : byte + { + CountingDown = 1, + Paused = 2, + } + + [AeroBlock] + public struct Timer + { + public TimerState State; + + [AeroIf(nameof(State), AeroIfAttribute.Ops.DoesntHaveFlag, TimerState.Paused)] + public ulong Micro; + + [AeroIf(nameof(State), AeroIfAttribute.Ops.HasFlag, TimerState.Paused)] + public float Seconds; + } + + public class EncounterTests + { + [SetUp] + public void Setup() + { + } + + [Test] + public void ArcHeaderTest() + { + var arc = new ArcView(); + + var expectedHeader = new byte[] + { + 0x61, 0x72, 0x63, 0x00, + 0x00, 0x00, 0x01, + 0x61, 0x72, 0x63, 0x5F, 0x6E, 0x61, 0x6D, 0x65, 0x00, + 0x01, 0x00, 0x01, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x5F, 0x73, 0x74, 0x72, 0x69, 0x6E, 0x67, 0x00, + 0x02, 0x06, 0x01, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x69, 0x74, 0x79, 0x5F, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6C, 0x65, 0x00, + 0x03, 0x02, 0x01, + 0x68, 0x65, 0x61, 0x6C, 0x74, 0x68, 0x62, 0x61, 0x72, 0x5F, 0x31, 0x00, + 0x04, 0x06, 0x01, + 0x68, 0x65, 0x61, 0x6C, 0x74, 0x68, 0x62, 0x61, 0x72, 0x5F, 0x31, 0x5F, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6C, 0x65, 0x00, + }; + + if (expectedHeader.SequenceEqual(arc.GetHeader())) + { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public void AirTrafficControlHeaderTest() + { + var atc = new AirTrafficControlView(); + + var expectedHeader = new byte[] + { + 0x41, 0x69, 0x72, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x43, 0x6F, 0x6E, 0x74, 0x72, 0x6F, 0x6C, 0x00, + }; + + if (expectedHeader.SequenceEqual(atc.GetHeader())) + { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public void HudTimerPackChangesTest() + { + var hudTimer = new HudTimerView + { + hudtimer_timerProp = new Timer() {State = TimerState.Paused, Seconds = 15}, + hudtimer_labelProp = 10082, + }; + + var bytes = new byte[] + { + 0x00, + 0x02, 0x00, 0x00, 0x70, 0x41, + 0x01, + 0x62, 0x27, 0x00, 0x00 + }; + var packedBuffer = new byte[bytes.Length]; + var lenPacked = hudTimer.PackChanges(packedBuffer); + + if (lenPacked == packedBuffer.Length && bytes.SequenceEqual(packedBuffer)) { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public void MoreTypesPackChangesTest() + { + var encounter = new MoreTypesView + { + nameProp = 10093, + booleansProp = new bool[] { true, false, true }, + }; + + var bytes = new byte[] + { + 0x00, + 0x6D, 0x27, 0x00, 0x00, + + 0x04, 0x00, + 0x01, + 0x04, 0x01, + 0x00, + 0x04, 0x02, + 0x01, + }; + var packedBuffer = new byte[bytes.Length]; + var lenPacked = encounter.PackChanges(packedBuffer); + + if (lenPacked == packedBuffer.Length && bytes.SequenceEqual(packedBuffer)) { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public void MoreTypesPackChangesWithNullableNullTest() + { + var encounter = new MoreTypesView + { + nameProp = 10329, + healthbar_1Prop = null, + }; + + var bytes = new byte[] + { + 0x00, + 0x59, 0x28, 0x00, 0x00, + + 0x83, + }; + var packedBuffer = new byte[bytes.Length]; + var lenPacked = encounter.PackChanges(packedBuffer); + + if (lenPacked == packedBuffer.Length && bytes.SequenceEqual(packedBuffer)) { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public void HudTimerUnpackChangesTest() + { + var hudTimer = new HudTimerView + { + hudtimer_timerProp = new Timer() {State = TimerState.Paused, Seconds = 15}, + hudtimer_labelProp = 10329, + }; + + var bytes = new byte[] + { + 0x00, + 0x01, 0x40, 0x79, 0x39, 0xB3, 0x81, 0x18, 0x06, 0x00, + 0x01, + 0x62, 0x27, 0x00, 0x00 + }; + var lenUnpacked = hudTimer.UnpackChanges(bytes); + + if (lenUnpacked == bytes.Length + && hudTimer.hudtimer_labelProp == 10082 + && hudTimer.hudtimer_timerProp.State == TimerState.CountingDown + && hudTimer.hudtimer_timerProp.Micro == 1715795197000000) { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public void MoreTypesUnpackChangesTest() + { + var encounter = new MoreTypesView + { + nameProp = 10082, + booleansProp = new bool[] { true, false, true }, + }; + + var bytes = new byte[] + { + 0x00, + 0x6D, 0x27, 0x00, 0x00, + + 0x04, 0x00, + 0x00, + 0x04, 0x01, + 0x01, + 0x04, 0x02, + 0x00, + }; + var lenUnpacked = encounter.UnpackChanges(bytes); + + if (lenUnpacked == bytes.Length + && encounter.nameProp == 10093 + && encounter.booleansProp.SequenceEqual(new bool[] { false, true, false }) + ) { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public void MoreTypesUnpackChangesWithNullableNullTest() + { + var encounter = new MoreTypesView + { + localizedTextsProp = new uint[] { 10342, 10343 }, + healthbar_1Prop = new EntityId() { Backing = 2305005520655996928 }, + }; + + var bytes = new byte[] + { + 0x01, 0x00, + 0x69, 0x28, 0x00, 0x00, + 0x01, 0x01, + 0x6A, 0x28, 0x00, 0x00, + + 0x83, + }; + var lenUnpacked = encounter.UnpackChanges(bytes); + + if (lenUnpacked == bytes.Length + && encounter.localizedTextsProp.SequenceEqual(new uint[] { 10345, 10346}) + && encounter.healthbar_1Prop == null) { + Assert.Pass(); + } + + Assert.Fail(); + } + + [Test] + public void MoreTypesGetPackedChangesSizeTest() + { + var encounter = new MoreTypesView + { + nameProp = 10093, + localizedTextsProp = new uint[] { 10142, 10227 }, + etaProp = 1715795197000000, + healthbar_1Prop = new EntityId() { Backing = 2305005520655996928 }, + booleansProp = new bool[] { false, false, true }, + }; + + var lenPacked = encounter.GetPackedChangesSize(); + + if (lenPacked == 44) { + Assert.Pass(); + } + + Assert.Fail(); + } + } +} \ No newline at end of file