diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d7a0cfa4..b22a8e315 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,10 @@ jobs: matrix: os: [windows-latest, ubuntu-latest] configuration: [Debug, Release] + include: + - os: ubuntu-latest + configuration: Release + coverage: true runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v5 @@ -26,4 +30,17 @@ jobs: run: dotnet build Source.NET.slnx -c ${{ matrix.configuration }} - name: Test - run: dotnet test --project Source.Common.Tests/Source.Common.Tests.csproj -c ${{ matrix.configuration }} --no-build + run: dotnet test --project Source.Common.Tests/Source.Common.Tests.csproj -c ${{ matrix.configuration }} --no-build ${{ matrix.coverage && '--coverage --coverage-output-format cobertura --coverage-output coverage.cobertura.xml --coverage-settings Source.Common.Tests/coverage.settings.xml --results-directory TestResults' || '' }} + + - name: Coverage report + if: matrix.coverage + uses: danielpalme/ReportGenerator-GitHub-Action@5 + with: + reports: TestResults/coverage.cobertura.xml + targetdir: CoverageReport + reporttypes: MarkdownSummaryGithub + + - name: Coverage summary + if: matrix.coverage + shell: bash + run: cat CoverageReport/SummaryGithub.md >> "$GITHUB_STEP_SUMMARY" diff --git a/Game.Server/BaseEntity.cs b/Game.Server/BaseEntity.cs index 60dcd5b9a..74ac6981f 100644 --- a/Game.Server/BaseEntity.cs +++ b/Game.Server/BaseEntity.cs @@ -2702,6 +2702,7 @@ public void SetModelName(ReadOnlySpan modelName) { public bool DynamicModelSetBounds; public bool DynamicModelPending; + static readonly IFieldAccessor ModelIndexField = FIELD.OF(nameof(ModelIndex)); public void SetModelIndex(int index) { if (IVModelInfo.IsDynamicModelIndex(index) && !(GetBaseAnimating() != null && DynamicModelAllowed)) { AssertMsg(false, "dynamic model support not enabled on server entity"); @@ -2717,6 +2718,7 @@ public void SetModelIndex(int index) { modelinfo.ReleaseDynamicModel(ModelIndex); modelinfo.AddRefDynamicModel(index); ModelIndex = index; + NetworkStateChanged(ModelIndexField); DynamicModelSetBounds = false; diff --git a/Source.CodeAnalysis.NetworkVars/NetworkVarGenerator.FieldAccessors.cs b/Source.CodeAnalysis.NetworkVars/NetworkVarGenerator.FieldAccessors.cs index af357b1f0..0721990fd 100644 --- a/Source.CodeAnalysis.NetworkVars/NetworkVarGenerator.FieldAccessors.cs +++ b/Source.CodeAnalysis.NetworkVars/NetworkVarGenerator.FieldAccessors.cs @@ -139,6 +139,7 @@ private static bool IsInvocationNamed(SyntaxNode node, HashSet names) { .Append(Literal(accessorName)).Append(", ").Append(Literal(networkName)).AppendLine(") { }"); body.Append("\t\t\tpublic static ref ").Append(field).Append(" RefOf(object o) => ref ").Append(path.Expression).AppendLine(";"); body.Append("\t\t\tpublic override ref ").Append(field).AppendLine(" Ref(object o) => ref RefOf(o);"); + body.Append("\t\t\tpublic override string FieldKey => ").Append(Literal(FieldKey("S", path, owner, expression))).AppendLine(";"); foreach (string accessor in path.Accessors) body.Append("\t\t\t").AppendLine(accessor); body.AppendLine("\t\t}"); @@ -191,6 +192,7 @@ private static bool IsInvocationNamed(SyntaxNode node, HashSet names) { .Append(Literal(expression)).Append(", ").Append(Literal(networkName)).Append(", ").Append(Literal(elementFormat)).Append(", ") .Append(listFlag ? "listMax" : length.ToString(CultureInfo.InvariantCulture)).Append(", ").Append(listFlag ? "true" : "false").AppendLine(") { }"); body.Append("\t\t\tpublic override ref ").Append(storing).Append(" Ref(object o) => ref ").Append(path.Expression).AppendLine(";"); + body.Append("\t\t\tpublic override string FieldKey => ").Append(Literal(FieldKey("A", path, owner, expression))).AppendLine(";"); body.Append("\t\t\tpublic override ref ").Append(elementDisplay).Append(" ElementRef(object o, int index) => ref ") .Append(IndexExpression(kind, path.Type, element, path.Expression, "index")).AppendLine(";"); foreach (string accessor in path.Accessors) @@ -287,6 +289,8 @@ private static bool IsInvocationNamed(SyntaxNode node, HashSet names) { } string? networkName = GetNetworkNameAttribute(member); + if (path.RootDeclaring == null) + path.RootDeclaring = declaring; path.Parts.Add(NamePart.Member(networkName ?? member.Name, networkName != null)); path.LastMember = member; return memberType; @@ -519,6 +523,9 @@ private static bool HasAttribute(ISymbol symbol, string attributeName) private static string Display(ITypeSymbol type) => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + private static string FieldKey(string kind, ResolvedPath path, INamedTypeSymbol owner, string expression) + => kind + "|" + Display(path.RootDeclaring ?? owner) + "|" + expression; + private static string Literal(string value) => SymbolDisplay.FormatLiteral(value, true); private static string Identifier(string name) => SyntaxFacts.GetKeywordKind(name) != SyntaxKind.None ? "@" + name : name; @@ -624,6 +631,7 @@ private sealed class ResolvedPath public string Expression = ""; public ITypeSymbol Type = null!; public ISymbol? LastMember; + public INamedTypeSymbol? RootDeclaring; public readonly List Parts = new List(); public readonly List Accessors = new List(); } diff --git a/Source.CodeAnalysis.NetworkVars/NetworkVarGenerator.cs b/Source.CodeAnalysis.NetworkVars/NetworkVarGenerator.cs index 927391481..04e56beb2 100644 --- a/Source.CodeAnalysis.NetworkVars/NetworkVarGenerator.cs +++ b/Source.CodeAnalysis.NetworkVars/NetworkVarGenerator.cs @@ -75,12 +75,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { List conversions = new List(); AddConversions(ctx.SemanticModel.Compilation, prop.Type, conversions); + string? inlineArrayElement = GetContainer(ctx.SemanticModel.Compilation, prop.Type, out ITypeSymbol? element, out _) == ContainerKind.InlineArray && element != null + ? element.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + : null; + return new PropertyModel( propertyName: prop.Name, networkName: GetNetworkNameAttribute(prop) ?? prop.Name, accessorBase: ScalarAccessorBase(ctx.SemanticModel.Compilation, prop.Type), conversions: string.Join("\n", conversions), typeDisplay: prop.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + inlineArrayElement: inlineArrayElement, @namespace: containingType.ContainingNamespace.IsGlobalNamespace ? null : containingType.ContainingNamespace.ToDisplayString(), @@ -153,7 +158,13 @@ private static string BuildSource(PropertyModel type, List props) sb.Append(indent).Append("public partial ").Append(t).Append(' ').Append(name).AppendLine(" {"); sb.Append(indent).Append("\tget => ").Append(backing).AppendLine(";"); sb.Append(indent).AppendLine("\tset {"); - sb.Append(indent).Append("\t\tif (!global::System.Collections.Generic.EqualityComparer<").Append(t).Append(">.Default.Equals(").Append(backing).AppendLine(", value)) {"); + if (p.InlineArrayElement == null) + sb.Append(indent).Append("\t\tif (!global::System.Collections.Generic.EqualityComparer<").Append(t).Append(">.Default.Equals(").Append(backing).AppendLine(", value)) {"); + else { + string e = p.InlineArrayElement; + sb.Append(indent).Append("\t\tif (!global::System.MemoryExtensions.SequenceEqual((global::System.ReadOnlySpan<").Append(e).Append(">)").Append(backing) + .Append(", (global::System.ReadOnlySpan<").Append(e).Append(">)value, global::System.Collections.Generic.EqualityComparer<").Append(e).AppendLine(">.Default)) {"); + } sb.Append(indent).Append("\t\t\t").Append(backing).AppendLine(" = value;"); sb.Append(indent).Append("\t\t\tNetworkStateChanged(NetworkVarFields.").Append(name).AppendLine(");"); sb.Append(indent).AppendLine("\t\t}"); @@ -178,6 +189,7 @@ private static string BuildSource(PropertyModel type, List props) sb.Append(indent).Append("\t\tpublic __").Append(p.PropertyName).Append("() : base(typeof(").Append(type.TypeFullyQualified).Append("), ") .Append(Literal(p.PropertyName)).Append(", ").Append(Literal(p.NetworkName)).AppendLine(") { }"); sb.Append(indent).Append("\t\tpublic override ref ").Append(p.TypeDisplay).Append(" Ref(object o) => ref ((").Append(type.TypeFullyQualified).Append(")o).__nv_").Append(p.PropertyName).AppendLine(";"); + sb.Append(indent).Append("\t\tpublic override string FieldKey => ").Append(Literal("S|" + type.TypeFullyQualified + "|" + p.PropertyName)).AppendLine(";"); sb.Append(indent).AppendLine("\t}"); } sb.Append(indent).AppendLine("}"); @@ -208,13 +220,14 @@ internal sealed class NetworkVarAttribute : global::System.Attribute private sealed class PropertyModel { - public PropertyModel(string propertyName, string networkName, string accessorBase, string conversions, string typeDisplay, string? @namespace, string typeName, + public PropertyModel(string propertyName, string networkName, string accessorBase, string conversions, string typeDisplay, string? inlineArrayElement, string? @namespace, string typeName, string typeFullyQualified, bool propertyIsPartial, bool typeIsPartial, bool unsupported, Location location) { PropertyName = propertyName; NetworkName = networkName; AccessorBase = accessorBase; Conversions = conversions; TypeDisplay = typeDisplay; + InlineArrayElement = inlineArrayElement; Namespace = @namespace; TypeName = typeName; TypeFullyQualified = typeFullyQualified; @@ -229,6 +242,7 @@ public PropertyModel(string propertyName, string networkName, string accessorBas public string AccessorBase { get; } public string Conversions { get; } public string TypeDisplay { get; } + public string? InlineArrayElement { get; } public string? Namespace { get; } public string TypeName { get; } public string TypeFullyQualified { get; } diff --git a/Source.Common.Tests/EdictChangeInfoTests.cs b/Source.Common.Tests/EdictChangeInfoTests.cs new file mode 100644 index 000000000..c8f2d5bf3 --- /dev/null +++ b/Source.Common.Tests/EdictChangeInfoTests.cs @@ -0,0 +1,149 @@ +using Source.Common.Engine; + +namespace Source.Common.Tests; + +public class EdictChangeInfoTests +{ + sealed class ChangeScope : IDisposable + { + readonly SharedEdictChangeInfo? oldShared = EdictGlobals.g_SharedChangeInfo; + readonly Func? oldGetter = BaseEdict.GetChangeAccessor; + + public readonly SharedEdictChangeInfo Shared = new(); + public readonly IChangeInfoAccessor Accessor = new(); + public readonly BaseEdict Edict = new(); + + public ChangeScope(bool withShared = true, bool withAccessor = true) { + EdictGlobals.g_SharedChangeInfo = withShared ? Shared : null; + BaseEdict.GetChangeAccessor = withAccessor ? _ => Accessor : _ => null!; + } + + public EdictChangeInfo Info => Shared.ChangeInfos[Accessor.GetChangeInfo()]; + + public void Record(params IFieldAccessor[] fields) { + foreach (IFieldAccessor field in fields) + Edict.StateChanged(field); + } + + public void Dispose() { + EdictGlobals.g_SharedChangeInfo = oldShared; + BaseEdict.GetChangeAccessor = oldGetter; + } + } + + static IFieldAccessor Element(int index) => FIELD.OF_LIST(nameof(TestFieldsBase.Items), 32).AtIndex(index)!; + + [Fact] + public void SameFieldThroughDifferentAccessorsIsRecordedOnce() { + using ChangeScope scope = new(); + scope.Record( + FIELD.OF(nameof(TestFieldsBase.Value)), + FIELD.OF(nameof(TestFieldsBase.Value))); + + Assert.Equal(1, scope.Info.NumChangeFields); + } + + [Fact] + public void DifferentFieldsAreRecordedSeparately() { + using ChangeScope scope = new(); + scope.Record( + FIELD.OF(nameof(TestFieldsBase.Value)), + FIELD.OF(nameof(TestFieldsBase.Other)), + FIELD.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), 0), + FIELD.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), 1)); + + Assert.Equal(4, scope.Info.NumChangeFields); + } + + [Fact] + public void FirstChangeClaimsChangeInfo() { + using ChangeScope scope = new(); + scope.Record(FIELD.OF(nameof(TestFieldsBase.Value))); + + Assert.Equal(1, scope.Shared.NumChangeInfos); + Assert.Equal(scope.Shared.SerialNumber, scope.Accessor.GetChangeInfoSerialNumber()); + Assert.True(scope.Edict.HasStateChanged()); + Assert.Equal(EdictFlags.EdictChanged, scope.Edict.StateFlags); + } + + [Fact] + public void FullChangeIgnoresFieldChanges() { + using ChangeScope scope = new(); + scope.Edict.StateChanged(); + scope.Record(FIELD.OF(nameof(TestFieldsBase.Value))); + + Assert.Equal(0, scope.Shared.NumChangeInfos); + Assert.True((scope.Edict.StateFlags & EdictFlags.FullEdictChanged) != 0); + Assert.Equal(0, scope.Accessor.GetChangeInfoSerialNumber()); + } + + [Fact] + public void WithoutSharedChangeInfoOnlyFlagsEdict() { + using ChangeScope scope = new(withShared: false); + scope.Record(FIELD.OF(nameof(TestFieldsBase.Value))); + + Assert.True(scope.Edict.HasStateChanged()); + Assert.Equal(0, scope.Shared.NumChangeInfos); + } + + [Fact] + public void WithoutChangeAccessorOnlyFlagsEdict() { + using ChangeScope scope = new(withAccessor: false); + scope.Record(FIELD.OF(nameof(TestFieldsBase.Value))); + + Assert.True(scope.Edict.HasStateChanged()); + Assert.Equal(0, scope.Shared.NumChangeInfos); + } + + [Fact] + public void TooManyChangedFieldsBecomesFullChange() { + using ChangeScope scope = new(); + for (int i = 0; i < BaseEdict.MAX_CHANGE_OFFSETS; i++) + scope.Record(Element(i)); + + Assert.Equal(BaseEdict.MAX_CHANGE_OFFSETS, scope.Info.NumChangeFields); + Assert.True((scope.Edict.StateFlags & EdictFlags.FullEdictChanged) == 0); + + scope.Record(Element(BaseEdict.MAX_CHANGE_OFFSETS)); + + Assert.True((scope.Edict.StateFlags & EdictFlags.FullEdictChanged) != 0); + Assert.Equal(0, scope.Accessor.GetChangeInfoSerialNumber()); + } + + [Fact] + public void NoFreeChangeInfoBecomesFullChange() { + using ChangeScope scope = new(); + scope.Shared.NumChangeInfos = BaseEdict.MAX_EDICT_CHANGE_INFOS; + + scope.Record(FIELD.OF(nameof(TestFieldsBase.Value))); + + Assert.True((scope.Edict.StateFlags & EdictFlags.FullEdictChanged) != 0); + Assert.Equal(0, scope.Accessor.GetChangeInfoSerialNumber()); + Assert.Equal(BaseEdict.MAX_EDICT_CHANGE_INFOS, scope.Shared.NumChangeInfos); + } + + [Fact] + public void NewSerialStartsFreshChangeInfo() { + using ChangeScope scope = new(); + scope.Record(FIELD.OF(nameof(TestFieldsBase.Value))); + + scope.Shared.SerialNumber++; + scope.Shared.NumChangeInfos = 0; + scope.Record(FIELD.OF(nameof(TestFieldsBase.Other))); + + Assert.Equal(1, scope.Info.NumChangeFields); + Assert.Equal(FIELD.OF(nameof(TestFieldsBase.Other)), scope.Info.ChangedFields[0]); + Assert.Equal(scope.Shared.SerialNumber, scope.Accessor.GetChangeInfoSerialNumber()); + } + + [Fact] + public void ClearStateChangedResetsFlags() { + using ChangeScope scope = new(); + scope.Edict.StateChanged(); + scope.Edict.ClearStateChanged(); + + Assert.False(scope.Edict.HasStateChanged()); + Assert.True((scope.Edict.StateFlags & EdictFlags.FullEdictChanged) == 0); + Assert.Equal(0, scope.Accessor.GetChangeInfoSerialNumber()); + } +} diff --git a/Source.Common.Tests/FieldAccessorTests.cs b/Source.Common.Tests/FieldAccessorTests.cs new file mode 100644 index 000000000..8320d6845 --- /dev/null +++ b/Source.Common.Tests/FieldAccessorTests.cs @@ -0,0 +1,875 @@ +using Source.Common.Mathematics; + +using System.Numerics; +using System.Runtime.CompilerServices; + +namespace Source.Common.Tests; + +[InlineArray(4)] +public struct TestFieldsInt4 +{ + int first; +} + +[InlineArray(8)] +public struct TestFieldsChar8 +{ + char first; +} + +public struct TestFieldsInner +{ + public int Value; + public float Scale; +} + +public struct TestFieldsWrapped +{ + public int Raw; + + public static implicit operator int(TestFieldsWrapped wrapped) => wrapped.Raw; + public static implicit operator TestFieldsWrapped(int raw) => new() { Raw = raw }; +} + +public partial class TestFieldsBase +{ + public int Value; + public int Other; + public TestFieldsInt4 Values; + [NetworkName("m_flRenamed")] + public float Renamed; + public TestFieldsInner Inner; + public Vector3 Position; + public QAngle Angles; + [NetworkArraySize(3)] + public NetworkArray Counts = new(3); + public List Items = []; + public TestFieldsChar8 Text; + public bool Flag; + public double Precise; + public readonly int ReadOnlyValue = 4; + int hidden = 2; + public TestFieldsWrapped Wrapped; + public string? Label; + public char Letter; + public byte Tiny; + public nint Native; + + public int Hidden => hidden; + + [NetworkVar] public partial int Tracked { get; set; } + [NetworkVar] public partial TestFieldsInt4 TrackedValues { get; set; } + [NetworkVar] public partial Vector3 TrackedPosition { get; set; } + + public readonly List Changes = []; + void NetworkStateChanged(IFieldAccessor field) => Changes.Add(field); +} + +public class TestFieldsDerived : TestFieldsBase +{ + public int DerivedValue; +} + +public sealed class TestKeylessAccessor() : FieldAccessor(typeof(TestFieldsBase), nameof(TestFieldsBase.Value), nameof(TestFieldsBase.Value)) +{ + public override ref int Ref(object instance) => ref ((TestFieldsBase)instance).Value; +} + +public class FieldAccessorTests +{ + [Fact] + public void ScalarGetAndSet() { + TestFieldsBase obj = new() { Value = 5 }; + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.Equal(5, field.GetValue(obj)); + Assert.True(field.SetValue(obj, 9)); + Assert.Equal(9, obj.Value); + } + + [Fact] + public void ScalarConvertsBetweenNumericTypes() { + TestFieldsBase obj = new() { Value = 7, Renamed = 2.5f, Precise = 1.25 }; + + Assert.Equal(7.0f, FIELD.OF(nameof(TestFieldsBase.Value)).GetValue(obj)); + Assert.Equal(7L, FIELD.OF(nameof(TestFieldsBase.Value)).GetValue(obj)); + Assert.Equal(2.5, FIELD.OF(nameof(TestFieldsBase.Renamed)).GetValue(obj)); + Assert.Equal(1.25f, FIELD.OF(nameof(TestFieldsBase.Precise)).GetValue(obj)); + + FIELD.OF(nameof(TestFieldsBase.Value)).SetValue(obj, 2.9f); + Assert.Equal(2, obj.Value); + + FIELD.OF(nameof(TestFieldsBase.Renamed)).SetValue(obj, 3); + Assert.Equal(3.0f, obj.Renamed); + } + + [Fact] + public void BoolConvertsToAndFromIntegers() { + TestFieldsBase obj = new(); + IFieldAccessor flag = FIELD.OF(nameof(TestFieldsBase.Flag)); + + flag.SetValue(obj, 1); + Assert.True(obj.Flag); + Assert.Equal(1, flag.GetValue(obj)); + + flag.SetValue(obj, 0); + Assert.False(obj.Flag); + Assert.Equal(0, flag.GetValue(obj)); + } + + [Fact] + public void ScalarMetadata() { + DynamicAccessor field = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.Equal(nameof(TestFieldsBase.Value), field.Name); + Assert.Equal(nameof(TestFieldsBase.Value), field.NetworkName); + Assert.Equal(typeof(TestFieldsBase), ((IFieldAccessor)field).DeclaringType); + Assert.Equal(typeof(int), ((IFieldAccessor)field).FieldType); + Assert.Equal(1, field.Length); + Assert.Equal(-1, field.Index); + } + + [Fact] + public void DeclaringTypeIsTheAccessorOwner() { + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.Equal(typeof(TestFieldsDerived), field.DeclaringType); + } + + [Fact] + public void NetworkNameComesFromAttribute() { + DynamicAccessor field = FIELD.OF(nameof(TestFieldsBase.Renamed)); + + Assert.Equal(nameof(TestFieldsBase.Renamed), field.Name); + Assert.Equal("m_flRenamed", field.NetworkName); + } + + [Fact] + public void NamedAccessorUsesGivenName() { + DynamicAccessor field = FIELD.OF_NAMED(nameof(TestFieldsBase.Value), "m_iCustom"); + TestFieldsBase obj = new() { Value = 3 }; + + Assert.Equal("m_iCustom", field.Name); + Assert.Equal("m_iCustom", field.NetworkName); + Assert.Equal(3, field.GetValue(obj)); + } + + [Fact] + public void DerivedAccessorReadsBaseField() { + TestFieldsDerived obj = new() { Value = 11, DerivedValue = 12 }; + + Assert.Equal(11, FIELD.OF(nameof(TestFieldsBase.Value)).GetValue(obj)); + Assert.Equal(12, FIELD.OF(nameof(TestFieldsDerived.DerivedValue)).GetValue(obj)); + Assert.Equal(11, FIELD.OF(nameof(TestFieldsBase.Value)).GetValue(obj)); + } + + [Fact] + public void NestedPathReadsAndWritesInPlace() { + TestFieldsBase obj = new(); + obj.Inner.Value = 4; + DynamicAccessor field = FIELD.OF("Inner.Value"); + + Assert.Equal("Inner.Value", field.Name); + Assert.Equal(4, field.GetValue(obj)); + + field.SetValue(obj, 8); + Assert.Equal(8, obj.Inner.Value); + } + + [Fact] + public void ScalarCopyToAndCopyFrom() { + TestFieldsBase obj = new() { Value = 21 }; + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.Value)); + + Span target = stackalloc int[1]; + field.CopyTo(obj, target); + Assert.Equal(21, target[0]); + + field.CopyTo(obj, Span.Empty); + + Span source = [33]; + field.CopyFrom(obj, source); + Assert.Equal(33, obj.Value); + } + + [Fact] + public void TryGetSpanAliasesValueTypeField() { + TestFieldsBase obj = new() { Value = 1 }; + DynamicAccessor field = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.True(field.TryGetSpan(obj, 1, out Span span)); + Assert.Equal(1, span.Length); + span[0] = 6; + Assert.Equal(6, obj.Value); + } + + [Fact] + public void TryGetSpanReinterpretsVector() { + TestFieldsBase obj = new() { Position = new(1, 2, 3) }; + DynamicAccessor field = FIELD.OF(nameof(TestFieldsBase.Position)); + + Assert.True(field.TryGetSpan(obj, 3, out Span span)); + Assert.Equal([1f, 2f, 3f], span.ToArray()); + } + + [Fact] + public void TryGetSpanRejectsSizeMismatchAndReferenceTypes() { + TestFieldsBase obj = new(); + + Assert.False(FIELD.OF(nameof(TestFieldsBase.Value)).TryGetSpan(obj, 1, out Span _)); + Assert.False(FIELD.OF(nameof(TestFieldsBase.Items)).TryGetSpan(obj, 1, out Span _)); + } + + [Fact] + public void InlineArrayElementsGetAndSet() { + TestFieldsBase obj = new(); + obj.Values[2] = 5; + DynamicArrayAccessor array = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)); + + Assert.Equal(4, array.Length); + Assert.Equal(typeof(int), array.Info.ContainedType); + Assert.False(array.Info.IsList); + Assert.Equal(5, array.AtIndex(2)!.GetValue(obj)); + Assert.Equal(5, array.GetElement(obj, 2)); + + array.AtIndex(1)!.SetValue(obj, 7); + array.SetElement(obj, 3, 9); + Assert.Equal(7, obj.Values[1]); + Assert.Equal(9, obj.Values[3]); + } + + [Fact] + public void ArrayAtIndexIsBoundedAndCached() { + DynamicArrayAccessor array = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)); + + Assert.Null(array.AtIndex(-1)); + Assert.Null(array.AtIndex(4)); + Assert.Same(array.AtIndex(2), array.AtIndex(2)); + } + + [Fact] + public void ArrayElementMetadata() { + DynamicArrayAccessor array = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)); + DynamicAccessor element = array.AtIndex(2)!; + + Assert.Equal(2, element.Index); + Assert.Equal("Values[2]", element.Name); + Assert.Equal("Values[2]", element.NetworkName); + Assert.Equal("Values[2]", array.ElementNetworkName(2)); + Assert.Equal(typeof(int), ((IFieldAccessor)element).FieldType); + } + + [Fact] + public void ArrayIndexAccessorMatchesAtIndex() { + TestFieldsBase obj = new(); + obj.Values[3] = 13; + + DynamicArrayIndexAccessor element = FIELD.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), 3); + + Assert.Equal(13, element.GetValue(obj)); + Assert.False(element.IsAVectorElement); + } + + [Fact] + public void SendInfoArrayUsesArrayNetworkName() { + DynamicArrayAccessor array = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)); + DynamicArrayIndexAccessor element = FIELD.OF_SENDINFO_ARRAY(nameof(TestFieldsBase.Values)); + + Assert.Equal(0, element.Index); + Assert.Equal(array.NetworkName, element.NetworkName); + } + + [Fact] + public void ArrayAccessorCopiesAllElements() { + TestFieldsBase obj = new(); + IFieldAccessor array = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)); + + array.CopyFrom(obj, [1, 2, 3, 4]); + Assert.Equal([1, 2, 3, 4], ((ReadOnlySpan)obj.Values).ToArray()); + + Span target = stackalloc int[4]; + array.CopyTo(obj, target); + Assert.Equal([1, 2, 3, 4], target.ToArray()); + } + + [Fact] + public void InlineArrayScalarCopyFromClearsTail() { + TestFieldsBase obj = new(); + obj.Values[2] = 50; + obj.Values[3] = 60; + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.Values)); + + field.CopyFrom(obj, [8, 9]); + Assert.Equal([8, 9, 0, 0], ((ReadOnlySpan)obj.Values).ToArray()); + + Span target = stackalloc int[4]; + field.CopyTo(obj, target); + Assert.Equal([8, 9, 0, 0], target.ToArray()); + } + + [Fact] + public void CharInlineArrayConvertsToString() { + TestFieldsBase obj = new(); + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.Text)); + + field.CopyFrom(obj, "abc".ToCharArray().AsSpan()); + Assert.Equal("abc", field.GetValue(obj)); + } + + [Fact] + public void VectorElementsGetAndSet() { + TestFieldsBase obj = new() { Position = new(1, 2, 3) }; + DynamicArrayIndexAccessor y = FIELD.OF_VECTORELEM(nameof(TestFieldsBase.Position), 1); + + Assert.True(y.IsAVectorElement); + Assert.Equal(2f, y.GetValue(obj)); + + y.SetValue(obj, 7f); + Assert.Equal(new Vector3(1, 7, 3), obj.Position); + Assert.Equal(3, FIELD.OF_ARRAY(nameof(TestFieldsBase.Position)).Length); + } + + [Fact] + public void AngleElementsGetAndSet() { + TestFieldsBase obj = new() { Angles = new(10, 20, 30) }; + DynamicArrayAccessor angles = FIELD.OF_ARRAY(nameof(TestFieldsBase.Angles)); + + Assert.Equal(3, angles.Length); + Assert.Equal(20f, angles.GetElement(obj, 1)); + + angles.SetElement(obj, 2, 45f); + Assert.Equal(45f, obj.Angles.Z); + } + + [Fact] + public void NetworkArrayElementsGetAndSet() { + TestFieldsBase obj = new(); + obj.Counts.Set(1, 4); + DynamicArrayAccessor counts = FIELD.OF_ARRAY(nameof(TestFieldsBase.Counts)); + + Assert.Equal(3, counts.Length); + Assert.Equal(4, counts.AtIndex(1)!.GetValue(obj)); + + counts.AtIndex(2)!.SetValue(obj, 6); + Assert.Equal(6, obj.Counts[2]); + } + + [Fact] + public void NetworkArrayScalarSpanCoversElements() { + TestFieldsBase obj = new(); + obj.Counts.Set(0, 1); + obj.Counts.Set(2, 3); + DynamicAccessor field = FIELD.OF(nameof(TestFieldsBase.Counts)); + + Assert.True(field.TryGetSpan(obj, 3, out Span span)); + Assert.Equal([1, 0, 3], span.ToArray()); + } + + [Fact] + public void ListElementsGetAndSet() { + TestFieldsBase obj = new() { Items = [1, 2, 3] }; + DynamicArrayAccessor items = FIELD.OF_LIST(nameof(TestFieldsBase.Items), 8); + + Assert.Equal(8, items.Length); + Assert.True(items.Info.IsList); + Assert.Equal(2, items.AtIndex(1)!.GetValue(obj)); + + items.AtIndex(2)!.SetValue(obj, 30); + Assert.Equal(30, obj.Items[2]); + } + + [Fact] + public void NetworkVarSetterRecordsChanges() { + TestFieldsBase obj = new(); + + obj.Tracked = 5; + obj.Tracked = 5; + Assert.Single(obj.Changes); + Assert.Same(TestFieldsBase.NetworkVarFields.Tracked, obj.Changes[0]); + + obj.TrackedForModify() = 6; + Assert.Equal(2, obj.Changes.Count); + Assert.Equal(6, obj.Tracked); + } + + [Fact] + public void NetworkVarAccessorReadsBackingField() { + TestFieldsBase obj = new() { Tracked = 12 }; + IFieldAccessor field = TestFieldsBase.NetworkVarFields.Tracked; + + Assert.Equal(12, field.GetValue(obj)); + field.SetValue(obj, 14); + Assert.Equal(14, obj.Tracked); + Assert.Equal(nameof(TestFieldsBase.Tracked), field.Name); + } + + [Fact] + public void FieldOfNetworkVarPropertyMatchesGeneratedAccessor() { + TestFieldsBase obj = new() { Tracked = 3 }; + IFieldAccessor viaField = FIELD.OF(nameof(TestFieldsBase.Tracked)); + IFieldAccessor viaDerived = FIELD.OF(nameof(TestFieldsBase.Tracked)); + + Assert.Equal(3, viaField.GetValue(obj)); + Assert.Equal(TestFieldsBase.NetworkVarFields.Tracked, viaField); + Assert.Equal(TestFieldsBase.NetworkVarFields.Tracked, viaDerived); + Assert.Equal(TestFieldsBase.NetworkVarFields.Tracked.GetHashCode(), viaField.GetHashCode()); + } + + [Fact] + public void SeparateAccessorsToSameFieldAreEqual() { + IFieldAccessor a = FIELD.OF(nameof(TestFieldsBase.Value)); + IFieldAccessor b = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.NotSame(a, b); + Assert.Equal(a, b); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void AccessorThroughDerivedTypeEqualsBaseAccessor() { + IFieldAccessor baseAccessor = FIELD.OF(nameof(TestFieldsBase.Value)); + IFieldAccessor derivedAccessor = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.Equal(baseAccessor, derivedAccessor); + Assert.Equal(baseAccessor.GetHashCode(), derivedAccessor.GetHashCode()); + } + + [Fact] + public void NamedAccessorEqualsUnnamedAccessorToSameField() { + IFieldAccessor named = FIELD.OF_NAMED(nameof(TestFieldsBase.Value), "m_iCustom"); + IFieldAccessor plain = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.Equal(plain, named); + } + + [Fact] + public void NestedPathAccessorsMatchByPath() { + IFieldAccessor value = FIELD.OF("Inner.Value"); + IFieldAccessor derivedValue = FIELD.OF("Inner.Value"); + IFieldAccessor scale = FIELD.OF("Inner.Scale"); + + Assert.Equal(value, derivedValue); + Assert.NotEqual(value, scale); + } + + [Fact] + public void DifferentFieldsAreNotEqual() { + IFieldAccessor value = FIELD.OF(nameof(TestFieldsBase.Value)); + IFieldAccessor other = FIELD.OF(nameof(TestFieldsBase.Other)); + IFieldAccessor derivedValue = FIELD.OF(nameof(TestFieldsDerived.DerivedValue)); + + Assert.NotEqual(value, other); + Assert.NotEqual(value, derivedValue); + } + + [Fact] + public void ArrayElementsMatchByIndex() { + IFieldAccessor baseElement = FIELD.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), 2); + IFieldAccessor derivedElement = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)).AtIndex(2)!; + IFieldAccessor otherElement = FIELD.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), 3); + + Assert.Equal(baseElement, derivedElement); + Assert.Equal(baseElement.GetHashCode(), derivedElement.GetHashCode()); + Assert.NotEqual(baseElement, otherElement); + } + + [Fact] + public void ArrayElementIsNotEqualToWholeArray() { + IFieldAccessor array = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)); + IFieldAccessor element = FIELD.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), 0); + + Assert.NotEqual(array, element); + } + + [Fact] + public void DictionaryLookupFindsFieldThroughAnotherAccessor() { + Dictionary map = new() { + [FIELD.OF(nameof(TestFieldsBase.Value))] = 7, + [FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)).AtIndex(1)!] = 9, + }; + + Assert.True(map.TryGetValue(FIELD.OF(nameof(TestFieldsBase.Value)), out ushort scalarIndex)); + Assert.Equal(7, scalarIndex); + + Assert.True(map.TryGetValue(FIELD.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), 1), out ushort elementIndex)); + Assert.Equal(9, elementIndex); + + Assert.False(map.ContainsKey(FIELD.OF(nameof(TestFieldsBase.Other)))); + } + + [Fact] + public void ReadOnlyFieldIsReadAndWritten() { + TestFieldsBase obj = new(); + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.ReadOnlyValue)); + + Assert.Equal(4, field.GetValue(obj)); + field.SetValue(obj, 9); + Assert.Equal(9, obj.ReadOnlyValue); + } + + [Fact] + public void PrivateFieldOnOwnerIsAccessible() { + TestFieldsBase obj = new(); + IFieldAccessor field = FIELD.OF("hidden"); + + Assert.Equal(2, field.GetValue(obj)); + field.SetValue(obj, 5); + Assert.Equal(5, obj.Hidden); + } + + [Fact] + public void PrivateBaseFieldIsNotAvailableThroughDerivedOwner() { + Assert.Throws(() => FIELD.OF("hidden")); + } + + [Fact] + public void MissingFieldThrows() { + Assert.Throws(() => FIELD.OF("DoesNotExist")); + Assert.Throws(() => FIELD.OF_ARRAY("DoesNotExist")); + } + + [Fact] + public void StructOwnerWritesThroughBox() { + object boxed = new TestFieldsInner { Value = 1 }; + IFieldAccessor field = FIELD.OF(nameof(TestFieldsInner.Value)); + + Assert.Equal(1, field.GetValue(boxed)); + field.SetValue(boxed, 3); + Assert.Equal(3, ((TestFieldsInner)boxed).Value); + } + + [Fact] + public void ConstantIndexExpressionAddressesElement() { + TestFieldsBase obj = new(); + obj.Values[2] = 17; + DynamicAccessor field = FIELD.OF("Values[2]"); + + Assert.Equal("Values[2]", field.Name); + Assert.Equal(17, field.GetValue(obj)); + + field.SetValue(obj, 18); + Assert.Equal(18, obj.Values[2]); + } + + [Fact] + public void NegativeArrayIndexUsesAbsoluteValue() { + TestFieldsBase obj = new(); + obj.Values[2] = 23; + DynamicArrayIndexAccessor element = FIELD.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), -2); + + Assert.Equal(2, element.Index); + Assert.Equal(23, element.GetValue(obj)); + } + + [Fact] + public void ImplicitOperatorsAreUsedForConversion() { + TestFieldsBase obj = new() { Wrapped = new() { Raw = 8 } }; + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.Wrapped)); + + Assert.Equal(8, field.GetValue(obj)); + field.SetValue(obj, 12); + Assert.Equal(12, obj.Wrapped.Raw); + } + + [Fact] + public void ReferenceTypesAreCast() { + TestFieldsBase obj = new() { Label = "crowbar" }; + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.Label)); + + Assert.Equal("crowbar", field.GetValue(obj)); + Assert.Equal("crowbar", field.GetValue(obj)); + + field.SetValue(obj, "pistol"); + Assert.Equal("pistol", obj.Label); + } + + [Fact] + public void UnsupportedConversionThrows() { + TestFieldsBase obj = new(); + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.Throws(() => field.GetValue(obj)); + } + + [Fact] + public void NarrowAndCharConversions() { + TestFieldsBase obj = new(); + + FIELD.OF(nameof(TestFieldsBase.Tiny)).SetValue(obj, 300); + Assert.Equal(44, obj.Tiny); + + FIELD.OF(nameof(TestFieldsBase.Letter)).SetValue(obj, 65); + Assert.Equal('A', obj.Letter); + Assert.Equal(65, FIELD.OF(nameof(TestFieldsBase.Letter)).GetValue(obj)); + + FIELD.OF(nameof(TestFieldsBase.Native)).SetValue(obj, -5L); + Assert.Equal((nint)(-5), obj.Native); + Assert.Equal(-5, FIELD.OF(nameof(TestFieldsBase.Native)).GetValue(obj)); + } + + [Fact] + public void ListElementAccessorReadsAndWrites() { + List list = [10, 20, 30]; + ListElementAccessor element = new(1); + + Assert.Equal("[1]", element.Name); + Assert.Equal(typeof(List), element.DeclaringType); + Assert.Equal(typeof(int), element.FieldType); + Assert.Equal(1, element.Length); + Assert.Equal(1, element.Index); + Assert.Equal(20, element.GetValue(list)); + + element.SetValue(list, 25); + Assert.Equal(25, list[1]); + + Span target = stackalloc int[1]; + element.CopyTo(list, target); + Assert.Equal(25, target[0]); + + element.CopyFrom(list, [27]); + Assert.Equal(27, list[1]); + } + + [Fact] + public void DataMapFieldUsesDeclaringClassAccessor() { + TestFieldsDerived obj = new() { Value = 31 }; + TypeDescription desc = DEFINE.FIELD(nameof(TestFieldsBase.Value), FieldType.Integer); + DynamicAccessor accessor = desc.Accessor; + + Assert.Equal(typeof(TestFieldsBase), accessor.TargetType); + Assert.Equal(31, accessor.GetValue(obj)); + Assert.Equal(FIELD.OF(nameof(TestFieldsBase.Value)), accessor); + Assert.Same(accessor, desc.Accessor); + } + + [Fact] + public void DataMapKeyFieldKeepsMapName() { + TypeDescription desc = DEFINE.KEYFIELD(nameof(TestFieldsBase.Value), FieldType.Integer, "value"); + + Assert.Equal("value", desc.ExternalName); + Assert.Equal(nameof(TestFieldsBase.Value), desc.FieldName); + Assert.True((desc.Flags & FieldTypeDescFlags.Key) != 0); + Assert.True((desc.Flags & FieldTypeDescFlags.Save) != 0); + } + + [Fact] + public void NetworkVarInlineArrayProperty() { + TestFieldsBase obj = new(); + TestFieldsInt4 values = default; + values[1] = 3; + + obj.TrackedValues = values; + obj.TrackedValues = values; + Assert.Single(obj.Changes); + Assert.Same(TestFieldsBase.NetworkVarFields.TrackedValues, obj.Changes[0]); + + values[3] = 1; + obj.TrackedValues = values; + Assert.Equal(2, obj.Changes.Count); + obj.Changes.Clear(); + values[3] = 0; + obj.TrackedValues = values; + Assert.Single(obj.Changes); + + Span target = stackalloc int[4]; + TestFieldsBase.NetworkVarFields.TrackedValues.CopyTo(obj, target); + Assert.Equal([0, 3, 0, 0], target.ToArray()); + + TestFieldsBase.NetworkVarFields.TrackedValues.CopyFrom(obj, [7]); + TestFieldsInt4 copied = obj.TrackedValues; + Assert.Equal([7, 0, 0, 0], ((ReadOnlySpan)copied).ToArray()); + } + + [Fact] + public void NetworkVarVectorProperty() { + TestFieldsBase obj = new(); + + obj.TrackedPosition = new(1, 2, 3); + obj.TrackedPosition = new(1, 2, 3); + Assert.Single(obj.Changes); + Assert.Equal(new Vector3(1, 2, 3), TestFieldsBase.NetworkVarFields.TrackedPosition.GetValue(obj)); + Assert.Equal(FIELD.OF(nameof(TestFieldsBase.TrackedPosition)), TestFieldsBase.NetworkVarFields.TrackedPosition); + } + + [Fact] + public void ArrayAtIndexThroughInterface() { + TestFieldsBase obj = new(); + obj.Values[1] = 8; + DynamicArrayAccessor array = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)); + + IFieldAccessor element = ((IFieldAccessorIndexable)array).AtIndex(1); + + Assert.Same(array.AtIndex(1), element); + Assert.Equal(8, element.GetValue(obj)); + } + + [Fact] + public void ScalarAtIndexReturnsItself() { + DynamicAccessor field = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.Same(field, field.AtIndex(3)); + Assert.Same(field, ((IFieldAccessorIndexable)field).AtIndex(3)); + } + + [Fact] + public void AccessorEqualsItself() { + DynamicAccessor field = FIELD.OF(nameof(TestFieldsBase.Value)); + + Assert.True(field.Equals(field)); + Assert.False(field.Equals(null)); + Assert.False(field.Equals(FIELD.OF(nameof(TestFieldsBase.Other)))); + } + + [Fact] + public void KeylessAccessorsCompareByReference() { + TestKeylessAccessor first = new(), second = new(); + + Assert.Null(first.FieldKey); + Assert.Equal(0, first.FieldKeyId); + Assert.Equal(first, first); + Assert.NotEqual(first, second); + Assert.NotEqual(first, FIELD.OF(nameof(TestFieldsBase.Value))); + Assert.NotEqual(FIELD.OF(nameof(TestFieldsBase.Value)), first); + Assert.Equal(RuntimeHelpers.GetHashCode(first), first.GetHashCode()); + + TestFieldsBase obj = new() { Value = 12 }; + Assert.Equal(12, first.GetValue(obj)); + } + + [Fact] + public void ArrayElementHasNoSpan() { + TestFieldsBase obj = new(); + DynamicAccessor element = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)).AtIndex(0)!; + + Assert.False(element.TryGetSpan(obj, 1, out Span span)); + Assert.True(span.IsEmpty); + } + + [Fact] + public void ListElementEmptySpans() { + List list = [10, 20]; + ListElementAccessor element = new(0); + + element.CopyTo(list, Span.Empty); + element.CopyFrom(list, Span.Empty); + + Assert.Equal(0, list[0]); + } + + [Fact] + public void InlineArrayCopiesWholeValue() { + TestFieldsBase obj = new(); + IFieldAccessor field = FIELD.OF(nameof(TestFieldsBase.Values)); + TestFieldsInt4 values = default; + values[0] = 1; + values[3] = 4; + + field.CopyFrom(obj, [values]); + Assert.Equal([1, 0, 0, 4], ((ReadOnlySpan)obj.Values).ToArray()); + + Span target = new TestFieldsInt4[1]; + field.CopyTo(obj, target); + Assert.Equal([1, 0, 0, 4], ((ReadOnlySpan)target[0]).ToArray()); + } + + [Fact] + public void ArrayAccessorWholeValue() { + TestFieldsBase obj = new(); + DynamicArrayAccessor array = FIELD.OF_ARRAY(nameof(TestFieldsBase.Values)); + TestFieldsInt4 values = default; + values[2] = 9; + + Assert.True(array.SetValue(obj, values)); + Assert.Equal(9, obj.Values[2]); + + TestFieldsInt4 read = array.GetValue(obj); + Assert.Equal([0, 0, 9, 0], ((ReadOnlySpan)read).ToArray()); + } + + [Fact] + public void SignedSourceConversions() { + Assert.Equal(-3, FieldConvert.Convert((sbyte)-3)); + Assert.Equal(-300, FieldConvert.Convert((short)-300)); + Assert.Equal(-7L, FieldConvert.Convert(-7)); + Assert.Equal(-9, FieldConvert.Convert(-9L)); + } + + [Fact] + public void UnsignedSourceConversions() { + Assert.Equal(5L, FieldConvert.Convert(5u)); + Assert.Equal(6, FieldConvert.Convert(6ul)); + Assert.Equal(7, FieldConvert.Convert((nuint)7)); + Assert.Equal(2f, FieldConvert.Convert(2u)); + Assert.Equal(3d, FieldConvert.Convert((ushort)3)); + Assert.True(FieldConvert.Convert((byte)1)); + Assert.False(FieldConvert.Convert((byte)0)); + } + + [Fact] + public void FloatSourceConversions() { + Assert.True(FieldConvert.Convert(0.5f)); + Assert.False(FieldConvert.Convert(0d)); + Assert.Equal((sbyte)-4, FieldConvert.Convert(-4.7)); + Assert.Equal((short)-500, FieldConvert.Convert(-500.2f)); + Assert.Equal(-6, FieldConvert.Convert(-6.9)); + Assert.Equal(1L << 40, FieldConvert.Convert((double)(1L << 40))); + Assert.Equal((byte)200, FieldConvert.Convert(200.9)); + Assert.Equal((ushort)60000, FieldConvert.Convert(60000f)); + Assert.Equal(4000000000u, FieldConvert.Convert(4000000000d)); + Assert.Equal(1ul << 50, FieldConvert.Convert((double)(1ul << 50))); + Assert.Equal(1.5f, FieldConvert.Convert(1.5)); + Assert.Equal(2.5, FieldConvert.Convert(2.5f)); + } + + [Fact] + public void NumericKindClassification() { + Assert.Equal(NumericKind.Signed, NumericConvert.Classify(typeof(nint))); + Assert.Equal(NumericKind.Unsigned, NumericConvert.Classify(typeof(nuint))); + Assert.Equal(NumericKind.Unsigned, NumericConvert.Classify(typeof(char))); + Assert.Equal(NumericKind.Bool, NumericConvert.Classify(typeof(bool))); + Assert.Equal(NumericKind.Float, NumericConvert.Classify(typeof(float))); + Assert.Equal(NumericKind.None, NumericConvert.Classify(typeof(decimal))); + Assert.Equal(NumericKind.None, NumericConvert.Classify(typeof(string))); + } + + [Fact] + public void NetworkArraySizeAttributeKeepsSize() { + Assert.Equal(5, new NetworkArraySizeAttribute(5).Size); + } + + [Fact] + public void NetworkArrayMembers() { + NetworkArray array = new(3); + int[] raw = array; + + Assert.Same(array.Value, raw); + Assert.Same(array.Value, array.Base); + Assert.Equal(3, array.Count()); + } + + [Fact] + public void NetworkArrayHookSeesChanges() { + NetworkArray array = new(3); + List changed = []; + array.Hook((ref int value) => changed.Add(value)); + + array.Set(1, 5); + array.Set(1, 5); + Assert.Single(changed); + Assert.Equal(5, array[1]); + + array.GetForModify(2) = 8; + Assert.Equal(2, changed.Count); + Assert.Equal(8, array.Get(2)); + } + + [Fact] + public void NetworkArrayWithoutHook() { + NetworkArray array = new(2); + + array.Set(0, 3); + array.GetForModify(1) = 4; + + Assert.Equal([3, 4], array.Value); + } +} diff --git a/Source.Common.Tests/Source.Common.Tests.csproj b/Source.Common.Tests/Source.Common.Tests.csproj index 7ca852cdb..12fa1e4b2 100644 --- a/Source.Common.Tests/Source.Common.Tests.csproj +++ b/Source.Common.Tests/Source.Common.Tests.csproj @@ -18,9 +18,11 @@ + + diff --git a/Source.Common.Tests/coverage.settings.xml b/Source.Common.Tests/coverage.settings.xml new file mode 100644 index 000000000..92a6ac7d8 --- /dev/null +++ b/Source.Common.Tests/coverage.settings.xml @@ -0,0 +1,17 @@ + + + + true + + + .*Source\.Common\.dll$ + .*Source\.Common\.Tests\.dll$ + + + + + .*[\\/]Source\.Common\.Tests[\\/][^\\/]+\.cs$ + + + + diff --git a/Source.Common/Engine/Edict.cs b/Source.Common/Engine/Edict.cs index 93fea7d53..8e808a931 100644 --- a/Source.Common/Engine/Edict.cs +++ b/Source.Common/Engine/Edict.cs @@ -128,7 +128,7 @@ public void StateChanged(IFieldAccessor field) { // Now add this offset to our list of changed variables. for (ushort i = 0; i < p.NumChangeFields; i++) - if (p.ChangedFields[i] == field) + if (Equals(p.ChangedFields[i], field)) return; if (p.NumChangeFields == MAX_CHANGE_OFFSETS) { diff --git a/Source.Common/FieldAccess.cs b/Source.Common/FieldAccess.cs index d63d2f877..06e6c5865 100644 --- a/Source.Common/FieldAccess.cs +++ b/Source.Common/FieldAccess.cs @@ -58,6 +58,8 @@ public DynamicArrayIndexAccessor(DynamicArrayAccessor baseArray, int index, bool BaseArrayAccessor = baseArray; Index = Math.Abs(index); IsAVectorElement = isVectorElem; + FieldKeyId = baseArray.FieldKeyId; + FieldIndex = Index; } public override T GetValue(object instance) => BaseArrayAccessor.GetElement(instance, Index); @@ -133,11 +135,31 @@ protected DynamicAccessor(Type targetType, Type storingType, string name, string TargetType = targetType; StoringType = storingType; this.networkName = networkName; + FieldKeyId = FieldKeyRegistry.GetId(FieldKey); } public abstract T GetValue(object instance); public abstract bool SetValue(object instance, in T value); + public virtual string? FieldKey => null; + + public int FieldKeyId { get; protected set; } + public int FieldIndex { get; protected set; } = -1; + + public override bool Equals(object? obj) { + if (ReferenceEquals(this, obj)) + return true; + + return obj is DynamicAccessor other && FieldKeyId != 0 && FieldKeyId == other.FieldKeyId && FieldIndex == other.FieldIndex; + } + + public override int GetHashCode() { + if (FieldKeyId == 0) + return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(this); + + return HashCode.Combine(FieldKeyId, FieldIndex); + } + public virtual bool TryGetSpan(object instance, int count, out Span span) { span = default; return false; diff --git a/Source.Common/FieldAccessors.cs b/Source.Common/FieldAccessors.cs index 053198121..7bda53301 100644 --- a/Source.Common/FieldAccessors.cs +++ b/Source.Common/FieldAccessors.cs @@ -34,6 +34,25 @@ public static DynamicArrayAccessor CreateArray(Type owner, string expression, in } } +public static class FieldKeyRegistry +{ + static readonly Lock RegistryLock = new(); + static readonly Dictionary Ids = []; + + public static int GetId(string? key) { + if (key == null) + return 0; + + lock (RegistryLock) { + if (!Ids.TryGetValue(key, out int id)) { + id = Ids.Count + 1; + Ids.Add(key, id); + } + return id; + } + } +} + public abstract class FieldAccessor : DynamicAccessor { protected FieldAccessor(Type targetType, string name, string networkName) : base(targetType, typeof(TField), name, networkName) { }