process TODO.md step 10
This commit is contained in:
parent
c5fbd8e36d
commit
fbc09186f3
|
|
@ -50,6 +50,36 @@ public static class ClientGameplayInputFlow
|
|||
TargetId = targetId ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
public static void SendShootInput(
|
||||
MessageManager messageManager,
|
||||
string playerId,
|
||||
long tick,
|
||||
Vector3 aimDirection,
|
||||
string targetId = "")
|
||||
{
|
||||
if (messageManager == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(messageManager));
|
||||
}
|
||||
|
||||
SendShootInput(messageManager, CreateShootInput(playerId, tick, aimDirection, targetId));
|
||||
}
|
||||
|
||||
public static void SendShootInput(MessageManager messageManager, ShootInput message)
|
||||
{
|
||||
if (messageManager == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(messageManager));
|
||||
}
|
||||
|
||||
if (message == null)
|
||||
{
|
||||
throw new System.ArgumentNullException(nameof(message));
|
||||
}
|
||||
|
||||
messageManager.SendMessage(message, MessageType.ShootInput);
|
||||
}
|
||||
}
|
||||
|
||||
public class MovementComponent : MonoBehaviour
|
||||
|
|
|
|||
|
|
@ -215,12 +215,13 @@ public class NetworkManager : MonoBehaviour
|
|||
|
||||
public void SendShootInput(string playerId, Vector3 direction, long tick = 0, string targetId = "")
|
||||
{
|
||||
SendShootInput(ClientGameplayInputFlow.CreateShootInput(playerId, tick, direction, targetId));
|
||||
ClientGameplayInputFlow.SendShootInput(_networkRuntime.MessageManager, playerId, tick, direction, targetId);
|
||||
Debug.Log($"PlayerShootSeq: {_sequence++}");
|
||||
}
|
||||
|
||||
public void SendShootInput(ShootInput message)
|
||||
{
|
||||
_networkRuntime.MessageManager.SendMessage(message, MessageType.ShootInput);
|
||||
ClientGameplayInputFlow.SendShootInput(_networkRuntime.MessageManager, message);
|
||||
Debug.Log($"PlayerShootSeq: {_sequence++}");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
using System.Net;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
namespace Tests.EditMode.Network
|
||||
{
|
||||
public class ClientGameplayFlowTests
|
||||
{
|
||||
private static readonly IPEndPoint Sender = new(IPAddress.Loopback, 9300);
|
||||
|
||||
[Test]
|
||||
public void ClientGameplayInputFlow_SendShootInput_UsesDedicatedGameplayPathAndReliableLane()
|
||||
{
|
||||
var reliableTransport = new GameplayFlowFakeTransport();
|
||||
var syncTransport = new GameplayFlowFakeTransport();
|
||||
var manager = new MessageManager(
|
||||
reliableTransport,
|
||||
new MainThreadNetworkDispatcher(),
|
||||
new DefaultMessageDeliveryPolicyResolver(),
|
||||
syncTransport);
|
||||
|
||||
ClientGameplayInputFlow.SendShootInput(
|
||||
manager,
|
||||
"player-1",
|
||||
17,
|
||||
new Vector3(3f, 0f, 4f),
|
||||
"enemy-1");
|
||||
|
||||
Assert.That(reliableTransport.SentMessages.Count, Is.EqualTo(1));
|
||||
Assert.That(syncTransport.SentMessages.Count, Is.EqualTo(0));
|
||||
|
||||
var envelope = Envelope.Parser.ParseFrom(reliableTransport.SentMessages[0]);
|
||||
var shootInput = ShootInput.Parser.ParseFrom(envelope.Payload);
|
||||
Assert.That((MessageType)envelope.Type, Is.EqualTo(MessageType.ShootInput));
|
||||
Assert.That(shootInput.PlayerId, Is.EqualTo("player-1"));
|
||||
Assert.That(shootInput.Tick, Is.EqualTo(17));
|
||||
Assert.That(shootInput.DirX, Is.EqualTo(0.6f).Within(0.0001f));
|
||||
Assert.That(shootInput.DirY, Is.EqualTo(0.8f).Within(0.0001f));
|
||||
Assert.That(shootInput.TargetId, Is.EqualTo("enemy-1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SharedNetworkRuntime_CombatEventReceivePath_AppliesAuthoritativeDamageAndDeath()
|
||||
{
|
||||
var transport = new GameplayFlowFakeTransport();
|
||||
var runtime = new SharedNetworkRuntime(transport, new MainThreadNetworkDispatcher());
|
||||
var harness = new ClientGameplayTestHarness("player-1");
|
||||
harness.Register(runtime.MessageManager);
|
||||
|
||||
transport.EmitReceive(
|
||||
GameplayFlowTestSupport.BuildEnvelope(
|
||||
MessageType.PlayerState,
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 10, Vector3.zero, hp: 100)),
|
||||
Sender);
|
||||
transport.EmitReceive(
|
||||
GameplayFlowTestSupport.BuildEnvelope(
|
||||
MessageType.CombatEvent,
|
||||
new CombatEvent
|
||||
{
|
||||
Tick = 11,
|
||||
EventType = CombatEventType.DamageApplied,
|
||||
AttackerId = "enemy-1",
|
||||
TargetId = "player-1",
|
||||
Damage = 35,
|
||||
HitPosition = new global::Network.Defines.Vector3 { X = 2f, Y = 0f, Z = 1f }
|
||||
}),
|
||||
Sender);
|
||||
transport.EmitReceive(
|
||||
GameplayFlowTestSupport.BuildEnvelope(
|
||||
MessageType.CombatEvent,
|
||||
new CombatEvent
|
||||
{
|
||||
Tick = 12,
|
||||
EventType = CombatEventType.Death,
|
||||
AttackerId = "enemy-1",
|
||||
TargetId = "player-1"
|
||||
}),
|
||||
Sender);
|
||||
|
||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(harness.TryGetState("player-1", out var snapshot), Is.True);
|
||||
Assert.That(snapshot.Hp, Is.EqualTo(0));
|
||||
Assert.That(harness.TryGetCombatPresentation("player-1", out var combatPresentation), Is.True);
|
||||
Assert.That(combatPresentation.HasLastEvent, Is.True);
|
||||
Assert.That(combatPresentation.LastEventType, Is.EqualTo(CombatEventType.Death));
|
||||
Assert.That(combatPresentation.IsDead, Is.True);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SharedNetworkRuntime_CombatEventReceivePath_RoutesShootRejectedToAttackerDiagnostics()
|
||||
{
|
||||
var transport = new GameplayFlowFakeTransport();
|
||||
var runtime = new SharedNetworkRuntime(transport, new MainThreadNetworkDispatcher());
|
||||
var harness = new ClientGameplayTestHarness("player-1");
|
||||
harness.Register(runtime.MessageManager);
|
||||
|
||||
transport.EmitReceive(
|
||||
GameplayFlowTestSupport.BuildEnvelope(
|
||||
MessageType.PlayerState,
|
||||
GameplayFlowTestSupport.CreatePlayerState("player-1", 20, Vector3.zero, hp: 90)),
|
||||
Sender);
|
||||
transport.EmitReceive(
|
||||
GameplayFlowTestSupport.BuildEnvelope(
|
||||
MessageType.CombatEvent,
|
||||
new CombatEvent
|
||||
{
|
||||
Tick = 21,
|
||||
EventType = CombatEventType.ShootRejected,
|
||||
AttackerId = "player-1",
|
||||
TargetId = "enemy-2"
|
||||
}),
|
||||
Sender);
|
||||
|
||||
runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(harness.TryGetState("player-1", out var snapshot), Is.True);
|
||||
Assert.That(snapshot.Hp, Is.EqualTo(90));
|
||||
Assert.That(harness.TryGetCombatPresentation("player-1", out var combatPresentation), Is.True);
|
||||
Assert.That(combatPresentation.LastEventType, Is.EqualTo(CombatEventType.ShootRejected));
|
||||
Assert.That(combatPresentation.LastDamage, Is.EqualTo(0));
|
||||
Assert.That(combatPresentation.IsDead, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ClientGameplayHarness_RemotePlayerStateFlow_RejectsStaleSnapshots_AndUsesInterpolationOrLatestClamp()
|
||||
{
|
||||
var harness = new ClientGameplayTestHarness("local-player");
|
||||
|
||||
var firstAccepted = harness.HandlePlayerState(
|
||||
GameplayFlowTestSupport.CreatePlayerState("remote-player", 10, new Vector3(0f, 0f, 0f), rotation: 0f),
|
||||
receivedAtSeconds: 0f);
|
||||
var secondAccepted = harness.HandlePlayerState(
|
||||
GameplayFlowTestSupport.CreatePlayerState("remote-player", 11, new Vector3(10f, 0f, 0f), rotation: 90f),
|
||||
receivedAtSeconds: 0.05f);
|
||||
var staleAccepted = harness.HandlePlayerState(
|
||||
GameplayFlowTestSupport.CreatePlayerState("remote-player", 9, new Vector3(99f, 0f, 0f), rotation: 180f),
|
||||
receivedAtSeconds: 0.06f);
|
||||
|
||||
var interpolated = harness.SampleRemote("remote-player", 0.125f);
|
||||
var clamped = harness.SampleRemote("remote-player", 0.35f);
|
||||
|
||||
Assert.That(firstAccepted, Is.True);
|
||||
Assert.That(secondAccepted, Is.True);
|
||||
Assert.That(staleAccepted, Is.False);
|
||||
Assert.That(harness.GetBufferedSnapshotCount("remote-player"), Is.EqualTo(1));
|
||||
Assert.That(harness.GetLatestBufferedTick("remote-player"), Is.EqualTo(11));
|
||||
Assert.That(interpolated.HasValue, Is.True);
|
||||
Assert.That(interpolated.UsedInterpolation, Is.True);
|
||||
Assert.That(interpolated.Position.x, Is.EqualTo(5f).Within(0.001f));
|
||||
Assert.That(interpolated.Rotation.eulerAngles.y, Is.EqualTo(45f).Within(0.01f));
|
||||
Assert.That(clamped.HasValue, Is.True);
|
||||
Assert.That(clamped.UsedInterpolation, Is.False);
|
||||
Assert.That(clamped.LatestSnapshot.Tick, Is.EqualTo(11));
|
||||
Assert.That(clamped.Position, Is.EqualTo(new Vector3(10f, 0f, 0f)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6a4fcc49ae3441f6bc9f251af0f1f067
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using Network.NetworkHost;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
namespace Tests.EditMode.Network
|
||||
{
|
||||
public class GameplayFlowRoundTripTests
|
||||
{
|
||||
private static readonly IPEndPoint ClientPeer = new(IPAddress.Loopback, 9401);
|
||||
private static readonly IPEndPoint RemotePeer = new(IPAddress.Loopback, 9402);
|
||||
private static readonly IPEndPoint ServerSender = new(IPAddress.Loopback, 9000);
|
||||
|
||||
[Test]
|
||||
public void FakeTransportRoundTrip_MoveInputAndShootInput_ProduceAuthoritativePlayerStateAndCombatEvent()
|
||||
{
|
||||
var clientReliableTransport = new GameplayFlowFakeTransport();
|
||||
var clientSyncTransport = new GameplayFlowFakeTransport();
|
||||
var clientRuntime = new SharedNetworkRuntime(
|
||||
clientReliableTransport,
|
||||
new MainThreadNetworkDispatcher(),
|
||||
syncTransport: clientSyncTransport);
|
||||
var clientHarness = new ClientGameplayTestHarness("player-a");
|
||||
clientHarness.Register(clientRuntime.MessageManager);
|
||||
|
||||
var serverTransports = new Dictionary<int, GameplayFlowFakeTransport>();
|
||||
var configuration = new ServerRuntimeConfiguration(9000)
|
||||
{
|
||||
SyncPort = 9001,
|
||||
Dispatcher = new MainThreadNetworkDispatcher(),
|
||||
TransportFactory = port => CreateTransport(serverTransports, port),
|
||||
AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration
|
||||
{
|
||||
MoveSpeed = 10f,
|
||||
BroadcastInterval = TimeSpan.FromMilliseconds(50),
|
||||
DefaultHp = 100
|
||||
},
|
||||
AuthoritativeCombat = new ServerAuthoritativeCombatConfiguration
|
||||
{
|
||||
DamagePerShot = 30
|
||||
}
|
||||
};
|
||||
|
||||
clientRuntime.StartAsync().GetAwaiter().GetResult();
|
||||
using var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
|
||||
|
||||
serverTransports[9001].EmitReceive(
|
||||
GameplayFlowTestSupport.BuildEnvelope(
|
||||
MessageType.MoveInput,
|
||||
new MoveInput
|
||||
{
|
||||
PlayerId = "player-b",
|
||||
Tick = 1,
|
||||
MoveX = 0f,
|
||||
MoveY = 0f
|
||||
}),
|
||||
RemotePeer);
|
||||
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
serverTransports[9001].ClearOutgoing();
|
||||
serverTransports[9000].ClearOutgoing();
|
||||
|
||||
clientRuntime.MessageManager.SendMessage(
|
||||
new MoveInput
|
||||
{
|
||||
PlayerId = "player-a",
|
||||
Tick = 1,
|
||||
MoveX = 1f,
|
||||
MoveY = 0f
|
||||
},
|
||||
MessageType.MoveInput);
|
||||
ClientGameplayInputFlow.SendShootInput(
|
||||
clientRuntime.MessageManager,
|
||||
"player-a",
|
||||
2,
|
||||
Vector3.right,
|
||||
"player-b");
|
||||
|
||||
TransferSentMessages(clientSyncTransport, serverTransports[9001], ClientPeer);
|
||||
TransferSentMessages(clientReliableTransport, serverTransports[9000], ClientPeer);
|
||||
|
||||
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
|
||||
|
||||
TransferBroadcastMessages(serverTransports[9000], clientReliableTransport, ServerSender);
|
||||
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
|
||||
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
|
||||
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeMovementState(ClientPeer, out var localServerState), Is.True);
|
||||
Assert.That(localServerState.PlayerId, Is.EqualTo("player-a"));
|
||||
Assert.That(localServerState.PositionX, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(RemotePeer, out var remoteCombatState), Is.True);
|
||||
Assert.That(remoteCombatState.PlayerId, Is.EqualTo("player-b"));
|
||||
Assert.That(remoteCombatState.Hp, Is.EqualTo(70));
|
||||
|
||||
Assert.That(clientHarness.TryGetState("player-a", out var localClientState), Is.True);
|
||||
Assert.That(localClientState.Tick, Is.EqualTo(1));
|
||||
Assert.That(localClientState.Position.x, Is.EqualTo(0.5f).Within(0.0001f));
|
||||
Assert.That(clientHarness.TryGetState("player-b", out var remoteClientState), Is.True);
|
||||
Assert.That(remoteClientState.Hp, Is.EqualTo(70));
|
||||
Assert.That(clientHarness.TryGetCombatPresentation("player-b", out var remoteCombatPresentation), Is.True);
|
||||
Assert.That(remoteCombatPresentation.LastEventType, Is.EqualTo(CombatEventType.DamageApplied));
|
||||
Assert.That(remoteCombatPresentation.LastDamage, Is.EqualTo(30));
|
||||
Assert.That(remoteCombatPresentation.IsDead, Is.False);
|
||||
}
|
||||
|
||||
private static GameplayFlowFakeTransport CreateTransport(IDictionary<int, GameplayFlowFakeTransport> serverTransports, int port)
|
||||
{
|
||||
var transport = new GameplayFlowFakeTransport();
|
||||
serverTransports.Add(port, transport);
|
||||
return transport;
|
||||
}
|
||||
|
||||
private static void TransferSentMessages(GameplayFlowFakeTransport source, GameplayFlowFakeTransport destination, IPEndPoint sender)
|
||||
{
|
||||
foreach (var payload in source.SentMessages)
|
||||
{
|
||||
destination.EmitReceive(payload, sender);
|
||||
}
|
||||
|
||||
source.ClearOutgoing();
|
||||
}
|
||||
|
||||
private static void TransferBroadcastMessages(GameplayFlowFakeTransport source, GameplayFlowFakeTransport destination, IPEndPoint sender)
|
||||
{
|
||||
foreach (var payload in source.BroadcastMessages)
|
||||
{
|
||||
destination.EmitReceive(payload, sender);
|
||||
}
|
||||
|
||||
source.ClearOutgoing();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bbe6dfdb34f64d4e895f57f7191b9407
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf;
|
||||
using Network.Defines;
|
||||
using Network.NetworkApplication;
|
||||
using Network.NetworkTransport;
|
||||
using UnityEngine;
|
||||
using Vector3 = UnityEngine.Vector3;
|
||||
|
||||
namespace Tests.EditMode.Network
|
||||
{
|
||||
internal sealed class ClientGameplayTestHarness
|
||||
{
|
||||
private readonly string localPlayerId;
|
||||
private readonly Dictionary<string, ClientAuthoritativePlayerState> states = new();
|
||||
private readonly Dictionary<string, RemotePlayerSnapshotInterpolator> remoteInterpolators = new();
|
||||
|
||||
public ClientGameplayTestHarness(string localPlayerId)
|
||||
{
|
||||
this.localPlayerId = localPlayerId ?? string.Empty;
|
||||
}
|
||||
|
||||
public void Register(MessageManager messageManager)
|
||||
{
|
||||
messageManager.RegisterHandler(MessageType.PlayerState, (payload, sender) =>
|
||||
{
|
||||
HandlePlayerState(PlayerState.Parser.ParseFrom(payload), receivedAtSeconds: 0f);
|
||||
});
|
||||
messageManager.RegisterHandler(MessageType.CombatEvent, (payload, sender) =>
|
||||
{
|
||||
HandleCombatEvent(CombatEvent.Parser.ParseFrom(payload));
|
||||
});
|
||||
}
|
||||
|
||||
public bool HandlePlayerState(PlayerState state, float receivedAtSeconds)
|
||||
{
|
||||
var owner = GetOrCreateOwner(state.PlayerId);
|
||||
var accepted = owner.TryAccept(state, out var snapshot);
|
||||
if (!accepted)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsLocalPlayer(state.PlayerId))
|
||||
{
|
||||
GetOrCreateInterpolator(state.PlayerId).TryAddSnapshot(snapshot, receivedAtSeconds);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool HandleCombatEvent(CombatEvent combatEvent)
|
||||
{
|
||||
if (!ClientCombatEventRouting.TryGetAffectedPlayerId(combatEvent, out var playerId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var owner = GetOrCreateOwner(playerId);
|
||||
return owner.TryApplyCombatEvent(combatEvent, playerId, out _, out _);
|
||||
}
|
||||
|
||||
public bool TryGetState(string playerId, out ClientAuthoritativePlayerStateSnapshot snapshot)
|
||||
{
|
||||
if (states.TryGetValue(playerId, out var owner) && owner.Current != null)
|
||||
{
|
||||
snapshot = owner.Current;
|
||||
return true;
|
||||
}
|
||||
|
||||
snapshot = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetCombatPresentation(string playerId, out ClientCombatPresentationSnapshot snapshot)
|
||||
{
|
||||
if (states.TryGetValue(playerId, out var owner))
|
||||
{
|
||||
snapshot = owner.CombatPresentation;
|
||||
return true;
|
||||
}
|
||||
|
||||
snapshot = ClientCombatPresentationSnapshot.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
public int GetBufferedSnapshotCount(string playerId)
|
||||
{
|
||||
return remoteInterpolators.TryGetValue(playerId, out var interpolator)
|
||||
? interpolator.BufferedSnapshotCount
|
||||
: 0;
|
||||
}
|
||||
|
||||
public long GetLatestBufferedTick(string playerId)
|
||||
{
|
||||
return remoteInterpolators.TryGetValue(playerId, out var interpolator)
|
||||
? interpolator.LatestBufferedTick
|
||||
: -1;
|
||||
}
|
||||
|
||||
public RemotePlayerInterpolationSample SampleRemote(string playerId, float nowSeconds)
|
||||
{
|
||||
return GetOrCreateInterpolator(playerId).Sample(nowSeconds);
|
||||
}
|
||||
|
||||
private bool IsLocalPlayer(string playerId)
|
||||
{
|
||||
return string.Equals(playerId, localPlayerId, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private ClientAuthoritativePlayerState GetOrCreateOwner(string playerId)
|
||||
{
|
||||
if (!states.TryGetValue(playerId, out var owner))
|
||||
{
|
||||
owner = new ClientAuthoritativePlayerState();
|
||||
states.Add(playerId, owner);
|
||||
}
|
||||
|
||||
return owner;
|
||||
}
|
||||
|
||||
private RemotePlayerSnapshotInterpolator GetOrCreateInterpolator(string playerId)
|
||||
{
|
||||
if (!remoteInterpolators.TryGetValue(playerId, out var interpolator))
|
||||
{
|
||||
interpolator = new RemotePlayerSnapshotInterpolator();
|
||||
remoteInterpolators.Add(playerId, interpolator);
|
||||
}
|
||||
|
||||
return interpolator;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GameplayFlowFakeTransport : ITransport
|
||||
{
|
||||
private readonly List<byte[]> sentMessages = new();
|
||||
private readonly List<byte[]> targetMessages = new();
|
||||
private readonly List<(byte[] Data, IPEndPoint Target)> targetedSends = new();
|
||||
private readonly List<byte[]> broadcastMessages = new();
|
||||
|
||||
public event Action<byte[], IPEndPoint> OnReceive;
|
||||
|
||||
public IReadOnlyList<byte[]> SentMessages => sentMessages;
|
||||
|
||||
public IReadOnlyList<byte[]> TargetMessages => targetMessages;
|
||||
|
||||
public IReadOnlyList<(byte[] Data, IPEndPoint Target)> TargetedSends => targetedSends;
|
||||
|
||||
public IReadOnlyList<byte[]> BroadcastMessages => broadcastMessages;
|
||||
|
||||
public Task StartAsync()
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
}
|
||||
|
||||
public void Send(byte[] data)
|
||||
{
|
||||
sentMessages.Add(Copy(data));
|
||||
}
|
||||
|
||||
public void SendTo(byte[] data, IPEndPoint target)
|
||||
{
|
||||
var copy = Copy(data);
|
||||
targetMessages.Add(copy);
|
||||
targetedSends.Add((copy, target));
|
||||
}
|
||||
|
||||
public void SendToAll(byte[] data)
|
||||
{
|
||||
broadcastMessages.Add(Copy(data));
|
||||
}
|
||||
|
||||
public void EmitReceive(byte[] data, IPEndPoint sender)
|
||||
{
|
||||
OnReceive?.Invoke(Copy(data), sender);
|
||||
}
|
||||
|
||||
public void ClearOutgoing()
|
||||
{
|
||||
sentMessages.Clear();
|
||||
targetMessages.Clear();
|
||||
targetedSends.Clear();
|
||||
broadcastMessages.Clear();
|
||||
}
|
||||
|
||||
private static byte[] Copy(byte[] data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var copy = new byte[data.Length];
|
||||
Array.Copy(data, copy, data.Length);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class GameplayFlowTestSupport
|
||||
{
|
||||
public static byte[] BuildEnvelope(MessageType type, IMessage payload)
|
||||
{
|
||||
return new Envelope
|
||||
{
|
||||
Type = (int)type,
|
||||
Payload = payload.ToByteString()
|
||||
}.ToByteArray();
|
||||
}
|
||||
|
||||
public static PlayerState CreatePlayerState(string playerId, long tick, Vector3 position, int hp = 100, float rotation = 0f, Vector3? velocity = null)
|
||||
{
|
||||
var resolvedVelocity = velocity ?? Vector3.zero;
|
||||
return new PlayerState
|
||||
{
|
||||
PlayerId = playerId,
|
||||
Tick = tick,
|
||||
Position = new global::Network.Defines.Vector3 { X = position.x, Y = position.y, Z = position.z },
|
||||
Velocity = new global::Network.Defines.Vector3 { X = resolvedVelocity.x, Y = resolvedVelocity.y, Z = resolvedVelocity.z },
|
||||
Rotation = rotation,
|
||||
Hp = hp
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c3db9ca1d45945ed9f5fd1b14922ca21
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
40
TODO.md
40
TODO.md
|
|
@ -31,8 +31,8 @@ Still missing for MVP:
|
|||
- [ ] Full `PlayerState` field application for rotation / HP / velocity
|
||||
- [ ] Remote-player snapshot buffering and interpolation strategy
|
||||
- [x] Explicit movement-stop handling via zero-input `MoveInput`
|
||||
- [ ] End-to-end gameplay regression coverage
|
||||
- [ ] Re-run build/test in an environment with the required .NET runtime installed
|
||||
- [x] End-to-end gameplay regression coverage
|
||||
- [x] Re-run build/test in an environment with the required .NET runtime installed
|
||||
|
||||
## Checklist
|
||||
|
||||
|
|
@ -154,33 +154,39 @@ Acceptance:
|
|||
|
||||
### 9. Expand Regression Coverage From Network Layer To Gameplay Flow
|
||||
|
||||
- [ ] Extend [`Assets/Tests/EditMode/Network/MessageManagerTests.cs`](./Assets/Tests/EditMode/Network/MessageManagerTests.cs) only as needed for lane policy regressions
|
||||
- [x] Extend [`Assets/Tests/EditMode/Network/MessageManagerTests.cs`](./Assets/Tests/EditMode/Network/MessageManagerTests.cs) only as needed for lane policy regressions
|
||||
- [x] Add tests that cover explicit zero-input movement stop behavior
|
||||
- [ ] Add tests for client `ShootInput` send routing
|
||||
- [ ] Add tests for `CombatEvent` receive/apply behavior
|
||||
- [ ] Add tests for remote `PlayerState` buffering / interpolation decisions where practical
|
||||
- [x] Add tests for client `ShootInput` send routing
|
||||
- [x] Add tests for `CombatEvent` receive/apply behavior
|
||||
- [x] Add tests for remote `PlayerState` buffering / interpolation decisions where practical
|
||||
- [x] Add tests for server-authoritative movement processing
|
||||
- [x] Add tests for server-authoritative shooting/combat result generation
|
||||
- [ ] Add at least one end-to-end fake-transport test that covers `MoveInput -> PlayerState` and `ShootInput -> CombatEvent`
|
||||
- [x] Add at least one end-to-end fake-transport test that covers `MoveInput -> PlayerState` and `ShootInput -> CombatEvent`
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [ ] MVP gameplay flow is covered beyond transport-only assertions
|
||||
- [ ] Both client single-session and server multi-session behaviors remain protected
|
||||
- [ ] Regression tests fail if movement/combat authority accidentally drifts back to the client
|
||||
- [x] MVP gameplay flow is covered beyond transport-only assertions
|
||||
- [x] Both client single-session and server multi-session behaviors remain protected
|
||||
- [x] Regression tests fail if movement/combat authority accidentally drifts back to the client
|
||||
|
||||
### 10. Re-Verify Build And Test
|
||||
|
||||
- [ ] Install or use an environment that contains the required .NET runtime for this repository
|
||||
- [ ] Run `dotnet build Network.EditMode.Tests.csproj -v minimal`
|
||||
- [ ] Run `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`
|
||||
- [ ] Record the actual result after the environment issue is resolved
|
||||
- [x] Install or use an environment that contains the required .NET runtime for this repository
|
||||
- [x] Run `dotnet build Network.EditMode.Tests.csproj -v minimal`
|
||||
- [x] Run `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`
|
||||
- [x] Record the actual result after the environment issue is resolved
|
||||
|
||||
Acceptance:
|
||||
|
||||
- [ ] Build succeeds in a runnable local environment
|
||||
- [ ] Edit-mode network tests succeed
|
||||
- [ ] New MVP gameplay regression tests succeed
|
||||
- [x] Build succeeds in a runnable local environment
|
||||
- [x] Edit-mode network tests succeed
|
||||
- [x] New MVP gameplay regression tests succeed
|
||||
|
||||
Recorded result:
|
||||
|
||||
- [x] Verified on 2026-03-29 in a local environment with .NET SDK 10.0.201 installed
|
||||
- [x] `dotnet build Network.EditMode.Tests.csproj -v minimal` succeeded with 4 non-fatal MSB3277 warning groups about `System.Net.Http` and `System.Security.Cryptography.Algorithms` assembly-version conflicts in Unity dependencies
|
||||
- [x] `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` succeeded, covering the edit-mode network and MVP gameplay regression suite
|
||||
|
||||
## Recommended Order
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-03-29
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
## Context
|
||||
|
||||
The repository already has solid shared-network regression coverage for message typing, lane policy, stale filtering, and recent server-authoritative movement/combat behavior. What remains thin is the gameplay-flow layer that joins client input send paths, client receive/apply paths, remote snapshot decisions, and at least one fake-transport round trip across the server authority loop. The main constraint is to improve confidence without turning edit-mode tests into Unity-scene integration tests or changing shared networking contracts just to satisfy test code.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Add gameplay-flow regression tests that protect the MVP contract from client input through authoritative server outputs and client-side application.
|
||||
- Keep low-level routing assertions in focused tests while moving broader gameplay expectations into runtime-level regression fixtures.
|
||||
- Reuse existing fake transports, host/runtime entry points, and explicit runtime handles where possible.
|
||||
- Introduce only minimal test seams when existing runtime surfaces cannot expose the state needed for stable assertions.
|
||||
|
||||
**Non-Goals:**
|
||||
- Rework production gameplay architecture beyond what is minimally required for observability in tests.
|
||||
- Add Unity play-mode or scene-driven end-to-end automation.
|
||||
- Change message definitions, delivery policy rules, or authority ownership as part of this testing change.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision: Keep regression coverage layered instead of expanding `MessageManagerTests`
|
||||
`MessageManagerTests` should remain responsible for lane-policy and message-routing invariants only. Gameplay send/receive/application assertions belong in higher-level edit-mode tests that exercise `NetworkManager`, authoritative client state application, interpolation buffers, and server runtime handles together.
|
||||
|
||||
Alternative considered: continue adding gameplay assertions into `MessageManagerTests`.
|
||||
- Rejected because it would blur transport-policy failures with gameplay-flow failures and make the tests harder to maintain.
|
||||
|
||||
### Decision: Prefer existing fake transports and runtime surfaces, add only narrow observability seams
|
||||
The test suite should keep using fake transports and explicit runtime handles to drive inputs and inspect outputs. If a client-side flow cannot be asserted without reaching into private state, add a narrow seam such as an inspectable snapshot buffer view or combat-application callback surface rather than introducing Unity-only dependencies into shared code.
|
||||
|
||||
Alternative considered: add broad test-only hooks or mock-heavy abstractions around the whole networking layer.
|
||||
- Rejected because it would distort the production architecture and create maintenance burden unrelated to MVP behavior.
|
||||
|
||||
### Decision: Add one explicit fake-transport gameplay round-trip that spans client and server responsibilities
|
||||
Beyond isolated unit-style tests, the suite should include at least one end-to-end fake-transport regression that proves `MoveInput -> PlayerState` and `ShootInput -> CombatEvent` still flow through the MVP authority model. This test should stay deterministic, edit-mode friendly, and limited to the shared/client runtime surfaces already used in the repository.
|
||||
|
||||
Alternative considered: rely only on separate client tests and separate server tests.
|
||||
- Rejected because it would leave the handoff between client send paths and authoritative server outputs unprotected.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Risk] Gameplay-flow tests may become brittle if they assert too many incidental details. → Mitigation: assert stable observable outcomes such as lane choice, accepted state updates, interpolation decisions, and authoritative event types rather than internal call order unless the order is part of the contract.
|
||||
- [Risk] End-to-end fake-transport tests may need extra setup and slow the suite. → Mitigation: keep the number of full-flow tests small and reuse focused fixtures/helpers.
|
||||
- [Risk] Client observability needs may tempt test-only architecture changes. → Mitigation: require every new seam to be narrow, production-safe, and useful for diagnostics as well as tests.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Add or extend edit-mode fixtures for client send/receive/application flow and remote snapshot buffering.
|
||||
2. Add one deterministic fake-transport end-to-end gameplay test that drives both movement and combat authority.
|
||||
3. Leave existing lane-policy tests in place, trimming or extending `MessageManagerTests` only where delivery-policy regressions specifically need coverage.
|
||||
4. Update TODO/OpenSpec task tracking after the regression suite protects the MVP gameplay loop.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None. The remaining work is implementation detail inside the agreed MVP gameplay surfaces.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
## Why
|
||||
|
||||
The MVP networking flow now has concrete client input, client authoritative-state application, remote interpolation, and server-authoritative movement/combat behavior, but regression coverage still skews toward transport and isolated network-layer rules. Step 9 is needed now so future changes cannot silently push gameplay authority, routing, or presentation flow back toward client-side guesswork.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a dedicated gameplay-flow regression coverage capability for edit-mode network tests.
|
||||
- Define regression expectations for client `ShootInput` send routing, client `CombatEvent` receive/apply flow, remote `PlayerState` buffering/interpolation decisions, and fake-transport end-to-end gameplay message flow.
|
||||
- Keep `MessageManagerTests` focused on lane-policy regressions only, with broader gameplay assertions moving into higher-level flow tests.
|
||||
- Require coverage for both client single-session behavior and server multi-session authority paths where the MVP gameplay loop crosses that boundary.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `gameplay-flow-regression-coverage`: Define the required regression coverage that protects the MVP gameplay loop from client input through authoritative server state/combat results and client-side application.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
Affected areas include `Assets/Tests/EditMode/Network/`, lightweight fake-transport/runtime test fixtures, and any minimal runtime/test seams needed to observe client gameplay flow without changing shared networking contracts. This change should not alter production transport policy or MVP message definitions.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Gameplay-flow regressions cover client gameplay send and receive paths
|
||||
The edit-mode regression suite SHALL cover the MVP client gameplay flow above the raw transport router, including `ShootInput` send routing and authoritative `CombatEvent` receive/apply behavior. Lane-policy assertions that belong to `MessageManager` MAY remain in `MessageManagerTests`, but gameplay-flow assertions MUST live in tests that exercise the client runtime or player-facing application path.
|
||||
|
||||
#### Scenario: Client fire intent regression proves dedicated `ShootInput` routing
|
||||
- **WHEN** the controlled client gameplay path is exercised in an edit-mode regression test for a fire action
|
||||
- **THEN** the test observes a `ShootInput` payload sent through the dedicated client shooting path
|
||||
- **THEN** any lane-policy assertion in that flow remains limited to confirming the MVP reliable-lane contract rather than replacing broader gameplay-flow coverage
|
||||
|
||||
#### Scenario: Authoritative combat event regression proves client-side application
|
||||
- **WHEN** an edit-mode regression test delivers an authoritative `CombatEvent` into the client gameplay receive path
|
||||
- **THEN** the relevant player-owned authoritative state, presentation model, or diagnostics surface reflects the authoritative hit, damage, death, or rejection result
|
||||
- **THEN** the test proves the outcome is applied from server truth rather than speculative local combat logic
|
||||
|
||||
### Requirement: Gameplay-flow regressions cover remote authoritative snapshot decisions
|
||||
The edit-mode regression suite SHALL cover the client path that buffers and consumes remote authoritative `PlayerState` snapshots, including stale rejection and interpolation/clamp behavior where practical.
|
||||
|
||||
#### Scenario: Remote interpolation regression proves buffering and stale rejection
|
||||
- **WHEN** an edit-mode regression test feeds ordered and stale remote `PlayerState` snapshots into the client remote-player path
|
||||
- **THEN** the test observes that newer authoritative snapshots enter the remote buffer while stale snapshots do not replace newer accepted state
|
||||
- **THEN** the test verifies the resulting interpolation or latest-snapshot clamp decision matches the MVP remote-presentation rules
|
||||
|
||||
### 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.
|
||||
|
||||
#### 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 combined test protects both client single-session input flow and server multi-session authoritative behavior from regression
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
## 1. Client Gameplay Flow Coverage
|
||||
|
||||
- [x] 1.1 Add or extend edit-mode client tests that prove fire intent sends `ShootInput` through the dedicated gameplay path and only retain `MessageManagerTests` assertions needed for reliable-lane policy regressions.
|
||||
- [x] 1.2 Add edit-mode regression tests that deliver authoritative `CombatEvent` messages through the client receive path and verify player-owned authoritative state, presentation, or diagnostics apply hit/damage/death/rejection results from server truth.
|
||||
- [x] 1.3 Add any minimal production-safe observability seams needed for the client gameplay tests without introducing Unity dependencies into shared networking code.
|
||||
|
||||
## 2. Snapshot And End-To-End Coverage
|
||||
|
||||
- [x] 2.1 Add edit-mode regression tests that cover remote `PlayerState` buffering, stale snapshot rejection, and interpolation-versus-clamp decisions where practical.
|
||||
- [x] 2.2 Add at least one deterministic fake-transport gameplay-flow test that drives `MoveInput -> PlayerState` through the authoritative server path.
|
||||
- [x] 2.3 Extend that fake-transport gameplay-flow coverage to also drive `ShootInput -> CombatEvent` and verify the combined flow protects client single-session input behavior plus server multi-session authority.
|
||||
|
||||
## 3. Tracking And Verification
|
||||
|
||||
- [x] 3.1 Update `TODO.md` to mark the gameplay-flow regression coverage items completed or narrowed to any remaining follow-up work.
|
||||
- [x] 3.2 Keep the new regression suite organized under `Assets/Tests/EditMode/Network/` with names and fixtures that separate lane-policy tests from higher-level gameplay-flow tests.
|
||||
- [x] 3.3 Run `dotnet build Network.EditMode.Tests.csproj -v minimal` and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`, then record the actual result in the implementation summary.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-03-29
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
## Context
|
||||
|
||||
The repository's MVP networking and gameplay-flow work is already implemented and covered by edit-mode tests, but TODO step 10 remains open until the build and test commands are re-run in an environment that actually contains the required .NET runtime. The change is intentionally narrow: it does not introduce new gameplay behavior, only a final verification pass and a recorded outcome in project tracking.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Define a repeatable verification path for the repository's edit-mode build and test commands.
|
||||
- Re-run the existing CLI commands in a runnable local environment instead of leaving the result inferred from partial or blocked attempts.
|
||||
- Record the actual outcome, including any remaining warnings, in the TODO and change tracking.
|
||||
|
||||
**Non-Goals:**
|
||||
- Introducing new runtime, gameplay, or transport behavior.
|
||||
- Expanding the regression suite beyond what step 9 already added.
|
||||
- Solving unrelated SDK or editor installation issues outside what is minimally needed to run the verification commands.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision: Treat this as a verification-only change
|
||||
This change stays focused on environment readiness, command execution, and result recording. That keeps the scope aligned with TODO step 10 and avoids reopening already-implemented gameplay work.
|
||||
|
||||
Alternative considered: Roll environment fixes and additional code cleanup into the same change. Rejected because it would blur whether a failure came from verification setup or from new functional modifications.
|
||||
|
||||
### Decision: Verify the exact documented commands
|
||||
The source of truth remains the repository commands already documented in `AGENTS.md` and `TODO.md`:
|
||||
- `dotnet build Network.EditMode.Tests.csproj -v minimal`
|
||||
- `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`
|
||||
|
||||
Alternative considered: Use ad hoc command variants or Unity editor-driven test execution. Rejected because the TODO explicitly calls for these CLI verification steps.
|
||||
|
||||
### Decision: Record warnings separately from pass/fail status
|
||||
If build and test succeed but still emit known Unity dependency warnings, the recorded result should preserve that nuance instead of flattening everything into a generic success line.
|
||||
|
||||
Alternative considered: Ignore warnings once commands pass. Rejected because the TODO asks for the actual result, not a simplified interpretation.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Environment drift] -> The runtime available on the current machine may differ from prior attempts. Mitigation: record the actual command outcome from the environment used for this change.
|
||||
- [Over-scoping] -> Verification-only work can accidentally turn into general cleanup. Mitigation: limit edits to tracking/docs unless command failures expose a clear regression that must be fixed to complete step 10.
|
||||
- [False confidence] -> A successful CLI run does not prove every Unity editor path. Mitigation: keep the scope explicit: this change verifies the documented build/test path, not all editor execution modes.
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
## Why
|
||||
|
||||
The MVP networking work is already implemented, but the TODO still requires a final build-and-test verification in a runnable local environment. This change closes that gap by making the verification step explicit, repeatable, and recorded against the current gameplay regression suite.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Define a small verification capability for running the repository's edit-mode build and test commands in an environment with the required .NET runtime.
|
||||
- Record the actual build and test result for the current MVP networking and gameplay-flow regression suite.
|
||||
- Update project tracking so TODO step 10 reflects the completed verification state and any remaining environment caveats.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `build-test-verification`: Defines the required local environment assumptions, commands, and recorded result for final MVP build/test verification.
|
||||
|
||||
### Modified Capabilities
|
||||
- None.
|
||||
|
||||
## Impact
|
||||
|
||||
Affected areas include OpenSpec tracking under `openspec/`, the root `TODO.md`, and the CLI verification path driven by `dotnet build Network.EditMode.Tests.csproj -v minimal` and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Runnable CLI verification environment
|
||||
The repository SHALL define step 10 completion in terms of a local environment that can execute the documented `dotnet build` and `dotnet test` commands for `Network.EditMode.Tests.csproj` without failing due to a missing required .NET runtime.
|
||||
|
||||
#### Scenario: Environment is suitable for verification
|
||||
- **WHEN** a maintainer performs the final MVP verification pass
|
||||
- **THEN** the environment used for that pass MUST contain the runtime needed to execute the documented CLI build and test commands
|
||||
- **AND** the verification record MUST distinguish environment readiness issues from actual build or test failures
|
||||
|
||||
### Requirement: Build and test commands are re-run and recorded
|
||||
The repository SHALL re-run the documented edit-mode CLI verification commands and record the actual outcome for the current MVP networking codebase.
|
||||
|
||||
#### Scenario: Build and test both succeed
|
||||
- **WHEN** `dotnet build Network.EditMode.Tests.csproj -v minimal` succeeds and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` succeeds
|
||||
- **THEN** project tracking MUST mark the build/test verification step complete
|
||||
- **AND** the recorded result MUST state that the edit-mode network test suite passed in the runnable environment
|
||||
|
||||
#### Scenario: Verification succeeds with warnings
|
||||
- **WHEN** the documented build and test commands succeed but emit non-fatal warnings
|
||||
- **THEN** the recorded result MUST preserve the warnings as part of the verification summary
|
||||
- **AND** the step MUST still be considered complete because the commands passed
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
## 1. Verification Environment
|
||||
|
||||
- [x] 1.1 Confirm or switch to a local environment that contains the required .NET runtime for `Network.EditMode.Tests.csproj`.
|
||||
- [x] 1.2 Re-check the documented verification commands and any required environment variables before execution.
|
||||
|
||||
## 2. CLI Verification
|
||||
|
||||
- [x] 2.1 Run `dotnet build Network.EditMode.Tests.csproj -v minimal` in the runnable environment.
|
||||
- [x] 2.2 Run `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` in the same runnable environment.
|
||||
- [x] 2.3 Capture the actual pass/fail outcome and any remaining non-fatal warnings from both commands.
|
||||
|
||||
## 3. Tracking Update
|
||||
|
||||
- [x] 3.1 Update `TODO.md` step 10 and acceptance items to reflect the real verification result.
|
||||
- [x] 3.2 Update this change's implementation tracking with the recorded verification summary so archive-ready state is explicit.
|
||||
|
||||
## Verification Summary
|
||||
|
||||
- Environment used: local machine with .NET SDK 10.0.201 and the repository's Unity project files available.
|
||||
- `dotnet build Network.EditMode.Tests.csproj -v minimal`: succeeded with 4 non-fatal MSB3277 warning groups related to `System.Net.Http` and `System.Security.Cryptography.Algorithms` Unity dependency conflicts.
|
||||
- `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal`: succeeded for the edit-mode network and MVP gameplay regression suite.
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# build-test-verification Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Define the runnable local environment and recorded CLI verification result required to close the final MVP build/test verification step.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Runnable CLI verification environment
|
||||
The repository SHALL define step 10 completion in terms of a local environment that can execute the documented `dotnet build` and `dotnet test` commands for `Network.EditMode.Tests.csproj` without failing due to a missing required .NET runtime.
|
||||
|
||||
#### Scenario: Environment is suitable for verification
|
||||
- **WHEN** a maintainer performs the final MVP verification pass
|
||||
- **THEN** the environment used for that pass MUST contain the runtime needed to execute the documented CLI build and test commands
|
||||
- **AND** the verification record MUST distinguish environment readiness issues from actual build or test failures
|
||||
|
||||
### Requirement: Build and test commands are re-run and recorded
|
||||
The repository SHALL re-run the documented edit-mode CLI verification commands and record the actual outcome for the current MVP networking codebase.
|
||||
|
||||
#### Scenario: Build and test both succeed
|
||||
- **WHEN** `dotnet build Network.EditMode.Tests.csproj -v minimal` succeeds and `dotnet test Network.EditMode.Tests.csproj --no-build -v minimal` succeeds
|
||||
- **THEN** project tracking MUST mark the build/test verification step complete
|
||||
- **AND** the recorded result MUST state that the edit-mode network test suite passed in the runnable environment
|
||||
|
||||
#### Scenario: Verification succeeds with warnings
|
||||
- **WHEN** the documented build and test commands succeed but emit non-fatal warnings
|
||||
- **THEN** the recorded result MUST preserve the warnings as part of the verification summary
|
||||
- **AND** the step MUST still be considered complete because the commands passed
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
# gameplay-flow-regression-coverage Specification
|
||||
|
||||
## Purpose
|
||||
Define the required edit-mode regression coverage that protects the MVP gameplay flow from client gameplay input through authoritative server outputs and client-side application.
|
||||
|
||||
## Requirements
|
||||
### Requirement: Gameplay-flow regressions cover client gameplay send and receive paths
|
||||
The edit-mode regression suite SHALL cover the MVP client gameplay flow above the raw transport router, including `ShootInput` send routing and authoritative `CombatEvent` receive/apply behavior. Lane-policy assertions that belong to `MessageManager` MAY remain in `MessageManagerTests`, but gameplay-flow assertions MUST live in tests that exercise the client runtime or player-facing application path.
|
||||
|
||||
#### Scenario: Client fire intent regression proves dedicated `ShootInput` routing
|
||||
- **WHEN** the controlled client gameplay path is exercised in an edit-mode regression test for a fire action
|
||||
- **THEN** the test observes a `ShootInput` payload sent through the dedicated client shooting path
|
||||
- **THEN** any lane-policy assertion in that flow remains limited to confirming the MVP reliable-lane contract rather than replacing broader gameplay-flow coverage
|
||||
|
||||
#### Scenario: Authoritative combat event regression proves client-side application
|
||||
- **WHEN** an edit-mode regression test delivers an authoritative `CombatEvent` into the client gameplay receive path
|
||||
- **THEN** the relevant player-owned authoritative state, presentation model, or diagnostics surface reflects the authoritative hit, damage, death, or rejection result
|
||||
- **THEN** the test proves the outcome is applied from server truth rather than speculative local combat logic
|
||||
|
||||
### Requirement: Gameplay-flow regressions cover remote authoritative snapshot decisions
|
||||
The edit-mode regression suite SHALL cover the client path that buffers and consumes remote authoritative `PlayerState` snapshots, including stale rejection and interpolation/clamp behavior where practical.
|
||||
|
||||
#### Scenario: Remote interpolation regression proves buffering and stale rejection
|
||||
- **WHEN** an edit-mode regression test feeds ordered and stale remote `PlayerState` snapshots into the client remote-player path
|
||||
- **THEN** the test observes that newer authoritative snapshots enter the remote buffer while stale snapshots do not replace newer accepted state
|
||||
- **THEN** the test verifies the resulting interpolation or latest-snapshot clamp decision matches the MVP remote-presentation rules
|
||||
|
||||
### 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.
|
||||
|
||||
#### 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 combined test protects both client single-session input flow and server multi-session authoritative behavior from regression
|
||||
Loading…
Reference in New Issue