using System.Collections.Concurrent; using SVSim.BattleNode.Bridge; using SVSim.BattleNode.Sessions; namespace SVSim.EmulatedEntrypoint.Matching; /// /// In-process FCFS pair-up: one waiting slot per mode, plus a per-viewer cache of /// resolved matches for the first arriver's next poll (consume-on-read). /// public sealed class InProcessPairUp : IMatchingPairUpService { private readonly IMatchingBridge _bridge; private readonly ConcurrentDictionary _slots = new(); public InProcessPairUp(IMatchingBridge bridge) { _bridge = bridge; } public Task TryPairAsync(string mode, BattlePlayer player, CancellationToken ct) { var slot = _slots.GetOrAdd(mode, _ => new ModeSlot()); lock (slot.Lock) { // 1. Already-resolved match cached for this viewer? Consume + return. // This caller is the FIRST arriver picking up their cached pair — owner role. if (slot.Resolved.TryGetValue(player.ViewerId, out var cached)) { slot.Resolved.Remove(player.ViewerId); return Task.FromResult(new PairUpResult(cached, IsOwner: true)); } // 2. Someone already waiting in this slot? Pair with them. // This caller is the SECOND arriver who triggered the pair — joiner role. if (slot.Waiting is not null) { if (slot.Waiting.ViewerId == player.ViewerId) { // Same viewer polled twice while parked — keep them parked. return Task.FromResult(null); } var p1 = slot.Waiting; var p2 = player; slot.Waiting = null; var match = _bridge.RegisterBattle(p1, p2, BattleType.Pvp); // Cache the result for the FIRST arriver's next poll (consume-on-read). slot.Resolved[p1.ViewerId] = match; return Task.FromResult(new PairUpResult(match, IsOwner: false)); } // 3. Empty slot — park this caller. slot.Waiting = player; return Task.FromResult(null); } } private sealed class ModeSlot { public BattlePlayer? Waiting { get; set; } public Dictionary Resolved { get; } = new(); public object Lock { get; } = new(); } }