Turbo.Cloud.sln is the main emulator solution. Turbo.Main is the runtime host and composition root.
Turbo.Main/- Host startup and wiring (
Program.cs), configuration, lifetime, and console commands.
- Host startup and wiring (
Turbo.Plugins/- Runtime discovery, loading, start/stop, and unload lifecycle for plugins.
Turbo.PacketHandlers/- Incoming message handling orchestration and domain dispatch.
Turbo.Events/- Event behavior/handler pipeline registration and execution.
Turbo.*domain modules (Rooms,Players,Catalog,Inventory, etc.)- Domain services, snapshot providers, and Orleans grain orchestration.
Turbo.Database/- EF Core context and persistence infrastructure.
Turbo.Primitives/- Cross-module contracts, identifiers, snapshots, and message types.
- Keep host composition and module registration in
Turbo.Main; avoid leaking host concerns into domain modules. - Keep packet handlers focused on request/response orchestration, not persistence infrastructure wiring.
- Keep database querying and persistence access out of packet handlers.
- Keep grain lifecycle/state logic within grain modules; do not bypass grain boundaries with direct cross-layer shortcuts.
- Keep plugin lifecycle operations inside
Turbo.Plugins; do not duplicate plugin loading logic in unrelated modules. - Protocol revision parser/serializer trees are owned by the plugin repo at:
../turbo-sample-plugin/TurboSamplePlugin/Revision/**- Do not create
Revision<id>/ParsersorRevision<id>/Serializerstrees inturbo-cloud.
- Extended profile flow boundary:
Turbo.PacketHandlers/Users/*ExtendedProfile*Handler.csorchestrates lookup + response mapping only.Turbo.Players/Grains/PlayerDirectoryGrain.csowns username/id lookup semantics and cache coherence.Turbo.Players/Grains/PlayerGrain.csexposes profile snapshots consumed by handlers.
- Username-to-id lookup behavior is case-insensitive.
- Directory cache updates must keep forward and reverse mappings consistent across set/invalidate paths.
- Avoid adding handler-level fallbacks that bypass directory-grain lookup responsibilities.
- Connection accepted:
- session is added to gateway/session tracking.
- After SSO success:
- session is registered to
PlayerPresenceGrainfor that player id.
- session is registered to
- Player outbound targeting:
- resolve target player's presence grain
- call
SendComposerAsync - fan-out to subscribed sessions happens inside presence flow.
- Room activity:
- active room indexing/lookup and keepalive ping responsibilities stay in
RoomDirectoryGrain.
- active room indexing/lookup and keepalive ping responsibilities stay in
- Lifecycle:
- inactive grains are Orleans-managed and can deactivate automatically unless explicitly marked
[KeepAlive].
- inactive grains are Orleans-managed and can deactivate automatically unless explicitly marked
- Every grain that does cross-grain calls or DB work must inject
ILogger<T>and log caught exceptions. No barecatch { }. - Independent grain calls (e.g. checking online status for N friends) must use
Task.WhenAll, not sequentialawaitin a loop. - Identical grain calls must not repeat inside loops — hoist before the loop.
- DB batch operations use single
WHERE ... IN (...)queries, not per-entityExecuteDeleteAsyncloops. - Housekeeping writes (e.g. delivered flags) follow the timer-flush pattern: queue dirty state, flush with
RegisterGrainTimer, flush onOnDeactivateAsync. SeeRoomPersistenceGrainfor reference. - Do not hardcode limits (
Take(N), capacity constants) in grains. Pass them from handlers viaIConfiguration. - When a delete + insert must be atomic, use EF tracked operations (
Remove+SaveChangesAsync), notExecuteDeleteAsync. - Replace
.Ignore()on grain tasks with aLogAndForgethelper that logs faulted continuations. - In-memory collections that grow per-event (message history, queues) must have a configurable cap.
- One grain per responsibility: isolate heavy I/O (DB writes, persistence) into secondary grains so the primary domain grain stays responsive (e.g.
RoomGrain→RoomPersistenceGrain). - Use grain single-threading for concurrency safety: per-player
PurchaseGrainfor catalog buys, dedicated grains for limited-edition items. Do not add manual locks inside grains. - Grains orchestrate their own outbound: when state changes, the grain sends the composer via
PlayerPresenceGrain.SendComposerAsync. Callers do not build or send composers after calling a grain method. - All mutations to grain-owned data must go through grain methods. Do not update the database directly — grains may hold cached state that will not reflect raw DB changes.
- New host startup/wiring behavior:
Turbo.Main/(usuallyProgram.cs,Extensions/, orConsole/)
- New incoming packet behavior:
Turbo.PacketHandlers/<Domain>/<Name>MessageHandler.cs
- New domain service/provider:
Turbo.<Domain>/...in the existing service/provider structure
- New grain behavior:
Turbo.<Domain>/Grains/...
Use and adapt these examples before inventing new structure:
docs/patterns/ServicePattern.csdocs/patterns/HandlerPattern.csdocs/patterns/UnitTestPattern.cs