Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
2 changes: 2 additions & 0 deletions Game.Server/BaseEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2702,6 +2702,7 @@ public void SetModelName(ReadOnlySpan<char> 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");
Expand All @@ -2717,6 +2718,7 @@ public void SetModelIndex(int index) {
modelinfo.ReleaseDynamicModel(ModelIndex);
modelinfo.AddRefDynamicModel(index);
ModelIndex = index;
NetworkStateChanged(ModelIndexField);

DynamicModelSetBounds = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ private static bool IsInvocationNamed(SyntaxNode node, HashSet<string> 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}");
Expand Down Expand Up @@ -191,6 +192,7 @@ private static bool IsInvocationNamed(SyntaxNode node, HashSet<string> 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)
Expand Down Expand Up @@ -287,6 +289,8 @@ private static bool IsInvocationNamed(SyntaxNode node, HashSet<string> 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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -624,6 +631,7 @@ private sealed class ResolvedPath
public string Expression = "";
public ITypeSymbol Type = null!;
public ISymbol? LastMember;
public INamedTypeSymbol? RootDeclaring;
public readonly List<NamePart> Parts = new List<NamePart>();
public readonly List<string> Accessors = new List<string>();
}
Expand Down
18 changes: 16 additions & 2 deletions Source.CodeAnalysis.NetworkVars/NetworkVarGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) {
List<string> conversions = new List<string>();
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(),
Expand Down Expand Up @@ -153,7 +158,13 @@ private static string BuildSource(PropertyModel type, List<PropertyModel> 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}");
Expand All @@ -178,6 +189,7 @@ private static string BuildSource(PropertyModel type, List<PropertyModel> 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("}");
Expand Down Expand Up @@ -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;
Expand All @@ -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; }
Expand Down
149 changes: 149 additions & 0 deletions Source.Common.Tests/EdictChangeInfoTests.cs
Original file line number Diff line number Diff line change
@@ -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<BaseEdict, IChangeInfoAccessor>? 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<TestFieldsBase>.OF_LIST(nameof(TestFieldsBase.Items), 32).AtIndex(index)!;

[Fact]
public void SameFieldThroughDifferentAccessorsIsRecordedOnce() {
using ChangeScope scope = new();
scope.Record(
FIELD<TestFieldsBase>.OF(nameof(TestFieldsBase.Value)),
FIELD<TestFieldsDerived>.OF(nameof(TestFieldsBase.Value)));

Assert.Equal(1, scope.Info.NumChangeFields);
}

[Fact]
public void DifferentFieldsAreRecordedSeparately() {
using ChangeScope scope = new();
scope.Record(
FIELD<TestFieldsBase>.OF(nameof(TestFieldsBase.Value)),
FIELD<TestFieldsBase>.OF(nameof(TestFieldsBase.Other)),
FIELD<TestFieldsBase>.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), 0),
FIELD<TestFieldsBase>.OF_ARRAYINDEX(nameof(TestFieldsBase.Values), 1));

Assert.Equal(4, scope.Info.NumChangeFields);
}

[Fact]
public void FirstChangeClaimsChangeInfo() {
using ChangeScope scope = new();
scope.Record(FIELD<TestFieldsBase>.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<TestFieldsBase>.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<TestFieldsBase>.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<TestFieldsBase>.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<TestFieldsBase>.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<TestFieldsBase>.OF(nameof(TestFieldsBase.Value)));

scope.Shared.SerialNumber++;
scope.Shared.NumChangeInfos = 0;
scope.Record(FIELD<TestFieldsBase>.OF(nameof(TestFieldsBase.Other)));

Assert.Equal(1, scope.Info.NumChangeFields);
Assert.Equal(FIELD<TestFieldsBase>.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());
}
}
Loading
Loading