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>
106 lines
3.3 KiB
TypeScript
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>
|
|
)}
|
|
</>
|
|
);
|
|
}
|