From 1b9140da3d489369957efce83a7854b2fbc88907 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 27 Jun 2026 21:53:28 +0100 Subject: [PATCH] Resolve C# subscription filters by schema alias --- .../CultNetDatabaseServer.cs | 71 +++++++++++++++++-- .../NetworkingTests.cs | 51 +++++++++++++ 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/src/GameCult.Networking/CultNetDatabaseServer.cs b/src/GameCult.Networking/CultNetDatabaseServer.cs index df8dbbbf..a136e1fc 100644 --- a/src/GameCult.Networking/CultNetDatabaseServer.cs +++ b/src/GameCult.Networking/CultNetDatabaseServer.cs @@ -344,7 +344,10 @@ namespace GameCult.Networking var changeType = change.GetType(); var key = (CultRecordKey)(changeType.GetProperty("Key")?.GetValue(change) ?? new CultRecordKey(string.Empty)); var schemaId = (string?)changeType.GetProperty("SchemaId")?.GetValue(change) ?? string.Empty; - if (!Matches(request, schemaId, key)) + var documentType = changeType.IsGenericType ? changeType.GetGenericArguments()[0] : null; + var descriptor = documentType == null ? null : _database.Cache.Registry.GetRequired(documentType); + var binding = documentType == null ? null : _database.Documents.GetByDocumentType(documentType); + if (!Matches(request, schemaId, key, descriptor, binding)) { return null; } @@ -387,17 +390,75 @@ namespace GameCult.Networking return ((CultNetDocumentPutRawMessage)message!).Document; } - private static bool Matches(CultNetDatabaseSubscribeMessage request, string schemaId, CultRecordKey key) + private static bool Matches( + CultNetDatabaseSubscribeMessage request, + string schemaId, + CultRecordKey key, + CultDocumentDescriptor? descriptor, + CultNetDocumentBinding? binding) { - var schemaMatches = request.SchemaIds == null || - request.SchemaIds.Length == 0 || - request.SchemaIds.Contains(schemaId, StringComparer.Ordinal); + var schemaMatches = MatchesSchema(request.SchemaIds, schemaId, descriptor, binding); var keyMatches = request.RecordKeys == null || request.RecordKeys.Length == 0 || request.RecordKeys.Contains(key.Value, StringComparer.Ordinal); return schemaMatches && keyMatches; } + private static bool MatchesSchema( + string[]? requestedSchemaIds, + string schemaId, + CultDocumentDescriptor? descriptor, + CultNetDocumentBinding? binding) + { + if (requestedSchemaIds == null || requestedSchemaIds.Length == 0) + { + return true; + } + + var requested = requestedSchemaIds.ToHashSet(StringComparer.Ordinal); + if (requested.Contains(schemaId)) + { + return true; + } + + if (descriptor == null) + { + return false; + } + + if (requested.Contains(descriptor.SchemaId) || + requested.Contains(descriptor.SchemaName) || + requested.Contains(descriptor.SchemaVersion) || + (binding != null && requested.Contains(binding.SchemaId))) + { + return true; + } + + if (descriptor.ToCatalogEntry().CompatibleSchemaIds.Any(requested.Contains)) + { + return true; + } + + return requested + .Select(InferSchemaName) + .Where(schemaName => !string.IsNullOrWhiteSpace(schemaName)) + .Any(schemaName => string.Equals(schemaName, descriptor.SchemaName, StringComparison.Ordinal)); + } + + private static string? InferSchemaName(string schemaVersion) + { + var marker = schemaVersion.LastIndexOf(".v", StringComparison.Ordinal); + if (marker <= 0 || marker + 2 >= schemaVersion.Length) + { + return null; + } + + var version = schemaVersion.Substring(marker + 2); + return version.All(char.IsDigit) + ? schemaVersion.Substring(0, marker) + : null; + } + private static string SubscriptionKey(NetPeer peer, string subscriptionId) { return $"{peer.Id}:{subscriptionId}"; diff --git a/tests/GameCult.Networking.Tests/NetworkingTests.cs b/tests/GameCult.Networking.Tests/NetworkingTests.cs index f8b3463a..dfd5555d 100644 --- a/tests/GameCult.Networking.Tests/NetworkingTests.cs +++ b/tests/GameCult.Networking.Tests/NetworkingTests.cs @@ -4039,6 +4039,57 @@ namespace GameCult.Networking.Tests Assert.That(message.Document!.Payload, Is.EqualTo(SerializePlayerDataPayload(player))); } + [Test] + public void CultNetDatabaseServer_Creates_Filtered_SubscriptionChange_ForSchemaAlias() + { + var cache = new CultCache(); + var registry = new CultNetDocumentRegistry(cache.Registry) + .Register(CultNetDocumentBinding.ForDocument(cache.Registry)); + var database = new CultNetDatabase(cache, new CultNetDatabaseOptions + { + DocumentRegistry = registry + }); + using var server = new Server(cache, DevelopmentServerSecurity); + using var databaseServer = new CultNetDatabaseServer(server, database); + var key = new CultRecordKey("network-note:alias-change"); + var note = new NetworkSchemaNote + { + Schema = "tests.networking_note.v1", + Text = "alias change", + Revision = 11 + }; + var descriptor = cache.Registry.GetRequired(); + var change = new CultNetDatabaseChange( + CultNetDatabaseChangeKind.Added, + key, + descriptor.SchemaId, + database.Shards[0], + note, + previousDocument: null); + var method = typeof(CultNetDatabaseServer).GetMethod( + "CreateChangeMessage", + BindingFlags.Instance | BindingFlags.NonPublic)!; + + var message = (CultNetDatabaseChangeRawMessage?)method.Invoke(databaseServer, new object[] + { + change, + "sub-alias", + new CultNetDatabaseSubscribeMessage + { + SubscriptionId = "sub-alias", + SchemaIds = ["tests.networking_note.v1"], + RecordKeys = [key.Value] + } + }); + + Assert.That(message, Is.Not.Null); + Assert.That(message!.SubscriptionId, Is.EqualTo("sub-alias")); + Assert.That(message.ChangeKind, Is.EqualTo("added")); + Assert.That(message.Document, Is.Not.Null); + Assert.That(message.Document!.SchemaId, Is.EqualTo(descriptor.SchemaId)); + Assert.That(message.Document.Payload, Is.EqualTo(CultDocumentMessagePackSerialization.Serialize(note))); + } + [Test] public void CultNetSchemaRegistry_BuiltInCatalog_AdvertisesRawLane_AndSharedGhostlightContract() {