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))); } } }