diff --git a/src/GameCult.Networking/CultNetDatabase.cs b/src/GameCult.Networking/CultNetDatabase.cs index 9ebe304f..d5b3773d 100644 --- a/src/GameCult.Networking/CultNetDatabase.cs +++ b/src/GameCult.Networking/CultNetDatabase.cs @@ -614,10 +614,27 @@ namespace GameCult.Networking ThrowIfDisposed(); if (shard == null) throw new ArgumentNullException(nameof(shard)); + var lowSchemas = CultNetV0SelectionLowering.Lower(filter?.SchemaIds); + var lowKeys = CultNetV0SelectionLowering.Lower(filter?.RecordKeys); + + // R-R: an explicit empty schemas or keys list is v0's own "answer nothing" - it must not + // reach the v1 door, which refuses an empty selection.schemas/keys outright. + if (lowSchemas is { Length: 0 } || lowKeys is { Length: 0 }) + { + return new CultNetSnapshotResponseRawMessage + { + MessageId = string.IsNullOrWhiteSpace(messageId) ? Guid.NewGuid().ToString("N") : messageId, + Documents = Array.Empty(), + ShardId = shard.ShardId, + ShardEpoch = shard.Epoch, + ShardLogSequence = GetLatestMutationLogSequence(shard.ShardId) + }; + } + var selection = new CultNetSelection { - Schemas = CultNetV0SelectionLowering.Lower(filter?.SchemaIds), - Keys = CultNetV0SelectionLowering.Lower(filter?.RecordKeys), + Schemas = lowSchemas, + Keys = lowKeys, Projection = CultNetSelectionProjections.Document }; bool RowFilter(CultDocumentDescriptor descriptor, CultRecordKey key) => diff --git a/src/GameCult.Networking/CultNetDocumentRegistry.cs b/src/GameCult.Networking/CultNetDocumentRegistry.cs index de275adf..6d2c778a 100644 --- a/src/GameCult.Networking/CultNetDocumentRegistry.cs +++ b/src/GameCult.Networking/CultNetDocumentRegistry.cs @@ -283,10 +283,25 @@ namespace GameCult.Networking CultNetDocumentMessageOptions? options = null) { if (cache == null) throw new ArgumentNullException(nameof(cache)); + var lowSchemas = CultNetV0SelectionLowering.Lower(filter?.SchemaIds); var lowKeys = CultNetV0SelectionLowering.Lower(filter?.RecordKeys); + + // R-R: an explicit empty schemas or keys list is v0's own "answer nothing" - it must not + // reach the v1 door, which refuses an empty selection.schemas/keys outright + // (CultNetSelectionValidation.ValidateShape). v0 and v1 disagree about what an empty list + // means, so v0 decides this before the v1 evaluator ever sees the selection. + if (lowSchemas is { Length: 0 } || lowKeys is { Length: 0 }) + { + return new CultNetSnapshotResponseRawMessage + { + MessageId = RequireNonEmpty(messageId, nameof(messageId)), + Documents = Array.Empty() + }; + } + var selection = new CultNetSelection { - Schemas = CultNetV0SelectionLowering.Lower(filter?.SchemaIds), + Schemas = lowSchemas, Keys = lowKeys, Projection = CultNetSelectionProjections.Document };