From cac17756c6e473c5e03f8c11fc6d500393188fd0 Mon Sep 17 00:00:00 2001 From: Michael Schoonmaker Date: Sun, 17 Sep 2023 22:43:18 -0400 Subject: [PATCH 1/2] API: Add server-only route to fetch replay files. Uses a single environment variable and a GET /replays/{id} with two fields: `info` metadata and `frames` replay. --- src/routes/replays/[id]/+server.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/routes/replays/[id]/+server.ts diff --git a/src/routes/replays/[id]/+server.ts b/src/routes/replays/[id]/+server.ts new file mode 100644 index 0000000..61aca0b --- /dev/null +++ b/src/routes/replays/[id]/+server.ts @@ -0,0 +1,30 @@ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import type { RequestHandler } from "./$types"; + +const ROOT_DIR = process.env.REPLAYS_DIR ?? process.cwd(); + +export const GET: RequestHandler = async ({ params }) => { + // Load the local NDJSON 'replay' file. + // TODO(schoon): Once we decide on a real extension, use that here. + // Check that the file exists, and return a 404 otherwise. + const replayFile = await readFile(join(ROOT_DIR, `${params.id}.ndjson`), "utf-8"); + const replayLines = replayFile.split("\n").filter((line) => line.trim().length); + + // TODO(schoon): This would be much better with a clear delimiter, + // not just a gentlemen's agreement that the first line is special. + const info = JSON.parse(replayLines[0]); + const frames = replayLines.slice(1).map((line) => JSON.parse(line)); + + return new Response( + JSON.stringify({ + frames, + info + }), + { + headers: { + "Content-Type": "application/json" + } + } + ); +}; From 262f108242097ad5d000170463c177e8d53548a7 Mon Sep 17 00:00:00 2001 From: Michael Schoonmaker Date: Sun, 17 Sep 2023 22:44:16 -0400 Subject: [PATCH 2/2] Board: Replay from files with 'local' engine URL. --- src/lib/playback/engine.ts | 60 +++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/src/lib/playback/engine.ts b/src/lib/playback/engine.ts index aa89617..b8de662 100644 --- a/src/lib/playback/engine.ts +++ b/src/lib/playback/engine.ts @@ -26,6 +26,45 @@ export function fetchGame( ) { console.debug(`[playback] loading game ${gameID}`); + /** + * Helper function to add a single frame, whether via live Websocket + * or replay JSON. + */ + // TODO(schoon): Real types for these arguments. + function ingestFrame(gameInfo: any, engineEvent: any) { + if (engineEvent.Type == "frame" && !loadedFrames.has(engineEvent.Data.Turn)) { + loadedFrames.add(engineEvent.Data.Turn); + + const frame = engineEventToFrame(gameInfo, engineEvent.Data); + frames.push(frame); + frames.sort((a: Frame, b: Frame) => a.turn - b.turn); + + // Fire frame callback + if (engineEvent.Data.Turn == 0) { + console.debug("[playback] received first frame"); + } + onFrameLoad(frame); + } else if (engineEvent.Type == "game_end") { + console.debug("[playback] received final frame"); + if (ws) ws.close(); + + // Flag last frame as the last one and fire callback + frames[frames.length - 1].isFinalFrame = true; + onFinalFrame(frames[frames.length - 1]); + } + } + + // Special "url" for local files. + if (engineURL === "local") { + console.log("Using local playback engine..."); + fetchFunc(`/replays/${gameID}`) + .then((response) => response.json()) + .then(({ info, frames }) => { + frames.forEach((frame: unknown) => ingestFrame(info, frame)); + }); + return; + } + // Reset if (ws) ws.close(); loadedFrames = new Set(); @@ -51,26 +90,7 @@ export function fetchGame( ws.onmessage = (message) => { const engineEvent = JSON.parse(message.data); - if (engineEvent.Type == "frame" && !loadedFrames.has(engineEvent.Data.Turn)) { - loadedFrames.add(engineEvent.Data.Turn); - - const frame = engineEventToFrame(gameInfo, engineEvent.Data); - frames.push(frame); - frames.sort((a: Frame, b: Frame) => a.turn - b.turn); - - // Fire frame callback - if (engineEvent.Data.Turn == 0) { - console.debug("[playback] received first frame"); - } - onFrameLoad(frame); - } else if (engineEvent.Type == "game_end") { - console.debug("[playback] received final frame"); - if (ws) ws.close(); - - // Flag last frame as the last one and fire callback - frames[frames.length - 1].isFinalFrame = true; - onFinalFrame(frames[frames.length - 1]); - } + ingestFrame(gameInfo, engineEvent); }; ws.onclose = () => {