diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 97b73c4..29a0171 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,8 @@ "Bash(openspec status:*)", "Bash(openspec instructions:*)", "Bash(dotnet build:*)", - "Bash(dotnet test:*)" + "Bash(dotnet test:*)", + "Bash(dotnet Temp/Bin/Debug/Network.EditMode.Tests/Network.EditMode.Tests.dll)" ] }, "outputStyle": "default" diff --git a/Assets/Scripts/Network/NetworkApplication/DefaultMessageDeliveryPolicyResolver.cs b/Assets/Scripts/Network/NetworkApplication/DefaultMessageDeliveryPolicyResolver.cs index b4d215c..18907b0 100644 --- a/Assets/Scripts/Network/NetworkApplication/DefaultMessageDeliveryPolicyResolver.cs +++ b/Assets/Scripts/Network/NetworkApplication/DefaultMessageDeliveryPolicyResolver.cs @@ -1,22 +1,60 @@ using System.Collections.Generic; +using System.Linq; using Network.Defines; namespace Network.NetworkApplication { + /// + /// Resolves the delivery policy for each . + /// Policies are intentionally explicit here so that adding a new + /// requires an intentional decision rather than silently falling through to a default. + /// public sealed class DefaultMessageDeliveryPolicyResolver : IMessageDeliveryPolicyResolver { - private static readonly IReadOnlyDictionary DefaultPolicies = + private static readonly IReadOnlyDictionary Policies = new Dictionary { + // High-frequency sync lane: latest-wins, stale-drop. + // These messages are sent every frame and a stale value is never useful. { MessageType.MoveInput, DeliveryPolicy.HighFrequencySync }, - { MessageType.PlayerState, DeliveryPolicy.HighFrequencySync } + { MessageType.PlayerState, DeliveryPolicy.HighFrequencySync }, + + // Reliable ordered lane: guaranteed delivery, ordered. + // ShootInput carries player intent; losing or reordering it changes gameplay outcomes. + { MessageType.ShootInput, DeliveryPolicy.ReliableOrdered }, + + // CombatEvent is a server-authoritative result; clients must receive it reliably + // and in order to maintain consistent HP/death state. + { MessageType.CombatEvent, DeliveryPolicy.ReliableOrdered }, + + // PlayerJoin carries spawn data that the client needs to instantiate a player. + // Reliable ordered ensures the client receives it before gameplay begins. + { MessageType.PlayerJoin, DeliveryPolicy.ReliableOrdered }, + + // Login/logout are session-control messages that must not be lost or reordered. + { MessageType.LoginRequest, DeliveryPolicy.ReliableOrdered }, + { MessageType.LoginResponse, DeliveryPolicy.ReliableOrdered }, + { MessageType.LogoutRequest, DeliveryPolicy.ReliableOrdered }, + + // Heartbeat carries server tick used for clock sync; a missing sample is + // simply a lost sample — no value in stale delivery. + { MessageType.Heartbeat, DeliveryPolicy.ReliableOrdered }, + { MessageType.HeartbeatResponse, DeliveryPolicy.ReliableOrdered }, }; public DeliveryPolicy Resolve(MessageType messageType) { - return DefaultPolicies.TryGetValue(messageType, out var policy) - ? policy - : DeliveryPolicy.ReliableOrdered; + if (Policies.TryGetValue(messageType, out var policy)) + { + return policy; + } + + // A new MessageType was added without an explicit policy decision. + // Fail fast so the omission is noticed rather than silently defaulting. + throw new System.ArgumentException( + $"MessageType '{messageType}' has no assigned {nameof(DeliveryPolicy)}. " + + "Add an explicit entry in the policy dictionary.", + nameof(messageType)); } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs b/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs index b2f0f6f..a7ea839 100644 --- a/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs +++ b/Assets/Scripts/Network/NetworkApplication/NetworkIntegrationFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Threading.Tasks; using Network.NetworkHost; using Network.NetworkTransport; @@ -63,8 +62,6 @@ namespace Network.NetworkApplication ServerAuthoritativeCombatConfiguration authoritativeCombat = null, IAuthoritativeMovementWorldValidator authoritativeMovementWorldValidator = null) { - ValidateDualPortConfiguration(reliablePort, syncPort); - transportFactory ??= static port => new KcpTransport(port); var reliableTransport = transportFactory(reliablePort) @@ -91,11 +88,6 @@ namespace Network.NetworkApplication authoritativeMovementWorldValidator ?? PermissiveAuthoritativeMovementWorldValidator.Instance); } - public static Task StartServerRuntimeAsync(ServerRuntimeConfiguration configuration) - { - return ServerRuntimeEntryPoint.StartAsync(configuration); - } - private static void ValidateDualPortConfiguration(int reliablePort, int? syncPort) { if (reliablePort <= 0) diff --git a/Assets/Tests/EditMode/Network/ServerRuntimeEntryPointTests.cs b/Assets/Tests/EditMode/Network/ServerRuntimeEntryPointTests.cs index 33a0689..0d13c3b 100644 --- a/Assets/Tests/EditMode/Network/ServerRuntimeEntryPointTests.cs +++ b/Assets/Tests/EditMode/Network/ServerRuntimeEntryPointTests.cs @@ -24,7 +24,7 @@ namespace Tests.EditMode.Network TransportFactory = port => CreateTransport(createdTransports, port) }; - using var runtime = NetworkIntegrationFactory.StartServerRuntimeAsync(configuration).GetAwaiter().GetResult(); + using var runtime = ServerRuntimeEntryPoint.StartAsync(configuration).GetAwaiter().GetResult(); Assert.That(createdTransports.Keys, Is.EquivalentTo(new[] { 9000 })); Assert.That(runtime.IsRunning, Is.True);