diff --git a/SVSim.BattleNode/Bridge/MatchingBridge.cs b/SVSim.BattleNode/Bridge/MatchingBridge.cs index b1940b8..2d4e419 100644 --- a/SVSim.BattleNode/Bridge/MatchingBridge.cs +++ b/SVSim.BattleNode/Bridge/MatchingBridge.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using SVSim.BattleNode.Sessions; namespace SVSim.BattleNode.Bridge; @@ -23,8 +24,13 @@ public sealed class MatchingBridge : IMatchingBridge public PendingMatch RegisterPendingBattle(long viewerId) { // 12-digit decimal battle id mirrors the captures (e.g. "975695075012"). - // Cast to long before Math.Abs to avoid OverflowException on int.MinValue. - var battleId = (Math.Abs((long)Guid.NewGuid().GetHashCode()) % 1_000_000_000_000L).ToString("D12"); + // Two unbiased 6-digit draws concatenated — RandomNumberGenerator.GetInt32 uses + // rejection sampling so the result is uniform on [0, 10^6). The previous + // implementation (Guid.GetHashCode + mod) collapsed 128 bits into ~32 of entropy + // and tripped Math.Abs(int.MinValue) UB on the unlucky hash. + var hi = RandomNumberGenerator.GetInt32(0, 1_000_000); + var lo = RandomNumberGenerator.GetInt32(0, 1_000_000); + var battleId = $"{hi:D6}{lo:D6}"; _store.RegisterPending(new PendingBattle(battleId, viewerId)); return new PendingMatch(battleId, _options.NodeServerUrl); } diff --git a/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs b/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs index 71e287e..c88d7cf 100644 --- a/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs +++ b/SVSim.BattleNode/Lifecycle/ScriptedLifecycle.cs @@ -117,7 +117,7 @@ public static class ScriptedLifecycle private static MsgEnvelope EnvelopeForPush(NetworkBattleUri uri, IMsgBody body, string? bid = null) => new(uri, ViewerId: FakeOpponentViewerId, - Uuid: "node-stub", + Uuid: WireConstants.ServerUuid, Bid: bid, Try: 0, Cat: EmitCategory.Battle, diff --git a/SVSim.BattleNode/Protocol/WireConstants.cs b/SVSim.BattleNode/Protocol/WireConstants.cs new file mode 100644 index 0000000..78a0504 --- /dev/null +++ b/SVSim.BattleNode/Protocol/WireConstants.cs @@ -0,0 +1,27 @@ +namespace SVSim.BattleNode.Protocol; + +/// +/// String constants that show up on the wire as opaque tags. Lifting them out of +/// inline string literals gives each one a single source of truth and a name that +/// reads at the use site. +/// +internal static class WireConstants +{ + /// SIO event name for ordered server-pushed frames (the lifecycle channel). + public const string SynchronizeEvent = "synchronize"; + + /// SIO event name for client-emitted msg frames + their ack-responses. + public const string MsgEvent = "msg"; + + /// SIO event name for Gungnir keepalive frames (both directions). + public const string AliveEvent = "alive"; + + /// + /// Placeholder UUID we stamp on every server-originated envelope. Prod servers stamp a + /// real per-request UUID; the client doesn't validate it. + /// + public const string ServerUuid = "node-stub"; + + /// Gungnir scs/ocs value the v1 server reports unconditionally. + public const string OnlineStatus = "ONLINE"; +} diff --git a/SVSim.BattleNode/Sessions/BattleSession.cs b/SVSim.BattleNode/Sessions/BattleSession.cs index a5afdd3..43b89c9 100644 --- a/SVSim.BattleNode/Sessions/BattleSession.cs +++ b/SVSim.BattleNode/Sessions/BattleSession.cs @@ -112,10 +112,10 @@ public sealed class BattleSession { switch (frame.EventName) { - case "msg" when frame.BinaryAttachments.Count == 1: + case WireConstants.MsgEvent when frame.BinaryAttachments.Count == 1: await HandleMsgEventAsync(frame); return; - case "alive" when frame.BinaryAttachments.Count == 1: + case WireConstants.AliveEvent when frame.BinaryAttachments.Count == 1: await HandleAliveEventAsync(frame); return; } @@ -178,14 +178,14 @@ public sealed class BattleSession var aliveEnv = new MsgEnvelope( Uri: NetworkBattleUri.Gungnir, ViewerId: ScriptedLifecycle.FakeOpponentViewerId, - Uuid: "node-stub", + Uuid: WireConstants.ServerUuid, Bid: null, Try: 0, Cat: EmitCategory.General, PubSeq: null, PlaySeq: null, - Body: new AlivePushBody(Scs: "ONLINE", Ocs: "ONLINE")); - await PushNoStockAsync(aliveEnv, eventName: "alive"); + Body: new AlivePushBody(Scs: WireConstants.OnlineStatus, Ocs: WireConstants.OnlineStatus)); + await PushNoStockAsync(aliveEnv, eventName: WireConstants.AliveEvent); } catch (Exception ex) { @@ -272,7 +272,7 @@ public sealed class BattleSession private MsgEnvelope BuildAckedEnvelope(NetworkBattleUri uri) => new( uri, ViewerId: ScriptedLifecycle.FakeOpponentViewerId, - Uuid: "node-stub", + Uuid: WireConstants.ServerUuid, Bid: null, Try: 0, Cat: EmitCategory.General, @@ -283,7 +283,7 @@ public sealed class BattleSession private MsgEnvelope BuildBattleFinishNoContest() => new( NetworkBattleUri.BattleFinish, ViewerId: ScriptedLifecycle.FakeOpponentViewerId, - Uuid: "node-stub", + Uuid: WireConstants.ServerUuid, Bid: null, Try: 0, Cat: EmitCategory.Battle, @@ -317,10 +317,10 @@ public sealed class BattleSession return Array.Empty(); } - private Task PushOrderedAsync(MsgEnvelope env, string eventName = "synchronize") => + private Task PushOrderedAsync(MsgEnvelope env, string eventName = WireConstants.SynchronizeEvent) => EncodeAndSendAsync(Outbound.AssignAndArchive(env), eventName); - private Task PushNoStockAsync(MsgEnvelope env, string eventName = "synchronize") => + private Task PushNoStockAsync(MsgEnvelope env, string eventName = WireConstants.SynchronizeEvent) => EncodeAndSendAsync(Outbound.WrapNoStock(env), eventName); private async Task EncodeAndSendAsync(MsgEnvelope env, string eventName)