feat(online): live queue count — friends see each other waiting
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 <noreply@anthropic.com>
This commit is contained in:
@@ -67,9 +67,10 @@ public sealed partial class GameManager
|
|||||||
if (_waiting.Any(w => w.player.UserId == p.UserId)) return;
|
if (_waiting.Any(w => w.player.UserId == p.UserId)) return;
|
||||||
var timer = new Timer(_ => FlushTicket(p.UserId), null, QueueWaitMs, Timeout.Infinite);
|
var timer = new Timer(_ => FlushTicket(p.UserId), null, QueueWaitMs, Timeout.Infinite);
|
||||||
_waiting.Add((p, timer, DateTime.UtcNow));
|
_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);
|
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,9 +79,23 @@ public sealed partial class GameManager
|
|||||||
lock (_mmLock)
|
lock (_mmLock)
|
||||||
{
|
{
|
||||||
var idx = _waiting.FindIndex(w => w.player.UserId == userId);
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Push the current queue size to every waiting player (call inside _mmLock).</summary>
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Client safety net: re-send the current game state to a player who
|
/// <summary>Client safety net: re-send the current game state to a player who
|
||||||
/// may have missed the initial broadcast (green-felt freeze guard).</summary>
|
/// may have missed the initial broadcast (green-felt freeze guard).</summary>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
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 { useEffect, useState } from "react";
|
||||||
import { ScreenShell } from "@/components/online/ScreenHeader";
|
import { ScreenShell } from "@/components/online/ScreenHeader";
|
||||||
import { Avatar } from "@/components/online/Avatar";
|
import { Avatar } from "@/components/online/Avatar";
|
||||||
@@ -140,6 +140,16 @@ export function MatchmakingScreen() {
|
|||||||
{searching && (
|
{searching && (
|
||||||
<>
|
<>
|
||||||
<div className="mt-2 text-3xl font-black gold-text tabular-nums">{elapsed}s</div>
|
<div className="mt-2 text-3xl font-black gold-text tabular-nums">{elapsed}s</div>
|
||||||
|
{(mm.waiting ?? 0) >= 2 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
className="mt-2 inline-flex items-center gap-1.5 rounded-full glass gold-border px-3 py-1 text-xs font-bold text-teal-300"
|
||||||
|
>
|
||||||
|
<Users className="size-3.5" />
|
||||||
|
{t("mm.inQueue", { n: mm.waiting! })}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
{elapsed >= 40 ? (
|
{elapsed >= 40 ? (
|
||||||
<p className="text-rose-300 text-xs mt-2 max-w-[18rem]">{t("mm.stuck")}</p>
|
<p className="text-rose-300 text-xs mt-2 max-w-[18rem]">{t("mm.stuck")}</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -256,6 +256,7 @@ const fa: Dict = {
|
|||||||
"mm.found": "بازیکنان پیدا شدند!",
|
"mm.found": "بازیکنان پیدا شدند!",
|
||||||
"mm.ready": "آماده شروع",
|
"mm.ready": "آماده شروع",
|
||||||
"mm.fillHint": "منتظر پیوستن یک بازیکن آنلاین هستیم… میتوانی همین حالا با ربات شروع کنی",
|
"mm.fillHint": "منتظر پیوستن یک بازیکن آنلاین هستیم… میتوانی همین حالا با ربات شروع کنی",
|
||||||
|
"mm.inQueue": "{n} بازیکن در صف",
|
||||||
"mm.stuck": "اتصال به سرور طول کشید. لطفاً «لغو» را بزنید و دوباره تلاش کنید.",
|
"mm.stuck": "اتصال به سرور طول کشید. لطفاً «لغو» را بزنید و دوباره تلاش کنید.",
|
||||||
"intro.found": "بازیکنان آمادهاند!",
|
"intro.found": "بازیکنان آمادهاند!",
|
||||||
"intro.getReady": "بازی در حال شروع است…",
|
"intro.getReady": "بازی در حال شروع است…",
|
||||||
@@ -644,6 +645,7 @@ const en: Dict = {
|
|||||||
"mm.found": "Players found!",
|
"mm.found": "Players found!",
|
||||||
"mm.ready": "Ready to start",
|
"mm.ready": "Ready to start",
|
||||||
"mm.fillHint": "Waiting for an online player to join… or start now with bots",
|
"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.stuck": "Connecting to the server is taking too long. Tap Cancel and try again.",
|
||||||
"mm.cancel": "Cancel",
|
"mm.cancel": "Cancel",
|
||||||
"mm.start": "Enter game",
|
"mm.start": "Enter game",
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export class SignalrService implements OnlineService {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
conn.on("matchmaking", (s: { phase: string; players: number; queuePosition: number | null }) =>
|
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", () => {
|
conn.on("matchFound", () => {
|
||||||
this.emitMM("ready");
|
this.emitMM("ready");
|
||||||
// Safety net: if the initial state never lands (dropped/raced), ask the
|
// 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 = {
|
const state: MatchmakingState = {
|
||||||
phase: phase as MatchmakingState["phase"],
|
phase: phase as MatchmakingState["phase"],
|
||||||
players: [],
|
players: [],
|
||||||
@@ -234,6 +234,7 @@ export class SignalrService implements OnlineService {
|
|||||||
ranked: this.mmRanked,
|
ranked: this.mmRanked,
|
||||||
stake: this.mmStake,
|
stake: this.mmStake,
|
||||||
queuePosition,
|
queuePosition,
|
||||||
|
waiting,
|
||||||
};
|
};
|
||||||
this.mmCbs.forEach((cb) => cb(state));
|
this.mmCbs.forEach((cb) => cb(state));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -390,6 +390,8 @@ export interface MatchmakingState {
|
|||||||
elapsedMs: number;
|
elapsedMs: number;
|
||||||
ranked: boolean;
|
ranked: boolean;
|
||||||
stake: number;
|
stake: number;
|
||||||
|
/** live count of real humans waiting in the matchmaking queue (incl. you). */
|
||||||
|
waiting?: number;
|
||||||
/** position in the queue when phase === "queued" */
|
/** position in the queue when phase === "queued" */
|
||||||
queuePosition?: number;
|
queuePosition?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user