From 940e2af6d29688da9ea9537965f2413122202fb3 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Fri, 19 Jun 2026 19:26:13 +0330 Subject: [PATCH] =?UTF-8?q?feat(online):=20live=20queue=20count=20?= =?UTF-8?q?=E2=80=94=20friends=20see=20each=20other=20waiting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The server only sent the queue size to the player who just joined, and the client dropped the count entirely (emitMM ignored s.players). So two friends queuing together never saw each other, even though the server does seat 2+ waiting humans together within ~25s. - Server: BroadcastQueueLocked() pushes the current queue size to EVERY waiting player on join/cancel (not just the joiner). - Client: thread the count through emitMM → MatchmakingState.waiting. - MatchmakingScreen shows "N players in queue" (mm.inQueue) when ≥2 humans wait, so friends can tell they're queued together before bots fill the empty seats. Co-Authored-By: Claude Opus 4.8 --- server/src/Hokm.Server/Game/GameManager.cs | 21 +++++++++++++++++--- src/components/screens/MatchmakingScreen.tsx | 12 ++++++++++- src/lib/i18n.tsx | 2 ++ src/lib/online/signalr-service.ts | 5 +++-- src/lib/online/types.ts | 2 ++ 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/server/src/Hokm.Server/Game/GameManager.cs b/server/src/Hokm.Server/Game/GameManager.cs index c771678..ef39b66 100644 --- a/server/src/Hokm.Server/Game/GameManager.cs +++ b/server/src/Hokm.Server/Game/GameManager.cs @@ -67,9 +67,10 @@ public sealed partial class GameManager if (_waiting.Any(w => w.player.UserId == p.UserId)) return; var timer = new Timer(_ => FlushTicket(p.UserId), null, QueueWaitMs, Timeout.Infinite); _waiting.Add((p, timer, DateTime.UtcNow)); - _ = _hub.Clients.User(p.UserId).SendAsync("matchmaking", - new MatchmakingStateDto("searching", _waiting.Count, null)); if (_waiting.Count >= 4) FormGroupLocked(4); + // Tell EVERYONE still waiting the new count (so friends queuing together + // see each other join), not just the player who just joined. + BroadcastQueueLocked(); } } @@ -78,10 +79,24 @@ public sealed partial class GameManager lock (_mmLock) { var idx = _waiting.FindIndex(w => w.player.UserId == userId); - if (idx >= 0) { _waiting[idx].timer.Dispose(); _waiting.RemoveAt(idx); } + if (idx >= 0) + { + _waiting[idx].timer.Dispose(); + _waiting.RemoveAt(idx); + BroadcastQueueLocked(); + } } } + /// Push the current queue size to every waiting player (call inside _mmLock). + private void BroadcastQueueLocked() + { + int n = _waiting.Count; + foreach (var w in _waiting) + _ = _hub.Clients.User(w.player.UserId).SendAsync("matchmaking", + new MatchmakingStateDto("searching", n, null)); + } + /// Client safety net: re-send the current game state to a player who /// may have missed the initial broadcast (green-felt freeze guard). public void Resync(string userId) diff --git a/src/components/screens/MatchmakingScreen.tsx b/src/components/screens/MatchmakingScreen.tsx index 29752f9..6342595 100644 --- a/src/components/screens/MatchmakingScreen.tsx +++ b/src/components/screens/MatchmakingScreen.tsx @@ -1,7 +1,7 @@ "use client"; import { AnimatePresence, motion } from "framer-motion"; -import { Crown, Loader2 } from "lucide-react"; +import { Crown, Loader2, Users } from "lucide-react"; import { useEffect, useState } from "react"; import { ScreenShell } from "@/components/online/ScreenHeader"; import { Avatar } from "@/components/online/Avatar"; @@ -140,6 +140,16 @@ export function MatchmakingScreen() { {searching && ( <>
{elapsed}s
+ {(mm.waiting ?? 0) >= 2 && ( + + + {t("mm.inQueue", { n: mm.waiting! })} + + )} {elapsed >= 40 ? (

{t("mm.stuck")}

) : ( diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx index 834f50b..0a7b0c4 100644 --- a/src/lib/i18n.tsx +++ b/src/lib/i18n.tsx @@ -256,6 +256,7 @@ const fa: Dict = { "mm.found": "بازیکنان پیدا شدند!", "mm.ready": "آماده شروع", "mm.fillHint": "منتظر پیوستن یک بازیکن آنلاین هستیم… می‌توانی همین حالا با ربات شروع کنی", + "mm.inQueue": "{n} بازیکن در صف", "mm.stuck": "اتصال به سرور طول کشید. لطفاً «لغو» را بزنید و دوباره تلاش کنید.", "intro.found": "بازیکنان آماده‌اند!", "intro.getReady": "بازی در حال شروع است…", @@ -644,6 +645,7 @@ const en: Dict = { "mm.found": "Players found!", "mm.ready": "Ready to start", "mm.fillHint": "Waiting for an online player to join… or start now with bots", + "mm.inQueue": "{n} players in queue", "mm.stuck": "Connecting to the server is taking too long. Tap Cancel and try again.", "mm.cancel": "Cancel", "mm.start": "Enter game", diff --git a/src/lib/online/signalr-service.ts b/src/lib/online/signalr-service.ts index c61ab0a..663f649 100644 --- a/src/lib/online/signalr-service.ts +++ b/src/lib/online/signalr-service.ts @@ -160,7 +160,7 @@ export class SignalrService implements OnlineService { .build(); conn.on("matchmaking", (s: { phase: string; players: number; queuePosition: number | null }) => - this.emitMM(s.phase, s.queuePosition ?? undefined)); + this.emitMM(s.phase, s.queuePosition ?? undefined, s.players)); conn.on("matchFound", () => { this.emitMM("ready"); // Safety net: if the initial state never lands (dropped/raced), ask the @@ -226,7 +226,7 @@ export class SignalrService implements OnlineService { } } - private emitMM(phase: string, queuePosition?: number) { + private emitMM(phase: string, queuePosition?: number, waiting?: number) { const state: MatchmakingState = { phase: phase as MatchmakingState["phase"], players: [], @@ -234,6 +234,7 @@ export class SignalrService implements OnlineService { ranked: this.mmRanked, stake: this.mmStake, queuePosition, + waiting, }; this.mmCbs.forEach((cb) => cb(state)); } diff --git a/src/lib/online/types.ts b/src/lib/online/types.ts index cb3c813..060dee9 100644 --- a/src/lib/online/types.ts +++ b/src/lib/online/types.ts @@ -390,6 +390,8 @@ export interface MatchmakingState { elapsedMs: number; ranked: boolean; stake: number; + /** live count of real humans waiting in the matchmaking queue (incl. you). */ + waiting?: number; /** position in the queue when phase === "queued" */ queuePosition?: number; }