Files
vpinball/components/hud/CoinDoorHud.tsx
T
valknarandClaude Sonnet 5 44504c718d Fix eject hanging the tab on ROM-based tables
Bump @valknar/vpinball-wasm to 0.3.3, which fixes a genuine infinite
busy-wait in the PinMAME plugin's Controller::Stop() under Emscripten
(no OS thread ever clears the flag it spins on) — this fired from
dispose()'s synchronous script Exit event on any ROM-based table,
hanging the tab permanently.

Sequence unmount cleanup as stop() then two animation frames then
dispose(), instead of dispose() alone, matching the consumer pattern
the engine fix expects (stop() only takes effect on its next internal
step, so disposing immediately after can still race it).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 10:27:35 +02:00

106 lines
3.3 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import type { PinballInstance } from "@valknar/vpinball-wasm";
import { useIdleTimer } from "./useIdleTimer";
import GameInfoPanel from "@/components/GameInfoPanel";
import type { TableInfo } from "@/lib/pinball/types";
export interface CoinDoorHudProps {
instanceRef: React.RefObject<PinballInstance | null>;
tableInfo: TableInfo;
loadTimeMs: number | null;
}
function HudButton({
label,
shortLabel,
active,
onClick,
}: {
label: string;
shortLabel?: string;
active?: boolean;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
aria-pressed={active}
className={`rounded-sm border px-2 py-1 font-mono text-xs tracking-widest transition sm:px-3 sm:py-1.5 ${
active
? "border-marquee bg-marquee/20 text-marquee"
: "border-chrome/40 bg-black/30 text-chrome hover:border-arc hover:text-arc"
}`}
>
{shortLabel ? (
<>
<span className="sm:hidden">{shortLabel}</span>
<span className="hidden sm:inline">{label}</span>
</>
) : (
label
)}
</button>
);
}
export default function CoinDoorHud({ instanceRef, tableInfo, loadTimeMs }: CoinDoorHudProps) {
const router = useRouter();
const isIdle = useIdleTimer(3000);
const [showInfo, setShowInfo] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const onChange = () => setIsFullscreen(Boolean(document.fullscreenElement));
document.addEventListener("fullscreenchange", onChange);
return () => document.removeEventListener("fullscreenchange", onChange);
}, []);
const handleFullscreen = () => {
instanceRef.current?.requestFullscreen();
};
const handleEject = () => {
// usePinballInstance's unmount cleanup (triggered by this navigation)
// handles stop()+dispose() itself, sequenced to avoid hanging the tab.
router.push("/");
};
return (
<>
<div
data-hud
className={`absolute inset-x-0 top-0 z-10 flex items-center justify-between gap-1.5 border-b border-chrome/30 bg-ash/90 px-2 py-2 backdrop-blur transition-opacity duration-300 sm:gap-3 sm:px-4 ${
isIdle ? "pointer-events-none opacity-0" : "opacity-100"
}`}
>
<div className="flex min-w-0 items-center gap-2">
<span className="shrink-0 font-display text-lg tracking-wide text-marquee">VPINBALL</span>
<span className="hidden truncate font-mono text-xs text-chrome sm:inline">
{tableInfo.name}
</span>
</div>
<div className="flex shrink-0 items-center gap-1.5 sm:gap-2">
<HudButton label="INFO" active={showInfo} onClick={() => setShowInfo((v) => !v)} />
<HudButton
label={isFullscreen ? "EXIT FS" : "FULLSCREEN"}
shortLabel={isFullscreen ? "EXIT" : "FS"}
active={isFullscreen}
onClick={handleFullscreen}
/>
<HudButton label="EJECT" onClick={handleEject} />
</div>
</div>
{showInfo && (
<div className="absolute inset-x-2 top-14 z-10 sm:inset-x-auto sm:right-4 sm:top-16">
<GameInfoPanel tableInfo={tableInfo} loadTimeMs={loadTimeMs} />
</div>
)}
</>
);
}