Initial public release of PSX Adventure Engine

Browser reference stack for PSX-era third-person adventure: fixed cameras,
inventory puzzles, Box3D physics, host-authoritative P2P co-op, and a modular
character harness. Ships the Ashgrove Precinct Level 1 investigation demo with
a full cast and nine linked rooms.
This commit is contained in:
ryanfitzpatrickio
2026-07-31 06:32:43 -05:00
commit 8a96ede9f2
181 changed files with 25807 additions and 0 deletions
+251
View File
@@ -0,0 +1,251 @@
import { createServer, type Server } from "node:http";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { WebSocket } from "ws";
import type { SignalMessage } from "@psx/shared";
import { PROTOCOL_VERSION } from "@psx/shared";
import { createApp } from "../app.js";
import { InMemoryStore } from "../db/store.js";
import { SignalingServer } from "./signaling.js";
/**
* End-to-end signalling tests against a real HTTP + WebSocket server.
*
* The impersonation test is the important one: the relay stamps `from` off the
* authenticated socket, so a peer claiming to be someone else must be ignored.
*/
let server: Server;
let signaling: SignalingServer;
let store: InMemoryStore;
let baseUrl: string;
let wsUrl: string;
/** Bridges Hono's fetch handler onto a plain Node server so `ws` can attach. */
function nodeAdapter(app: ReturnType<typeof createApp>["app"]): Server {
return createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
const url = `http://localhost${req.url ?? "/"}`;
const hasBody = chunks.length > 0;
const request = new Request(url, {
method: req.method,
headers: req.headers as Record<string, string>,
...(hasBody ? { body: Buffer.concat(chunks) } : {}),
});
// Hono's `fetch` is sync-or-async depending on the route.
void Promise.resolve(app.fetch(request)).then(async (response: Response) => {
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
res.end(Buffer.from(await response.arrayBuffer()));
});
});
});
}
beforeEach(async () => {
store = new InMemoryStore();
const { app } = createApp(store);
server = nodeAdapter(app);
signaling = new SignalingServer(server, store);
await new Promise<void>((resolve) => server.listen(0, resolve));
const address = server.address();
const port = typeof address === "object" && address ? address.port : 0;
baseUrl = `http://localhost:${port}`;
wsUrl = `ws://localhost:${port}/ws`;
});
afterEach(async () => {
signaling.close();
await new Promise<void>((resolve) => server.close(() => resolve()));
});
async function register(username: string): Promise<string> {
const response = await fetch(`${baseUrl}/auth/register`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ username, email: `${username}@example.com`, password: "hunter2hunter2" }),
});
expect(response.status).toBe(201);
return ((await response.json()) as { token: string }).token;
}
/** Opens a socket, sends `hello`, and resolves once `welcome` arrives. */
async function connect(token: string, lobbyCode: string) {
const socket = new WebSocket(wsUrl);
const inbox: SignalMessage[] = [];
await new Promise<void>((resolve, reject) => {
socket.on("open", resolve);
socket.on("error", reject);
});
socket.on("message", (raw) => inbox.push(JSON.parse(raw.toString()) as SignalMessage));
socket.send(JSON.stringify({ t: "hello", token, lobbyCode, protocol: PROTOCOL_VERSION }));
const welcome = await waitFor(inbox, (m) => m.t === "welcome" || m.t === "error");
return { socket, inbox, welcome };
}
/** Polls `inbox` until a matching message appears, or times out. */
async function waitFor(
inbox: SignalMessage[],
predicate: (message: SignalMessage) => boolean,
timeoutMs = 3000,
): Promise<SignalMessage> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const found = inbox.find(predicate);
if (found) return found;
await new Promise((resolve) => setTimeout(resolve, 10));
}
throw new Error(`timed out; inbox was ${JSON.stringify(inbox)}`);
}
describe("signalling", () => {
it("rejects a socket with an invalid token", async () => {
const hostToken = await register("host");
const created = await fetch(`${baseUrl}/lobbies`, {
method: "POST",
headers: { authorization: `Bearer ${hostToken}`, "content-type": "application/json" },
body: "{}",
});
const { code } = (await created.json()) as { code: string };
const { welcome, socket } = await connect("not-a-real-token", code);
expect(welcome.t).toBe("error");
socket.close();
});
it("refuses a peer that has not joined the lobby over REST", async () => {
const hostToken = await register("host");
const strangerToken = await register("stranger");
const created = await fetch(`${baseUrl}/lobbies`, {
method: "POST",
headers: { authorization: `Bearer ${hostToken}`, "content-type": "application/json" },
body: "{}",
});
const { code } = (await created.json()) as { code: string };
const { welcome, socket } = await connect(strangerToken, code);
expect(welcome.t).toBe("error");
socket.close();
});
it("welcomes host as peer 0 and announces a guest joining", async () => {
const hostToken = await register("host");
const guestToken = await register("guest");
const created = await fetch(`${baseUrl}/lobbies`, {
method: "POST",
headers: { authorization: `Bearer ${hostToken}`, "content-type": "application/json" },
body: "{}",
});
const { code } = (await created.json()) as { code: string };
await fetch(`${baseUrl}/lobbies/${code}/join`, {
method: "POST",
headers: { authorization: `Bearer ${guestToken}` },
});
const host = await connect(hostToken, code);
expect(host.welcome).toMatchObject({ t: "welcome", peerId: 0, isHost: true });
const guest = await connect(guestToken, code);
expect(guest.welcome).toMatchObject({ t: "welcome", peerId: 1, isHost: false });
const announced = await waitFor(host.inbox, (m) => m.t === "peer-joined");
expect(announced).toMatchObject({ t: "peer-joined", peer: { peerId: 1 } });
host.socket.close();
guest.socket.close();
});
it("relays SDP between peers and stamps the true sender", async () => {
const hostToken = await register("host");
const guestToken = await register("guest");
const created = await fetch(`${baseUrl}/lobbies`, {
method: "POST",
headers: { authorization: `Bearer ${hostToken}`, "content-type": "application/json" },
body: "{}",
});
const { code } = (await created.json()) as { code: string };
await fetch(`${baseUrl}/lobbies/${code}/join`, {
method: "POST",
headers: { authorization: `Bearer ${guestToken}` },
});
const host = await connect(hostToken, code);
const guest = await connect(guestToken, code);
host.socket.send(
JSON.stringify({
t: "signal",
// Lie about the sender; the relay must overwrite this with peer 0.
from: 99,
to: 1,
payload: { sdp: { type: "offer", sdp: "v=0 fake" } },
}),
);
const relayed = await waitFor(guest.inbox, (m) => m.t === "signal");
expect(relayed).toMatchObject({
t: "signal",
from: 0,
to: 1,
payload: { sdp: { type: "offer", sdp: "v=0 fake" } },
});
host.socket.close();
guest.socket.close();
});
it("rejects a protocol version mismatch", async () => {
const hostToken = await register("host");
const created = await fetch(`${baseUrl}/lobbies`, {
method: "POST",
headers: { authorization: `Bearer ${hostToken}`, "content-type": "application/json" },
body: "{}",
});
const { code } = (await created.json()) as { code: string };
const socket = new WebSocket(wsUrl);
const inbox: SignalMessage[] = [];
await new Promise<void>((resolve) => socket.on("open", () => resolve()));
socket.on("message", (raw) => inbox.push(JSON.parse(raw.toString()) as SignalMessage));
socket.send(JSON.stringify({ t: "hello", token: hostToken, lobbyCode: code, protocol: 999 }));
const error = await waitFor(inbox, (m) => m.t === "error");
expect(error).toMatchObject({ t: "error" });
expect((error as { message: string }).message).toContain("protocol mismatch");
socket.close();
});
it("tells remaining peers when the host disconnects", async () => {
const hostToken = await register("host");
const guestToken = await register("guest");
const created = await fetch(`${baseUrl}/lobbies`, {
method: "POST",
headers: { authorization: `Bearer ${hostToken}`, "content-type": "application/json" },
body: "{}",
});
const { code } = (await created.json()) as { code: string };
await fetch(`${baseUrl}/lobbies/${code}/join`, {
method: "POST",
headers: { authorization: `Bearer ${guestToken}` },
});
const host = await connect(hostToken, code);
const guest = await connect(guestToken, code);
host.socket.close();
const left = await waitFor(guest.inbox, (m) => m.t === "peer-left");
expect(left).toMatchObject({ t: "peer-left", peerId: 0 });
guest.socket.close();
});
});