v0.22.0 - Versioned RPC + entity-to-entity notifications
[0.22.0]
Backwards-compatible multi-version operation. Old clients keep working against newer servers; new clients keep working against entities pinned at older config branches. Plus ExecutionMode.Notification for entity → entity fire-and-forget, and SharedMeta.Debug.Mux for stress tests that need many simulated players on few sockets.
Added — Compatibility negotiation pipeline
Two-phase handshake (ClientSignatureHash → RegisterClientSignature if missing) populates a per-build ClientCapabilities blob; generated *ApiClient consults it before every call and the server-side MetaConnectionHandler enforces the same on receive. Covers four kinds of drift: structural state breaks, config-structure breaks, method-body version drift, method-signature drift.
[MetaStateVersion(Breaking = bool)]—Breaking = truerejects clients below threshold with structuredIncompatibleFeatureException.[MetaMethod(Version, MinCompatibleVersion, Alias)]—(Alias, Version)dispatch withcase 0legacy fallback.[MetaConfigStructureBoundary("X.Y", Reason)]— asymmetric force-patch trigger: fires iffclientCode < V && pinned >= V. New client on old entity runs natively.[assembly: SharedMetaCompatibilityOptions(Enabled = false)]opt-out for projects that don't use negotiation.- Per-subscriber broadcast tailoring: legacy subscribers get
PatchBytes, modern subscribers getReplayPayload— single execution, dual-format fan-out. - Per-entity overlay (
EntityAugmentedCapabilities) stacks on session-level caps; computed at subscribe time from the entity's pinned config version.
Full design in docs/GUIDE.md § Per-Client Config Branches & State Migration and com.coregame.sharedmeta/SharedMeta-AI.md. Reference example: examples/ClanWars (v1 + v2 clients sharing a pinned-2.0 clan).
Added — ExecutionMode.Notification
Entity → entity fire-and-forget — peer of Signal on the cross-entity axis. Source grain dispatches via Orleans [OneWay] and continues; no result recorded into replay payload. Generator emits void {Name}(args) on the EntityCaller (any pre-0.22 await GetIFoo(id).BarAsync(...) site compile-errors and migrates).
[MetaMethod(Mode = ExecutionMode.Notification)]
Task AddPower(int delta);Implicit GenerateClientApi = false. Removes one grain-to-grain await from the latency path when caller doesn't read target state after the call. Full contract + perf in docs/GUIDE.md § Notification Methods.
Added — SharedMeta.Debug.Mux transport
Debug-only transport where N logical client sessions share one physical SignalR socket. Map app.MapMetaMuxHub("/meta-mux") server-side; build a MuxChannel pool client-side and call channel.CreateConnection(tag) per simulator. Each MuxConnection implements IConnection, so the rest of MetaClient is unchanged. Useful for stress tests driving thousands of simulated players from one process. See docs/GUIDE.md § Mux Transport.
Changed — PlayerVersionGrain moved into SharedMeta.Server.Core
The default version-gate grain (consulted by MetaConnectionHandler on connect when the client transmits ClientVersion) now ships with SharedMeta.Server.Core instead of SharedMeta.Auth — no transitive ASP.NET/JWT dependency needed for the base version-gate.
Changed — ValidateClientCompatibleWithPins is now permissive
Cross-version subscribe to a shared-scoped entity is allowed by default. Open-Closed config evolution + [MetaConfigStructureBoundary]-driven force-patch handle compatibility — the pre-0.22 strict reject was redundant once boundary-driven force-patch landed.
Fixed — Query method local sync wrapper now establishes MetaContext
SimplifiedApiClientGenerator.GenerateLocalQueryMethod now wraps the body with SetupQueryContext() / RestoreQueryContext(prev) so Context.State / Context.EntityId reads always succeed (previously worked only when AsyncLocal happened to carry a context from a prior async op).
Fixed — Query single-primitive-arg framing
QueryClientGenerator now uses _serializer.CreateWriter() / writer.Complete() for all arg counts and serializers, matching the server-side dispatcher's length-prefixed framing (previously raw MemoryPackSerializer.Serialize(int) produced 4 raw bytes that the dispatcher's ReadWithAutoUnbox<int> mis-interpreted).