fix: 修复 spec 与实现的多处差异

- CombatEvent Hit 消息的 Damage 字段改为传递 configuration.DamagePerShot
- SyncSequenceTracker MoveInput streamKey 改为只按 playerId 追踪
- 登录成功后新增 AuthoritativeCombatState bootstrap 逻辑
- 更新 network-gameplay-message-types spec 字段命名以匹配实际 proto 定义
This commit is contained in:
SepComet 2026-04-07 20:42:18 +08:00
parent 2c46012800
commit b7c003f227
4 changed files with 46 additions and 7 deletions

View File

@ -41,7 +41,7 @@ namespace Network.NetworkApplication
case MessageType.MoveInput:
{
var input = MoveInput.Parser.ParseFrom(payload);
streamKey = $"input:{Normalize(sender)}:{input.PlayerId}";
streamKey = $"input:{input.PlayerId}";
sequence = input.Tick;
return true;
}

View File

@ -42,6 +42,29 @@ namespace Network.NetworkHost
}
}
public void BootstrapState(IPEndPoint remoteEndPoint, string playerId, int hp, bool isDead)
{
if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))
{
return;
}
var key = Normalize(remoteEndPoint).ToString();
lock (gate)
{
if (statesByPeer.ContainsKey(key))
{
return;
}
statesByPeer.Add(key, new ServerAuthoritativeCombatState(
Normalize(remoteEndPoint),
playerId,
hp,
isDead));
}
}
public Task HandleShootInputAsync(byte[] payload, IPEndPoint sender)
{
if (payload == null || sender == null)
@ -95,7 +118,7 @@ namespace Network.NetworkHost
EventType = CombatEventType.Hit,
AttackerId = attackerState.PlayerId,
TargetId = targetState.PlayerId,
Damage = 0,
Damage = configuration.DamagePerShot,
HitPosition = hitPosition
}, MessageType.CombatEvent);

View File

@ -284,6 +284,7 @@ namespace Network.NetworkHost
RememberPlayerId(remoteEndPoint, playerId);
SessionCoordinator.NotifyLoginSucceeded(remoteEndPoint);
BootstrapAuthoritativeMovementState(remoteEndPoint, speed);
BootstrapAuthoritativeCombatState(remoteEndPoint, playerId);
PublishMetricsSessionSnapshot(remoteEndPoint);
}
@ -382,6 +383,21 @@ namespace Network.NetworkHost
authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, speed, out _);
}
private void BootstrapAuthoritativeCombatState(IPEndPoint remoteEndPoint, string playerId)
{
if (!TryGetKnownPlayerId(remoteEndPoint, out var resolvedPlayerId))
{
return;
}
if (!authoritativeMovementCoordinator.TryGetState(remoteEndPoint, out var movementState))
{
return;
}
authoritativeCombatCoordinator.BootstrapState(remoteEndPoint, resolvedPlayerId, movementState.Hp, movementState.IsDead);
}
private void RememberPlayerId(IPEndPoint remoteEndPoint, string playerId)
{
if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId))

View File

@ -17,26 +17,26 @@ The repository SHALL keep the source protobuf schema that defines gameplay netwo
- **THEN** the checked-in generated code matches the schema contract used by client and server hosts
### 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.
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 `player_id`, `tick`, `turn_input`, and `throttle_input`; `ShootInput` MUST expose `player_id`, `tick`, `dir_x`, `dir_y`, and an optional `target_id`; `PlayerState` MUST expose `player_id`, `tick`, `acknowledged_move_tick`, `position`, `rotation`, `hp`, and `velocity`; `CombatEvent` MUST expose `tick`, `event_type`, `attacker_id`, `target_id`, `damage`, and an optional `hit_position`. 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** the message exposes `player_id`, `tick`, `turn_input`, and `throttle_input`
- **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** the message exposes `player_id`, `tick`, `dir_x`, `dir_y`, and `target_id`
- **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** the message exposes `player_id`, `tick`, `acknowledged_move_tick`, `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** the message exposes `tick`, `event_type`, `attacker_id`, `target_id`, `damage`, and `hit_position`
- **THEN** `CombatEventType` provides explicit combat-result categories for interpreting that event payload
### Requirement: Client gameplay actions use split gameplay messages directly