This commit is contained in:
SepComet
2026-04-06 11:58:36 +08:00
parent b1b38b485e
commit aebc4011c7
84 changed files with 5796 additions and 238 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-05
@@ -0,0 +1,72 @@
## Context
当前客户端本地预测和服务端权威同步存在两个 P0 级错位。第一,客户端把 `PlayerState.Tick` 当成“服务端已确认到哪个 `MoveInput.Tick`”来修剪 prediction buffer,但服务端实际把它作为广播快照序号生成,导致客户端错误移除或错误重放输入。第二,客户端移动速度来源于 UI / 登录返回链路,服务端权威速度来源于 `ServerAuthoritativeMovementConfiguration`,两边没有统一的真相来源。
这次改动跨越 protobuf 契约、共享网络同步语义、客户端本地预测初始化、服务端登录后移动 bootstrap,以及回归测试,因此需要显式设计文档先固定决策。
## Goals / Non-Goals
**Goals:**
- 明确分离权威快照 tick 和已确认移动输入 tick。
- 让客户端 reconciliation 只依赖显式 ack move tick。
- 建立服务器确认的移动参数启动流程,让客户端预测参数与服务端权威参数共享同一真相来源。
- 用回归测试保护上述行为,避免再次回到“一个字段承载两种语义”的状态。
**Non-Goals:**
- 不在本次 P0 中解决本地 controlled player 的视觉平滑策略。
- 不在本次 P0 中重构完整的服务端 movement cadence 管理,只要求现有语义能正确对账。
- 不扩展新的移动玩法或额外状态字段,除非它们是承载 ack tick / 权威参数所必需的最小改动。
## Decisions
### 1. `PlayerState.Tick` 保留为权威快照序号,新增显式 `AcknowledgedMoveTick`
`PlayerState.Tick` 现在已经被客户端和服务端用作“最新快照 / 最新同步状态”的序号。直接把它改成 ack tick 会让 remote interpolation、stale rejection、日志语义全部混乱。更稳妥的做法是保留 `PlayerState.Tick` 作为快照序号,并新增 `AcknowledgedMoveTick` 用于本地 controlled player reconciliation。
备选方案:
- 把 `PlayerState.Tick` 改成 ack tick,并额外引入 snapshot tick。这个方案会让已有 stale rejection 和 remote snapshot buffer 全部迁移到新字段,破坏面更大。
- 继续复用单字段并靠注释区分。这个方案无法阻止后续实现再次误用,直接排除。
### 2. 服务器在生成每个 `PlayerState` 时回填该玩家最后接受的 `MoveInput.Tick`
ack tick 属于每个玩家独立的服务器权威状态,而不是整个广播循环共享状态。服务端应当从玩家的权威移动状态中读取 `LastAcceptedMoveTick`,并在构造该玩家 `PlayerState` 时填入 `AcknowledgedMoveTick`。这样客户端拿到同一条快照时,既能用 `Tick` 做 stale rejection / 插值排序,也能用 `AcknowledgedMoveTick` 做 prediction buffer 修剪。
备选方案:
- 发送独立 ack 消息。这样会增加协议复杂度和时序耦合,本次只需要在已有 `PlayerState` 中补充字段即可满足需求。
### 3. 客户端本地预测参数必须在登录成功后切换到服务器确认值
本地预测只要继续使用 UI 本地速度,而服务端继续使用配置速度,就算 ack tick 语义正确也会不断被回拉。P0 需要把移动参数所有权收回到服务器,客户端只允许在登录前临时持有候选值,登录成功后必须切换到服务器确认的移动参数后再继续长期预测。
备选方案:
- 彻底删除客户端可配置速度。这个方向可行,但会扩大 MVP 工作面;当前先保留输入渠道,只是不再允许它绕过服务器确认值成为长期预测真相。
- 仅在代码里假定两边常量相等。这个方案没有可验证的契约,不能接受。
### 4. 回归测试以“语义分离”而不是“视觉无抖动”作为 P0 验收目标
P0 的核心是让协议与 reconciliation 语义正确。测试应精确验证:
- 服务器广播 tick 增长时,ack tick 仍保持玩家最后确认输入值。
- 客户端 reconciliation 只修剪 `<= AcknowledgedMoveTick` 的输入。
- 客户端在登录成功后使用服务器确认的移动参数建立或刷新本地预测配置。
视觉平滑与误差阈值可以在后续 P2 再处理,不应混入本次验收。
## Risks / Trade-offs
- [风险] protobuf 字段变更会影响生成代码与现有消息构造路径 -> 缓解:把改动限制在 `PlayerState` 最小增量字段,并补齐协议层回归测试。
- [风险] 客户端仍可能在登录前使用本地默认速度开始预测 -> 缓解:在 bootstrap 规范中明确“长期预测必须切换到服务器确认参数”,实现阶段为未确认参数增加显式初始化路径。
- [风险] 某些现有测试默认把 `PlayerState.Tick` 视作 ack tick -> 缓解:把这些测试改成分别断言 snapshot tick 与 `AcknowledgedMoveTick`。
- [风险] 只修复语义不修复 cadence 后,极端情况下仍会看到少量位置纠正 -> 缓解:将 cadence 统一保留在后续 P1,并确保本次不会再由错账语义导致持续拉扯。
## Migration Plan
1. 更新 OpenSpec 契约与任务清单,固定 `PlayerState` 双 tick 语义与权威参数 bootstrap 规则。
2. 实现协议字段与服务端状态生成逻辑,确保服务器能填充 `AcknowledgedMoveTick`。
3. 更新客户端 reconciliation 与本地预测初始化逻辑。
4. 补齐回归测试后运行 `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`。
5. 如果发现客户端初始化阶段需要兼容旧字段,使用默认值路径临时兜底,但不改变最终权威参数所有权。
## Open Questions
- 服务器确认的移动参数是否只需要 `MoveSpeed`,还是要同时把旋转速度也纳入同一 bootstrap 契约。如果现有客户端和服务端旋转速度并非单一常量来源,实现时应一并纳入。
- 登录成功消息是否已经稳定承载移动参数;如果没有,是否需要通过初始 `PlayerState` 或其他启动消息承载该参数。该问题在实现前需要结合现有消息结构做最小改动决策。
@@ -0,0 +1,30 @@
## Why
客户端当前把 `PlayerState.Tick` 同时当作权威快照序号和已确认输入 tick 使用,而服务端广播的 `PlayerState.Tick` 实际上代表广播序号。这会让本地 prediction buffer 错误修剪与重放,持续制造可见抖动。与此同时,客户端本地预测速度与服务端权威速度来自不同来源,导致即使 tick 语义修正后,客户端轨迹仍可能系统性偏离。
## What Changes
- 为权威移动同步引入明确的已确认输入 tick 语义,禁止继续复用 `PlayerState.Tick` 同时表达广播序号和输入确认序号。
- 调整 `PlayerState` 消息契约,使权威快照同时携带快照 tick 与已确认移动输入 tick。
- 定义客户端权威移动参数启动能力,要求客户端本地预测使用服务器确认的移动参数,而不是独立 UI 本地值。
- 更新客户端 reconciliation 规则,使 prediction buffer 只按已确认移动输入 tick 修剪。
- 补充编辑模式回归覆盖,保护 ack tick / broadcast tick 分离以及权威移动参数启动流程。
## Capabilities
### New Capabilities
- `authoritative-movement-bootstrap`: 定义客户端在开始本地预测前如何接收并应用服务器确认的权威移动参数。
### Modified Capabilities
- `client-authoritative-player-state`: 本地 reconciliation 从按 `PlayerState.Tick` 对账改为按显式 ack move tick 对账。
- `network-gameplay-message-types`: `PlayerState` 消息契约新增显式已确认移动输入 tick 字段。
- `network-sync-strategy`: prediction history 修剪规则从快照 tick 改为 ack move tick。
- `server-authoritative-movement`: 服务器广播的权威状态同时暴露快照序号与最后确认的移动输入 tick。
- `gameplay-flow-regression-coverage`: 回归测试新增 ack/broadcast tick 分离与权威移动参数启动覆盖。
## Impact
- 影响共享协议与生成消息代码,包括 `PlayerState` 的字段契约。
- 影响客户端 `MovementComponent`、`ClientPredictionBuffer` 与本地预测初始化流程。
- 影响服务端权威移动协调器、登录成功后的移动参数建立、以及权威状态广播逻辑。
- 影响 edit-mode 网络回归测试与假传输端到端测试。
@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Client prediction bootstraps from server-confirmed movement parameters
The client SHALL establish controlled-player prediction parameters from server-confirmed authoritative movement settings before treating local prediction as steady-state truth. Client-local candidate values MAY exist before login succeeds, but long-lived prediction MUST switch to the server-confirmed parameters for the controlled player.
#### Scenario: Login success provides authoritative movement parameters for prediction
- **WHEN** the controlled client completes login and receives the server-confirmed movement bootstrap data
- **THEN** the client stores the authoritative movement parameters for that controlled player
- **THEN** subsequent local movement prediction uses those server-confirmed parameters instead of continuing to rely on an unrelated local UI value
### Requirement: Server-owned movement parameters remain the single gameplay authority
The server SHALL keep authoritative ownership of movement tuning used for authoritative movement resolution, and any client-visible movement parameters used for prediction MUST be derived from that server-owned configuration rather than from an independent client-only truth source.
#### Scenario: Client candidate speed does not override server movement authority
- **WHEN** a client proposes or locally configures a movement speed that differs from the server-owned movement speed
- **THEN** the server-owned movement configuration remains authoritative for gameplay resolution
- **THEN** the client's steady-state prediction parameters converge to the server-confirmed value instead of preserving the divergent local candidate
@@ -0,0 +1,11 @@
## MODIFIED Requirements
### Requirement: Local player reconciliation applies the full authoritative state by tick
The controlled client SHALL continue reconciling local prediction from authoritative `PlayerState` updates, but it MUST distinguish between the authoritative snapshot tick and the acknowledged movement-input tick carried by that snapshot. Reconciliation MUST use the acknowledged movement-input tick to prune and replay predicted movement, while continuing to apply the accepted authoritative `position` and `rotation` and keeping authoritative HP and optional velocity synchronized with the owned player-state snapshot.
#### Scenario: Local authoritative state corrects predicted presentation
- **WHEN** the controlled player accepts an authoritative `PlayerState` snapshot with snapshot tick `S` and acknowledged movement-input tick `N`
- **THEN** local reconciliation prunes or replays predicted movement using acknowledged tick `N` according to the sync strategy
- **THEN** stale rejection or snapshot ordering for authoritative state continues to use snapshot tick `S`
- **THEN** the local player's visible transform is corrected toward authoritative `position` and `rotation`
- **THEN** the local player's authoritative HP on the client matches the accepted `PlayerState`
@@ -0,0 +1,11 @@
## MODIFIED Requirements
### Requirement: Gameplay-flow regressions include a fake-transport authoritative round trip
The edit-mode regression suite SHALL include at least one deterministic fake-transport test that spans client send behavior, server-authoritative processing, and outgoing authoritative results. That round-trip regression MUST cover `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` within the same MVP gameplay-flow suite. Movement round-trip coverage MUST also prove that authoritative `PlayerState` snapshots preserve a distinct snapshot tick and acknowledged movement-input tick, and that controlled-client prediction bootstraps from server-confirmed movement parameters rather than a divergent local-only value.
#### Scenario: Fake-transport round trip preserves server authority across movement and combat
- **WHEN** an edit-mode regression test drives gameplay input through fake client/server transports and advances the server authority loop
- **THEN** the authoritative server path emits `PlayerState` snapshots in response to movement input
- **THEN** the authoritative server path emits `CombatEvent` results in response to shooting input
- **THEN** the movement assertions prove snapshot ordering and acknowledged-input reconciliation use distinct `PlayerState` fields
- **THEN** the combined test protects both client single-session input flow and server multi-session authoritative behavior from regression
@@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: Gameplay messages expose explicit MVP payload fields
The shared networking contract SHALL define the MVP payload fields for gameplay messages explicitly in the source protobuf schema and generated C# messages. `MoveInput` MUST expose `playerId`, `tick`, `moveX`, and `moveY`; `ShootInput` MUST expose `playerId`, `tick`, `dirX`, `dirY`, and an optional `targetId`; `PlayerState` MUST expose `playerId`, `tick`, `acknowledgedMoveTick`, `position`, `rotation`, `hp`, and an optional `velocity`; `CombatEvent` MUST expose `tick`, `eventType`, `attackerId`, `targetId`, `damage`, and an optional `hitPosition`. The shared contract MUST also provide `CombatEventType` so combat results use explicit event categories rather than ad hoc integer payload conventions.
#### Scenario: Movement input carries explicit movement fields
- **WHEN** client or server code constructs or parses `MoveInput`
- **THEN** the message exposes `playerId`, `tick`, `moveX`, and `moveY`
- **THEN** movement intent does not rely on an overloaded payload extension
#### Scenario: Shooting input carries explicit aim fields
- **WHEN** client or server code constructs or parses `ShootInput`
- **THEN** the message exposes `playerId`, `tick`, `dirX`, `dirY`, and `targetId`
- **THEN** shooting direction and optional target selection are represented directly in the message contract
#### Scenario: Authoritative player state carries explicit gameplay state fields
- **WHEN** client or server code constructs or parses `PlayerState`
- **THEN** the message exposes `playerId`, `tick`, `acknowledgedMoveTick`, `position`, `rotation`, `hp`, and `velocity`
- **THEN** snapshot ordering and acknowledged-input reconciliation are both expressed without ad hoc payload extensions or overloaded tick semantics
#### Scenario: Combat events carry explicit result fields and event categories
- **WHEN** client or server code constructs or parses `CombatEvent`
- **THEN** the message exposes `tick`, `eventType`, `attackerId`, `targetId`, `damage`, and `hitPosition`
- **THEN** `CombatEventType` provides explicit combat-result categories for interpreting that event payload
@@ -0,0 +1,10 @@
## MODIFIED Requirements
### Requirement: Authoritative correction prunes acknowledged prediction history
The client sync strategy SHALL reconcile local prediction against authoritative player-state updates by pruning acknowledged movement inputs at or before the acknowledged movement-input tick carried by the authoritative snapshot and only reapplying newer pending `MoveInput` messages. The snapshot tick used for stale rejection or remote interpolation MUST NOT be reused as the local prediction-acknowledgement boundary.
#### Scenario: Reconciliation removes already acknowledged movement inputs
- **WHEN** the client accepts an authoritative `PlayerState` update whose acknowledged movement-input tick is `N`
- **THEN** locally buffered predicted `MoveInput` messages with tick less than or equal to `N` are removed from the replay buffer
- **THEN** only `MoveInput` messages newer than `N` remain eligible for re-simulation
- **THEN** the client does not infer the acknowledgement boundary solely from the snapshot tick
@@ -0,0 +1,19 @@
## MODIFIED Requirements
### Requirement: Server broadcasts authoritative `PlayerState` snapshots on the sync cadence
The shared server networking path SHALL emit authoritative `PlayerState` snapshots for managed peers at a fixed cadence using the existing sync-lane message contract. Each snapshot MUST be derived from the server-owned authoritative player state and include both the authoritative snapshot tick for client stale rejection or interpolation and the last acknowledged `MoveInput.Tick` for client reconciliation. Authoritative HP changes produced by server-side combat resolution MUST be reflected in later snapshots for the affected peer.
#### Scenario: Authority update step emits sync-lane player snapshots
- **WHEN** the server reaches a configured authority broadcast cadence while one or more managed peers have authoritative player state
- **THEN** it sends `PlayerState` snapshots using the sync-lane delivery policy when a distinct sync transport exists
- **THEN** each snapshot includes the authoritative position, rotation, velocity, HP, snapshot tick, and acknowledged movement-input tick from server-owned state
#### Scenario: Combat-driven HP changes appear in later player snapshots
- **WHEN** the server applies authoritative combat damage or death to a managed peer
- **THEN** later `PlayerState` snapshots for that peer carry the updated authoritative HP value
- **THEN** clients do not need to invent or persist a separate HP truth outside authoritative server snapshots
#### Scenario: Reliable transport remains fallback when no sync transport exists
- **WHEN** the server broadcasts authoritative `PlayerState` snapshots without a dedicated sync transport
- **THEN** the shared routing path still emits `PlayerState` through the existing fallback lane behavior
- **THEN** the authoritative snapshot contract remains unchanged
@@ -0,0 +1,17 @@
## 1. Protocol And Spec Alignment
- [x] 1.1 Update the shared gameplay message schema and generated code so `PlayerState` carries an explicit acknowledged movement-input tick.
- [x] 1.2 Align OpenSpec-linked message construction and parsing paths with the new `PlayerState` field semantics.
- [x] 1.3 Define or wire the server-confirmed movement bootstrap data used by the controlled client after login succeeds.
## 2. Authoritative Movement Runtime
- [x] 2.1 Update the server authoritative movement state and broadcast builder so each `PlayerState` includes both snapshot tick and last accepted `MoveInput.Tick`.
- [x] 2.2 Update client reconciliation and prediction-buffer pruning to use the acknowledged movement-input tick instead of `PlayerState.Tick`.
- [x] 2.3 Switch controlled-client steady-state prediction parameters to the server-confirmed authoritative movement values.
## 3. Regression Coverage
- [x] 3.1 Add or update edit-mode tests that prove snapshot tick and acknowledged movement-input tick remain distinct in authoritative movement broadcasts.
- [x] 3.2 Add or update client reconciliation tests so only inputs at or before the acknowledged tick are pruned.
- [x] 3.3 Add or update gameplay-flow round-trip coverage for server-confirmed movement bootstrap and authoritative movement convergence.