Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions lang/dev/lobby.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@
"declineInvite": "Denlice",
"kickMember": "Kcik Meembr",
"cancelInvite": "Ceancl Iivnte",
"noInvites": "No crerunt iiinnattvos."
"noInvites": "You crelnrtuy hvae no piendng iiinnattvos.",
"partyTooLarge": "Party Too Lgare",
"partyTooLargeTooltip": "Your party is too lgare to jion tihs qeuue."
},
"watch": {
"replays": {
Expand Down Expand Up @@ -153,9 +155,11 @@
"searchGame": "Sacreh gmae",
"joinRequested": "Jniniog qeuue",
"searchingForOpponent": "Searnichg for oeonnppt",
"matchFound": "Mceathd - Ccilk to Raedy",
"accepted": "Acpteecd",
"cancel": "Ceancl"
"matchFound": "Ccilk to Raedy ({seconds})",
"accepted": "Acpteecd ({players} raedy)",
"cancel": "Ceancl",
"partyTooLarge": "Party Too Lgare",
"partyTooLargeTooltip": "Your party is too lgare to jion tihs qeuue."
},
"modalTitle": "Macanihmtkg Quuee Daleits",
"modal": {
Expand Down
12 changes: 8 additions & 4 deletions lang/en/lobby.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@
"declineInvite": "Decline",
"kickMember": "Kick Member",
"cancelInvite": "Cancel Invite",
"noInvites": "No current invitations."
"noInvites": "You currently have no pending invitations.",
"partyTooLarge": "Party Too Large",
"partyTooLargeTooltip": "Your party is too large to join this queue."
},
"watch": {
"replays": {
Expand Down Expand Up @@ -153,9 +155,11 @@
"searchGame": "Search game",
"joinRequested": "Joining queue",
"searchingForOpponent": "Searching for opponent",
"matchFound": "Matched - Click to Ready",
"accepted": "Accepted",
"cancel": "Cancel"
"matchFound": "Click to Ready ({seconds})",
"accepted": "Accepted ({players} ready)",
"cancel": "Cancel",
"partyTooLarge": "Party Too Large",
"partyTooLargeTooltip": "Your party is too large to join this queue."
},
"modalTitle": "Matchmaking Queue Details",
"modal": {
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/assets/languages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2079,7 +2079,9 @@
"declineInvite": null,
"kickMember": null,
"cancelInvite": null,
"noInvites": null
"noInvites": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"watch": {
"replays": {
Expand Down Expand Up @@ -2138,7 +2140,9 @@
"searchingForOpponent": null,
"matchFound": null,
"accepted": null,
"cancel": null
"cancel": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"modalTitle": null,
"modal": {
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/assets/languages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1950,7 +1950,9 @@
"declineInvite": null,
"kickMember": null,
"cancelInvite": null,
"noInvites": null
"noInvites": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"watch": {
"replays": {
Expand Down Expand Up @@ -2009,7 +2011,9 @@
"searchingForOpponent": null,
"matchFound": null,
"accepted": null,
"cancel": null
"cancel": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"modalTitle": null,
"modal": {
Expand Down
12 changes: 8 additions & 4 deletions src/renderer/assets/languages/dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@
"declineInvite": "Denlice",
"kickMember": "Kcik Meembr",
"cancelInvite": "Ceancl Iivnte",
"noInvites": "No crerunt iiinnattvos."
"noInvites": "You crelnrtuy hvae no piendng iiinnattvos.",
"partyTooLarge": "Party Too Lgare",
"partyTooLargeTooltip": "Your party is too lgare to jion tihs qeuue."
},
"watch": {
"replays": {
Expand Down Expand Up @@ -153,9 +155,11 @@
"searchGame": "Sacreh gmae",
"joinRequested": "Jniniog qeuue",
"searchingForOpponent": "Searnichg for oeonnppt",
"matchFound": "Mceathd - Ccilk to Raedy",
"accepted": "Acpteecd",
"cancel": "Ceancl"
"matchFound": "Ccilk to Raedy ({seconds})",
"accepted": "Acpteecd ({players} raedy)",
"cancel": "Ceancl",
"partyTooLarge": "Party Too Lgare",
"partyTooLargeTooltip": "Your party is too lgare to jion tihs qeuue."
},
"modalTitle": "Macanihmtkg Quuee Daleits",
"modal": {
Expand Down
12 changes: 8 additions & 4 deletions src/renderer/assets/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1927,7 +1927,9 @@
"declineInvite": "Decline",
"kickMember": "Kick Member",
"cancelInvite": "Cancel Invite",
"noInvites": "No current invitations."
"noInvites": "You currently have no pending invitations.",
"partyTooLarge": "Party Too Large",
"partyTooLargeTooltip": "Your party is too large to join this queue."
},
"watch": {
"replays": {
Expand Down Expand Up @@ -1984,9 +1986,11 @@
"searchGame": "Search game",
"joinRequested": "Joining queue",
"searchingForOpponent": "Searching for opponent",
"matchFound": "Matched - Click to Ready",
"accepted": "Accepted",
"cancel": "Cancel"
"matchFound": "Click to Ready ({seconds})",
"accepted": "Accepted ({players} ready)",
"cancel": "Cancel",
"partyTooLarge": "Party Too Large",
"partyTooLargeTooltip": "Your party is too large to join this queue."
},
"modalTitle": "Matchmaking Queue Details",
"modal": {
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/assets/languages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -3932,7 +3932,9 @@
"declineInvite": null,
"kickMember": null,
"cancelInvite": null,
"noInvites": null
"noInvites": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"watch": {
"replays": {
Expand Down Expand Up @@ -3991,7 +3993,9 @@
"searchingForOpponent": null,
"matchFound": null,
"accepted": null,
"cancel": null
"cancel": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"modalTitle": null,
"modal": {
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/assets/languages/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -3901,7 +3901,9 @@
"declineInvite": null,
"kickMember": null,
"cancelInvite": null,
"noInvites": null
"noInvites": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"watch": {
"replays": {
Expand Down Expand Up @@ -3960,7 +3962,9 @@
"searchingForOpponent": null,
"matchFound": null,
"accepted": null,
"cancel": null
"cancel": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"modalTitle": null,
"modal": {
Expand Down
8 changes: 6 additions & 2 deletions src/renderer/assets/languages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -4043,7 +4043,9 @@
"declineInvite": null,
"kickMember": null,
"cancelInvite": null,
"noInvites": null
"noInvites": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"watch": {
"replays": {
Expand Down Expand Up @@ -4102,7 +4104,9 @@
"searchingForOpponent": null,
"matchFound": null,
"accepted": null,
"cancel": null
"cancel": null,
"partyTooLarge": null,
"partyTooLargeTooltip": null
},
"modalTitle": null,
"modal": {
Expand Down
59 changes: 59 additions & 0 deletions src/renderer/composables/usePartySizeMatchmaking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2025 The BAR Lobby Authors
//
// SPDX-License-Identifier: MIT

import { matchmakingStore } from "@renderer/store/matchmaking.store";
import { partyStore } from "@renderer/store/party.store";
import { ref, watch } from "vue";

export function usePartySizeMatchmaking() {
const partyTooLarge = ref(false);

watch(
() => ({
activeParty: partyStore.activeParty,
queueId: matchmakingStore.selectedQueue,
playlists: [...matchmakingStore.playlists],
// Spreading map entries forces Vue to evaluate the Map's contents
members: [...(partyStore.parties.get(partyStore.activeParty ?? "")?.members ?? [])],
invited: [...(partyStore.parties.get(partyStore.activeParty ?? "")?.invited ?? [])],
}),
(newData) => {
const { activeParty, queueId, playlists } = newData;

if (!activeParty || !queueId || !playlists.length) {
partyTooLarge.value = false;
return;
}

const party = partyStore.parties.get(activeParty);
const playlist = playlists.find((p) => p.id === queueId);

if (!party || !playlist) {
partyTooLarge.value = false;
return;
}

partyTooLarge.value = party.members.length > playlist.teamSize;
},
{ immediate: true }
);

return {
partyTooLarge,
};
}

export function getPartySize() {
const activeParty = partyStore.activeParty;
if (!activeParty) {
return 0;
}

const party = partyStore.parties.get(activeParty);
if (!party) {
return 0;
}

return party.members.length;
}
46 changes: 41 additions & 5 deletions src/renderer/store/matchmaking.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { notificationsApi } from "@renderer/api/notifications";
import { isTachyonErrorForCommand, tachyonRequest } from "@renderer/api/tachyon";
import { onWentOffline } from "@renderer/utils/offline-signal";

// The server is the authority on the real ready-up deadline and will send its own
// matchmaking/cancelled event if we miss it; this margin just narrows the window
// where our local countdown and the server's deadline can disagree.
const READY_TIMEOUT_SAFETY_MARGIN_MS = 100;

export enum MatchmakingStatus {
Idle = "Idle",
JoinRequested = "JoinRequested",
Expand Down Expand Up @@ -43,6 +48,9 @@ export const matchmakingStore: {
maps: string[];
};
};
queueTimeout?: number;
readyCountdownInterval?: number;
readySecondsRemaining?: number;
} = reactive({
isInitialized: false,
isDrawerOpen: false,
Expand All @@ -55,15 +63,31 @@ export const matchmakingStore: {
playersReady: 0,
playersQueued: 0,
downloadsRequired: {},
queueTimeout: undefined,
readyCountdownInterval: undefined,
readySecondsRemaining: undefined,
});

function clearReadyTimers() {
if (matchmakingStore.queueTimeout !== undefined) {
window.clearTimeout(matchmakingStore.queueTimeout);
matchmakingStore.queueTimeout = undefined;
}
if (matchmakingStore.readyCountdownInterval !== undefined) {
window.clearInterval(matchmakingStore.readyCountdownInterval);
matchmakingStore.readyCountdownInterval = undefined;
}
matchmakingStore.readySecondsRemaining = undefined;
}

function onQueueUpdateEvent(data: MatchmakingQueueUpdateEventData) {
console.log("Tachyon event: matchmaking/queueUpdate:", data);
matchmakingStore.playersQueued = data.playersQueued;
}

function onLostEvent() {
console.log("Tachyon event: matchmaking/lost: no data");
clearReadyTimers();
matchmakingStore.status = MatchmakingStatus.Searching;
}

Expand All @@ -74,6 +98,7 @@ function onFoundUpdateEvent(data: MatchmakingFoundUpdateEventData) {

function onCancelledEvent(data: MatchmakingCancelledEventData) {
console.log("Tachyon event: matchmaking/cancelled:", data);
clearReadyTimers();
matchmakingStore.status = MatchmakingStatus.Idle;
if (data.reason === "version_changed") {
sendListRequest();
Expand All @@ -82,16 +107,24 @@ function onCancelledEvent(data: MatchmakingCancelledEventData) {

function onFoundEvent(data: MatchmakingFoundEventData) {
console.log("Tachyon event: matchmaking/found:", data);
clearReadyTimers();
matchmakingStore.status = MatchmakingStatus.MatchFound;
// Per spec, we have 10 seconds to send the ``matchmaking/ready`` request or we get cancelled from queue.
// Probably better to track this timer on the UI side because the user will either need to 'ready' or 'cancel'
// and they need to know this. Plus the UI has to "pop up" because they need to respond to it.
// But we don't want to be "triggering" the UI from the store. Instead, we should add a watcher,
// and when this value updates to MatchFound we can start our timer. Probably want a progress bar "counting down" too.
// Deadline is our local prediction of the server's timeout, not the source of truth for it.
const deadline = Date.now() + data.timeoutMs - READY_TIMEOUT_SAFETY_MARGIN_MS;
matchmakingStore.readySecondsRemaining = Math.max(0, Math.ceil((deadline - Date.now()) / 1000));
matchmakingStore.readyCountdownInterval = window.setInterval(() => {
matchmakingStore.readySecondsRemaining = Math.max(0, Math.ceil((deadline - Date.now()) / 1000));
}, 1000);
matchmakingStore.queueTimeout = window.setTimeout(() => {
clearReadyTimers();
matchmakingStore.status = MatchmakingStatus.Idle;
}, deadline - Date.now());
}

function onQueuesJoinedEvent(data: MatchmakingQueuesJoinedEventData) {
console.log("Tachyon event: matchmaking/queuesJoined:", data);
// Current design is a single queue joined at a time. If the user has a *different* queue selected when their party joins, we switch so they match their party's selection.
matchmakingStore.selectedQueue = data.queues[0];
notificationsApi.alert({ text: "Successfully joined matchmaking queue(s)", severity: "info" });
matchmakingStore.status = MatchmakingStatus.Searching;
}
Expand Down Expand Up @@ -211,6 +244,7 @@ async function sendQueueRequest() {
* Sends a Tachyon 'matchmaking/cancel' request.
*/
async function sendCancelRequest() {
clearReadyTimers();
matchmakingStore.status = MatchmakingStatus.Idle;
try {
const response = await window.tachyon.request("matchmaking/cancel");
Expand All @@ -226,6 +260,7 @@ async function sendCancelRequest() {
* Sends a Tachyon 'matchmaking/ready' request.
*/
async function sendReadyRequest() {
clearReadyTimers();
matchmakingStore.status = MatchmakingStatus.MatchAccepted;
try {
const response = await window.tachyon.request("matchmaking/ready");
Expand Down Expand Up @@ -264,6 +299,7 @@ export async function initializeMatchmakingStore() {
// selectedQueue is the user's pick, not server state, and downloadsRequired is derived from
// installed content rather than the session - the next list response recomputes it.
export function clearOnlineState() {
clearReadyTimers();
matchmakingStore.status = MatchmakingStatus.Idle;
matchmakingStore.playlists = [];
matchmakingStore.isLoadingQueues = false;
Expand Down
Loading
Loading