mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-23 19:23:08 +00:00
Merge branch 'main' into patterned-territory
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -188,9 +188,10 @@ export class ClientGameRunner {
|
||||
}
|
||||
|
||||
private saveGame(update: WinUpdate) {
|
||||
if (this.myPlayer === null) throw new Error("Not initialized");
|
||||
const players: PlayerRecord[] = [
|
||||
{
|
||||
ip: null,
|
||||
playerID: this.myPlayer.id(),
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
username: this.lobby.playerName,
|
||||
clientID: this.lobby.clientID,
|
||||
@@ -211,7 +212,7 @@ export class ClientGameRunner {
|
||||
}
|
||||
const record = createGameRecord(
|
||||
this.lobby.gameStartInfo.gameID,
|
||||
this.lobby.gameStartInfo,
|
||||
this.lobby.gameStartInfo.config,
|
||||
players,
|
||||
// Not saving turns locally
|
||||
[],
|
||||
|
||||
@@ -47,7 +47,7 @@ export function endGame(gameRecord: GameRecord) {
|
||||
}
|
||||
|
||||
const stats = getStats();
|
||||
const gameStat = stats[gameRecord.id];
|
||||
const gameStat = stats[gameRecord.info.gameID];
|
||||
|
||||
if (!gameStat) {
|
||||
consolex.log("LocalPersistantStats: game not found");
|
||||
|
||||
@@ -176,7 +176,7 @@ export class LocalServer {
|
||||
}
|
||||
const players: PlayerRecord[] = [
|
||||
{
|
||||
ip: null,
|
||||
playerID: this.lobbyConfig.clientID, // hack?
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
username: this.lobbyConfig.playerName,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
@@ -188,7 +188,7 @@ export class LocalServer {
|
||||
}
|
||||
const record = createGameRecord(
|
||||
this.lobbyConfig.gameStartInfo.gameID,
|
||||
this.lobbyConfig.gameStartInfo,
|
||||
this.lobbyConfig.gameStartInfo.config,
|
||||
players,
|
||||
this.turns,
|
||||
this.startedAt,
|
||||
|
||||
+1
-1
@@ -305,7 +305,7 @@ class Client {
|
||||
playerName: this.usernameInput?.getCurrentUsername() ?? "",
|
||||
token: localStorage.getItem("token") ?? getPersistentIDFromCookie(),
|
||||
clientID: lobby.clientID,
|
||||
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.gameStartInfo,
|
||||
gameStartInfo: lobby.gameStartInfo ?? lobby.gameRecord?.info,
|
||||
gameRecord: lobby.gameRecord,
|
||||
},
|
||||
() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import nuke from "../../../resources/sprites/nukeExplosion.png";
|
||||
import SAMExplosion from "../../../resources/sprites/samExplosion.png";
|
||||
import { AnimatedSprite } from "./AnimatedSprite";
|
||||
import { FxType } from "./fx/Fx";
|
||||
|
||||
@@ -22,6 +23,15 @@ const ANIMATED_SPRITE_CONFIG: Partial<Record<FxType, AnimatedSpriteConfig>> = {
|
||||
originX: 30,
|
||||
originY: 30,
|
||||
},
|
||||
[FxType.SAMExplosion]: {
|
||||
url: SAMExplosion,
|
||||
frameWidth: 48,
|
||||
frameCount: 9,
|
||||
frameDuration: 70,
|
||||
looping: false,
|
||||
originX: 23,
|
||||
originY: 19,
|
||||
},
|
||||
};
|
||||
|
||||
const animatedSpriteImageMap: Map<FxType, CanvasImageSource> = new Map();
|
||||
|
||||
@@ -4,4 +4,5 @@ export interface Fx {
|
||||
|
||||
export enum FxType {
|
||||
Nuke = "Nuke",
|
||||
SAMExplosion = "SAMExplosion",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { AnimatedSprite } from "../AnimatedSprite";
|
||||
import { createAnimatedSpriteForUnit } from "../AnimatedSpriteLoader";
|
||||
import { Fx, FxType } from "./Fx";
|
||||
|
||||
/**
|
||||
* Explosion effect: sprite animation of an explosion
|
||||
*/
|
||||
export class SAMExplosionFx implements Fx {
|
||||
private lifeTime: number = 0;
|
||||
private explosionSprite: AnimatedSprite | null;
|
||||
constructor(
|
||||
private x: number,
|
||||
private y: number,
|
||||
private duration: number,
|
||||
) {
|
||||
this.explosionSprite = createAnimatedSpriteForUnit(FxType.SAMExplosion);
|
||||
}
|
||||
|
||||
renderTick(frameTime: number, ctx: CanvasRenderingContext2D): boolean {
|
||||
if (this.explosionSprite) {
|
||||
this.lifeTime += frameTime;
|
||||
if (this.lifeTime >= this.duration) {
|
||||
return false;
|
||||
}
|
||||
if (this.explosionSprite.isActive()) {
|
||||
this.explosionSprite.update(frameTime);
|
||||
this.explosionSprite.draw(ctx, this.x, this.y);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { GameView, UnitView } from "../../../core/game/GameView";
|
||||
import { loadAllAnimatedSpriteImages } from "../AnimatedSpriteLoader";
|
||||
import { Fx } from "../fx/Fx";
|
||||
import { NukeExplosionFx, ShockwaveFx } from "../fx/NukeFx";
|
||||
import { SAMExplosionFx } from "../fx/SAMExplosionFx";
|
||||
import { Layer } from "./Layer";
|
||||
|
||||
export class FxLayer implements Layer {
|
||||
@@ -35,25 +36,43 @@ export class FxLayer implements Layer {
|
||||
switch (unit.type()) {
|
||||
case UnitType.AtomBomb:
|
||||
case UnitType.MIRVWarhead:
|
||||
this.handleNukeExplosion(unit, 70);
|
||||
this.handleNukes(unit, 70);
|
||||
break;
|
||||
case UnitType.HydrogenBomb:
|
||||
this.handleNukeExplosion(unit, 250);
|
||||
this.handleNukes(unit, 250);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
handleNukeExplosion(unit: UnitView, shockwaveRadius: number) {
|
||||
handleNukes(unit: UnitView, shockwaveRadius: number) {
|
||||
if (!unit.isActive()) {
|
||||
const x = this.game.x(unit.lastTile());
|
||||
const y = this.game.y(unit.lastTile());
|
||||
const nuke = new NukeExplosionFx(x, y, 1000);
|
||||
this.allFx.push(nuke as Fx);
|
||||
const shockwave = new ShockwaveFx(x, y, 1500, shockwaveRadius);
|
||||
this.allFx.push(shockwave as Fx);
|
||||
if (unit.wasInterceptedBySAM()) {
|
||||
this.handleSAMInterception(unit);
|
||||
} else {
|
||||
// Kaboom
|
||||
this.handleNukeExplosion(unit, shockwaveRadius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNukeExplosion(unit: UnitView, shockwaveRadius: number) {
|
||||
const x = this.game.x(unit.lastTile());
|
||||
const y = this.game.y(unit.lastTile());
|
||||
const nuke = new NukeExplosionFx(x, y, 1000);
|
||||
this.allFx.push(nuke as Fx);
|
||||
const shockwave = new ShockwaveFx(x, y, 1500, shockwaveRadius);
|
||||
this.allFx.push(shockwave as Fx);
|
||||
}
|
||||
|
||||
handleSAMInterception(unit: UnitView) {
|
||||
const x = this.game.x(unit.lastTile());
|
||||
const y = this.game.y(unit.lastTile());
|
||||
const interception = new SAMExplosionFx(x, y, 1000);
|
||||
this.allFx.push(interception as Fx);
|
||||
const shockwave = new ShockwaveFx(x, y, 800, 40);
|
||||
this.allFx.push(shockwave as Fx);
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.redraw();
|
||||
try {
|
||||
|
||||
+18
-16
@@ -1,6 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import quickChatData from "../../resources/QuickChat.json" with { type: "json" };
|
||||
import { PlayerStatsSchema } from "./ArchiveSchemas";
|
||||
import {
|
||||
AllPlayers,
|
||||
Difficulty,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
PlayerType,
|
||||
UnitType,
|
||||
} from "./game/Game";
|
||||
import { PlayerStatsSchema } from "./StatsSchemas";
|
||||
import { flattenedEmojiTable } from "./Util";
|
||||
|
||||
export type GameID = string;
|
||||
@@ -88,9 +88,6 @@ export type ClientJoinMessage = z.infer<typeof ClientJoinMessageSchema>;
|
||||
export type ClientLogMessage = z.infer<typeof ClientLogMessageSchema>;
|
||||
export type ClientHashMessage = z.infer<typeof ClientHashSchema>;
|
||||
|
||||
export type PlayerRecord = z.infer<typeof PlayerRecordSchema>;
|
||||
export type GameRecord = z.infer<typeof GameRecordSchema>;
|
||||
|
||||
export type AllPlayersStats = z.infer<typeof AllPlayersStatsSchema>;
|
||||
export type Player = z.infer<typeof PlayerSchema>;
|
||||
export type GameStartInfo = z.infer<typeof GameStartInfoSchema>;
|
||||
@@ -445,26 +442,31 @@ export const ClientMessageSchema = z.union([
|
||||
ClientHashSchema,
|
||||
]);
|
||||
|
||||
export const PlayerRecordSchema = z.object({
|
||||
clientID: ID,
|
||||
username: SafeString,
|
||||
ip: SafeString.nullable(), // WARNING: PII
|
||||
export const PlayerRecordSchema = PlayerSchema.extend({
|
||||
persistentID: PersistentIdSchema, // WARNING: PII
|
||||
stats: PlayerStatsSchema,
|
||||
});
|
||||
export type PlayerRecord = z.infer<typeof PlayerRecordSchema>;
|
||||
|
||||
export const GameRecordSchema = z.object({
|
||||
id: ID,
|
||||
gameStartInfo: GameStartInfoSchema,
|
||||
export const GameEndInfoSchema = GameStartInfoSchema.extend({
|
||||
players: z.array(PlayerRecordSchema),
|
||||
startTimestampMS: z.number(),
|
||||
endTimestampMS: z.number(),
|
||||
durationSeconds: z.number(),
|
||||
date: SafeString,
|
||||
start: z.number(),
|
||||
end: z.number(),
|
||||
duration: z.number().nonnegative(),
|
||||
num_turns: z.number(),
|
||||
turns: z.array(TurnSchema),
|
||||
winner: z.union([ID, SafeString]).nullable().optional(),
|
||||
winnerType: z.enum(["player", "team"]).nullable().optional(),
|
||||
});
|
||||
export type GameEndInfo = z.infer<typeof GameEndInfoSchema>;
|
||||
|
||||
export const AnalyticsRecordSchema = z.object({
|
||||
info: GameEndInfoSchema,
|
||||
version: z.literal("v0.0.2"),
|
||||
gitCommit: z.string(),
|
||||
});
|
||||
export type AnalyticsRecord = z.infer<typeof AnalyticsRecordSchema>;
|
||||
|
||||
export const GameRecordSchema = AnalyticsRecordSchema.extend({
|
||||
turns: z.array(TurnSchema),
|
||||
});
|
||||
export type GameRecord = z.infer<typeof GameRecordSchema>;
|
||||
|
||||
+25
-39
@@ -5,9 +5,9 @@ import { Cell, Team, Unit } from "./game/Game";
|
||||
import { GameMap, TileRef } from "./game/GameMap";
|
||||
import {
|
||||
ClientID,
|
||||
GameConfig,
|
||||
GameID,
|
||||
GameRecord,
|
||||
GameStartInfo,
|
||||
PlayerRecord,
|
||||
Turn,
|
||||
} from "./Schemas";
|
||||
@@ -184,53 +184,39 @@ export function onlyImages(html: string) {
|
||||
}
|
||||
|
||||
export function createGameRecord(
|
||||
id: GameID,
|
||||
gameStartInfo: GameStartInfo,
|
||||
gameID: GameID,
|
||||
config: GameConfig,
|
||||
// username does not need to be set.
|
||||
players: PlayerRecord[],
|
||||
turns: Turn[],
|
||||
startTimestampMS: number,
|
||||
endTimestampMS: number,
|
||||
allTurns: Turn[],
|
||||
start: number,
|
||||
end: number,
|
||||
winner: ClientID | Team | null,
|
||||
winnerType: "player" | "team" | null,
|
||||
): GameRecord {
|
||||
const durationSeconds = Math.floor(
|
||||
(endTimestampMS - startTimestampMS) / 1000,
|
||||
);
|
||||
const date = new Date().toISOString().split("T")[0];
|
||||
const duration = Math.floor((end - start) / 1000);
|
||||
const version = "v0.0.2";
|
||||
const gitCommit = "";
|
||||
const num_turns = allTurns.length;
|
||||
const turns = allTurns.filter(
|
||||
(t) => t.intents.length !== 0 || t.hash !== undefined,
|
||||
);
|
||||
const record: GameRecord = {
|
||||
gitCommit,
|
||||
id,
|
||||
gameStartInfo,
|
||||
players,
|
||||
startTimestampMS,
|
||||
endTimestampMS,
|
||||
durationSeconds,
|
||||
date,
|
||||
num_turns: 0,
|
||||
turns: [],
|
||||
info: {
|
||||
gameID,
|
||||
config,
|
||||
players,
|
||||
start,
|
||||
end,
|
||||
duration,
|
||||
num_turns,
|
||||
winner,
|
||||
winnerType,
|
||||
},
|
||||
version,
|
||||
winner,
|
||||
winnerType,
|
||||
gitCommit,
|
||||
turns,
|
||||
};
|
||||
|
||||
for (const turn of turns) {
|
||||
if (turn.intents.length !== 0 || turn.hash !== undefined) {
|
||||
record.turns.push(turn);
|
||||
for (const intent of turn.intents) {
|
||||
if (intent.type === "spawn") {
|
||||
for (const playerRecord of players) {
|
||||
if (playerRecord.clientID === intent.clientID) {
|
||||
playerRecord.username = intent.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
record.num_turns = turns.length;
|
||||
return record;
|
||||
}
|
||||
|
||||
@@ -249,7 +235,7 @@ export function decompressGameRecord(gameRecord: GameRecord) {
|
||||
lastTurnNum = turn.turnNumber;
|
||||
}
|
||||
const turnLength = turns.length;
|
||||
for (let i = turnLength; i < gameRecord.num_turns; i++) {
|
||||
for (let i = turnLength; i < gameRecord.info.num_turns; i++) {
|
||||
turns.push({
|
||||
turnNumber: i,
|
||||
intents: [],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { NukeType } from "../ArchiveSchemas";
|
||||
import { consolex } from "../Consolex";
|
||||
import {
|
||||
Execution,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { ParabolaPathFinder } from "../pathfinding/PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { NukeType } from "../StatsSchemas";
|
||||
|
||||
export class NukeExecution implements Execution {
|
||||
private active = true;
|
||||
|
||||
@@ -180,7 +180,10 @@ export class SAMLauncherExecution implements Execution {
|
||||
this.sam.owner().id(),
|
||||
);
|
||||
// Delete warheads
|
||||
mirvWarheadTargets.forEach((u) => u.delete());
|
||||
mirvWarheadTargets.forEach((u) => {
|
||||
u.setInterceptedBySam();
|
||||
u.delete();
|
||||
});
|
||||
} else if (target !== null) {
|
||||
target.setTargetedBySAM(true);
|
||||
this.mg.addExecution(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { NukeType } from "../ArchiveSchemas";
|
||||
import {
|
||||
Execution,
|
||||
Game,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
import { TileRef } from "../game/GameMap";
|
||||
import { AirPathFinder } from "../pathfinding/PathFinding";
|
||||
import { PseudoRandom } from "../PseudoRandom";
|
||||
import { NukeType } from "../StatsSchemas";
|
||||
|
||||
export class SAMMissileExecution implements Execution {
|
||||
private active = true;
|
||||
@@ -66,6 +66,7 @@ export class SAMMissileExecution implements Execution {
|
||||
this._owner.id(),
|
||||
);
|
||||
this.active = false;
|
||||
this.target.setInterceptedBySam();
|
||||
this.target.delete(true, this._owner);
|
||||
this.SAMMissile.delete(false);
|
||||
|
||||
|
||||
@@ -171,6 +171,13 @@ export const BOT_NAME_PREFIXES = [
|
||||
"Baloch",
|
||||
"Afghan",
|
||||
"Persian",
|
||||
"Kenyan",
|
||||
"Ugandan",
|
||||
"Bhutanese",
|
||||
"Latin",
|
||||
"Moldovan",
|
||||
"Militant",
|
||||
"Spartan",
|
||||
];
|
||||
export const BOT_NAME_SUFFIXES = [
|
||||
"Empire",
|
||||
@@ -224,9 +231,24 @@ export const BOT_NAME_SUFFIXES = [
|
||||
"Ascendancy",
|
||||
"Supremacy",
|
||||
"Province",
|
||||
"Kingdoms",
|
||||
"Tribes",
|
||||
"Tribe",
|
||||
"Dominion",
|
||||
"Assembly",
|
||||
"Republics",
|
||||
"Army",
|
||||
"Dictatorship",
|
||||
"Country",
|
||||
"Oligarchy",
|
||||
"Monkdom",
|
||||
"Throng",
|
||||
"Host",
|
||||
"Area",
|
||||
"District",
|
||||
"Fief",
|
||||
"Wilderness",
|
||||
"Settlement",
|
||||
"Parliament",
|
||||
"Anarchy",
|
||||
"Democracy",
|
||||
"Autocracy",
|
||||
];
|
||||
|
||||
@@ -346,6 +346,8 @@ export interface Unit {
|
||||
targetUnit(): Unit | undefined;
|
||||
setTargetedBySAM(targeted: boolean): void;
|
||||
targetedBySAM(): boolean;
|
||||
setInterceptedBySam(): void;
|
||||
interceptedBySam(): boolean;
|
||||
|
||||
// Health
|
||||
hasHealth(): boolean;
|
||||
|
||||
@@ -73,6 +73,7 @@ export interface UnitUpdate {
|
||||
pos: TileRef;
|
||||
lastPos: TileRef;
|
||||
isActive: boolean;
|
||||
wasIntercepted: boolean;
|
||||
retreating: boolean;
|
||||
targetUnitId?: number; // Only for trade ships
|
||||
targetTile?: TileRef; // Only for nukes
|
||||
|
||||
@@ -93,6 +93,9 @@ export class UnitView {
|
||||
isActive(): boolean {
|
||||
return this.data.isActive;
|
||||
}
|
||||
wasInterceptedBySAM(): boolean {
|
||||
return this.data.wasIntercepted;
|
||||
}
|
||||
hasHealth(): boolean {
|
||||
return this.data.health !== undefined;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NukeType, OtherUnitType, PlayerStats } from "../ArchiveSchemas";
|
||||
import { AllPlayersStats } from "../Schemas";
|
||||
import { NukeType, OtherUnitType, PlayerStats } from "../StatsSchemas";
|
||||
import { Player, TerraNullius } from "./Game";
|
||||
|
||||
export interface Stats {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AllPlayersStats } from "../Schemas";
|
||||
import {
|
||||
ATTACK_INDEX_CANCEL,
|
||||
ATTACK_INDEX_RECV,
|
||||
@@ -23,8 +24,7 @@ import {
|
||||
PlayerStats,
|
||||
unitTypeToBombUnit,
|
||||
unitTypeToOtherUnit,
|
||||
} from "../ArchiveSchemas";
|
||||
import { AllPlayersStats } from "../Schemas";
|
||||
} from "../StatsSchemas";
|
||||
import { Player, TerraNullius } from "./Game";
|
||||
import { Stats } from "./Stats";
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export class UnitImpl implements Unit {
|
||||
private _lastTile: TileRef;
|
||||
private _retreating: boolean = false;
|
||||
private _targetedBySAM = false;
|
||||
private _interceptedBySAM = false;
|
||||
private _lastSetSafeFromPirates: number; // Only for trade ships
|
||||
private _constructionType: UnitType | undefined;
|
||||
private _lastOwner: PlayerImpl | null = null;
|
||||
@@ -85,6 +86,7 @@ export class UnitImpl implements Unit {
|
||||
ownerID: this._owner.smallID(),
|
||||
lastOwnerID: this._lastOwner?.smallID(),
|
||||
isActive: this._active,
|
||||
wasIntercepted: this._interceptedBySAM,
|
||||
retreating: this._retreating,
|
||||
pos: this._tile,
|
||||
lastPos: this._lastTile,
|
||||
@@ -304,6 +306,14 @@ export class UnitImpl implements Unit {
|
||||
return this._targetedBySAM;
|
||||
}
|
||||
|
||||
setInterceptedBySam(): void {
|
||||
this._interceptedBySAM = true;
|
||||
}
|
||||
|
||||
interceptedBySam(): boolean {
|
||||
return this._interceptedBySAM;
|
||||
}
|
||||
|
||||
setSafeFromPirates(): void {
|
||||
this._lastSetSafeFromPirates = this.mg.ticks();
|
||||
}
|
||||
|
||||
+18
-34
@@ -1,6 +1,6 @@
|
||||
import { S3 } from "@aws-sdk/client-s3";
|
||||
import { getServerConfigFromServer } from "../core/configuration/ConfigLoader";
|
||||
import { GameID, GameRecord } from "../core/Schemas";
|
||||
import { AnalyticsRecord, GameID, GameRecord } from "../core/Schemas";
|
||||
import { logger } from "./Logger";
|
||||
|
||||
const config = getServerConfigFromServer();
|
||||
@@ -30,12 +30,12 @@ export async function archive(gameRecord: GameRecord) {
|
||||
// Archive full game if there are turns
|
||||
if (gameRecord.turns.length > 0) {
|
||||
log.info(
|
||||
`${gameRecord.id}: game has more than zero turns, attempting to write to full game to R2`,
|
||||
`${gameRecord.info.gameID}: game has more than zero turns, attempting to write to full game to R2`,
|
||||
);
|
||||
await archiveFullGameToR2(gameRecord);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`${gameRecord.id}: Final archive error: ${error}`, {
|
||||
log.error(`${gameRecord.info.gameID}: Final archive error: ${error}`, {
|
||||
message: error?.message || error,
|
||||
stack: error?.stack,
|
||||
name: error?.name,
|
||||
@@ -46,29 +46,16 @@ export async function archive(gameRecord: GameRecord) {
|
||||
|
||||
async function archiveAnalyticsToR2(gameRecord: GameRecord) {
|
||||
// Create analytics data object
|
||||
const analyticsData = {
|
||||
id: gameRecord.id,
|
||||
env: config.env(),
|
||||
start_time: new Date(gameRecord.startTimestampMS).toISOString(),
|
||||
end_time: new Date(gameRecord.endTimestampMS).toISOString(),
|
||||
duration_seconds: gameRecord.durationSeconds,
|
||||
number_turns: gameRecord.num_turns,
|
||||
game_mode: gameRecord.gameStartInfo.config.gameType,
|
||||
winner: gameRecord.winner,
|
||||
difficulty: gameRecord.gameStartInfo.config.difficulty,
|
||||
mapType: gameRecord.gameStartInfo.config.gameMap,
|
||||
players: gameRecord.players.map((p) => ({
|
||||
username: p.username,
|
||||
ip: p.ip,
|
||||
persistentID: p.persistentID,
|
||||
clientID: p.clientID,
|
||||
stats: p.stats,
|
||||
})),
|
||||
const { info, version, gitCommit } = gameRecord;
|
||||
const analyticsData: AnalyticsRecord = {
|
||||
info,
|
||||
version,
|
||||
gitCommit,
|
||||
};
|
||||
|
||||
try {
|
||||
// Store analytics data using just the game ID as the key
|
||||
const analyticsKey = `${gameRecord.id}.json`;
|
||||
const analyticsKey = `${info.gameID}.json`;
|
||||
|
||||
await r2.putObject({
|
||||
Bucket: bucket,
|
||||
@@ -77,17 +64,14 @@ async function archiveAnalyticsToR2(gameRecord: GameRecord) {
|
||||
ContentType: "application/json",
|
||||
});
|
||||
|
||||
log.info(`${gameRecord.id}: successfully wrote game analytics to R2`);
|
||||
log.info(`${info.gameID}: successfully wrote game analytics to R2`);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
`${gameRecord.id}: Error writing game analytics to R2: ${error}`,
|
||||
{
|
||||
message: error?.message || error,
|
||||
stack: error?.stack,
|
||||
name: error?.name,
|
||||
...(error && typeof error === "object" ? error : {}),
|
||||
},
|
||||
);
|
||||
log.error(`${info.gameID}: Error writing game analytics to R2: ${error}`, {
|
||||
message: error?.message || error,
|
||||
stack: error?.stack,
|
||||
name: error?.name,
|
||||
...(error && typeof error === "object" ? error : {}),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -110,11 +94,11 @@ async function archiveFullGameToR2(gameRecord: GameRecord) {
|
||||
ContentType: "application/json",
|
||||
});
|
||||
} catch (error) {
|
||||
log.error(`error saving game ${gameRecord.id}`);
|
||||
log.error(`error saving game ${gameRecord.info.gameID}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
log.info(`${gameRecord.id}: game record successfully written to R2`);
|
||||
log.info(`${gameRecord.info.gameID}: game record successfully written to R2`);
|
||||
}
|
||||
|
||||
export async function readGameRecord(
|
||||
|
||||
@@ -394,7 +394,7 @@ export class GameServer {
|
||||
);
|
||||
}
|
||||
return {
|
||||
ip: ipAnonymize(client.ip),
|
||||
playerID: client.playerID,
|
||||
clientID: client.clientID,
|
||||
username: client.username,
|
||||
persistentID: client.persistentID,
|
||||
@@ -404,7 +404,7 @@ export class GameServer {
|
||||
archive(
|
||||
createGameRecord(
|
||||
this.id,
|
||||
this.gameStartInfo,
|
||||
this.gameStartInfo.config,
|
||||
playerRecords,
|
||||
this.turns,
|
||||
this._startTime ?? 0,
|
||||
|
||||
@@ -242,14 +242,12 @@ export function startWorker() {
|
||||
"/api/archive_singleplayer_game",
|
||||
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
|
||||
const gameRecord: GameRecord = req.body;
|
||||
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
|
||||
|
||||
if (!gameRecord) {
|
||||
log.info("game record not found in request");
|
||||
res.status(404).json({ error: "Game record not found" });
|
||||
return;
|
||||
}
|
||||
gameRecord.players.forEach((p) => (p.ip = clientIP));
|
||||
archive(gameRecord);
|
||||
res.json({
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user