Release v0.1.20 2026-07-19

This commit is contained in:
Warren H
2026-07-19 16:37:37 -04:00
parent 437e70fc58
commit 018e060cdd
22 changed files with 884 additions and 121 deletions
+77
View File
@@ -651,6 +651,7 @@ export function createGameApiHandler(options = {}) {
match: {
id: match.id,
seed: match.seed,
generation: match.generation,
countdownEndsAtMs: match.countdownEndsAtMs,
opponentName: opponent.hunterName,
role: ticket.side,
@@ -694,10 +695,12 @@ export function createGameApiHandler(options = {}) {
const match = {
id: matchId,
seed: randomBytes(4).readUInt32BE(0) || 1,
generation: 1,
createdAt: now,
countdownEndsAtMs: now + HOCKEY_PVP_COUNTDOWN_MS,
players: { host: opponent, guest: ticket },
snapshots: { host: null, guest: null },
rematch: null,
};
opponent.matchId = matchId;
opponent.side = "host";
@@ -725,6 +728,72 @@ export function createGameApiHandler(options = {}) {
return { match, side };
}
function hockeyPvpRematchResult(match, side, requestedGeneration) {
const rematch = match.rematch;
if (!rematch || rematch.fromGeneration !== requestedGeneration || !rematch.ready) {
return { status: "waiting" };
}
const opponentSide = side === "host" ? "guest" : "host";
return {
status: "matched",
match: {
id: match.id,
seed: rematch.seed,
generation: rematch.toGeneration,
countdownEndsAtMs: rematch.countdownEndsAtMs,
opponentName: match.players[opponentSide].hunterName,
role: side,
},
};
}
function requestHockeyPvpRematch(session, matchId, payload) {
const { match, side } = requireHockeyPvpMatch(session, matchId);
const generation = Number(payload?.generation);
if (!Number.isSafeInteger(generation) || generation < 1) throw apiError("PVP match generation is invalid.");
if (generation < match.generation) {
if (match.rematch?.fromGeneration !== generation || !match.rematch.ready) {
throw apiError("PVP match generation is stale.", 409);
}
return hockeyPvpRematchResult(match, side, generation);
}
if (generation > match.generation) throw apiError("PVP match generation is invalid.", 409);
if (!match.rematch || match.rematch.fromGeneration !== generation) {
match.rematch = {
fromGeneration: generation,
toGeneration: generation + 1,
requested: { host: false, guest: false },
ready: false,
seed: 0,
countdownEndsAtMs: 0,
};
}
match.rematch.requested[side] = true;
if (!match.rematch.ready && match.rematch.requested.host && match.rematch.requested.guest) {
const now = Date.now();
match.rematch.ready = true;
match.rematch.seed = randomBytes(4).readUInt32BE(0) || 1;
match.rematch.countdownEndsAtMs = now + HOCKEY_PVP_COUNTDOWN_MS;
match.seed = match.rematch.seed;
match.generation = match.rematch.toGeneration;
match.countdownEndsAtMs = match.rematch.countdownEndsAtMs;
match.snapshots = { host: null, guest: null };
}
return hockeyPvpRematchResult(match, side, generation);
}
function cancelHockeyPvpRematch(session, matchId, payload) {
const { match, side } = requireHockeyPvpMatch(session, matchId);
const generation = Number(payload?.generation);
if (!Number.isSafeInteger(generation) || generation < 1) throw apiError("PVP match generation is invalid.");
if (match.rematch?.fromGeneration === generation && !match.rematch.ready) {
match.rematch.requested[side] = false;
}
return { ok: true };
}
async function handle(request, response, next) {
if (!request.url?.startsWith("/api/")) return next();
setCorsHeaders(request, response);
@@ -779,6 +848,7 @@ export function createGameApiHandler(options = {}) {
if (pvpStateMatch && request.method === "PUT") {
const { match, side } = requireHockeyPvpMatch(session, pvpStateMatch[1]);
const payload = await readJson(request);
if (payload?.generation !== match.generation) throw apiError("PVP match generation is stale.", 409);
if (!payload?.snapshot || typeof payload.snapshot !== "object") throw apiError("PVP snapshot is invalid.");
match.snapshots[side] = payload.snapshot;
return sendJson(response, 200, {
@@ -786,6 +856,13 @@ export function createGameApiHandler(options = {}) {
hostSnapshot: match.snapshots.host,
});
}
const pvpRematchMatch = path.match(/^\/api\/hockey-pvp\/matches\/([A-Za-z0-9_-]+)\/rematch$/);
if (pvpRematchMatch && request.method === "POST") {
return sendJson(response, 200, requestHockeyPvpRematch(session, pvpRematchMatch[1], await readJson(request)));
}
if (pvpRematchMatch && request.method === "DELETE") {
return sendJson(response, 200, cancelHockeyPvpRematch(session, pvpRematchMatch[1], await readJson(request)));
}
if (path === "/api/saves" && request.method === "GET") {
return sendJson(response, 200, { slots: listSaves(database, session.accountId) });
}