收口 Client 端,添加服务端约束

This commit is contained in:
SepComet
2026-03-29 15:06:17 +08:00
parent fbc09186f3
commit f6ca6fa6e4
12 changed files with 854 additions and 53 deletions
@@ -109,6 +109,133 @@ namespace Tests.EditMode.Network
Assert.That(remoteCombatPresentation.IsDead, Is.False);
}
[Test]
public void FakeTransportRoundTrip_IdleLoggedInPlayer_ReceivesPlayerState_AndCanShootWithoutMoveInput()
{
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[9000].EmitReceive(
GameplayFlowTestSupport.BuildEnvelope(
MessageType.LoginRequest,
new LoginRequest
{
PlayerId = "player-a",
Speed = 5
}),
ClientPeer);
serverTransports[9000].EmitReceive(
GameplayFlowTestSupport.BuildEnvelope(
MessageType.LoginRequest,
new LoginRequest
{
PlayerId = "player-b",
Speed = 5
}),
RemotePeer);
serverRuntime.Host.NotifyLoginStarted(ClientPeer);
serverRuntime.Host.NotifyLoginSucceeded(ClientPeer);
serverRuntime.Host.NotifyLoginStarted(RemotePeer);
serverRuntime.Host.NotifyLoginSucceeded(RemotePeer);
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender);
clientRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
Assert.That(clientHarness.TryGetState("player-a", out var idleLocalState), Is.True);
Assert.That(idleLocalState.Tick, Is.EqualTo(1));
Assert.That(idleLocalState.Position.x, Is.EqualTo(0f).Within(0.0001f));
Assert.That(idleLocalState.Position.z, Is.EqualTo(0f).Within(0.0001f));
Assert.That(idleLocalState.Hp, Is.EqualTo(100));
serverTransports[9001].ClearOutgoing();
serverTransports[9000].ClearOutgoing();
serverTransports[9001].EmitReceive(
GameplayFlowTestSupport.BuildEnvelope(
MessageType.MoveInput,
new MoveInput
{
PlayerId = "player-b",
Tick = 1,
MoveX = 1f,
MoveY = 0f
}),
RemotePeer);
serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult();
serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(100));
serverTransports[9001].ClearOutgoing();
serverTransports[9000].ClearOutgoing();
ClientGameplayInputFlow.SendShootInput(
clientRuntime.MessageManager,
"player-a",
3,
Vector3.right);
Assert.That(clientReliableTransport.SentMessages.Count, Is.EqualTo(1));
var outboundEnvelope = Envelope.Parser.ParseFrom(clientReliableTransport.SentMessages[0]);
var outboundShootInput = ShootInput.Parser.ParseFrom(outboundEnvelope.Payload);
Assert.That((MessageType)outboundEnvelope.Type, Is.EqualTo(MessageType.ShootInput));
Assert.That(outboundShootInput.TargetId, Is.EqualTo(string.Empty));
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 localMovementState), Is.True);
Assert.That(localMovementState.PlayerId, Is.EqualTo("player-a"));
Assert.That(localMovementState.LastAcceptedMoveTick, Is.EqualTo(0));
Assert.That(localMovementState.PositionX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(localMovementState.PositionZ, Is.EqualTo(0f).Within(0.0001f));
Assert.That(serverRuntime.TryGetAuthoritativeCombatState(ClientPeer, out var localCombatState), Is.True);
Assert.That(localCombatState.LastAcceptedShootTick, Is.EqualTo(3));
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-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();
@@ -138,6 +138,53 @@ namespace Tests.EditMode.Network
Assert.That(secondBroadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
}
[Test]
public void NotifyLoginSucceeded_CreatesIdleAuthoritativeState_AndBroadcastsPlayerStateWithoutMoveInput()
{
var createdTransports = new Dictionary<int, FakeTransport>();
var configuration = new ServerRuntimeConfiguration(9000)
{
SyncPort = 9001,
Dispatcher = new MainThreadNetworkDispatcher(),
TransportFactory = port => CreateTransport(createdTransports, port),
AuthoritativeMovement = new ServerAuthoritativeMovementConfiguration
{
MoveSpeed = 10f,
BroadcastInterval = TimeSpan.FromMilliseconds(50),
DefaultHp = 100
}
};
using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult();
createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.LoginRequest, new LoginRequest
{
PlayerId = "player-a",
Speed = 5
}), PeerA);
runtime.Host.NotifyLoginStarted(PeerA);
runtime.Host.NotifyLoginSucceeded(PeerA);
runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50));
Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var state), Is.True);
Assert.That(state.PlayerId, Is.EqualTo("player-a"));
Assert.That(state.LastAcceptedMoveTick, Is.EqualTo(0));
Assert.That(state.PositionX, Is.EqualTo(0f).Within(0.0001f));
Assert.That(state.PositionZ, Is.EqualTo(0f).Within(0.0001f));
Assert.That(state.Hp, Is.EqualTo(100));
Assert.That(createdTransports[9001].BroadcastMessages.Count, Is.EqualTo(1));
var broadcast = ParsePlayerState(createdTransports[9001].BroadcastMessages[0]);
Assert.That(broadcast.PlayerId, Is.EqualTo("player-a"));
Assert.That(broadcast.Tick, Is.EqualTo(1));
Assert.That(broadcast.Position.X, Is.EqualTo(0f).Within(0.0001f));
Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f));
Assert.That(broadcast.Velocity.X, Is.EqualTo(0f).Within(0.0001f));
Assert.That(broadcast.Velocity.Z, Is.EqualTo(0f).Within(0.0001f));
Assert.That(broadcast.Hp, Is.EqualTo(100));
}
[Test]
public void UpdateAuthoritativeMovement_UsesReliableLaneWhenSyncTransportIsUnavailable()
{
@@ -54,6 +54,33 @@ namespace Tests.EditMode.Network
Assert.That(shootInput.TargetId, Is.EqualTo(string.Empty));
}
[Test]
public void ClientGameplayInputFlow_TryCreateShootInput_LocalFirePathKeepsTargetOptional()
{
var created = ClientGameplayInputFlow.TryCreateShootInput(
"player-1",
21,
true,
new Vector3(2f, 0f, 0f),
out var shootInput);
var ignored = ClientGameplayInputFlow.TryCreateShootInput(
"player-1",
22,
false,
Vector3.forward,
out var ignoredShootInput);
Assert.That(created, Is.True);
Assert.That(shootInput, Is.Not.Null);
Assert.That(shootInput.PlayerId, Is.EqualTo("player-1"));
Assert.That(shootInput.Tick, Is.EqualTo(21));
Assert.That(shootInput.DirX, Is.EqualTo(1f));
Assert.That(shootInput.DirY, Is.EqualTo(0f));
Assert.That(shootInput.TargetId, Is.EqualTo(string.Empty));
Assert.That(ignored, Is.False);
Assert.That(ignoredShootInput, Is.Null);
}
[Test]
public void ClientPredictionBuffer_AuthoritativeState_PrunesAcknowledgedMoveInputs()
{