You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: CLAUDE.md
+22-9Lines changed: 22 additions & 9 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -95,7 +95,11 @@ Standard protobuf `optional` fields map to a plugin-generated field_mask on the
95
95
96
96
**No proactive STATE_FULL mid-session.** The client drives resync, the server never anticipates it.
97
97
98
-
**Snapshot History.** The server keeps a small rolling history of snapshots per subject. When a `RESYNC_REQUEST` arrives with the client's last known seq, and the seq is still in the ring, the server sends a targeted delta instead of a full snapshot.
98
+
**Snapshot History.** The server keeps a small rolling history of snapshots per subject (`SnapshotBoard` ring buffer). Each snapshot carries positional/animation state and optional `EmoteState` metadata (emote ID, start tick, duration, stop reason). The ring is the single source of truth for all per-peer state — there is no separate emote board.
99
+
100
+
**Intermediate snapshot scanning.** Between two simulation ticks, multiple snapshots may be published (movement, teleports, emote starts/stops). The simulation scans all intermediates from `lastSentSeq+1` to `latestSeq`, collecting the **last** of each discrete event type (teleport, emote start, emote stop). Earlier events of the same type are superseded. An emote that started and stopped in the same batch is invisible to the observer.
101
+
102
+
**Resync.** Default: always responds with `STATE_FULL`. When `Peers.ResyncWithDelta` is enabled, the server first attempts a targeted delta from the client's `knownSeq` baseline (if still in the ring), falling back to `STATE_FULL` when evicted. Configurable via `appsettings.json`, Docker env var (`Peers__ResyncWithDelta`), or GitHub manual deploy input.
99
103
100
104
**Interest management** on the server limits which players receive updates about which other players. Per-observer fan-out is the primary bandwidth concern.
101
105
@@ -113,19 +117,21 @@ Standard protobuf `optional` fields map to a plugin-generated field_mask on the
113
117
- Tiered quantization based on interest management
114
118
115
119
**EMOTE_START** (ch0, reliable)
116
-
- Emote string ID, emote type (one_shot / looping) is not transmitted, it is resolved from the DTO on the client side
120
+
- Emote string ID, optional duration_ms, and full PlayerState
121
+
- Server publishes a snapshot with `EmoteState` (emote ID, start tick, duration) — no separate emote board
117
122
- Client stops sending MovementInput while emoting
118
123
119
124
**EMOTE_STOP** (ch0, reliable)
120
-
- Looping emotes only; one-shots are terminated by the server timer
125
+
- Looping emotes only; one-shots expire via server-side time check against `EmoteState.DurationMs`
126
+
- Server publishes a stop snapshot (EmoteId=null, StopReason=Cancelled) preserving the subject's position
121
127
122
128
**TELEPORT_REQUEST** (ch0, reliable)
123
129
- Client-initiated teleport (e.g. triggered by game logic)
124
130
- Server validates and rebroadcasts as TELEPORT to all observers
125
131
126
132
**RESYNC_REQUEST** (ch0, reliable)
127
133
- Sent when a received STATE_DELTA can't be applied (gap in seq)
128
-
- Server responds with STATE_FULL
134
+
- Server responds with STATE_FULL (or targeted delta when `Peers.ResyncWithDelta` is enabled)
129
135
130
136
### Server → Client
131
137
@@ -140,12 +146,14 @@ Standard protobuf `optional` fields map to a plugin-generated field_mask on the
140
146
- Quantized floats, same ranges as MovementInput
141
147
142
148
**EMOTE_STARTED** (ch0, reliable, broadcast to interest set)
143
-
- Emote string ID, type, server_tick, and piggybacked anchor position (Vec3)
144
-
-Anchor position sent reliably because no further position updates will arrive during the emote
149
+
- Emote string ID, sequence, server_tick, and full PlayerState
150
+
-PlayerState sent reliably because no further position updates will arrive during the emote
145
151
- Observers use server_tick to scrub animation forward by transit latency
152
+
- Only the last emote start per batch is broadcast; earlier ones superseded by the latest
146
153
147
154
**EMOTE_STOPPED** (ch0, reliable, broadcast to interest set)
148
-
- Reason: completed (one-shot timer) or cancelled (client sent EMOTE_STOP)
155
+
- Reason: completed (one-shot duration expired) or cancelled (client sent EMOTE_STOP)
156
+
- Carries sequence and full PlayerState so the client can snap to the correct position on resume
149
157
- Client resumes MovementInput only after receiving this (gates resume on server clock)
150
158
151
159
**TELEPORT** (ch0, reliable, broadcast to interest set)
@@ -168,8 +176,13 @@ Standard protobuf `optional` fields map to a plugin-generated field_mask on the
168
176
169
177
**Teleport as a separate message, not an is_instant flag.** Teleports are discrete events, not a property of continuous movement. Keeping them as a dedicated reliable message guarantees the interpolation-skip instruction arrives before subsequent position updates.
170
178
171
-
**server_tick is a single unified clock** across all messages (STATE_DELTA, EMOTE_STARTED, EMOTE_STOPPED, TELEPORT). Client uses it for animation scrubbing and dead reckoning. `peer->roundTrip
172
-
` (available on both client and server via ENet) provides latency without requiring client_tick fields in packets.
179
+
**server_tick is a single unified clock** across all messages (STATE_DELTA, EMOTE_STARTED, EMOTE_STOPPED, TELEPORT). Client uses it for animation scrubbing and dead reckoning. `peer->roundTripTime` (available on both client and server via ENet) provides latency without requiring client_tick fields in packets.
180
+
181
+
**Emote state inlined into PeerSnapshot.**`EmoteState` (emote ID, start tick, duration, stop reason) is a nullable struct on `PeerSnapshot`, stored in the ring buffer alongside positional data. No separate emote board — the snapshot ring is the single source of truth. EmoteStartHandler writes emote metadata directly into the snapshot. EmoteStopHandler publishes a stop snapshot. One-shot emote expiry is computed lazily from the view's cached duration at observation time, not eagerly mutated.
182
+
183
+
**Simulation loop: scan → broadcast → stop → delta.** Each tick, the simulation scans intermediate snapshots and collects the last teleport, emote start, and emote stop. Only the final event of each type is broadcast — intermediate positions or superseded emotes are discarded. An emote that started and stopped in the same batch is invisible to the observer. Discrete events (teleport, emote start) suppress the unreliable delta for that tick to prevent baseline races. Emote stop does not suppress delta — the client needs the position update on resume.
184
+
185
+
**Protobuf optional fields = field_mask on wire.** The schema expresses intent with `optional`. The plugin generates a compact bitmask for the wire. These are the same concept at different layers — the plugin bridges them.
173
186
174
187
**Protobuf optional fields = field_mask on wire.** The schema expresses intent with `optional`. The plugin generates a compact bitmask for the wire. These are the same concept at different layers — the plugin bridges them.
0 commit comments