From de1947573ae79e5ed9f9157aec630977bc006eed Mon Sep 17 00:00:00 2001 From: zk Date: Wed, 20 May 2026 02:13:10 +0100 Subject: [PATCH] fix: make bms/pms use normal sorting on tables (#1519) --- Dockerfile.dev | 3 ++ .../folders/TableEvolutionReplay.tsx | 4 +-- .../_playtype/folders/TableFolderList.tsx | 4 +-- .../_playtype/folders/folderTableShared.ts | 16 +++++++++-- typescript/client/src/lib/games/bms-pms.tsx | 1 + typescript/client/src/lib/types.ts | 8 ++++++ typescript/client/vite.config.ts | 28 +++++++++++-------- 7 files changed, 46 insertions(+), 18 deletions(-) diff --git a/Dockerfile.dev b/Dockerfile.dev index 490cd132b..73f9d41df 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -86,6 +86,9 @@ RUN apt-get update \ gh \ # MinIO CLI (binary is `minio-client`, not Midnight Commander's `mc`) minio-client \ + # `xdg-open` so the client vite plugin can launch a browser without + # crashing on missing binary (see typescript/client/vite.config.ts). + xdg-utils \ && rm -rf /var/lib/apt/lists/* ENV DEBIAN_FRONTEND=readline diff --git a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/TableEvolutionReplay.tsx b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/TableEvolutionReplay.tsx index f03dd3b41..e7597ef1a 100644 --- a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/TableEvolutionReplay.tsx +++ b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/TableEvolutionReplay.tsx @@ -123,9 +123,9 @@ export default function TableEvolutionReplay({ const folderSlugOrder = useMemo( () => scope.kind === "table" - ? tableFolderSlugsDisplayOrder(scope.table) + ? tableFolderSlugsDisplayOrder(scope.table, game) : [scope.folder.slug], - [scope], + [scope, game], ); const scrubRef = useRef(0); diff --git a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/TableFolderList.tsx b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/TableFolderList.tsx index 8890c0830..8c356f4c2 100644 --- a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/TableFolderList.tsx +++ b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/TableFolderList.tsx @@ -45,7 +45,7 @@ export default function TableFolderList({ const dataset = useMemo(() => { const arr = []; - for (const folder of tableFolderSlugsDisplayOrder(table)) { + for (const folder of tableFolderSlugsDisplayOrder(table, game)) { const data = dataMap.get(folder); if (!data) { @@ -56,7 +56,7 @@ export default function TableFolderList({ } return arr; - }, [dataMap, table]); + }, [dataMap, table, game]); const { user } = useContext(UserContext); diff --git a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/folderTableShared.ts b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/folderTableShared.ts index 18817a1de..3e772155c 100644 --- a/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/folderTableShared.ts +++ b/typescript/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/folderTableShared.ts @@ -1,5 +1,6 @@ import type { FolderStatsInfo, TableEvolutionEventAPI } from "#types/api-returns"; +import { GPT_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations"; import { type FolderDocument, type GameConfig, @@ -20,9 +21,18 @@ export interface UGPTFolderStats { stats: FolderStatsInfo; } -/** API `table.folders` is ascending (e.g. Level 1 … 12); show highest folder / level first in chart + table. */ -export function tableFolderSlugsDisplayOrder(table: TableDocument): string[] { - return [...table.folders].reverse(); +/** + * Resolve the order folder slugs should be rendered for `table` in `game`. + * + * `table.folders` is ascending (e.g. Level 1 … 12), and by default we reverse so the + * highest folder / level renders first. Games can opt out via `reverseFolderOrder` on + * their GPT client impl, in which case we reverse again — i.e. render in the table's + * declared order (currently used for BMS/PMS). + */ +export function tableFolderSlugsDisplayOrder(table: TableDocument, game: V3Game): string[] { + const reverseAgain = GPT_CLIENT_IMPLEMENTATIONS[game].reverseFolderOrder ?? false; + + return reverseAgain ? [...table.folders] : [...table.folders].reverse(); } export function evoEventTimeMs(ev: TableEvolutionEventAPI): number { diff --git a/typescript/client/src/lib/games/bms-pms.tsx b/typescript/client/src/lib/games/bms-pms.tsx index 2ad62a7fa..c6223279a 100644 --- a/typescript/client/src/lib/games/bms-pms.tsx +++ b/typescript/client/src/lib/games/bms-pms.tsx @@ -14,6 +14,7 @@ import { bgc, CreateRatingSys } from "./_util"; const BASE_IMPL: GPTClientImplementation = { sessionImportantScoreCount: 20, + reverseFolderOrder: true, difficultyColours: { CHART: COLOUR_SET.gray, // lol }, diff --git a/typescript/client/src/lib/types.ts b/typescript/client/src/lib/types.ts index b71c75118..9cb74fe49 100644 --- a/typescript/client/src/lib/types.ts +++ b/typescript/client/src/lib/types.ts @@ -148,4 +148,12 @@ export interface GPTClientImplementation { * display? */ sessionImportantScoreCount: integer; + + /** + * By default folders are rendered in reverse of `table.folders` (so the highest + * level / hardest folder shows first). Set this to flip that back — folders will + * then be rendered in the order declared by the table. Used for games like BMS/PMS + * where the table is already authored in the order the UI should display. + */ + reverseFolderOrder?: boolean; } diff --git a/typescript/client/vite.config.ts b/typescript/client/vite.config.ts index 70098e9b0..3007d5e10 100644 --- a/typescript/client/vite.config.ts +++ b/typescript/client/vite.config.ts @@ -21,6 +21,8 @@ function openLoopbackInBrowserPlugin(): Plugin { useHttps: boolean, ) => { if (!httpServer) return; + // Opt out for headless dev containers / CI where no browser opener exists. + if (process.env.TACHI_NO_OPEN === "1" || process.env.CI) return; const run = () => { const addr = httpServer.address(); const port = @@ -29,17 +31,21 @@ function openLoopbackInBrowserPlugin(): Plugin { : (portFallback ?? 3000); const protocol = useHttps ? "https" : "http"; const url = `${protocol}://localhost:${port}/`; - if (process.platform === "darwin") { - spawn("open", [url], { detached: true, stdio: "ignore" }).unref(); - } else if (process.platform === "win32") { - spawn("cmd", ["/c", "start", "", url], { - detached: true, - stdio: "ignore", - shell: false, - }).unref(); - } else { - spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref(); - } + const [cmd, args]: [string, string[]] = + process.platform === "darwin" + ? ["open", [url]] + : process.platform === "win32" + ? ["cmd", ["/c", "start", "", url]] + : ["xdg-open", [url]]; + const child = spawn(cmd, args, { + detached: true, + stdio: "ignore", + shell: false, + }); + // Without this, a missing opener binary (e.g. no xdg-open in a dev + // container) emits an unhandled 'error' event and crashes vite. + child.on("error", () => {}); + child.unref(); }; if (httpServer.listening) run(); else httpServer.once("listening", run);