process step 6
This commit is contained in:
parent
fc8675f081
commit
101694a3b0
|
|
@ -78,8 +78,12 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(syncTransport.SendCallCount, Is.EqualTo(1));
|
Assert.That(syncTransport.SendCallCount, Is.EqualTo(1));
|
||||||
|
|
||||||
var envelope = Envelope.Parser.ParseFrom(syncTransport.LastSentData);
|
var envelope = Envelope.Parser.ParseFrom(syncTransport.LastSentData);
|
||||||
|
var parsed = MoveInput.Parser.ParseFrom(envelope.Payload);
|
||||||
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput));
|
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput));
|
||||||
Assert.That(MoveInput.Parser.ParseFrom(envelope.Payload).Tick, Is.EqualTo(12));
|
Assert.That(parsed.PlayerId, Is.EqualTo("player-1"));
|
||||||
|
Assert.That(parsed.Tick, Is.EqualTo(12));
|
||||||
|
Assert.That(parsed.MoveX, Is.EqualTo(1));
|
||||||
|
Assert.That(parsed.MoveY, Is.EqualTo(-1));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -107,8 +111,51 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(syncTransport.SendCallCount, Is.EqualTo(0));
|
Assert.That(syncTransport.SendCallCount, Is.EqualTo(0));
|
||||||
|
|
||||||
var envelope = Envelope.Parser.ParseFrom(reliableTransport.LastSentData);
|
var envelope = Envelope.Parser.ParseFrom(reliableTransport.LastSentData);
|
||||||
|
var parsed = ShootInput.Parser.ParseFrom(envelope.Payload);
|
||||||
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.ShootInput));
|
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.ShootInput));
|
||||||
Assert.That(ShootInput.Parser.ParseFrom(envelope.Payload).TargetId, Is.EqualTo("enemy-1"));
|
Assert.That(parsed.PlayerId, Is.EqualTo("player-1"));
|
||||||
|
Assert.That(parsed.Tick, Is.EqualTo(15));
|
||||||
|
Assert.That(parsed.DirX, Is.EqualTo(0.5f));
|
||||||
|
Assert.That(parsed.DirY, Is.EqualTo(1f));
|
||||||
|
Assert.That(parsed.TargetId, Is.EqualTo("enemy-1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SendMessage_PlayerState_UsesSyncLanePolicyAndPreservesAuthoritativeFields()
|
||||||
|
{
|
||||||
|
var reliableTransport = new FakeTransport();
|
||||||
|
var syncTransport = new FakeTransport();
|
||||||
|
var manager = new MessageManager(
|
||||||
|
reliableTransport,
|
||||||
|
new MainThreadNetworkDispatcher(),
|
||||||
|
new DefaultMessageDeliveryPolicyResolver(),
|
||||||
|
syncTransport);
|
||||||
|
var message = new PlayerState
|
||||||
|
{
|
||||||
|
PlayerId = "player-1",
|
||||||
|
Tick = 21,
|
||||||
|
Position = new global::Network.Defines.Vector3 { X = 3f, Y = 0f, Z = -2f },
|
||||||
|
Velocity = new global::Network.Defines.Vector3 { X = 1f, Y = 0f, Z = 0.5f },
|
||||||
|
Rotation = 90f,
|
||||||
|
Hp = 87
|
||||||
|
};
|
||||||
|
|
||||||
|
manager.SendMessage(message, MessageType.PlayerState);
|
||||||
|
|
||||||
|
Assert.That(reliableTransport.SendCallCount, Is.EqualTo(0));
|
||||||
|
Assert.That(syncTransport.SendCallCount, Is.EqualTo(1));
|
||||||
|
|
||||||
|
var envelope = Envelope.Parser.ParseFrom(syncTransport.LastSentData);
|
||||||
|
var parsed = PlayerState.Parser.ParseFrom(envelope.Payload);
|
||||||
|
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.PlayerState));
|
||||||
|
Assert.That(parsed.PlayerId, Is.EqualTo("player-1"));
|
||||||
|
Assert.That(parsed.Tick, Is.EqualTo(21));
|
||||||
|
Assert.That(parsed.Position.X, Is.EqualTo(3f));
|
||||||
|
Assert.That(parsed.Position.Z, Is.EqualTo(-2f));
|
||||||
|
Assert.That(parsed.Velocity.X, Is.EqualTo(1f));
|
||||||
|
Assert.That(parsed.Velocity.Z, Is.EqualTo(0.5f));
|
||||||
|
Assert.That(parsed.Rotation, Is.EqualTo(90f));
|
||||||
|
Assert.That(parsed.Hp, Is.EqualTo(87));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -127,7 +174,8 @@ namespace Tests.EditMode.Network
|
||||||
EventType = CombatEventType.DamageApplied,
|
EventType = CombatEventType.DamageApplied,
|
||||||
AttackerId = "player-1",
|
AttackerId = "player-1",
|
||||||
TargetId = "enemy-1",
|
TargetId = "enemy-1",
|
||||||
Damage = 7
|
Damage = 7,
|
||||||
|
HitPosition = new global::Network.Defines.Vector3 { X = 8f, Y = 0f, Z = 4f }
|
||||||
};
|
};
|
||||||
|
|
||||||
manager.SendMessage(message, MessageType.CombatEvent);
|
manager.SendMessage(message, MessageType.CombatEvent);
|
||||||
|
|
@ -136,8 +184,15 @@ namespace Tests.EditMode.Network
|
||||||
Assert.That(syncTransport.SendCallCount, Is.EqualTo(0));
|
Assert.That(syncTransport.SendCallCount, Is.EqualTo(0));
|
||||||
|
|
||||||
var envelope = Envelope.Parser.ParseFrom(reliableTransport.LastSentData);
|
var envelope = Envelope.Parser.ParseFrom(reliableTransport.LastSentData);
|
||||||
|
var parsed = CombatEvent.Parser.ParseFrom(envelope.Payload);
|
||||||
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.CombatEvent));
|
Assert.That(envelope.Type, Is.EqualTo((int)MessageType.CombatEvent));
|
||||||
Assert.That(CombatEvent.Parser.ParseFrom(envelope.Payload).Damage, Is.EqualTo(7));
|
Assert.That(parsed.Tick, Is.EqualTo(20));
|
||||||
|
Assert.That(parsed.EventType, Is.EqualTo(CombatEventType.DamageApplied));
|
||||||
|
Assert.That(parsed.AttackerId, Is.EqualTo("player-1"));
|
||||||
|
Assert.That(parsed.TargetId, Is.EqualTo("enemy-1"));
|
||||||
|
Assert.That(parsed.Damage, Is.EqualTo(7));
|
||||||
|
Assert.That(parsed.HitPosition.X, Is.EqualTo(8f));
|
||||||
|
Assert.That(parsed.HitPosition.Z, Is.EqualTo(4f));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -221,56 +276,40 @@ namespace Tests.EditMode.Network
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Receive_InvalidBytes_DoesNotBreakFollowingDispatch()
|
public void Receive_InvalidEnvelope_DoesNotThrow()
|
||||||
{
|
{
|
||||||
var transport = new FakeTransport();
|
var transport = new FakeTransport();
|
||||||
var manager = new MessageManager(transport, new MainThreadNetworkDispatcher());
|
var manager = new MessageManager(transport, new MainThreadNetworkDispatcher());
|
||||||
var handledCount = 0;
|
|
||||||
|
|
||||||
manager.RegisterHandler(MessageType.Heartbeat, (payload, sender) =>
|
Assert.DoesNotThrow(() => transport.EmitReceive(new byte[] { 1, 2, 3 }, Sender));
|
||||||
{
|
Assert.That(manager.PendingMessageCount, Is.EqualTo(0));
|
||||||
handledCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.DoesNotThrow(() => transport.EmitReceive(new byte[] { 0x01, 0x02, 0x03 }, Sender));
|
|
||||||
transport.EmitReceive(BuildEnvelope(MessageType.Heartbeat, new Heartbeat()), Sender);
|
|
||||||
|
|
||||||
Assert.That(handledCount, Is.EqualTo(0));
|
|
||||||
Assert.That(manager.PendingMessageCount, Is.EqualTo(1));
|
|
||||||
|
|
||||||
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
Assert.That(handledCount, Is.EqualTo(1));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Receive_MultipleMessages_PreserveEnqueueOrder()
|
public void Receive_UnknownMessageType_IsIgnored()
|
||||||
{
|
{
|
||||||
var transport = new FakeTransport();
|
var transport = new FakeTransport();
|
||||||
var manager = new MessageManager(transport, new MainThreadNetworkDispatcher());
|
var manager = new MessageManager(transport, new MainThreadNetworkDispatcher());
|
||||||
var handledSpeeds = new List<int>();
|
var handled = false;
|
||||||
|
|
||||||
manager.RegisterHandler(MessageType.LoginRequest, (payload, sender) =>
|
manager.RegisterHandler(MessageType.Heartbeat, (payload, sender) => handled = true);
|
||||||
{
|
|
||||||
handledSpeeds.Add(LoginRequest.Parser.ParseFrom(payload).Speed);
|
|
||||||
});
|
|
||||||
|
|
||||||
transport.EmitReceive(BuildEnvelope(MessageType.LoginRequest, new LoginRequest { PlayerId = "a", Speed = 1 }), Sender);
|
transport.EmitReceive(BuildEnvelope(unchecked((MessageType)999), new Heartbeat()), Sender);
|
||||||
transport.EmitReceive(BuildEnvelope(MessageType.LoginRequest, new LoginRequest { PlayerId = "b", Speed = 2 }), Sender);
|
|
||||||
|
|
||||||
Assert.That(handledSpeeds, Is.Empty);
|
Assert.That(handled, Is.False);
|
||||||
Assert.That(manager.PendingMessageCount, Is.EqualTo(2));
|
Assert.That(manager.PendingMessageCount, Is.EqualTo(0));
|
||||||
|
|
||||||
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
|
||||||
|
|
||||||
Assert.That(handledSpeeds, Is.EqualTo(new[] { 1, 2 }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Receive_StaleMoveInput_IsDropped()
|
public void Receive_StaleMoveInput_IsDropped()
|
||||||
{
|
{
|
||||||
var transport = new FakeTransport();
|
var transport = new FakeTransport();
|
||||||
var manager = new MessageManager(transport, new MainThreadNetworkDispatcher());
|
var manager = new MessageManager(
|
||||||
|
transport,
|
||||||
|
new MainThreadNetworkDispatcher(),
|
||||||
|
new DefaultMessageDeliveryPolicyResolver(),
|
||||||
|
syncTransport: null,
|
||||||
|
syncSequenceTracker: new SyncSequenceTracker());
|
||||||
var handledTicks = new List<long>();
|
var handledTicks = new List<long>();
|
||||||
|
|
||||||
manager.RegisterHandler(MessageType.MoveInput, (payload, sender) =>
|
manager.RegisterHandler(MessageType.MoveInput, (payload, sender) =>
|
||||||
|
|
@ -288,14 +327,19 @@ namespace Tests.EditMode.Network
|
||||||
Sender);
|
Sender);
|
||||||
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
Assert.That(handledTicks, Is.EqualTo(new long[] { 8 }));
|
Assert.That(handledTicks, Is.EqualTo(new[] { 8L }));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Receive_StalePlayerState_IsDropped()
|
public void Receive_StalePlayerState_IsDropped()
|
||||||
{
|
{
|
||||||
var transport = new FakeTransport();
|
var transport = new FakeTransport();
|
||||||
var manager = new MessageManager(transport, new MainThreadNetworkDispatcher());
|
var manager = new MessageManager(
|
||||||
|
transport,
|
||||||
|
new MainThreadNetworkDispatcher(),
|
||||||
|
new DefaultMessageDeliveryPolicyResolver(),
|
||||||
|
syncTransport: null,
|
||||||
|
syncSequenceTracker: new SyncSequenceTracker());
|
||||||
var handledTicks = new List<long>();
|
var handledTicks = new List<long>();
|
||||||
|
|
||||||
manager.RegisterHandler(MessageType.PlayerState, (payload, sender) =>
|
manager.RegisterHandler(MessageType.PlayerState, (payload, sender) =>
|
||||||
|
|
@ -313,14 +357,19 @@ namespace Tests.EditMode.Network
|
||||||
Sender);
|
Sender);
|
||||||
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
Assert.That(handledTicks, Is.EqualTo(new long[] { 8 }));
|
Assert.That(handledTicks, Is.EqualTo(new[] { 8L }));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void Receive_ShootInput_IsNotDroppedBySequenceTracker()
|
public void Receive_ShootInput_IsNotDroppedBySequenceTracker()
|
||||||
{
|
{
|
||||||
var transport = new FakeTransport();
|
var transport = new FakeTransport();
|
||||||
var manager = new MessageManager(transport, new MainThreadNetworkDispatcher());
|
var manager = new MessageManager(
|
||||||
|
transport,
|
||||||
|
new MainThreadNetworkDispatcher(),
|
||||||
|
new DefaultMessageDeliveryPolicyResolver(),
|
||||||
|
syncTransport: null,
|
||||||
|
syncSequenceTracker: new SyncSequenceTracker());
|
||||||
var handledTicks = new List<long>();
|
var handledTicks = new List<long>();
|
||||||
|
|
||||||
manager.RegisterHandler(MessageType.ShootInput, (payload, sender) =>
|
manager.RegisterHandler(MessageType.ShootInput, (payload, sender) =>
|
||||||
|
|
@ -338,7 +387,7 @@ namespace Tests.EditMode.Network
|
||||||
Sender);
|
Sender);
|
||||||
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
manager.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||||
|
|
||||||
Assert.That(handledTicks, Is.EqualTo(new long[] { 8, 6 }));
|
Assert.That(handledTicks, Is.EqualTo(new[] { 8L, 6L }));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] BuildEnvelope(MessageType type, IMessage payload)
|
private static byte[] BuildEnvelope(MessageType type, IMessage payload)
|
||||||
|
|
@ -356,10 +405,10 @@ namespace Tests.EditMode.Network
|
||||||
|
|
||||||
public byte[] LastSendToData { get; private set; }
|
public byte[] LastSendToData { get; private set; }
|
||||||
|
|
||||||
public IPEndPoint LastSendTarget { get; private set; }
|
|
||||||
|
|
||||||
public byte[] LastBroadcastData { get; private set; }
|
public byte[] LastBroadcastData { get; private set; }
|
||||||
|
|
||||||
|
public IPEndPoint LastSendTarget { get; private set; }
|
||||||
|
|
||||||
public int SendCallCount { get; private set; }
|
public int SendCallCount { get; private set; }
|
||||||
|
|
||||||
public int SendToCallCount { get; private set; }
|
public int SendToCallCount { get; private set; }
|
||||||
|
|
@ -414,4 +463,4 @@ namespace Tests.EditMode.Network
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
TODO.md
14
TODO.md
|
|
@ -79,16 +79,16 @@ Acceptance:
|
||||||
|
|
||||||
### 6. Finalize MVP Message Fields
|
### 6. Finalize MVP Message Fields
|
||||||
|
|
||||||
- [ ] Define `MoveInput` fields: `playerId`, `tick`, `moveX`, `moveY`
|
- [x] Define `MoveInput` fields: `playerId`, `tick`, `moveX`, `moveY`
|
||||||
- [ ] Define `ShootInput` fields: `playerId`, `tick`, `dirX`, `dirY`, optional `targetId`
|
- [x] Define `ShootInput` fields: `playerId`, `tick`, `dirX`, `dirY`, optional `targetId`
|
||||||
- [ ] Define `PlayerState` fields: `playerId`, `tick`, `position`, `rotation`, `hp`, optional `velocity`
|
- [x] Define `PlayerState` fields: `playerId`, `tick`, `position`, `rotation`, `hp`, optional `velocity`
|
||||||
- [ ] Define `CombatEvent` fields: `tick`, `eventType`, `attackerId`, `targetId`, `damage`, optional `hitPosition`
|
- [x] Define `CombatEvent` fields: `tick`, `eventType`, `attackerId`, `targetId`, `damage`, optional `hitPosition`
|
||||||
- [ ] Add `CombatEventType` if needed
|
- [x] Add `CombatEventType` if needed
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
- [ ] MVP gameplay data can be expressed without ad hoc payload extensions
|
- [x] MVP gameplay data can be expressed without ad hoc payload extensions
|
||||||
- [ ] Position, HP, and combat results all have explicit authoritative messages
|
- [x] Position, HP, and combat results all have explicit authoritative messages
|
||||||
|
|
||||||
### 7. Add Message Routing Tests
|
### 7. Add Message Routing Tests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-28
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The repository already has split gameplay message types in `message.proto`, and the generated `Message.cs` currently exposes most of the MVP fields listed in TODO step 6. The remaining gap is not the existence of message identities, but the lack of a formal field-level contract that says which gameplay data each message must carry for the MVP.
|
||||||
|
|
||||||
|
This matters because later gameplay, prediction, and integration work will depend on these fields being stable. If the contract stays implicit, contributors may treat fields like `hp`, `targetId`, `velocity`, or `hitPosition` as optional implementation details and drift into ad hoc payload extensions.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Define the MVP field contract for `MoveInput`, `ShootInput`, `PlayerState`, and `CombatEvent` at the spec level.
|
||||||
|
- Preserve `CombatEventType` as the explicit enum used by combat-result messages.
|
||||||
|
- Align the source protobuf schema, generated C# output, and regression tests with the same field contract.
|
||||||
|
- Keep the change minimal if the repository is already compliant.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Redesigning message routing, prediction, or transport policy.
|
||||||
|
- Introducing a new protocol version or broad schema migration.
|
||||||
|
- Expanding message payloads beyond the fields listed in TODO step 6.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Treat field shape as part of the gameplay message capability
|
||||||
|
The change will extend `network-gameplay-message-types` from message identity only to field-level requirements. This keeps type identity and payload contract in the same capability instead of scattering message semantics across unrelated specs.
|
||||||
|
|
||||||
|
Alternative considered: create a separate capability just for message payload schemas. Rejected because these fields are part of what makes the gameplay messages meaningful in the first place.
|
||||||
|
|
||||||
|
### Keep protobuf as the source of truth and generated C# as the checked-in reflection of that contract
|
||||||
|
Implementation tasks will verify `message.proto` first and only update generated `Message.cs` if the protobuf contract changes. This preserves the existing workflow where the schema is authoritative and generated output must match it.
|
||||||
|
|
||||||
|
Alternative considered: update only generated C# tests without touching the schema contract. Rejected because field-level requirements must remain anchored in the source protobuf.
|
||||||
|
|
||||||
|
### Interpret MVP optional fields using existing protobuf semantics
|
||||||
|
`targetId`, `velocity`, and `hitPosition` will remain part of the explicit contract without forcing a new transport or protocol redesign. Message-valued fields already support presence in protobuf, and string optionality can stay represented through the existing schema shape unless implementation work reveals a stricter presence requirement.
|
||||||
|
|
||||||
|
Alternative considered: require proto3 `optional` field presence immediately for all optional fields. Rejected for now because TODO step 6 only requires explicit MVP fields, not a broader presence-tracking migration.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- [Risk] The repository may already satisfy most of the contract, making the change look documentation-heavy. → Mitigation: include tasks to verify generated output and add regression tests so the spec still protects against future drift.
|
||||||
|
- [Risk] Optional-field semantics for `targetId` may remain slightly looser than full presence tracking. → Mitigation: capture the MVP requirement as field availability now and defer stricter presence semantics until there is a concrete gameplay need.
|
||||||
|
- [Risk] Regenerating `Message.cs` can create large diffs if tooling versions change. → Mitigation: only regenerate if the source protobuf actually needs edits.
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The MVP already split gameplay messages into separate message types, but TODO step 6 still needs the field-level contract to be locked down explicitly. We need to formalize the exact protobuf payload shape now so later gameplay and integration work does not drift into ad hoc payload extensions or ambiguous authoritative state semantics.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Specify the required MVP fields for `MoveInput`, `ShootInput`, `PlayerState`, and `CombatEvent` in the shared gameplay message contract.
|
||||||
|
- Require the protobuf schema and generated C# messages to continue exposing `CombatEventType` and the explicit gameplay fields needed for movement, shooting, authoritative state, and combat results.
|
||||||
|
- Add implementation tasks to verify the checked-in schema and generated output match the MVP field contract and to add regression coverage if field-level tests are missing.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- `network-gameplay-message-types`: Extend the gameplay message-type requirement from message identity only to explicit MVP field definitions for movement input, shooting input, authoritative player state, and combat events.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Affected files are expected in `Assets/Scripts/Network/Defines/message.proto`, generated `Assets/Scripts/Network/Defines/Message.cs`, and edit-mode regression tests that validate message field availability or serialization shape. This change should preserve the existing split-message design and avoid broad protocol redesign.
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
## ADDED 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`, `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`, `position`, `rotation`, `hp`, and `velocity`
|
||||||
|
- **THEN** authoritative movement and health state are expressed without ad hoc payload extensions
|
||||||
|
|
||||||
|
#### 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,14 @@
|
||||||
|
## 1. Verify the MVP protobuf field contract
|
||||||
|
|
||||||
|
- [x] 1.1 Verify `Assets/Scripts/Network/Defines/message.proto` defines the MVP fields for `MoveInput`, `ShootInput`, `PlayerState`, and `CombatEvent`, and still exposes `CombatEventType`.
|
||||||
|
- [x] 1.2 Regenerate or verify `Assets/Scripts/Network/Defines/Message.cs` so the checked-in generated C# fields match the protobuf contract.
|
||||||
|
|
||||||
|
## 2. Add regression coverage for gameplay message fields
|
||||||
|
|
||||||
|
- [x] 2.1 Add or extend edit-mode tests to prove `MoveInput` and `ShootInput` expose the expected MVP movement and shooting fields through serialization or envelope parsing.
|
||||||
|
- [x] 2.2 Add or extend edit-mode tests to prove `PlayerState`, `CombatEvent`, and `CombatEventType` expose the expected authoritative-state and combat-result fields.
|
||||||
|
|
||||||
|
## 3. Validate the finalized message contract
|
||||||
|
|
||||||
|
- [x] 3.1 Run `dotnet build Network.EditMode.Tests.csproj -v minimal`.
|
||||||
|
- [x] 3.2 Run `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`.
|
||||||
|
|
@ -14,4 +14,27 @@ The repository SHALL keep the source protobuf schema that defines gameplay netwo
|
||||||
#### Scenario: Gameplay message schema changes regenerate shared C# types
|
#### Scenario: Gameplay message schema changes regenerate shared C# types
|
||||||
- **WHEN** a contributor adds or changes `MoveInput`, `ShootInput`, `CombatEvent`, or `PlayerState` fields in the source protobuf schema
|
- **WHEN** a contributor adds or changes `MoveInput`, `ShootInput`, `CombatEvent`, or `PlayerState` fields in the source protobuf schema
|
||||||
- **THEN** the shared generated `Message.cs` output is regenerated from that schema
|
- **THEN** the shared generated `Message.cs` output is regenerated from that schema
|
||||||
- **THEN** the checked-in generated code matches the schema contract used by client and server hosts
|
- **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`, `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`, `position`, `rotation`, `hp`, and `velocity`
|
||||||
|
- **THEN** authoritative movement and health state are expressed without ad hoc payload extensions
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue