Reference C# Client SDK
A drop-in
Typhon.Clientlibrary that turns wire bytes into a ready-to-read local entity cache.
Status: โ Implemented ยท Visibility: Public ยท Level: ๐ต Core ยท Category: Subscriptions
๐ฏ What it solves
Every other Subscriptions feature describes what the server sends. Something still has to open the
socket, frame the bytes, deserialize them, and keep a local copy of each View's entities in sync as
Added/Modified/Removed deltas arrive โ tedious, easy-to-get-wrong plumbing that has nothing to do with
game logic. Typhon.Client is the reference implementation of that consumer: connect, register interest
in a View by name, and read a live entity cache. No socket code, no MemoryPack calls, no manual delta
application in application code.
โ๏ธ How it works (in brief)
TyphonClient.Connect opens a TCP socket and starts a dedicated receive thread on a TyphonConnection.
That thread reads each length-prefixed frame, deserializes it as a TickDeltaMessage (MemoryPack), and
dispatches it: SubscriptionEvents drive per-View state (Subscribed/Unsubscribed/SyncComplete/Resync),
and ViewDeltaMessages are applied to a ViewSubscription's entity cache โ Added entities are inserted,
Modified entities are patched in place, Removed entities are deleted. The cache is a plain
Id โ CachedEntity map; application code reads it (or reacts to the subscription's events) on its own
schedule. All callbacks fire synchronously on the receive thread. Subscribe(viewName) only registers
local interest by name โ matching is against whatever the server already pushed via its own
server-driven SetSubscriptions; the v1 client cannot request a subscription from the server.
๐ป Usage
using var conn = TyphonClient.Connect("server-host", 9000);
// Required once per component type before CachedEntity.Get<T>/TryGet<T> can decode it.
conn.RegisterComponent<Position>(componentId: 1);
conn.RegisterComponent<Health>(componentId: 2);
var npcs = conn.Subscribe("world_npcs");
npcs.OnEntityAdded += entity => SpawnClientEntity(entity.Id, entity.Get<Position>(1));
npcs.OnEntityModified += (entity, changed) => PatchClientEntity(entity.Id, entity.Get<Position>(1));
npcs.OnEntityRemoved += entityId => DespawnClientEntity(entityId);
npcs.OnSyncComplete += () => MarkViewReady("world_npcs");
npcs.OnResync += () => RebuildClientEntitiesFrom(npcs.Entities);
conn.OnDisconnected += (_, ex) => LogDisconnect(ex);
Option (TyphonConnectionOptions) |
Default | Effect |
|---|---|---|
ReceiveBufferSize |
65536 (64 KB) | Initial frame buffer; grows automatically for larger frames |
AutoReconnect |
true |
Reconnect with exponential backoff (1s, doubling) on connection loss |
ReconnectMaxDelay |
30s | Cap on the backoff delay |
Logger |
null |
Optional ILogger; null disables logging at zero cost |
โ ๏ธ Guarantees & limits
- All events fire on the receive thread โ
OnConnected,OnDisconnected,OnReconnected,OnTick, and everyViewSubscriptionevent. Application code must not block in a callback; it stalls delta processing for that connection. Subscribeis local-only โ it does not send a request to the server. It registers a name to match against Views the server already pushed (server-drivenSetSubscriptions); client-initiated subscription requests are not part of v1.- The cache is the source of truth between ticks โ
ViewSubscription.Entitiesreflects exactly the last applied tick; read it for rendering/logic rather than re-deriving state from individual callbacks. CachedEntity.Get<T>/TryGet<T>require priorRegisterComponent<T>for that component ID, andT's size must match the cached byte length โ a mismatch throws (Get) or returnsfalse(TryGet).- v1 component encoding is full-replace โ
Modifiedalways carries full component bytes (FieldDirtyBits = ~0UL); per-field patching is a future wire upgrade the client already tolerates. - Reconnect clears all caches โ on connection loss with
AutoReconnect = true, everyViewSubscription's cache is cleared andViewIdreset; the server re-establishes subscriptions and resyncs afterOnReconnectedfires. - C#-only โ this is the one reference SDK for v1; other languages would need to reimplement framing, MemoryPack decoding, and the cache-apply logic described here.
๐งช Tests
- DeltaProcessingTests โ Added/Modified/Removed/ SyncComplete/Resync/Disconnect dispatch over a fake server, plus lazy-subscription for server-pushed Views the client hasn't named locally yet
- ViewSubscriptionTests โ the per-View entity cache itself: add/remove/modify, sync/resync state resets
- CachedEntityTests โ
Get<T>/TryGet<T>decode guarantees: missing component, size mismatch - TyphonConnectionIntegrationTests
โ
Client_Connects_SubscribesAndReceivesAddedEntity: real socket, real server, end to end
๐ Related
- Related feature: TCP Transport & Wire Format, Incremental Sync, Backpressure & Resync