Fix archive (#2035)

## Description:

Describe the PR.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
This commit is contained in:
evanpelle
2025-09-09 14:37:06 -07:00
committed by GitHub
parent defb6bb1d4
commit fd0fbfab9e
8 changed files with 83 additions and 47 deletions
+2 -3
View File
@@ -8,7 +8,7 @@ import {
PlayerRecord, PlayerRecord,
ServerMessage, ServerMessage,
} from "../core/Schemas"; } from "../core/Schemas";
import { createGameRecord } from "../core/Util"; import { createPartialGameRecord } from "../core/Util";
import { ServerConfig } from "../core/configuration/Config"; import { ServerConfig } from "../core/configuration/Config";
import { getConfig } from "../core/configuration/ConfigLoader"; import { getConfig } from "../core/configuration/ConfigLoader";
import { PlayerActions, UnitType } from "../core/game/Game"; import { PlayerActions, UnitType } from "../core/game/Game";
@@ -221,7 +221,7 @@ export class ClientGameRunner {
if (this.lobby.gameStartInfo === undefined) { if (this.lobby.gameStartInfo === undefined) {
throw new Error("missing gameStartInfo"); throw new Error("missing gameStartInfo");
} }
const record = createGameRecord( const record = createPartialGameRecord(
this.lobby.gameStartInfo.gameID, this.lobby.gameStartInfo.gameID,
this.lobby.gameStartInfo.config, this.lobby.gameStartInfo.config,
players, players,
@@ -230,7 +230,6 @@ export class ClientGameRunner {
startTime(), startTime(),
Date.now(), Date.now(),
update.winner, update.winner,
this.lobby.serverConfig,
); );
endGame(record); endGame(record);
} }
+3 -3
View File
@@ -1,11 +1,11 @@
import { GameConfig, GameID, GameRecord } from "../core/Schemas"; import { GameConfig, GameID, PartialGameRecord } from "../core/Schemas";
import { replacer } from "../core/Util"; import { replacer } from "../core/Util";
export interface LocalStatsData { export interface LocalStatsData {
[key: GameID]: { [key: GameID]: {
lobby: Partial<GameConfig>; lobby: Partial<GameConfig>;
// Only once the game is over // Only once the game is over
gameRecord?: GameRecord; gameRecord?: PartialGameRecord;
}; };
} }
@@ -41,7 +41,7 @@ export function startTime() {
return _startTime; return _startTime;
} }
export function endGame(gameRecord: GameRecord) { export function endGame(gameRecord: PartialGameRecord) {
if (localStorage === undefined) { if (localStorage === undefined) {
return; return;
} }
+8 -5
View File
@@ -4,14 +4,18 @@ import {
AllPlayersStats, AllPlayersStats,
ClientMessage, ClientMessage,
ClientSendWinnerMessage, ClientSendWinnerMessage,
GameRecordSchema,
Intent, Intent,
PartialGameRecordSchema,
PlayerRecord, PlayerRecord,
ServerMessage, ServerMessage,
ServerStartGameMessage, ServerStartGameMessage,
Turn, Turn,
} from "../core/Schemas"; } from "../core/Schemas";
import { createGameRecord, decompressGameRecord, replacer } from "../core/Util"; import {
createPartialGameRecord,
decompressGameRecord,
replacer,
} from "../core/Util";
import { LobbyConfig } from "./ClientGameRunner"; import { LobbyConfig } from "./ClientGameRunner";
import { ReplaySpeedChangeEvent } from "./InputHandler"; import { ReplaySpeedChangeEvent } from "./InputHandler";
import { getPersistentID } from "./Main"; import { getPersistentID } from "./Main";
@@ -188,7 +192,7 @@ export class LocalServer {
if (this.lobbyConfig.gameStartInfo === undefined) { if (this.lobbyConfig.gameStartInfo === undefined) {
throw new Error("missing gameStartInfo"); throw new Error("missing gameStartInfo");
} }
const record = createGameRecord( const record = createPartialGameRecord(
this.lobbyConfig.gameStartInfo.gameID, this.lobbyConfig.gameStartInfo.gameID,
this.lobbyConfig.gameStartInfo.config, this.lobbyConfig.gameStartInfo.config,
players, players,
@@ -196,10 +200,9 @@ export class LocalServer {
this.startedAt, this.startedAt,
Date.now(), Date.now(),
this.winner?.winner, this.winner?.winner,
this.lobbyConfig.serverConfig,
); );
const result = GameRecordSchema.safeParse(record); const result = PartialGameRecordSchema.safeParse(record);
if (!result.success) { if (!result.success) {
const error = z.prettifyError(result.error); const error = z.prettifyError(result.error);
console.error("Error parsing game record", error); console.error("Error parsing game record", error);
+16 -2
View File
@@ -492,7 +492,7 @@ export const ClientMessageSchema = z.discriminatedUnion("type", [
// //
export const PlayerRecordSchema = PlayerSchema.extend({ export const PlayerRecordSchema = PlayerSchema.extend({
persistentID: PersistentIdSchema, // WARNING: PII persistentID: PersistentIdSchema.nullable(), // WARNING: PII
stats: PlayerStatsSchema, stats: PlayerStatsSchema,
}); });
export type PlayerRecord = z.infer<typeof PlayerRecordSchema>; export type PlayerRecord = z.infer<typeof PlayerRecordSchema>;
@@ -512,16 +512,30 @@ const GitCommitSchema = z
.regex(/^[0-9a-fA-F]{40}$/) .regex(/^[0-9a-fA-F]{40}$/)
.or(z.literal("DEV")); .or(z.literal("DEV"));
export const AnalyticsRecordSchema = z.object({ export const PartialAnalyticsRecordSchema = z.object({
info: GameEndInfoSchema, info: GameEndInfoSchema,
version: z.literal("v0.0.2"), version: z.literal("v0.0.2"),
});
export type ClientAnalyticsRecord = z.infer<
typeof PartialAnalyticsRecordSchema
>;
export const AnalyticsRecordSchema = PartialAnalyticsRecordSchema.extend({
gitCommit: GitCommitSchema, gitCommit: GitCommitSchema,
subdomain: z.string(), subdomain: z.string(),
domain: z.string(), domain: z.string(),
}); });
export type AnalyticsRecord = z.infer<typeof AnalyticsRecordSchema>; export type AnalyticsRecord = z.infer<typeof AnalyticsRecordSchema>;
export const GameRecordSchema = AnalyticsRecordSchema.extend({ export const GameRecordSchema = AnalyticsRecordSchema.extend({
turns: TurnSchema.array(), turns: TurnSchema.array(),
}); });
export const PartialGameRecordSchema = PartialAnalyticsRecordSchema.extend({
turns: TurnSchema.array(),
});
export type PartialGameRecord = z.infer<typeof PartialGameRecordSchema>;
export type GameRecord = z.infer<typeof GameRecordSchema>; export type GameRecord = z.infer<typeof GameRecordSchema>;
+5 -13
View File
@@ -6,12 +6,12 @@ import {
GameConfig, GameConfig,
GameID, GameID,
GameRecord, GameRecord,
PartialGameRecord,
PlayerRecord, PlayerRecord,
Turn, Turn,
Winner, Winner,
} from "./Schemas"; } from "./Schemas";
import { ServerConfig } from "./configuration/Config";
import { import {
BOT_NAME_PREFIXES, BOT_NAME_PREFIXES,
BOT_NAME_SUFFIXES, BOT_NAME_SUFFIXES,
@@ -150,7 +150,7 @@ export function onlyImages(html: string) {
}); });
} }
export function createGameRecord( export function createPartialGameRecord(
gameID: GameID, gameID: GameID,
config: GameConfig, config: GameConfig,
// username does not need to be set. // username does not need to be set.
@@ -159,18 +159,13 @@ export function createGameRecord(
start: number, start: number,
end: number, end: number,
winner: Winner, winner: Winner,
serverConfig: ServerConfig, ): PartialGameRecord {
): GameRecord {
const duration = Math.floor((end - start) / 1000); const duration = Math.floor((end - start) / 1000);
const version = "v0.0.2";
const gitCommit = serverConfig.gitCommit();
const subdomain = serverConfig.subdomain();
const domain = serverConfig.domain();
const num_turns = allTurns.length; const num_turns = allTurns.length;
const turns = allTurns.filter( const turns = allTurns.filter(
(t) => t.intents.length !== 0 || t.hash !== undefined, (t) => t.intents.length !== 0 || t.hash !== undefined,
); );
const record: GameRecord = { const record: PartialGameRecord = {
info: { info: {
gameID, gameID,
config, config,
@@ -181,10 +176,7 @@ export function createGameRecord(
num_turns, num_turns,
winner, winner,
}, },
version, version: "v0.0.2",
gitCommit,
subdomain,
domain,
turns, turns,
}; };
return record; return record;
+26 -2
View File
@@ -1,5 +1,12 @@
import z from "zod";
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader"; import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { GameID, GameRecord, GameRecordSchema, ID } from "../core/Schemas"; import {
GameID,
GameRecord,
GameRecordSchema,
ID,
PartialGameRecord,
} from "../core/Schemas";
import { logger } from "./Logger"; import { logger } from "./Logger";
const config = getServerConfigFromServer(); const config = getServerConfigFromServer();
@@ -8,7 +15,13 @@ const log = logger.child({ component: "Archive" });
export async function archive(gameRecord: GameRecord) { export async function archive(gameRecord: GameRecord) {
try { try {
gameRecord.gitCommit = config.gitCommit(); const parsed = GameRecordSchema.safeParse(gameRecord);
if (!parsed.success) {
log.error(`invalid game record: ${z.prettifyError(parsed.error)}`, {
gameID: gameRecord.info.gameID,
});
return;
}
const url = `${config.jwtIssuer()}/game/${gameRecord.info.gameID}`; const url = `${config.jwtIssuer()}/game/${gameRecord.info.gameID}`;
const response = await fetch(url, { const response = await fetch(url, {
method: "POST", method: "POST",
@@ -62,3 +75,14 @@ export async function readGameRecord(
return null; return null;
} }
} }
export function finalizeGameRecord(
clientRecord: PartialGameRecord,
): GameRecord {
return {
...clientRecord,
gitCommit: config.gitCommit(),
subdomain: config.subdomain(),
domain: config.domain(),
};
}
+14 -13
View File
@@ -2,6 +2,8 @@ import ipAnonymize from "ip-anonymize";
import { Logger } from "winston"; import { Logger } from "winston";
import WebSocket from "ws"; import WebSocket from "ws";
import { z } from "zod"; import { z } from "zod";
import { GameEnv, ServerConfig } from "../core/configuration/Config";
import { GameType } from "../core/game/Game";
import { import {
ClientID, ClientID,
ClientMessageSchema, ClientMessageSchema,
@@ -19,10 +21,8 @@ import {
ServerTurnMessage, ServerTurnMessage,
Turn, Turn,
} from "../core/Schemas"; } from "../core/Schemas";
import { createGameRecord } from "../core/Util"; import { createPartialGameRecord } from "../core/Util";
import { GameEnv, ServerConfig } from "../core/configuration/Config"; import { archive, finalizeGameRecord } from "./Archive";
import { GameType } from "../core/game/Game";
import { archive } from "./Archive";
import { Client } from "./Client"; import { Client } from "./Client";
export enum GamePhase { export enum GamePhase {
Lobby = "LOBBY", Lobby = "LOBBY",
@@ -680,15 +680,16 @@ export class GameServer {
}, },
); );
archive( archive(
createGameRecord( finalizeGameRecord(
this.id, createPartialGameRecord(
this.gameStartInfo.config, this.id,
playerRecords, this.gameStartInfo.config,
this.turns, playerRecords,
this._startTime ?? 0, this.turns,
Date.now(), this._startTime ?? 0,
this.winner?.winner, Date.now(),
this.config, this.winner?.winner,
),
), ),
); );
} }
+9 -6
View File
@@ -12,13 +12,12 @@ import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
import { GameType } from "../core/game/Game"; import { GameType } from "../core/game/Game";
import { import {
ClientMessageSchema, ClientMessageSchema,
GameRecord,
GameRecordSchema,
ID, ID,
PartialGameRecordSchema,
ServerErrorMessage, ServerErrorMessage,
} from "../core/Schemas"; } from "../core/Schemas";
import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas"; import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas";
import { archive, readGameRecord } from "./Archive"; import { archive, finalizeGameRecord, readGameRecord } from "./Archive";
import { Client } from "./Client"; import { Client } from "./Client";
import { GameManager } from "./GameManager"; import { GameManager } from "./GameManager";
import { getUserMe, verifyClientToken } from "./jwt"; import { getUserMe, verifyClientToken } from "./jwt";
@@ -252,13 +251,13 @@ export async function startWorker() {
try { try {
const record = req.body; const record = req.body;
const result = GameRecordSchema.safeParse(record); const result = PartialGameRecordSchema.safeParse(record);
if (!result.success) { if (!result.success) {
const error = z.prettifyError(result.error); const error = z.prettifyError(result.error);
log.info(error); log.info(error);
return res.status(400).json({ error }); return res.status(400).json({ error });
} }
const gameRecord: GameRecord = result.data; const gameRecord = result.data;
if (gameRecord.info.config.gameType !== GameType.Singleplayer) { if (gameRecord.info.config.gameType !== GameType.Singleplayer) {
log.warn( log.warn(
@@ -277,7 +276,11 @@ export async function startWorker() {
return res.status(400).json({ error: "Invalid request" }); return res.status(400).json({ error: "Invalid request" });
} }
archive(gameRecord); log.info("archiving singleplayer game", {
gameID: gameRecord.info.gameID,
});
archive(finalizeGameRecord(gameRecord));
res.json({ res.json({
success: true, success: true,
}); });