diff --git a/.gitignore b/.gitignore index 63acec8..ec04993 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,6 @@ crashlytics-build.properties /.dotnet /.dotnet-home + +EditMode-err.txt + diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index d9a0439..2b72319 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -1595,90 +1595,6 @@ MeshFilter: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 490537900} m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0} ---- !u!1 &520045882 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 520045884} - - component: {fileID: 520045883} - m_Layer: 0 - m_Name: GameObject - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!212 &520045883 -SpriteRenderer: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 520045882} - m_Enabled: 1 - m_CastShadows: 0 - m_ReceiveShadows: 0 - m_DynamicOccludee: 1 - m_StaticShadowCaster: 0 - m_MotionVectors: 1 - m_LightProbeUsage: 1 - m_ReflectionProbeUsage: 1 - m_RayTracingMode: 0 - m_RayTraceProcedural: 0 - m_RenderingLayerMask: 1 - m_RendererPriority: 0 - m_Materials: - - {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} - m_StaticBatchInfo: - firstSubMesh: 0 - subMeshCount: 0 - m_StaticBatchRoot: {fileID: 0} - m_ProbeAnchor: {fileID: 0} - m_LightProbeVolumeOverride: {fileID: 0} - m_ScaleInLightmap: 1 - m_ReceiveGI: 1 - m_PreserveUVs: 0 - m_IgnoreNormalsForChartDetection: 0 - m_ImportantGI: 0 - m_StitchLightmapSeams: 1 - m_SelectedEditorRenderState: 0 - m_MinimumChartSize: 4 - m_AutoUVMaxDistance: 0.5 - m_AutoUVMaxAngle: 89 - m_LightmapParameters: {fileID: 0} - m_SortingLayerID: 0 - m_SortingLayer: 0 - m_SortingOrder: 0 - m_Sprite: {fileID: 0} - m_Color: {r: 1, g: 1, b: 1, a: 1} - m_FlipX: 0 - m_FlipY: 0 - m_DrawMode: 0 - m_Size: {x: 1, y: 1} - m_AdaptiveModeThreshold: 0.5 - m_SpriteTileMode: 0 - m_WasSpriteAssigned: 0 - m_MaskInteraction: 0 - m_SpriteSortPoint: 0 ---- !u!4 &520045884 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 520045882} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: -23.49001, y: 0.5700337, z: 11.206667} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 0} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &632206104 GameObject: m_ObjectHideFlags: 0 @@ -3733,7 +3649,7 @@ Transform: m_GameObject: {fileID: 1186082399} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalPosition: {x: 0, y: 0, z: -30} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: @@ -6194,4 +6110,3 @@ SceneRoots: - {fileID: 1298430417} - {fileID: 789249236} - {fileID: 679132025} - - {fileID: 520045884} diff --git a/Assets/Scripts/ClientAuthoritativePlayerState.cs b/Assets/Scripts/ClientAuthoritativePlayerState.cs index aea0728..6e66994 100644 --- a/Assets/Scripts/ClientAuthoritativePlayerState.cs +++ b/Assets/Scripts/ClientAuthoritativePlayerState.cs @@ -118,7 +118,18 @@ public sealed class ClientAuthoritativePlayerStateSnapshot public int Hp { get; } - public Quaternion RotationQuaternion => Quaternion.Euler(0f, Rotation, 0f); + public Quaternion RotationQuaternion => Quaternion.Euler(0f, NormalizeDegrees(90f - Rotation), 0f); + + private static float NormalizeDegrees(float degrees) + { + var normalized = degrees % 360f; + if (normalized < 0f) + { + normalized += 360f; + } + + return normalized; + } } public sealed class ClientCombatPresentationSnapshot diff --git a/Assets/Scripts/MovementComponent.cs b/Assets/Scripts/MovementComponent.cs index cbec8a1..9d8bdf7 100644 --- a/Assets/Scripts/MovementComponent.cs +++ b/Assets/Scripts/MovementComponent.cs @@ -23,8 +23,8 @@ public static class ClientGameplayInputFlow { PlayerId = playerId, Tick = tick, - MoveX = input.x, - MoveY = input.z + TurnInput = -input.x, + ThrottleInput = input.z }; return true; } @@ -104,6 +104,8 @@ public class MovementComponent : MonoBehaviour { [SerializeField] private float _sendInterval = 0.05f; private Player _master; + private const float TurnSpeedDegreesPerSecond = 180f; + private const float UnityYawOffsetDegrees = 90f; private int _speed = 2; [SerializeField] private Rigidbody _rigid; private float _lastSendTime = 0; @@ -131,9 +133,10 @@ public class MovementComponent : MonoBehaviour _isControlled = isControlled; _speed = speed; _startTickOffset = serverTick; - _rigid.interpolation = RigidbodyInterpolation.Interpolate; + _rigid.interpolation = isControlled ? RigidbodyInterpolation.None : RigidbodyInterpolation.Interpolate; _rigid.isKinematic = !isControlled; _rigid.velocity = Vector3.zero; + _rigid.angularVelocity = Vector3.zero; if (serverTick != 0 && _isControlled && MainUI.Instance != null) MainUI.Instance.OnStartTickOffsetChanged(serverTick); } @@ -145,7 +148,6 @@ public class MovementComponent : MonoBehaviour var hasMovement = ClientGameplayInputFlow.HasPlanarInput(_cachedMoveInput); if (hasMovement) { - _lastAimDirection = _cachedMoveInput; _stopMessagePending = false; } else if (_wasMovingLastFrame) @@ -215,9 +217,10 @@ public class MovementComponent : MonoBehaviour } _serverPosition = snapshot.Position; - _rigid.position = Vector3.Lerp(_rigid.position, _serverPosition, _lerpRate); - _rigid.rotation = Quaternion.Slerp(_rigid.rotation, snapshot.RotationQuaternion, _lerpRate); + _rigid.position = _serverPosition; + _rigid.rotation = snapshot.RotationQuaternion; _rigid.velocity = snapshot.Velocity; + _rigid.angularVelocity = Vector3.zero; ReplayPendingInputs(replayInputs); } @@ -240,19 +243,19 @@ public class MovementComponent : MonoBehaviour private Vector3 ResolveAimDirection() { - if (ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection)) + var planarForward = Vector3.ProjectOnPlane(_rigid.transform.forward, Vector3.up); + if (ClientGameplayInputFlow.HasPlanarInput(planarForward)) { - return _lastAimDirection; + _lastAimDirection = planarForward; + return planarForward; } - var forward = _master != null ? _master.transform.forward : transform.forward; - var planarForward = new Vector3(forward.x, 0f, forward.z); - return ClientGameplayInputFlow.HasPlanarInput(planarForward) ? planarForward : Vector3.forward; + return ClientGameplayInputFlow.HasPlanarInput(_lastAimDirection) ? _lastAimDirection : ResolveHeadingForward(UnityYawToHeading(_rigid.rotation.eulerAngles.y)); } private void Simulate(Vector3 input) { - _rigid.velocity = _speed * input; + ApplyTankMovement(-input.x, input.z, Time.fixedDeltaTime); if (_isControlled) { if (MainUI.Instance != null) @@ -301,7 +304,7 @@ public class MovementComponent : MonoBehaviour { foreach (var replayInput in replayInputs) { - _rigid.position += _speed * new Vector3(replayInput.MoveX, 0f, replayInput.MoveY) * _sendInterval; + ApplyTankMovement(replayInput.TurnInput, replayInput.ThrottleInput, _sendInterval); } if (_isControlled) @@ -312,4 +315,50 @@ public class MovementComponent : MonoBehaviour } } } + + private void ApplyTankMovement(float turnInput, float throttleInput, float deltaTime) + { + if (deltaTime <= 0f) + { + _rigid.velocity = Vector3.zero; + return; + } + + var clampedTurnInput = Mathf.Clamp(turnInput, -1f, 1f); + var clampedThrottleInput = Mathf.Clamp(throttleInput, -1f, 1f); + var heading = NormalizeDegrees(UnityYawToHeading(_rigid.rotation.eulerAngles.y) + (clampedTurnInput * TurnSpeedDegreesPerSecond * deltaTime)); + _rigid.rotation = Quaternion.Euler(0f, HeadingToUnityYaw(heading), 0f); + + var forward = ResolveHeadingForward(heading); + var velocity = forward * (clampedThrottleInput * _speed); + _rigid.velocity = velocity; + _rigid.position += velocity * deltaTime; + } + + private static Vector3 ResolveHeadingForward(float headingDegrees) + { + var rotationRadians = headingDegrees * Mathf.Deg2Rad; + return new Vector3(Mathf.Cos(rotationRadians), 0f, Mathf.Sin(rotationRadians)); + } + + private static float HeadingToUnityYaw(float headingDegrees) + { + return NormalizeDegrees(UnityYawOffsetDegrees - headingDegrees); + } + + private static float UnityYawToHeading(float unityYawDegrees) + { + return NormalizeDegrees(UnityYawOffsetDegrees - unityYawDegrees); + } + + private static float NormalizeDegrees(float degrees) + { + var normalized = degrees % 360f; + if (normalized < 0f) + { + normalized += 360f; + } + + return normalized; + } } diff --git a/Assets/Scripts/Network/Defines/Message.cs b/Assets/Scripts/Network/Defines/Message.cs index 4f6f8e0..517a6cf 100644 --- a/Assets/Scripts/Network/Defines/Message.cs +++ b/Assets/Scripts/Network/Defines/Message.cs @@ -60,7 +60,7 @@ namespace Network.Defines { new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.LoginResponse), global::Network.Defines.LoginResponse.Parser, new[]{ "PlayerId", "Positions", "Speed", "ServerTick", "Result" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerJoin), global::Network.Defines.PlayerJoin.Parser, new[]{ "PlayerId", "Position" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.LogoutRequest), global::Network.Defines.LogoutRequest.Parser, new[]{ "PlayerId" }, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.MoveInput), global::Network.Defines.MoveInput.Parser, new[]{ "PlayerId", "Tick", "MoveX", "MoveY" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.MoveInput), global::Network.Defines.MoveInput.Parser, new[]{ "PlayerId", "Tick", "TurnInput", "ThrottleInput" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.ShootInput), global::Network.Defines.ShootInput.Parser, new[]{ "PlayerId", "Tick", "DirX", "DirY", "TargetId" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.CombatEvent), global::Network.Defines.CombatEvent.Parser, new[]{ "Tick", "EventType", "AttackerId", "TargetId", "Damage", "HitPosition" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Network.Defines.PlayerState), global::Network.Defines.PlayerState.Parser, new[]{ "PlayerId", "Position", "Velocity", "Rotation", "Tick", "Hp" }, null, null, null, null), @@ -1628,8 +1628,8 @@ namespace Network.Defines { public MoveInput(MoveInput other) : this() { playerId_ = other.playerId_; tick_ = other.tick_; - moveX_ = other.moveX_; - moveY_ = other.moveY_; + turnInput_ = other.turnInput_; + throttleInput_ = other.throttleInput_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -1663,27 +1663,27 @@ namespace Network.Defines { } } - /// Field number for the "move_x" field. - public const int MoveXFieldNumber = 3; - private float moveX_; + /// Field number for the "turn_input" field. + public const int TurnInputFieldNumber = 3; + private float turnInput_; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public float MoveX { - get { return moveX_; } + public float TurnInput { + get { return turnInput_; } set { - moveX_ = value; + turnInput_ = value; } } - /// Field number for the "move_y" field. - public const int MoveYFieldNumber = 4; - private float moveY_; + /// Field number for the "throttle_input" field. + public const int ThrottleInputFieldNumber = 4; + private float throttleInput_; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public float MoveY { - get { return moveY_; } + public float ThrottleInput { + get { return throttleInput_; } set { - moveY_ = value; + throttleInput_ = value; } } @@ -1704,8 +1704,8 @@ namespace Network.Defines { } if (PlayerId != other.PlayerId) return false; if (Tick != other.Tick) return false; - if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(MoveX, other.MoveX)) return false; - if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(MoveY, other.MoveY)) return false; + if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(TurnInput, other.TurnInput)) return false; + if (!pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.Equals(ThrottleInput, other.ThrottleInput)) return false; return Equals(_unknownFields, other._unknownFields); } @@ -1715,8 +1715,8 @@ namespace Network.Defines { int hash = 1; if (PlayerId.Length != 0) hash ^= PlayerId.GetHashCode(); if (Tick != 0L) hash ^= Tick.GetHashCode(); - if (MoveX != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(MoveX); - if (MoveY != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(MoveY); + if (TurnInput != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(TurnInput); + if (ThrottleInput != 0F) hash ^= pbc::ProtobufEqualityComparers.BitwiseSingleEqualityComparer.GetHashCode(ThrottleInput); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -1743,13 +1743,13 @@ namespace Network.Defines { output.WriteRawTag(16); output.WriteInt64(Tick); } - if (MoveX != 0F) { + if (TurnInput != 0F) { output.WriteRawTag(29); - output.WriteFloat(MoveX); + output.WriteFloat(TurnInput); } - if (MoveY != 0F) { + if (ThrottleInput != 0F) { output.WriteRawTag(37); - output.WriteFloat(MoveY); + output.WriteFloat(ThrottleInput); } if (_unknownFields != null) { _unknownFields.WriteTo(output); @@ -1769,13 +1769,13 @@ namespace Network.Defines { output.WriteRawTag(16); output.WriteInt64(Tick); } - if (MoveX != 0F) { + if (TurnInput != 0F) { output.WriteRawTag(29); - output.WriteFloat(MoveX); + output.WriteFloat(TurnInput); } - if (MoveY != 0F) { + if (ThrottleInput != 0F) { output.WriteRawTag(37); - output.WriteFloat(MoveY); + output.WriteFloat(ThrottleInput); } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); @@ -1793,10 +1793,10 @@ namespace Network.Defines { if (Tick != 0L) { size += 1 + pb::CodedOutputStream.ComputeInt64Size(Tick); } - if (MoveX != 0F) { + if (TurnInput != 0F) { size += 1 + 4; } - if (MoveY != 0F) { + if (ThrottleInput != 0F) { size += 1 + 4; } if (_unknownFields != null) { @@ -1817,11 +1817,11 @@ namespace Network.Defines { if (other.Tick != 0L) { Tick = other.Tick; } - if (other.MoveX != 0F) { - MoveX = other.MoveX; + if (other.TurnInput != 0F) { + TurnInput = other.TurnInput; } - if (other.MoveY != 0F) { - MoveY = other.MoveY; + if (other.ThrottleInput != 0F) { + ThrottleInput = other.ThrottleInput; } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -1851,11 +1851,11 @@ namespace Network.Defines { break; } case 29: { - MoveX = input.ReadFloat(); + TurnInput = input.ReadFloat(); break; } case 37: { - MoveY = input.ReadFloat(); + ThrottleInput = input.ReadFloat(); break; } } @@ -1886,11 +1886,11 @@ namespace Network.Defines { break; } case 29: { - MoveX = input.ReadFloat(); + TurnInput = input.ReadFloat(); break; } case 37: { - MoveY = input.ReadFloat(); + ThrottleInput = input.ReadFloat(); break; } } diff --git a/Assets/Scripts/Network/Defines/MessageType.cs b/Assets/Scripts/Network/Defines/MessageType.cs index e4bfd66..eac0836 100644 --- a/Assets/Scripts/Network/Defines/MessageType.cs +++ b/Assets/Scripts/Network/Defines/MessageType.cs @@ -4,34 +4,18 @@ namespace Network.Defines { Unknow = 0, - // Gameplay + // Canonical dedicated-server MVP runtime vocabulary. MoveInput = 1, PlayerState = 2, ShootInput = 3, CombatEvent = 4, PlayerJoin = 5, - PlayerLeave = 6, - PlayerAction = 7, - GameState = 8, - // Chat - ChatMessage = 10, - PrivateMessage = 11, - SystemMessage = 12, - - // Session - HeartBeat = 20, LoginRequest = 21, LoginResponse = 22, LogoutRequest = 23, - // Room management - CreateRoom = 30, - JoinRoom = 31, - LeaveRoom = 32, - RoomList = 33, - Heartbeat = 40, HeartbeatResponse = 41, } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Network/Defines/message.proto b/Assets/Scripts/Network/Defines/message.proto index 510a57e..fbf877f 100644 --- a/Assets/Scripts/Network/Defines/message.proto +++ b/Assets/Scripts/Network/Defines/message.proto @@ -39,8 +39,8 @@ message LogoutRequest { message MoveInput { string player_id = 1; int64 tick = 2; - float move_x = 3; - float move_y = 4; + float turn_input = 3; + float throttle_input = 4; } message ShootInput { diff --git a/Assets/Scripts/Network/NetworkApplication/MainThreadNetworkDispatcher.cs.meta b/Assets/Scripts/Network/NetworkApplication/MainThreadNetworkDispatcher.cs.meta index edfdef0..407f6de 100644 --- a/Assets/Scripts/Network/NetworkApplication/MainThreadNetworkDispatcher.cs.meta +++ b/Assets/Scripts/Network/NetworkApplication/MainThreadNetworkDispatcher.cs.meta @@ -1,4 +1,4 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 3dc7b1ecbad541ea86a9d700dd5148e0 MonoImporter: externalObjects: {} diff --git a/Assets/Scripts/Network/NetworkApplication/MultiSessionManager.cs b/Assets/Scripts/Network/NetworkApplication/MultiSessionManager.cs index c45f526..227c207 100644 --- a/Assets/Scripts/Network/NetworkApplication/MultiSessionManager.cs +++ b/Assets/Scripts/Network/NetworkApplication/MultiSessionManager.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -87,9 +87,10 @@ namespace Network.NetworkApplication if (sessionManager.State == ConnectionState.Disconnected) { sessionManager.NotifyTransportConnected(); + return; } - sessionManager.NotifyInboundActivity(); + sessionManager.NotifyTransportActivity(); } public void NotifyTransportConnected(IPEndPoint remoteEndPoint) diff --git a/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs b/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs index aa23760..b2f0f6f 100644 --- a/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs +++ b/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs @@ -60,7 +60,8 @@ namespace Network.NetworkApplication SyncSequenceTracker syncSequenceTracker = null, Func transportFactory = null, ServerAuthoritativeMovementConfiguration authoritativeMovement = null, - ServerAuthoritativeCombatConfiguration authoritativeCombat = null) + ServerAuthoritativeCombatConfiguration authoritativeCombat = null, + IAuthoritativeMovementWorldValidator authoritativeMovementWorldValidator = null) { ValidateDualPortConfiguration(reliablePort, syncPort); @@ -86,7 +87,8 @@ namespace Network.NetworkApplication deliveryPolicyResolver, syncSequenceTracker, authoritativeMovement, - authoritativeCombat); + authoritativeCombat, + authoritativeMovementWorldValidator ?? PermissiveAuthoritativeMovementWorldValidator.Instance); } public static Task StartServerRuntimeAsync(ServerRuntimeConfiguration configuration) diff --git a/Assets/Scripts/Network/NetworkApplication/SessionManager.cs b/Assets/Scripts/Network/NetworkApplication/SessionManager.cs index c3b8e77..7238329 100644 --- a/Assets/Scripts/Network/NetworkApplication/SessionManager.cs +++ b/Assets/Scripts/Network/NetworkApplication/SessionManager.cs @@ -1,11 +1,12 @@ -using System; +using System; namespace Network.NetworkApplication { public sealed class SessionManager { private readonly Func utcNowProvider; - private DateTimeOffset? lastLivenessUtc; + private DateTimeOffset? lastAcceptedLivenessUtc; + private DateTimeOffset? lastTransportActivityUtc; private DateTimeOffset? lastHeartbeatSentUtc; private DateTimeOffset? nextReconnectAtUtc; @@ -24,7 +25,9 @@ namespace Network.NetworkApplication public SessionReconnectPolicy ReconnectPolicy { get; } - public DateTimeOffset? LastLivenessUtc => lastLivenessUtc; + public DateTimeOffset? LastLivenessUtc => lastAcceptedLivenessUtc; + + public DateTimeOffset? LastTransportActivityUtc => lastTransportActivityUtc; public DateTimeOffset? LastHeartbeatSentUtc => lastHeartbeatSentUtc; @@ -70,13 +73,18 @@ namespace Network.NetworkApplication public void NotifyTransportConnected() { var now = utcNowProvider(); - lastLivenessUtc = now; + lastTransportActivityUtc = now; lastHeartbeatSentUtc = null; nextReconnectAtUtc = null; LastFailureReason = null; TransitionTo(ConnectionState.TransportConnected, SessionEventKind.TransportConnected, now); } + public void NotifyTransportActivity() + { + lastTransportActivityUtc = utcNowProvider(); + } + public void NotifyLoginStarted() { TransitionTo(ConnectionState.LoginPending, SessionEventKind.LoginStarted, utcNowProvider()); @@ -85,7 +93,8 @@ namespace Network.NetworkApplication public void NotifyLoginSucceeded() { var now = utcNowProvider(); - lastLivenessUtc = now; + lastTransportActivityUtc = now; + lastAcceptedLivenessUtc = now; LastFailureReason = null; TransitionTo(ConnectionState.LoggedIn, SessionEventKind.LoginSucceeded, now); } @@ -105,7 +114,8 @@ namespace Network.NetworkApplication public void NotifyHeartbeatReceived() { var now = utcNowProvider(); - lastLivenessUtc = now; + lastTransportActivityUtc = now; + lastAcceptedLivenessUtc = now; if (lastHeartbeatSentUtc.HasValue) { LastRoundTripTime = now - lastHeartbeatSentUtc.Value; @@ -116,7 +126,9 @@ namespace Network.NetworkApplication public void NotifyInboundActivity() { - lastLivenessUtc = utcNowProvider(); + var now = utcNowProvider(); + lastTransportActivityUtc = now; + lastAcceptedLivenessUtc = now; } public void NotifyTransportDisconnected(string reason = null) @@ -152,17 +164,18 @@ namespace Network.NetworkApplication private bool ShouldTimeout(DateTimeOffset now) { - if (State != ConnectionState.TransportConnected && State != ConnectionState.LoginPending && State != ConnectionState.LoggedIn) + switch (State) { - return false; + case ConnectionState.TransportConnected: + case ConnectionState.LoginPending: + return lastTransportActivityUtc.HasValue && + now - lastTransportActivityUtc.Value >= ReconnectPolicy.HeartbeatTimeout; + case ConnectionState.LoggedIn: + return lastAcceptedLivenessUtc.HasValue && + now - lastAcceptedLivenessUtc.Value >= ReconnectPolicy.HeartbeatTimeout; + default: + return false; } - - if (!lastLivenessUtc.HasValue) - { - return false; - } - - return now - lastLivenessUtc.Value >= ReconnectPolicy.HeartbeatTimeout; } private void TransitionTo( diff --git a/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs new file mode 100644 index 0000000..c7eae16 --- /dev/null +++ b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs @@ -0,0 +1,56 @@ +using System; +using System.Net; + +namespace Network.NetworkHost +{ + public sealed class AuthoritativeMovementWorldValidationRequest + { + public AuthoritativeMovementWorldValidationRequest( + IPEndPoint remoteEndPoint, + string playerId, + float currentPositionX, + float currentPositionY, + float currentPositionZ, + float candidatePositionX, + float candidatePositionY, + float candidatePositionZ, + float velocityX, + float velocityY, + float velocityZ) + { + RemoteEndPoint = remoteEndPoint ?? throw new ArgumentNullException(nameof(remoteEndPoint)); + PlayerId = playerId ?? throw new ArgumentNullException(nameof(playerId)); + CurrentPositionX = currentPositionX; + CurrentPositionY = currentPositionY; + CurrentPositionZ = currentPositionZ; + CandidatePositionX = candidatePositionX; + CandidatePositionY = candidatePositionY; + CandidatePositionZ = candidatePositionZ; + VelocityX = velocityX; + VelocityY = velocityY; + VelocityZ = velocityZ; + } + + public IPEndPoint RemoteEndPoint { get; } + + public string PlayerId { get; } + + public float CurrentPositionX { get; } + + public float CurrentPositionY { get; } + + public float CurrentPositionZ { get; } + + public float CandidatePositionX { get; } + + public float CandidatePositionY { get; } + + public float CandidatePositionZ { get; } + + public float VelocityX { get; } + + public float VelocityY { get; } + + public float VelocityZ { get; } + } +} diff --git a/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs.meta b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs.meta new file mode 100644 index 0000000..5d77096 --- /dev/null +++ b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 44fc9b6a7ea518c419516af9014d6a46 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs new file mode 100644 index 0000000..93f8cc4 --- /dev/null +++ b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs @@ -0,0 +1,22 @@ +namespace Network.NetworkHost +{ + public readonly struct AuthoritativeMovementWorldValidationResult + { + private AuthoritativeMovementWorldValidationResult(bool isAllowed) + { + IsAllowed = isAllowed; + } + + public bool IsAllowed { get; } + + public static AuthoritativeMovementWorldValidationResult Allow() + { + return new AuthoritativeMovementWorldValidationResult(true); + } + + public static AuthoritativeMovementWorldValidationResult Reject() + { + return new AuthoritativeMovementWorldValidationResult(false); + } + } +} diff --git a/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs.meta b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs.meta new file mode 100644 index 0000000..1b4c5d2 --- /dev/null +++ b/Assets/Scripts/Network/NetworkHost/AuthoritativeMovementWorldValidationResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4838ecf8df980f8498d225a15ab84df3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs b/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs new file mode 100644 index 0000000..e435a8c --- /dev/null +++ b/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs @@ -0,0 +1,7 @@ +namespace Network.NetworkHost +{ + public interface IAuthoritativeMovementWorldValidator + { + AuthoritativeMovementWorldValidationResult Validate(AuthoritativeMovementWorldValidationRequest request); + } +} diff --git a/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs.meta b/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs.meta new file mode 100644 index 0000000..dd3785c --- /dev/null +++ b/Assets/Scripts/Network/NetworkHost/IAuthoritativeMovementWorldValidator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b6ae924c07d515d44b00298c08c121b2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs b/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs new file mode 100644 index 0000000..5988ba8 --- /dev/null +++ b/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs @@ -0,0 +1,23 @@ +using System; + +namespace Network.NetworkHost +{ + public sealed class PermissiveAuthoritativeMovementWorldValidator : IAuthoritativeMovementWorldValidator + { + private PermissiveAuthoritativeMovementWorldValidator() + { + } + + public static PermissiveAuthoritativeMovementWorldValidator Instance { get; } = new PermissiveAuthoritativeMovementWorldValidator(); + + public AuthoritativeMovementWorldValidationResult Validate(AuthoritativeMovementWorldValidationRequest request) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + return AuthoritativeMovementWorldValidationResult.Allow(); + } + } +} diff --git a/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs.meta b/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs.meta new file mode 100644 index 0000000..6a9635e --- /dev/null +++ b/Assets/Scripts/Network/NetworkHost/PermissiveAuthoritativeMovementWorldValidator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 251269ca4373a134b955c8a2a5786f95 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs index 0ffbd29..43b1921 100644 --- a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs +++ b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeCombatCoordinator.cs @@ -11,16 +11,19 @@ namespace Network.NetworkHost internal sealed class ServerAuthoritativeCombatCoordinator { private readonly object gate = new(); + private readonly ServerNetworkHost host; private readonly MessageManager messageManager; private readonly ServerAuthoritativeMovementCoordinator movementCoordinator; private readonly Dictionary statesByPeer = new(); private readonly ServerAuthoritativeCombatConfiguration configuration; public ServerAuthoritativeCombatCoordinator( + ServerNetworkHost host, MessageManager messageManager, ServerAuthoritativeMovementCoordinator movementCoordinator, ServerAuthoritativeCombatConfiguration configuration) { + this.host = host ?? throw new ArgumentNullException(nameof(host)); this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager)); this.movementCoordinator = movementCoordinator ?? throw new ArgumentNullException(nameof(movementCoordinator)); this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); @@ -56,13 +59,13 @@ namespace Network.NetworkHost return Task.CompletedTask; } - if (!TryValidateAcceptedShot(input, sender, out var attackerState, out var targetState)) + if (!TryValidateAcceptedShot(input, sender, out var acceptedPeer, out var attackerState, out var targetState)) { BroadcastRejectedShot(input); return Task.CompletedTask; } - movementCoordinator.TryUpdateState(sender, state => + movementCoordinator.TryUpdateState(acceptedPeer, state => { state.LastAcceptedShootTick = input.Tick; }, out attackerState); @@ -85,6 +88,7 @@ namespace Network.NetworkHost Z = targetState.PositionZ }; + // Keep the coordinator as the only gameplay-relevant authoritative combat-result emitter. messageManager.BroadcastMessage(new CombatEvent { Tick = input.Tick, @@ -157,16 +161,19 @@ namespace Network.NetworkHost private bool TryValidateAcceptedShot( ShootInput input, IPEndPoint sender, + out IPEndPoint acceptedPeer, out ServerAuthoritativeMovementState attackerState, out ServerAuthoritativeMovementState targetState) { + acceptedPeer = null; attackerState = null; targetState = null; if (input == null || string.IsNullOrWhiteSpace(input.PlayerId) || !IsFinite(input.DirX) || - !IsFinite(input.DirY)) + !IsFinite(input.DirY) || + !host.TryResolveAcceptedPeer(sender, input.PlayerId, out acceptedPeer)) { return false; } @@ -177,8 +184,8 @@ namespace Network.NetworkHost return false; } - if (!movementCoordinator.TryGetState(sender, out attackerState) && - !movementCoordinator.EnsureState(sender, input.PlayerId, out attackerState)) + if (!movementCoordinator.TryGetState(acceptedPeer, out attackerState) && + !movementCoordinator.EnsureState(acceptedPeer, input.PlayerId, out attackerState)) { return false; } @@ -191,7 +198,12 @@ namespace Network.NetworkHost return false; } - return TryResolveTargetState(input, attackerState, out targetState); + if (!TryResolveTargetState(input, attackerState, out targetState)) + { + return false; + } + + return host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId); } private bool TryResolveTargetState( diff --git a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementConfiguration.cs b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementConfiguration.cs index 2c97ee9..95632da 100644 --- a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementConfiguration.cs +++ b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementConfiguration.cs @@ -6,6 +6,8 @@ namespace Network.NetworkHost { public float MoveSpeed { get; set; } = 5f; + public float TurnSpeedDegreesPerSecond { get; set; } = 180f; + public TimeSpan BroadcastInterval { get; set; } = TimeSpan.FromMilliseconds(50); public int DefaultHp { get; set; } = 100; @@ -17,6 +19,11 @@ namespace Network.NetworkHost throw new ArgumentOutOfRangeException(nameof(MoveSpeed), "Move speed must be finite and non-negative."); } + if (float.IsNaN(TurnSpeedDegreesPerSecond) || float.IsInfinity(TurnSpeedDegreesPerSecond) || TurnSpeedDegreesPerSecond < 0f) + { + throw new ArgumentOutOfRangeException(nameof(TurnSpeedDegreesPerSecond), "Turn speed must be finite and non-negative."); + } + if (BroadcastInterval <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(BroadcastInterval), "Broadcast interval must be positive."); diff --git a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.cs b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.cs index 2432ba0..8dbcd3e 100644 --- a/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.cs +++ b/Assets/Scripts/Network/NetworkHost/ServerAuthoritativeMovementCoordinator.cs @@ -14,6 +14,7 @@ namespace Network.NetworkHost private readonly MessageManager messageManager; private readonly ServerNetworkHost host; private readonly ServerAuthoritativeMovementConfiguration configuration; + private readonly IAuthoritativeMovementWorldValidator worldValidator; private readonly Dictionary statesByPeer = new(); private long nextBroadcastTick = 1; private TimeSpan accumulatedBroadcastTime; @@ -21,11 +22,13 @@ namespace Network.NetworkHost public ServerAuthoritativeMovementCoordinator( ServerNetworkHost host, MessageManager messageManager, - ServerAuthoritativeMovementConfiguration configuration) + ServerAuthoritativeMovementConfiguration configuration, + IAuthoritativeMovementWorldValidator worldValidator) { this.host = host ?? throw new ArgumentNullException(nameof(host)); this.messageManager = messageManager ?? throw new ArgumentNullException(nameof(messageManager)); this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + this.worldValidator = worldValidator ?? throw new ArgumentNullException(nameof(worldValidator)); } public IReadOnlyList States @@ -48,7 +51,7 @@ namespace Network.NetworkHost throw new ArgumentNullException(nameof(remoteEndPoint)); } - if (string.IsNullOrWhiteSpace(playerId)) + if (string.IsNullOrWhiteSpace(playerId) || !host.IsAcceptedPlayer(remoteEndPoint, playerId)) { state = null; return false; @@ -99,13 +102,14 @@ namespace Network.NetworkHost } if (string.IsNullOrWhiteSpace(input.PlayerId) || - !IsFinite(input.MoveX) || - !IsFinite(input.MoveY)) + !IsFinite(input.TurnInput) || + !IsFinite(input.ThrottleInput) || + !host.TryResolveAcceptedPeer(sender, input.PlayerId, out var acceptedPeer)) { return Task.CompletedTask; } - var normalizedSender = Normalize(sender); + var normalizedSender = Normalize(acceptedPeer); var key = normalizedSender.ToString(); lock (gate) @@ -113,7 +117,8 @@ namespace Network.NetworkHost if (statesByPeer.TryGetValue(key, out var existingState)) { if (!string.Equals(existingState.PlayerId, input.PlayerId, StringComparison.Ordinal) || - input.Tick <= existingState.LastAcceptedMoveTick) + input.Tick <= existingState.LastAcceptedMoveTick || + !host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId)) { return Task.CompletedTask; } @@ -122,6 +127,11 @@ namespace Network.NetworkHost return Task.CompletedTask; } + if (!host.TryRefreshAcceptedGameplayActivity(sender, input.PlayerId)) + { + return Task.CompletedTask; + } + var state = new ServerAuthoritativeMovementState( normalizedSender, input.PlayerId, @@ -172,6 +182,7 @@ namespace Network.NetworkHost foreach (var pendingBroadcast in pendingBroadcasts) { + // Keep the coordinator as the only gameplay-relevant PlayerState broadcast path. messageManager.BroadcastMessage(pendingBroadcast.PlayerState, MessageType.PlayerState); host.ObserveAuthoritativeState(pendingBroadcast.RemoteEndPoint, pendingBroadcast.PlayerState.Tick); } @@ -330,32 +341,20 @@ namespace Network.NetworkHost private static void ApplyInput(ServerAuthoritativeMovementState state, MoveInput input) { state.LastAcceptedMoveTick = input.Tick; - state.InputX = input.MoveX; - state.InputY = input.MoveY; + state.InputX = ClampInput(input.TurnInput); + state.InputY = ClampInput(input.ThrottleInput); - if (input.MoveX == 0f && input.MoveY == 0f) + if (state.InputY == 0f) { state.VelocityX = 0f; state.VelocityY = 0f; state.VelocityZ = 0f; - return; } - - var length = MathF.Sqrt((input.MoveX * input.MoveX) + (input.MoveY * input.MoveY)); - if (length <= 0f) - { - state.VelocityX = 0f; - state.VelocityY = 0f; - state.VelocityZ = 0f; - return; - } - - state.Rotation = MathF.Atan2(input.MoveY, input.MoveX) * (180f / MathF.PI); } private void IntegrateState(ServerAuthoritativeMovementState state, TimeSpan elapsed) { - if (state.IsDead || (state.InputX == 0f && state.InputY == 0f)) + if (state.IsDead) { state.VelocityX = 0f; state.VelocityY = 0f; @@ -363,25 +362,83 @@ namespace Network.NetworkHost return; } - var length = MathF.Sqrt((state.InputX * state.InputX) + (state.InputY * state.InputY)); - if (length <= 0f) - { - state.VelocityX = 0f; - state.VelocityY = 0f; - state.VelocityZ = 0f; - return; - } - - var normalizedX = state.InputX / length; - var normalizedY = state.InputY / length; - state.VelocityX = normalizedX * configuration.MoveSpeed; - state.VelocityY = 0f; - state.VelocityZ = normalizedY * configuration.MoveSpeed; - var deltaSeconds = (float)elapsed.TotalSeconds; - state.PositionX += state.VelocityX * deltaSeconds; - state.PositionY += state.VelocityY * deltaSeconds; - state.PositionZ += state.VelocityZ * deltaSeconds; + if (deltaSeconds <= 0f) + { + state.VelocityX = 0f; + state.VelocityY = 0f; + state.VelocityZ = 0f; + return; + } + + var turnInput = ClampInput(state.InputX); + var throttleInput = ClampInput(state.InputY); + if (turnInput != 0f) + { + state.Rotation = NormalizeDegrees(state.Rotation + (turnInput * configuration.TurnSpeedDegreesPerSecond * deltaSeconds)); + } + + if (throttleInput == 0f) + { + state.VelocityX = 0f; + state.VelocityY = 0f; + state.VelocityZ = 0f; + return; + } + + var rotationRadians = state.Rotation * (MathF.PI / 180f); + var forwardX = MathF.Cos(rotationRadians); + var forwardZ = MathF.Sin(rotationRadians); + state.VelocityX = forwardX * (throttleInput * configuration.MoveSpeed); + state.VelocityY = 0f; + state.VelocityZ = forwardZ * (throttleInput * configuration.MoveSpeed); + + var candidatePositionX = state.PositionX + (state.VelocityX * deltaSeconds); + var candidatePositionY = state.PositionY + (state.VelocityY * deltaSeconds); + var candidatePositionZ = state.PositionZ + (state.VelocityZ * deltaSeconds); + var validationResult = worldValidator.Validate(new AuthoritativeMovementWorldValidationRequest( + state.RemoteEndPoint, + state.PlayerId, + state.PositionX, + state.PositionY, + state.PositionZ, + candidatePositionX, + candidatePositionY, + candidatePositionZ, + state.VelocityX, + state.VelocityY, + state.VelocityZ)); + if (!validationResult.IsAllowed) + { + state.VelocityX = 0f; + state.VelocityY = 0f; + state.VelocityZ = 0f; + return; + } + + state.PositionX = candidatePositionX; + state.PositionY = candidatePositionY; + state.PositionZ = candidatePositionZ; + } + + private static float ClampInput(float value) + { + return MathF.Max(-1f, MathF.Min(1f, value)); + } + + private static float NormalizeDegrees(float degrees) + { + var normalized = degrees % 360f; + if (normalized <= -180f) + { + normalized += 360f; + } + else if (normalized > 180f) + { + normalized -= 360f; + } + + return normalized; } private static PlayerState BuildPlayerState(ServerAuthoritativeMovementState state, long tick) diff --git a/Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs b/Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs index 2ff9c11..36f3dc9 100644 --- a/Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs +++ b/Assets/Scripts/Network/NetworkHost/ServerNetworkHost.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Threading.Tasks; using Network.Defines; @@ -17,6 +18,8 @@ namespace Network.NetworkHost private readonly ServerAuthoritativeCombatCoordinator authoritativeCombatCoordinator; private readonly object playerIdentityGate = new(); private readonly Dictionary playerIdsByPeer = new(); + private readonly Dictionary canonicalPeersByPlayerId = new(StringComparer.Ordinal); + private readonly Dictionary> peerKeysByPlayerId = new(StringComparer.Ordinal); public ServerNetworkHost( ITransport transport, @@ -27,7 +30,8 @@ namespace Network.NetworkHost IMessageDeliveryPolicyResolver deliveryPolicyResolver = null, SyncSequenceTracker syncSequenceTracker = null, ServerAuthoritativeMovementConfiguration authoritativeMovement = null, - ServerAuthoritativeCombatConfiguration authoritativeCombat = null) + ServerAuthoritativeCombatConfiguration authoritativeCombat = null, + IAuthoritativeMovementWorldValidator authoritativeMovementWorldValidator = null) { this.transport = transport ?? throw new ArgumentNullException(nameof(transport)); this.syncTransport = syncTransport; @@ -44,11 +48,14 @@ namespace Network.NetworkHost deliveryPolicyResolver ?? new DefaultMessageDeliveryPolicyResolver(), this.syncTransport, syncSequenceTracker ?? new SyncSequenceTracker()); + var resolvedWorldValidator = authoritativeMovementWorldValidator ?? PermissiveAuthoritativeMovementWorldValidator.Instance; authoritativeMovementCoordinator = new ServerAuthoritativeMovementCoordinator( this, messageManager, - authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration()); + authoritativeMovement ?? new ServerAuthoritativeMovementConfiguration(), + resolvedWorldValidator); authoritativeCombatCoordinator = new ServerAuthoritativeCombatCoordinator( + this, messageManager, authoritativeMovementCoordinator, authoritativeCombat ?? new ServerAuthoritativeCombatConfiguration()); @@ -102,6 +109,8 @@ namespace Network.NetworkHost lock (playerIdentityGate) { playerIdsByPeer.Clear(); + canonicalPeersByPlayerId.Clear(); + peerKeysByPlayerId.Clear(); } PublishMetricsSessionSnapshots(); } @@ -137,6 +146,117 @@ namespace Network.NetworkHost return authoritativeCombatCoordinator.TryGetState(remoteEndPoint, out state); } + public bool TryGetAcceptedPlayerId(IPEndPoint remoteEndPoint, out string playerId) + { + return TryGetKnownPlayerId(remoteEndPoint, out playerId); + } + + public bool IsAcceptedPlayer(IPEndPoint remoteEndPoint, string playerId) + { + return !string.IsNullOrWhiteSpace(playerId) && + TryGetKnownPlayerId(remoteEndPoint, out var acceptedPlayerId) && + string.Equals(acceptedPlayerId, playerId, StringComparison.Ordinal); + } + + public bool TryResolveAcceptedPeer(IPEndPoint remoteEndPoint, string playerId, out IPEndPoint acceptedPeer) + { + acceptedPeer = null; + if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId)) + { + return false; + } + + var normalizedRemoteEndPoint = Normalize(remoteEndPoint); + var remoteKey = normalizedRemoteEndPoint.ToString(); + + lock (playerIdentityGate) + { + if (playerIdsByPeer.TryGetValue(remoteKey, out var mappedPlayerId)) + { + if (!string.Equals(mappedPlayerId, playerId, StringComparison.Ordinal)) + { + return false; + } + + if (!canonicalPeersByPlayerId.TryGetValue(playerId, out acceptedPeer)) + { + acceptedPeer = normalizedRemoteEndPoint; + } + + return true; + } + + if (!canonicalPeersByPlayerId.TryGetValue(playerId, out acceptedPeer)) + { + return false; + } + + playerIdsByPeer[remoteKey] = playerId; + if (!peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys)) + { + peerKeys = new HashSet(StringComparer.Ordinal); + peerKeysByPlayerId[playerId] = peerKeys; + } + + peerKeys.Add(remoteKey); + return true; + } + } + + public bool IsPlayerIdInUse(string playerId) + { + if (string.IsNullOrWhiteSpace(playerId)) + { + return false; + } + + lock (playerIdentityGate) + { + foreach (var acceptedPlayerId in playerIdsByPeer.Values) + { + if (string.Equals(acceptedPlayerId, playerId, StringComparison.Ordinal)) + { + return true; + } + } + } + + foreach (var state in authoritativeMovementCoordinator.States) + { + if (string.Equals(state.PlayerId, playerId, StringComparison.Ordinal)) + { + return true; + } + } + + foreach (var state in authoritativeCombatCoordinator.States) + { + if (string.Equals(state.PlayerId, playerId, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + public bool TryRefreshAcceptedGameplayActivity(IPEndPoint remoteEndPoint, string playerId) + { + if (!TryResolveAcceptedPeer(remoteEndPoint, playerId, out var acceptedPeer)) + { + return false; + } + + if (!TryGetSession(acceptedPeer, out var session) || + session.SessionManager.State != ConnectionState.LoggedIn) + { + return false; + } + + NotifyInboundActivity(acceptedPeer); + return true; + } + public void NotifyLoginStarted(IPEndPoint remoteEndPoint) { SessionCoordinator.NotifyLoginStarted(remoteEndPoint); @@ -159,7 +279,7 @@ namespace Network.NetworkHost public void NotifyLoginFailed(IPEndPoint remoteEndPoint, string reason = null) { SessionCoordinator.NotifyLoginFailed(remoteEndPoint, reason); - ForgetPlayerId(remoteEndPoint); + ForgetPeerIdentity(remoteEndPoint); PublishMetricsSessionSnapshot(remoteEndPoint); } @@ -189,20 +309,34 @@ namespace Network.NetworkHost public bool RemoveSession(IPEndPoint remoteEndPoint, string reason = null) { - if (!SessionCoordinator.TryGetSession(remoteEndPoint, out var session)) + if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId) || + !TryResolveAcceptedPeer(remoteEndPoint, playerId, out var acceptedPeer) || + !SessionCoordinator.TryGetSession(acceptedPeer, out var session)) { return false; } - var removed = SessionCoordinator.RemoveSession(remoteEndPoint, reason); + var removed = SessionCoordinator.RemoveSession(acceptedPeer, reason); if (!removed) { return false; } - authoritativeMovementCoordinator.RemoveState(remoteEndPoint); - authoritativeCombatCoordinator.RemoveState(remoteEndPoint); - ForgetPlayerId(remoteEndPoint); + authoritativeMovementCoordinator.RemoveState(acceptedPeer); + authoritativeCombatCoordinator.RemoveState(acceptedPeer); + + var knownPeerEndpoints = GetKnownPeerEndpointsForPlayerId(playerId); + ForgetPlayerId(playerId); + + foreach (var peerEndPoint in knownPeerEndpoints) + { + SessionCoordinator.RemoveSession(peerEndPoint, reason); + RemoveTransportPeerSession(transport, peerEndPoint); + if (syncTransport != null && !ReferenceEquals(syncTransport, transport)) + { + RemoveTransportPeerSession(syncTransport, peerEndPoint); + } + } RecordMetricsSessionSnapshot(transport, "server-host", session, ConnectionState.Disconnected); if (syncTransport != null && !ReferenceEquals(syncTransport, transport)) @@ -216,10 +350,17 @@ namespace Network.NetworkHost private void HandleTransportReceive(byte[] data, IPEndPoint sender) { SessionCoordinator.ObserveTransportActivity(sender); - ObservePlayerIdentity(data, sender); PublishMetricsSessionSnapshot(sender); } + private static void RemoveTransportPeerSession(ITransport transport, IPEndPoint remoteEndPoint) + { + if (transport is IPeerSessionTransport peerSessionTransport) + { + peerSessionTransport.RemovePeerSession(remoteEndPoint); + } + } + private void BootstrapAuthoritativeMovementState(IPEndPoint remoteEndPoint) { if (!TryGetKnownPlayerId(remoteEndPoint, out var playerId)) @@ -230,41 +371,6 @@ namespace Network.NetworkHost authoritativeMovementCoordinator.EnsureState(remoteEndPoint, playerId, out _); } - private void ObservePlayerIdentity(byte[] data, IPEndPoint sender) - { - if (data == null || sender == null) - { - return; - } - - Envelope envelope; - try - { - envelope = Envelope.Parser.ParseFrom(data); - } - catch - { - return; - } - - if ((MessageType)envelope.Type != MessageType.LoginRequest) - { - return; - } - - LoginRequest request; - try - { - request = LoginRequest.Parser.ParseFrom(envelope.Payload); - } - catch - { - return; - } - - RememberPlayerId(sender, request.PlayerId); - } - private void RememberPlayerId(IPEndPoint remoteEndPoint, string playerId) { if (remoteEndPoint == null || string.IsNullOrWhiteSpace(playerId)) @@ -272,10 +378,19 @@ namespace Network.NetworkHost return; } - var key = Normalize(remoteEndPoint).ToString(); + var normalizedRemoteEndPoint = Normalize(remoteEndPoint); + var key = normalizedRemoteEndPoint.ToString(); lock (playerIdentityGate) { playerIdsByPeer[key] = playerId; + canonicalPeersByPlayerId[playerId] = normalizedRemoteEndPoint; + if (!peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys)) + { + peerKeys = new HashSet(StringComparer.Ordinal); + peerKeysByPlayerId[playerId] = peerKeys; + } + + peerKeys.Add(key); } } @@ -294,7 +409,51 @@ namespace Network.NetworkHost } } - private void ForgetPlayerId(IPEndPoint remoteEndPoint) + private IReadOnlyList GetKnownPeerEndpointsForPlayerId(string playerId) + { + if (string.IsNullOrWhiteSpace(playerId)) + { + return Array.Empty(); + } + + lock (playerIdentityGate) + { + if (!peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys)) + { + return Array.Empty(); + } + + return peerKeys + .Select(ParseEndPoint) + .Where(static endpoint => endpoint != null) + .ToArray(); + } + } + + private void ForgetPlayerId(string playerId) + { + if (string.IsNullOrWhiteSpace(playerId)) + { + return; + } + + lock (playerIdentityGate) + { + if (peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys)) + { + foreach (var peerKey in peerKeys) + { + playerIdsByPeer.Remove(peerKey); + } + + peerKeysByPlayerId.Remove(playerId); + } + + canonicalPeersByPlayerId.Remove(playerId); + } + } + + private void ForgetPeerIdentity(IPEndPoint remoteEndPoint) { if (remoteEndPoint == null) { @@ -304,10 +463,49 @@ namespace Network.NetworkHost var key = Normalize(remoteEndPoint).ToString(); lock (playerIdentityGate) { + if (!playerIdsByPeer.TryGetValue(key, out var playerId)) + { + return; + } + playerIdsByPeer.Remove(key); + if (peerKeysByPlayerId.TryGetValue(playerId, out var peerKeys)) + { + peerKeys.Remove(key); + if (peerKeys.Count == 0) + { + peerKeysByPlayerId.Remove(playerId); + canonicalPeersByPlayerId.Remove(playerId); + } + } } } + private static IPEndPoint ParseEndPoint(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var lastColonIndex = value.LastIndexOf(':'); + if (lastColonIndex <= 0 || lastColonIndex >= value.Length - 1) + { + return null; + } + + var addressText = value.Substring(0, lastColonIndex); + if (addressText.Length > 1 && addressText[0] == '[' && addressText[addressText.Length - 1] == ']') + { + addressText = addressText.Substring(1, addressText.Length - 2); + } + + return IPAddress.TryParse(addressText, out var address) && + int.TryParse(value.Substring(lastColonIndex + 1), out var port) + ? new IPEndPoint(address, port) + : null; + } + private static IPEndPoint Normalize(IPEndPoint remoteEndPoint) { if (remoteEndPoint == null) diff --git a/Assets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.cs b/Assets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.cs index 26af0f5..8557986 100644 --- a/Assets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.cs +++ b/Assets/Scripts/Network/NetworkHost/ServerRuntimeConfiguration.cs @@ -14,6 +14,7 @@ namespace Network.NetworkHost } ReliablePort = reliablePort; + AuthoritativeMovementWorldValidator = PermissiveAuthoritativeMovementWorldValidator.Instance; } public int ReliablePort { get; } @@ -36,6 +37,8 @@ namespace Network.NetworkHost public ServerAuthoritativeCombatConfiguration AuthoritativeCombat { get; set; } + public IAuthoritativeMovementWorldValidator AuthoritativeMovementWorldValidator { get; set; } + internal void Validate() { if (ReliablePort <= 0) @@ -58,6 +61,10 @@ namespace Network.NetworkHost AuthoritativeMovement?.Validate(); AuthoritativeCombat?.Validate(); + if (AuthoritativeMovementWorldValidator == null) + { + throw new ArgumentNullException(nameof(AuthoritativeMovementWorldValidator)); + } } } } diff --git a/Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs b/Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs index ffaa31a..36432e4 100644 --- a/Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs +++ b/Assets/Scripts/Network/NetworkHost/ServerRuntimeEntryPoint.cs @@ -25,7 +25,8 @@ namespace Network.NetworkHost configuration.SyncSequenceTracker, configuration.TransportFactory, configuration.AuthoritativeMovement, - configuration.AuthoritativeCombat); + configuration.AuthoritativeCombat, + configuration.AuthoritativeMovementWorldValidator); try { diff --git a/Assets/Scripts/Network/NetworkTransport/ClientSession.cs b/Assets/Scripts/Network/NetworkTransport/ClientSession.cs new file mode 100644 index 0000000..8375d81 --- /dev/null +++ b/Assets/Scripts/Network/NetworkTransport/ClientSession.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; + +namespace Network.NetworkTransport +{ + public class ClientSession + { + public IPEndPoint EndPoint { get; } + public DateTime LastActivity { get; private set; } + + public uint SendSequenceNumber { get; private set; } = 0; + + //TODO: 数据结构——ConcurrentDictionary + public ConcurrentDictionary PendingAcks { get; } = + new ConcurrentDictionary(); + + public uint ExpectedReceiveSequence { get; private set; } = 0; + private HashSet _receivedSequences { get; } = new HashSet(); + + private readonly object _lockObj = new object(); + + public ClientSession(IPEndPoint endPoint) + { + EndPoint = endPoint; + LastActivity = DateTime.Now; + } + + public uint GetNextSendSequence() + { + lock (_lockObj) + { + return SendSequenceNumber++; + } + } + + public bool TryProcessReceiveSequence(uint sequenceNumber, out bool shouldDeliver) + { + lock (_lockObj) + { + LastActivity = DateTime.Now; + + if (sequenceNumber == ExpectedReceiveSequence) + { + ExpectedReceiveSequence++; + _receivedSequences.Add(sequenceNumber); + shouldDeliver = true; + return true; + } + else if (sequenceNumber < ExpectedReceiveSequence) + { + shouldDeliver = false; + return _receivedSequences.Contains(sequenceNumber); + } + else + { + shouldDeliver = false; + return false; + } + } + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Network/NetworkTransport/ClientSession.cs.meta b/Assets/Scripts/Network/NetworkTransport/ClientSession.cs.meta new file mode 100644 index 0000000..38fb162 --- /dev/null +++ b/Assets/Scripts/Network/NetworkTransport/ClientSession.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c007e0986e972b14cb4b4ab2151459c3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs b/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs new file mode 100644 index 0000000..1e94787 --- /dev/null +++ b/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace Network.NetworkTransport +{ + public interface IPeerSessionTransport + { + bool RemovePeerSession(IPEndPoint remoteEndPoint); + } +} diff --git a/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs.meta b/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs.meta new file mode 100644 index 0000000..7a9ba10 --- /dev/null +++ b/Assets/Scripts/Network/NetworkTransport/IPeerSessionTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b2637543abd875349aaa27b3e31f9dbe +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Network/NetworkTransport/KcpTransport.KcpSession.cs.meta b/Assets/Scripts/Network/NetworkTransport/KcpTransport.KcpSession.cs.meta index b03a95a..87dad67 100644 --- a/Assets/Scripts/Network/NetworkTransport/KcpTransport.KcpSession.cs.meta +++ b/Assets/Scripts/Network/NetworkTransport/KcpTransport.KcpSession.cs.meta @@ -1,3 +1,11 @@ -fileFormatVersion: 2 +fileFormatVersion: 2 guid: 63fb533d620e4ac0bf15ba0a9a71331e -timeCreated: 1774593005 \ No newline at end of file +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Network/NetworkTransport/KcpTransport.cs b/Assets/Scripts/Network/NetworkTransport/KcpTransport.cs index 510457a..5f6e977 100644 --- a/Assets/Scripts/Network/NetworkTransport/KcpTransport.cs +++ b/Assets/Scripts/Network/NetworkTransport/KcpTransport.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Linq; using System.Net; @@ -10,7 +10,7 @@ using kcp; namespace Network.NetworkTransport { - public partial class KcpTransport : ITransport, ITransportMetricsSink + public partial class KcpTransport : ITransport, ITransportMetricsSink, IPeerSessionTransport { private const uint DefaultConv = 1; private const int DefaultNoDelay = 1; @@ -233,6 +233,26 @@ namespace Network.NetworkTransport } } + public bool RemovePeerSession(IPEndPoint remoteEndPoint) + { + if (remoteEndPoint == null) + { + throw new ArgumentNullException(nameof(remoteEndPoint)); + } + + var normalizedEndPoint = NormalizeEndPoint(remoteEndPoint); + var key = normalizedEndPoint.ToString(); + if (!_sessions.TryRemove(key, out var session)) + { + return false; + } + + RecordSessionDiagnostics(session, "removed"); + session.Dispose(); + _metricsModule.RecordSessionClosed(session.RemoteEndPoint); + return true; + } + private KcpSession GetOrCreateSession(IPEndPoint remoteEndPoint, uint conv) { var normalizedEndPoint = NormalizeEndPoint(remoteEndPoint); diff --git a/Assets/Scripts/Network/NetworkTransport/Packet.cs b/Assets/Scripts/Network/NetworkTransport/Packet.cs new file mode 100644 index 0000000..f7bc8f8 --- /dev/null +++ b/Assets/Scripts/Network/NetworkTransport/Packet.cs @@ -0,0 +1,58 @@ +using System; +using System.Linq; + +namespace Network.NetworkTransport +{ + public enum PacketType : byte + { + Data = 1, + Ack = 2, + } + + public struct Packet + { + public PacketType Type; + public uint SequenceNumber; + public byte[] Data; + + public byte[] ToBytes() + { + var result = new byte[1 + 4 + Data.Length]; + result[0] = (byte)Type; + BitConverter.GetBytes(SequenceNumber).CopyTo(result, 1); + Data.CopyTo(result, 5); + return result; + } + + public static Packet FromBytes(byte[] data) + { + return new Packet + { + Type = (PacketType)data[0], + SequenceNumber = BitConverter.ToUInt32(data, 1), + //TODO: 结构体——ArraySegment + Data = new ArraySegment(data, 5, data.Length - 5).ToArray() + }; + } + + public static Packet CreateDataPacket(uint seqNum, byte[] data) + { + return new Packet + { + Type = PacketType.Data, + SequenceNumber = seqNum, + Data = data + }; + } + + public static Packet CreateAckPacket(uint seqNum) + { + return new Packet + { + Type = PacketType.Ack, + SequenceNumber = seqNum, + Data = Array.Empty() + }; + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/Network/NetworkTransport/Packet.cs.meta b/Assets/Scripts/Network/NetworkTransport/Packet.cs.meta new file mode 100644 index 0000000..e25a990 --- /dev/null +++ b/Assets/Scripts/Network/NetworkTransport/Packet.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a16c0628d84e6274bbe164e0e57533e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs b/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs new file mode 100644 index 0000000..bcd9a3a --- /dev/null +++ b/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs @@ -0,0 +1,312 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace Network.NetworkTransport +{ + internal sealed class LegacyUdpTransportAdapter : ITransport, IPeerSessionTransport + { + private readonly UdpClient _client; + private readonly IPEndPoint? _defaultRemoteEndPoint; + private readonly bool _isServer; + + private readonly ConcurrentDictionary _sessions = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _resentPacketTimes = new ConcurrentDictionary(); + private readonly Timer _retransmitTimer; + private readonly Timer _cleanupTimer; + + //TODO: volatile 关键字 + private volatile bool _isRunning; + + // 配置参数 + private const int RetransmitTimeoutMs = 1000; + private const int SessionTimeoutMs = 30000; + private const int MaxRetransmitAttempts = 5; + + public event Action? OnReceive; + + // 构造函数——服务端模式 + public LegacyUdpTransportAdapter(int listenPort) + { + _client = new UdpClient(listenPort); + _isServer = true; + _retransmitTimer = new Timer(CheckRetransmit, null, 100, 100); + _cleanupTimer = new Timer(CleanupSessions, null, 5000, 5000); + Console.WriteLine($"[Transport] 服务端模式,监听端口: {listenPort}"); + } + + // 构造函数——客户端模式 + public LegacyUdpTransportAdapter(string serverIP, int serverPort) + { + _client = new UdpClient(0); + _defaultRemoteEndPoint = new IPEndPoint(IPAddress.Parse(serverIP), serverPort); + + _isServer = false; + _retransmitTimer = new Timer(CheckRetransmit, null, 100, 100); + _cleanupTimer = new Timer(CleanupSessions, null, 5000, 5000); + Console.WriteLine($"[Transport] 客户端模式,目标: {_defaultRemoteEndPoint}"); + } + + public async Task StartAsync() + { + _sessions.Clear(); + + _isRunning = true; + Console.WriteLine("[Transport] 传输层启动"); + + // 开始接收数据 + _ = Task.Run(ReceiveLoop); + await Task.Delay(100); // 给接收循环一点启动时间 + } + + public void Stop() + { + _isRunning = false; + _retransmitTimer.Dispose(); + _cleanupTimer.Dispose(); + _client.Close(); + _sessions.Clear(); + Console.WriteLine("[Transport] 传输层停止"); + } + + public void Send(byte[] data) + { + if (!_isServer && _defaultRemoteEndPoint != null) + { + SendTo(data, _defaultRemoteEndPoint); + } + else + { + throw new InvalidOperationException("服务端模式必须使用 SendTo 指定目标"); + } + } + + public void SendTo(byte[] data, IPEndPoint target) + { + if (!_isRunning) + { + return; + } + + var session = GetOrCreateSession(target); + uint seqNum = session.GetNextSendSequence(); + var packet = Packet.CreateDataPacket(seqNum, data); + + session.PendingAcks[seqNum] = (packet, DateTime.Now); + + SendPacketTo(packet, target); + Console.WriteLine($"[Transport] 发送数据包到 {target} SeqNum={seqNum}, DataLen={data.Length}"); + } + + public void SendToAll(byte[] data) + { + foreach (var session in _sessions.Values) + { + SendTo(data, session.EndPoint); + } + } + + public bool RemovePeerSession(IPEndPoint remoteEndPoint) + { + if (remoteEndPoint == null) + { + throw new ArgumentNullException(nameof(remoteEndPoint)); + } + + return _sessions.TryRemove(remoteEndPoint.ToString(), out _); + } + + private async void ReceiveLoop() + { + while (_isRunning) + { + try + { + var result = await _client.ReceiveAsync(); + var packet = Packet.FromBytes(result.Buffer); + + if (packet.Type == PacketType.Data) + { + HandleDataPacket(packet, result.RemoteEndPoint); + } + else if (packet.Type == PacketType.Ack) + { + HandleAckPacket(packet, result.RemoteEndPoint); + } + } + catch (ObjectDisposedException) + { + return; // 正常关闭 + } + catch (Exception e) + { + Console.WriteLine($"[Transport] 接收错误:{e.Message}"); + } + } + } + + private void HandleDataPacket(Packet packet, IPEndPoint senderEndPoint) + { + var session = GetOrCreateSession(senderEndPoint); + + Console.WriteLine( + $"[Transport] 收到数据包从{senderEndPoint} SeqNum={packet.SequenceNumber}, DataLen={packet.Data.Length}"); + + // 发送ACK + var ackPacket = Packet.CreateAckPacket(packet.SequenceNumber); + SendPacketTo(ackPacket, senderEndPoint); + Console.WriteLine($"[Transport] 发送ACK 到 {senderEndPoint} SeqNum={packet.SequenceNumber}"); + + // 检查是否应该交付 + if (session.TryProcessReceiveSequence(packet.SequenceNumber, out bool shouldDeliver)) + { + if (shouldDeliver) + { + OnReceive?.Invoke(packet.Data, senderEndPoint); + Console.WriteLine($"[Transport] 交付数据包从 {senderEndPoint} SeqNum={packet.SequenceNumber}"); + } + else + { + Console.WriteLine($"[Transport] 重复包从 {senderEndPoint} SeqNum={packet.SequenceNumber},忽略"); + } + } + else + { + // 乱序到达,暂存(简化处理:直接丢弃,依赖重传) + Console.WriteLine($"[Transport] 乱序包从 {senderEndPoint} SeqNum={packet.SequenceNumber},丢弃"); + } + } + + private void HandleAckPacket(Packet packet, IPEndPoint senderEndPoint) + { + var session = GetOrCreateSession(senderEndPoint); + Console.WriteLine($"[Transport] 收到ACK从 {senderEndPoint} SeqNum={packet.SequenceNumber}"); + + if (session.PendingAcks.TryRemove(packet.SequenceNumber, out _)) + { + Console.WriteLine($"[Transport] 确认包到 {senderEndPoint} SeqNum={packet.SequenceNumber}"); + } + } + + private ClientSession GetOrCreateSession(IPEndPoint endPoint) + { + string key = endPoint.ToString(); + return _sessions.GetOrAdd(key, _ => + { + var session = new ClientSession(endPoint); + Console.WriteLine($"创建新会话:{endPoint}"); + return session; + }); + } + + private void CheckRetransmit(object? state) + { + if (!_isRunning) + { + return; + } + + var now = DateTime.Now; + var toRetransmit = new List<(IPEndPoint target, uint seqNum, Packet packet)>(); + + foreach (var sessionKvp in _sessions) + { + var session = sessionKvp.Value; + foreach (var ackKvp in session.PendingAcks) + { + var timeSinceLastSend = now - ackKvp.Value.sendTime; + if (timeSinceLastSend.TotalMilliseconds > RetransmitTimeoutMs) + { + toRetransmit.Add((session.EndPoint, ackKvp.Key, ackKvp.Value.packet)); + _resentPacketTimes.TryAdd(ackKvp.Value.packet, 0); + } + } + } + + + foreach (var (target, seqNum, packet) in toRetransmit) + { + var session = GetOrCreateSession(target); + if (session.PendingAcks.ContainsKey(seqNum)) + { + // 更新发送时间 + session.PendingAcks[seqNum] = (packet, now); + SendPacketTo(packet, target); + Console.WriteLine($"[Transport] 重传包到 {target} SeqNum={seqNum}"); + + _resentPacketTimes[packet]++; + if (_resentPacketTimes[packet] >= MaxRetransmitAttempts) + { + // 达到最大重传次数,放弃该会话 + Console.WriteLine($"[Transport] 达到最大重传次数,放弃会话 {target}"); + _sessions.TryRemove(target.ToString(), out _); + } + } + } + } + + private void CleanupSessions(object? state) + { + if (!_isRunning) + { + return; + } + + var now = DateTime.Now; + var toRemove = new List(); + + foreach (var sessionKvp in _sessions) + { + var session = sessionKvp.Value; + var timeSinceLastActivity = now - session.LastActivity; + + if (timeSinceLastActivity.TotalMilliseconds > SessionTimeoutMs) + { + toRemove.Add(sessionKvp.Key); + } + } + + foreach (string key in toRemove) + { + //TODO: 清理会话的同时清理PlayerManager中的玩家数据 + if (_sessions.TryRemove(key, out var session)) + { + Console.WriteLine($"[Transport] 清理超时会话:{session.EndPoint}"); + } + } + + if (_isServer) + { + PrintSessionInfo(); + } + } + + private async void SendPacketTo(Packet packet, IPEndPoint? endPoint) + { + try + { + var data = packet.ToBytes(); + await _client.SendAsync(data, data.Length, endPoint); + } + catch (Exception e) + { + Console.WriteLine($"[Transport] 发送错误:{e.Message}"); + } + } + + private void PrintSessionInfo() + { + Console.WriteLine($"当前活跃会话数:{_sessions.Count}"); + foreach (var sessionKvp in _sessions) + { + var session = sessionKvp.Value; + Console.WriteLine( + $" 会话:{session.EndPoint},发送SeqNum:{session.SendSequenceNumber},期望接收:{session.ExpectedReceiveSequence},待确认: {session.PendingAcks.Count}"); + } + } + } +} diff --git a/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs.meta b/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs.meta new file mode 100644 index 0000000..23516e2 --- /dev/null +++ b/Assets/Scripts/Network/NetworkTransport/ReliableUdpTransport.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e740e71ab20f62d40bf5f56c2142bf02 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Network/NetworkTransport/TransportMetricsModule.cs.meta b/Assets/Scripts/Network/NetworkTransport/TransportMetricsModule.cs.meta index c5a663a..8c711a5 100644 --- a/Assets/Scripts/Network/NetworkTransport/TransportMetricsModule.cs.meta +++ b/Assets/Scripts/Network/NetworkTransport/TransportMetricsModule.cs.meta @@ -8,4 +8,4 @@ MonoImporter: icon: {instanceID: 0} userData: assetBundleName: - assetBundleVariant: \ No newline at end of file + assetBundleVariant: diff --git a/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs b/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs index af5d8cf..7048a5a 100644 --- a/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs +++ b/Assets/Tests/EditMode/Network/GameplayFlowRoundTripTests.cs @@ -49,6 +49,11 @@ namespace Tests.EditMode.Network clientRuntime.StartAsync().GetAwaiter().GetResult(); using var serverRuntime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); + serverRuntime.Host.NotifyLoginStarted(ClientPeer); + serverRuntime.Host.NotifyLoginSucceeded(ClientPeer, "player-a"); + serverRuntime.Host.NotifyLoginStarted(RemotePeer); + serverRuntime.Host.NotifyLoginSucceeded(RemotePeer, "player-b"); + serverTransports[9001].EmitReceive( GameplayFlowTestSupport.BuildEnvelope( MessageType.MoveInput, @@ -56,8 +61,8 @@ namespace Tests.EditMode.Network { PlayerId = "player-b", Tick = 1, - MoveX = 0f, - MoveY = 0f + TurnInput = 0f, + ThrottleInput = 0f }), RemotePeer); serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); @@ -69,8 +74,8 @@ namespace Tests.EditMode.Network { PlayerId = "player-a", Tick = 1, - MoveX = 1f, - MoveY = 0f + TurnInput = 0f, + ThrottleInput = 1f }, MessageType.MoveInput); ClientGameplayInputFlow.SendShootInput( @@ -162,9 +167,9 @@ namespace Tests.EditMode.Network RemotePeer); serverRuntime.Host.NotifyLoginStarted(ClientPeer); - serverRuntime.Host.NotifyLoginSucceeded(ClientPeer); + serverRuntime.Host.NotifyLoginSucceeded(ClientPeer, "player-a"); serverRuntime.Host.NotifyLoginStarted(RemotePeer); - serverRuntime.Host.NotifyLoginSucceeded(RemotePeer); + serverRuntime.Host.NotifyLoginSucceeded(RemotePeer, "player-b"); serverRuntime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50)); TransferBroadcastMessages(serverTransports[9001], clientSyncTransport, ServerSender); @@ -186,8 +191,8 @@ namespace Tests.EditMode.Network { PlayerId = "player-b", Tick = 1, - MoveX = 1f, - MoveY = 0f + TurnInput = 0f, + ThrottleInput = 1f }), RemotePeer); serverRuntime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); diff --git a/Assets/Tests/EditMode/Network/MessageManagerTests.cs b/Assets/Tests/EditMode/Network/MessageManagerTests.cs index b273025..7c15814 100644 --- a/Assets/Tests/EditMode/Network/MessageManagerTests.cs +++ b/Assets/Tests/EditMode/Network/MessageManagerTests.cs @@ -68,8 +68,8 @@ namespace Tests.EditMode.Network { PlayerId = "player-1", Tick = 12, - MoveX = 1, - MoveY = -1 + TurnInput = 1, + ThrottleInput = -1 }; manager.SendMessage(message, MessageType.MoveInput); @@ -82,8 +82,8 @@ namespace Tests.EditMode.Network Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput)); 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)); + Assert.That(parsed.TurnInput, Is.EqualTo(1)); + Assert.That(parsed.ThrottleInput, Is.EqualTo(-1)); } [Test] @@ -100,8 +100,8 @@ namespace Tests.EditMode.Network { PlayerId = "player-1", Tick = 13, - MoveX = 0f, - MoveY = 0f + TurnInput = 0f, + ThrottleInput = 0f }; manager.SendMessage(message, MessageType.MoveInput); @@ -114,8 +114,8 @@ namespace Tests.EditMode.Network Assert.That(envelope.Type, Is.EqualTo((int)MessageType.MoveInput)); Assert.That(parsed.PlayerId, Is.EqualTo("player-1")); Assert.That(parsed.Tick, Is.EqualTo(13)); - Assert.That(parsed.MoveX, Is.EqualTo(0f)); - Assert.That(parsed.MoveY, Is.EqualTo(0f)); + Assert.That(parsed.TurnInput, Is.EqualTo(0f)); + Assert.That(parsed.ThrottleInput, Is.EqualTo(0f)); } [Test] @@ -350,12 +350,12 @@ namespace Tests.EditMode.Network }); transport.EmitReceive( - BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 8, MoveX = 1 }), + BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 8, ThrottleInput = 1 }), Sender); manager.DrainPendingMessagesAsync().GetAwaiter().GetResult(); transport.EmitReceive( - BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 6, MoveX = -1 }), + BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-1", Tick = 6, ThrottleInput = -1 }), Sender); manager.DrainPendingMessagesAsync().GetAwaiter().GetResult(); diff --git a/Assets/Tests/EditMode/Network/ServerAuthoritativeCombatTests.cs b/Assets/Tests/EditMode/Network/ServerAuthoritativeCombatTests.cs index b8e4f4d..bfd1dc6 100644 --- a/Assets/Tests/EditMode/Network/ServerAuthoritativeCombatTests.cs +++ b/Assets/Tests/EditMode/Network/ServerAuthoritativeCombatTests.cs @@ -39,8 +39,8 @@ namespace Tests.EditMode.Network using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); - PrimePlayer(createdTransports[9000], PeerA, "player-a", 1); - PrimePlayer(createdTransports[9000], PeerB, "player-b", 1); + PrimePlayer(runtime, PeerA, "player-a"); + PrimePlayer(runtime, PeerB, "player-b"); runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput @@ -111,8 +111,8 @@ namespace Tests.EditMode.Network using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); - PrimePlayer(createdTransports[9001], PeerA, "player-a", 1); - PrimePlayer(createdTransports[9001], PeerB, "player-b", 1); + PrimePlayer(runtime, PeerA, "player-a"); + PrimePlayer(runtime, PeerB, "player-b"); runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput @@ -166,8 +166,8 @@ namespace Tests.EditMode.Network using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); - PrimePlayer(createdTransports[9000], PeerA, "player-a", 1); - PrimePlayer(createdTransports[9000], PeerB, "player-b", 1); + PrimePlayer(runtime, PeerA, "player-a"); + PrimePlayer(runtime, PeerB, "player-b"); runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.ShootInput, new ShootInput @@ -192,15 +192,10 @@ namespace Tests.EditMode.Network Assert.That(runtime.AuthoritativeCombatStates.Count, Is.EqualTo(0)); } - private static void PrimePlayer(FakeTransport transport, IPEndPoint peer, string playerId, long tick) + private static void PrimePlayer(ServerRuntimeHandle runtime, IPEndPoint peer, string playerId) { - transport.EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput - { - PlayerId = playerId, - Tick = tick, - MoveX = 0f, - MoveY = 0f - }), peer); + runtime.Host.NotifyLoginStarted(peer); + runtime.Host.NotifyLoginSucceeded(peer, playerId); } private static FakeTransport CreateTransport(IDictionary createdTransports, int port) diff --git a/Assets/Tests/EditMode/Network/ServerAuthoritativeMovementTests.cs b/Assets/Tests/EditMode/Network/ServerAuthoritativeMovementTests.cs index b2ccfcf..e75d2b3 100644 --- a/Assets/Tests/EditMode/Network/ServerAuthoritativeMovementTests.cs +++ b/Assets/Tests/EditMode/Network/ServerAuthoritativeMovementTests.cs @@ -33,26 +33,31 @@ namespace Tests.EditMode.Network using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); + runtime.Host.NotifyLoginStarted(PeerA); + runtime.Host.NotifyLoginSucceeded(PeerA, "player-a"); + runtime.Host.NotifyLoginStarted(PeerB); + runtime.Host.NotifyLoginSucceeded(PeerB, "player-b"); + createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 10, - MoveX = 1f, - MoveY = 0f + TurnInput = 0f, + ThrottleInput = 1f }), PeerA); createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 8, - MoveX = 0f, - MoveY = 1f + TurnInput = 1f, + ThrottleInput = 0f }), PeerA); createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-b", Tick = 3, - MoveX = 0f, - MoveY = 1f + TurnInput = 0f, + ThrottleInput = -1f }), PeerB); runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); @@ -66,8 +71,8 @@ namespace Tests.EditMode.Network Assert.That(stateA.PositionZ, Is.EqualTo(0f).Within(0.0001f)); Assert.That(stateB.PlayerId, Is.EqualTo("player-b")); Assert.That(stateB.LastAcceptedMoveTick, Is.EqualTo(3)); - Assert.That(stateB.PositionX, Is.EqualTo(0f).Within(0.0001f)); - Assert.That(stateB.PositionZ, Is.EqualTo(0.2f).Within(0.0001f)); + Assert.That(stateB.PositionX, Is.EqualTo(-0.2f).Within(0.0001f)); + Assert.That(stateB.PositionZ, Is.EqualTo(0f).Within(0.0001f)); Assert.That(createdTransports[9000].BroadcastMessages.Count, Is.EqualTo(2)); } @@ -89,12 +94,15 @@ namespace Tests.EditMode.Network using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); + runtime.Host.NotifyLoginStarted(PeerA); + runtime.Host.NotifyLoginSucceeded(PeerA, "player-a"); + createdTransports[9001].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 1, - MoveX = 1f, - MoveY = 0f + TurnInput = 0f, + ThrottleInput = 1f }), PeerA); runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); @@ -118,8 +126,8 @@ namespace Tests.EditMode.Network { PlayerId = "player-a", Tick = 2, - MoveX = 0f, - MoveY = 0f + TurnInput = 0f, + ThrottleInput = 0f }), PeerA); runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); @@ -164,7 +172,7 @@ namespace Tests.EditMode.Network }), PeerA); runtime.Host.NotifyLoginStarted(PeerA); - runtime.Host.NotifyLoginSucceeded(PeerA); + runtime.Host.NotifyLoginSucceeded(PeerA, "player-a"); runtime.UpdateAuthoritativeMovement(TimeSpan.FromMilliseconds(50)); Assert.That(runtime.TryGetAuthoritativeMovementState(PeerA, out var state), Is.True); @@ -202,12 +210,15 @@ namespace Tests.EditMode.Network using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); + runtime.Host.NotifyLoginStarted(PeerA); + runtime.Host.NotifyLoginSucceeded(PeerA, "player-a"); + createdTransports[9000].EmitReceive(BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 5, - MoveX = 0f, - MoveY = -1f + TurnInput = 0f, + ThrottleInput = -1f }), PeerA); runtime.DrainPendingMessagesAsync().GetAwaiter().GetResult(); @@ -217,9 +228,9 @@ namespace Tests.EditMode.Network var broadcast = ParsePlayerState(createdTransports[9000].BroadcastMessages[0]); 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(-0.3f).Within(0.0001f)); - Assert.That(broadcast.Velocity.Z, Is.EqualTo(-6f).Within(0.0001f)); + Assert.That(broadcast.Position.X, Is.EqualTo(-0.3f).Within(0.0001f)); + Assert.That(broadcast.Position.Z, Is.EqualTo(0f).Within(0.0001f)); + Assert.That(broadcast.Velocity.X, Is.EqualTo(-6f).Within(0.0001f)); } private static FakeTransport CreateTransport(IDictionary createdTransports, int port) diff --git a/Assets/Tests/EditMode/Network/SessionLifecycleTests.cs b/Assets/Tests/EditMode/Network/SessionLifecycleTests.cs index ca39a7f..6107868 100644 --- a/Assets/Tests/EditMode/Network/SessionLifecycleTests.cs +++ b/Assets/Tests/EditMode/Network/SessionLifecycleTests.cs @@ -196,6 +196,10 @@ namespace Tests.EditMode.Network host.StartAsync().GetAwaiter().GetResult(); transport.EmitReceive(CreateEnvelope(MessageType.Heartbeat), peerA); transport.EmitReceive(CreateEnvelope(MessageType.Heartbeat), peerB); + host.NotifyLoginStarted(peerA); + host.NotifyLoginSucceeded(peerA, "player-a"); + host.NotifyLoginStarted(peerB); + host.NotifyLoginSucceeded(peerB, "player-b"); var removed = host.RemoveSession(peerA, "peer closed"); @@ -203,7 +207,7 @@ namespace Tests.EditMode.Network Assert.That(host.ManagedSessions.Count, Is.EqualTo(1)); Assert.That(host.TryGetSession(peerA, out _), Is.False); Assert.That(host.TryGetSession(peerB, out var sessionB), Is.True); - Assert.That(sessionB.SessionManager.State, Is.EqualTo(ConnectionState.TransportConnected)); + Assert.That(sessionB.SessionManager.State, Is.EqualTo(ConnectionState.LoggedIn)); } [Test] diff --git a/Assets/Tests/EditMode/Network/SharedNetworkFoundationTests.cs b/Assets/Tests/EditMode/Network/SharedNetworkFoundationTests.cs index 8cdf072..fbf50f4 100644 --- a/Assets/Tests/EditMode/Network/SharedNetworkFoundationTests.cs +++ b/Assets/Tests/EditMode/Network/SharedNetworkFoundationTests.cs @@ -110,8 +110,8 @@ namespace Tests.EditMode.Network { PlayerId = "shared-player", Tick = 33, - MoveX = 1f, - MoveY = -1f + TurnInput = 1f, + ThrottleInput = -1f }; runtime.MessageManager.SendMessage(message, MessageType.MoveInput); @@ -161,7 +161,7 @@ namespace Tests.EditMode.Network { PlayerId = "shared-player", Tick = 77, - MoveX = 1f + ThrottleInput = 1f }; runtime.MessageManager.SendMessage(moveInput, MessageType.MoveInput); @@ -191,7 +191,7 @@ namespace Tests.EditMode.Network { PlayerId = "server-player", Tick = 88, - MoveY = 1f + ThrottleInput = 1f }; host.MessageManager.SendMessage(moveInput, MessageType.MoveInput); @@ -215,7 +215,7 @@ namespace Tests.EditMode.Network { PlayerId = "fallback-player", Tick = 99, - MoveX = -1f + TurnInput = -1f }; host.MessageManager.SendMessage(moveInput, MessageType.MoveInput); diff --git a/Assets/Tests/EditMode/Network/SyncStrategyTests.cs b/Assets/Tests/EditMode/Network/SyncStrategyTests.cs index b8083ba..b74442d 100644 --- a/Assets/Tests/EditMode/Network/SyncStrategyTests.cs +++ b/Assets/Tests/EditMode/Network/SyncStrategyTests.cs @@ -33,8 +33,8 @@ namespace Tests.EditMode.Network Assert.That(stopInput, Is.Not.Null); Assert.That(stopInput.PlayerId, Is.EqualTo("player-1")); Assert.That(stopInput.Tick, Is.EqualTo(8)); - Assert.That(stopInput.MoveX, Is.EqualTo(0f)); - Assert.That(stopInput.MoveY, Is.EqualTo(0f)); + Assert.That(stopInput.TurnInput, Is.EqualTo(0f)); + Assert.That(stopInput.ThrottleInput, Is.EqualTo(0f)); Assert.That(continuedIdle, Is.False); Assert.That(idleInput, Is.Null); } @@ -85,9 +85,9 @@ namespace Tests.EditMode.Network public void ClientPredictionBuffer_AuthoritativeState_PrunesAcknowledgedMoveInputs() { var buffer = new ClientPredictionBuffer(); - buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, MoveX = 1f }); - buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 11, MoveX = 1f }); - buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, MoveX = 1f }); + buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f }); + buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 11, ThrottleInput = 1f }); + buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 12, ThrottleInput = 1f }); var accepted = buffer.TryApplyAuthoritativeState( new PlayerState { PlayerId = "player-1", Tick = 11 }, @@ -104,7 +104,7 @@ namespace Tests.EditMode.Network public void ClientPredictionBuffer_StaleAuthoritativeState_IsIgnored() { var buffer = new ClientPredictionBuffer(); - buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, MoveX = 1f }); + buffer.Record(new MoveInput { PlayerId = "player-1", Tick = 10, ThrottleInput = 1f }); buffer.TryApplyAuthoritativeState(new PlayerState { PlayerId = "player-1", Tick = 10 }, out _); var accepted = buffer.TryApplyAuthoritativeState( @@ -139,7 +139,7 @@ namespace Tests.EditMode.Network Assert.That(snapshot.Position, Is.EqualTo(new Vector3(5f, 0f, -3f))); Assert.That(snapshot.Velocity, Is.EqualTo(new Vector3(1.5f, 0f, 0.25f))); Assert.That(snapshot.Rotation, Is.EqualTo(90f)); - Assert.That(snapshot.RotationQuaternion.eulerAngles.y, Is.EqualTo(90f).Within(0.01f)); + Assert.That(snapshot.RotationQuaternion.eulerAngles.y, Is.EqualTo(0f).Within(0.01f)); Assert.That(snapshot.Hp, Is.EqualTo(73)); } @@ -413,13 +413,13 @@ namespace Tests.EditMode.Network }); transport.EmitReceive( - BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 5, MoveX = 1f }), + BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 5, ThrottleInput = 1f }), peerA); transport.EmitReceive( - BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 4, MoveX = -1f }), + BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-a", Tick = 4, ThrottleInput = -1f }), peerA); transport.EmitReceive( - BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-b", Tick = 4, MoveY = 1f }), + BuildEnvelope(MessageType.MoveInput, new MoveInput { PlayerId = "player-b", Tick = 4, TurnInput = 1f }), peerB); Assert.That(handledTicksByPeer[peerA.ToString()], Is.EqualTo(new long[] { 5 }));