mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-28 17:07:57 +00:00
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:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user