Enable the sort-keys eslint rule (#1746)

## Description:

Enable the `sort-keys` eslint rule.

Fixes #1629

## 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
- [ ] I have read and accepted the CLA agreement (only required once).
This commit is contained in:
Scott Anderson
2025-08-07 19:13:42 -04:00
committed by GitHub
parent 63cb51a4f9
commit b56e380107
48 changed files with 320 additions and 254 deletions
+11
View File
@@ -50,6 +50,17 @@ export default [
// Enable rules
"@typescript-eslint/prefer-nullish-coalescing": "error",
eqeqeq: "error",
"sort-keys": "error",
},
},
{
files: [
"**/*.config.{js,ts,jsx,tsx}",
"**/*.test.{js,ts,jsx,tsx}",
"src/client/**/*.{js,ts,jsx,tsx}",
],
rules: {
"sort-keys": "off",
},
},
];
+1
View File
@@ -6,6 +6,7 @@ export const RefreshResponseSchema = z.object({
});
export type RefreshResponse = z.infer<typeof RefreshResponseSchema>;
/* eslint-disable sort-keys */
export const TokenPayloadSchema = z.object({
jti: z.string(),
sub: z
+4
View File
@@ -3,8 +3,10 @@ import { RequiredPatternSchema } from "./Schemas";
export const ProductSchema = z.object({
productId: z.string(),
/* eslint-disable sort-keys */
priceId: z.string(),
price: z.string(),
/* eslint-enable sort-keys */
});
const PatternSchema = z.object({
@@ -16,6 +18,7 @@ const PatternSchema = z.object({
// Schema for resources/cosmetics/cosmetics.json
export const CosmeticsSchema = z.object({
patterns: z.record(z.string(), PatternSchema),
/* eslint-disable sort-keys */
flag: z
.object({
layers: z.record(
@@ -35,6 +38,7 @@ export const CosmeticsSchema = z.object({
),
})
.optional(),
/* eslint-enable sort-keys */
});
export type Cosmetics = z.infer<typeof CosmeticsSchema>;
export type Pattern = z.infer<typeof PatternSchema>;
+3
View File
@@ -2,12 +2,14 @@ import { Cosmetics } from "./CosmeticSchemas";
const ANIMATION_DURATIONS: Record<string, number> = {
rainbow: 4000,
/* eslint-disable sort-keys */
"bright-rainbow": 4000,
"copper-glow": 3000,
"silver-glow": 3000,
"gold-glow": 3000,
neon: 3000,
lava: 6000,
/* eslint-enable sort-keys */
water: 6200,
};
@@ -28,6 +30,7 @@ export function renderPlayerFlag(
const code = flag.slice("!".length);
const layers = code.split("_").map((segment) => {
const [layerKey, colorKey] = segment.split("-");
// eslint-disable-next-line sort-keys
return { layerKey, colorKey };
});
+7 -7
View File
@@ -165,10 +165,10 @@ export class GameRunner {
updates[GameUpdateType.Tile] = [];
this.callBack({
tick: this.game.ticks(),
packedTileUpdates: new BigUint64Array(packedTileUpdates),
updates: updates,
playerNameViewData: this.playerViewData,
tick: this.game.ticks(),
updates: updates,
});
this.isExecuting = false;
}
@@ -181,21 +181,21 @@ export class GameRunner {
const player = this.game.player(playerID);
const tile = this.game.ref(x, y);
const actions = {
canAttack: player.canAttack(tile),
buildableUnits: player.buildableUnits(tile),
canAttack: player.canAttack(tile),
canSendEmojiAllPlayers: player.canSendEmoji(AllPlayers),
} as PlayerActions;
if (this.game.hasOwner(tile)) {
const other = this.game.owner(tile) as Player;
actions.interaction = {
sharedBorder: player.sharesBorderWith(other),
canSendEmoji: player.canSendEmoji(other),
canTarget: player.canTarget(other),
canSendAllianceRequest: player.canSendAllianceRequest(other),
canBreakAlliance: player.isAlliedWith(other),
canDonate: player.canDonate(other),
canEmbargo: !player.hasEmbargoAgainst(other),
canSendAllianceRequest: player.canSendAllianceRequest(other),
canSendEmoji: player.canSendEmoji(other),
canTarget: player.canTarget(other),
sharedBorder: player.sharesBorderWith(other),
};
const alliance = player.allianceWith(other as Player);
if (alliance) {
+60 -60
View File
@@ -144,17 +144,17 @@ const TeamCountConfigSchema = z.union([
export type TeamCountConfig = z.infer<typeof TeamCountConfigSchema>;
export const GameConfigSchema = z.object({
gameMap: z.enum(GameMapType),
difficulty: z.enum(Difficulty),
gameType: z.enum(GameType),
gameMode: z.enum(GameMode),
disableNPCs: z.boolean(),
bots: z.number().int().min(0).max(400),
difficulty: z.enum(Difficulty),
disableNPCs: z.boolean(),
disabledUnits: z.enum(UnitType).array().optional(),
gameMap: z.enum(GameMapType),
gameMode: z.enum(GameMode),
gameType: z.enum(GameType),
infiniteGold: z.boolean(),
infiniteTroops: z.boolean(),
instantBuild: z.boolean(),
maxPlayers: z.number().optional(),
disabledUnits: z.enum(UnitType).array().optional(),
playerTeams: TeamCountConfigSchema.optional(),
});
@@ -244,82 +244,82 @@ const BaseIntentSchema = z.object({
});
export const AllianceExtensionIntentSchema = BaseIntentSchema.extend({
type: z.literal("allianceExtension"),
recipient: ID,
type: z.literal("allianceExtension"),
});
export const AttackIntentSchema = BaseIntentSchema.extend({
type: z.literal("attack"),
targetID: ID.nullable(),
troops: z.number().nonnegative().nullable(),
type: z.literal("attack"),
});
export const SpawnIntentSchema = BaseIntentSchema.extend({
type: z.literal("spawn"),
name: UsernameSchema,
flag: FlagSchema,
name: UsernameSchema,
pattern: PatternSchema,
playerType: PlayerTypeSchema,
tile: z.number(),
type: z.literal("spawn"),
});
export const BoatAttackIntentSchema = BaseIntentSchema.extend({
type: z.literal("boat"),
targetID: ID.nullable(),
troops: z.number().nonnegative(),
dst: z.number(),
src: z.number().nullable(),
targetID: ID.nullable(),
troops: z.number().nonnegative(),
type: z.literal("boat"),
});
export const AllianceRequestIntentSchema = BaseIntentSchema.extend({
type: z.literal("allianceRequest"),
recipient: ID,
type: z.literal("allianceRequest"),
});
export const AllianceRequestReplyIntentSchema = BaseIntentSchema.extend({
type: z.literal("allianceRequestReply"),
requestor: ID, // The one who made the original alliance request
accept: z.boolean(),
requestor: ID, // The one who made the original alliance request
type: z.literal("allianceRequestReply"),
});
export const BreakAllianceIntentSchema = BaseIntentSchema.extend({
type: z.literal("breakAlliance"),
recipient: ID,
type: z.literal("breakAlliance"),
});
export const TargetPlayerIntentSchema = BaseIntentSchema.extend({
type: z.literal("targetPlayer"),
target: ID,
type: z.literal("targetPlayer"),
});
export const EmojiIntentSchema = BaseIntentSchema.extend({
type: z.literal("emoji"),
recipient: z.union([ID, z.literal(AllPlayers)]),
emoji: EmojiSchema,
recipient: z.union([ID, z.literal(AllPlayers)]),
type: z.literal("emoji"),
});
export const EmbargoIntentSchema = BaseIntentSchema.extend({
type: z.literal("embargo"),
targetID: ID,
action: z.union([z.literal("start"), z.literal("stop")]),
targetID: ID,
type: z.literal("embargo"),
});
export const DonateGoldIntentSchema = BaseIntentSchema.extend({
type: z.literal("donate_gold"),
recipient: ID,
gold: z.bigint().nullable(),
recipient: ID,
type: z.literal("donate_gold"),
});
export const DonateTroopIntentSchema = BaseIntentSchema.extend({
type: z.literal("donate_troops"),
recipient: ID,
troops: z.number().nullable(),
type: z.literal("donate_troops"),
});
export const BuildUnitIntentSchema = BaseIntentSchema.extend({
tile: z.number(),
type: z.literal("build_unit"),
unit: z.enum(UnitType),
tile: z.number(),
});
export const UpgradeStructureIntentSchema = BaseIntentSchema.extend({
@@ -329,8 +329,8 @@ export const UpgradeStructureIntentSchema = BaseIntentSchema.extend({
});
export const CancelAttackIntentSchema = BaseIntentSchema.extend({
type: z.literal("cancel_attack"),
attackID: z.string(),
type: z.literal("cancel_attack"),
});
export const CancelBoatIntentSchema = BaseIntentSchema.extend({
@@ -339,9 +339,9 @@ export const CancelBoatIntentSchema = BaseIntentSchema.extend({
});
export const MoveWarshipIntentSchema = BaseIntentSchema.extend({
tile: z.number(),
type: z.literal("move_warship"),
unitId: z.number(),
tile: z.number(),
});
export const DeleteUnitIntentSchema = BaseIntentSchema.extend({
@@ -350,20 +350,20 @@ export const DeleteUnitIntentSchema = BaseIntentSchema.extend({
});
export const QuickChatIntentSchema = BaseIntentSchema.extend({
type: z.literal("quick_chat"),
recipient: ID,
quickChatKey: QuickChatKeySchema,
recipient: ID,
target: ID.optional(),
type: z.literal("quick_chat"),
});
export const MarkDisconnectedIntentSchema = BaseIntentSchema.extend({
type: z.literal("mark_disconnected"),
isDisconnected: z.boolean(),
type: z.literal("mark_disconnected"),
});
export const KickPlayerIntentSchema = BaseIntentSchema.extend({
type: z.literal("kick_player"),
target: ID,
type: z.literal("kick_player"),
});
const IntentSchema = z.discriminatedUnion("type", [
@@ -395,22 +395,22 @@ const IntentSchema = z.discriminatedUnion("type", [
//
export const TurnSchema = z.object({
turnNumber: z.number(),
intents: IntentSchema.array(),
// The hash of the game state at the end of the turn.
hash: z.number().nullable().optional(),
intents: IntentSchema.array(),
turnNumber: z.number(),
});
export const PlayerSchema = z.object({
clientID: ID,
username: UsernameSchema,
flag: FlagSchema,
pattern: PatternSchema,
username: UsernameSchema,
});
export const GameStartInfoSchema = z.object({
gameID: ID,
config: GameConfigSchema,
gameID: ID,
players: PlayerSchema.array(),
});
@@ -427,8 +427,8 @@ export type Winner = z.infer<typeof WinnerSchema>;
//
export const ServerTurnMessageSchema = z.object({
type: z.literal("turn"),
turn: TurnSchema,
type: z.literal("turn"),
});
export const ServerPingMessageSchema = z.object({
@@ -436,30 +436,30 @@ export const ServerPingMessageSchema = z.object({
});
export const ServerPrestartMessageSchema = z.object({
type: z.literal("prestart"),
gameMap: z.nativeEnum(GameMapType),
type: z.literal("prestart"),
});
export const ServerStartGameMessageSchema = z.object({
type: z.literal("start"),
gameStartInfo: GameStartInfoSchema,
// Turns the client missed if they are late to the game.
turns: TurnSchema.array(),
gameStartInfo: GameStartInfoSchema,
type: z.literal("start"),
});
export const ServerDesyncSchema = z.object({
type: z.literal("desync"),
turn: z.number(),
correctHash: z.number().nullable(),
clientsWithCorrectHash: z.number(),
correctHash: z.number().nullable(),
totalActiveClients: z.number(),
turn: z.number(),
type: z.literal("desync"),
yourHash: z.number().optional(),
});
export const ServerErrorSchema = z.object({
type: z.literal("error"),
error: z.string(),
message: z.string().optional(),
type: z.literal("error"),
});
export const ServerMessageSchema = z.discriminatedUnion("type", [
@@ -476,21 +476,21 @@ export const ServerMessageSchema = z.discriminatedUnion("type", [
//
export const ClientSendWinnerSchema = z.object({
allPlayersStats: AllPlayersStatsSchema,
type: z.literal("winner"),
winner: WinnerSchema,
allPlayersStats: AllPlayersStatsSchema,
});
export const ClientHashSchema = z.object({
type: z.literal("hash"),
hash: z.number(),
turnNumber: z.number(),
type: z.literal("hash"),
});
export const ClientLogMessageSchema = z.object({
type: z.literal("log"),
severity: z.enum(LogSeverity),
log: ID,
severity: z.enum(LogSeverity),
type: z.literal("log"),
});
export const ClientPingMessageSchema = z.object({
@@ -498,20 +498,20 @@ export const ClientPingMessageSchema = z.object({
});
export const ClientIntentMessageSchema = z.object({
type: z.literal("intent"),
intent: IntentSchema,
type: z.literal("intent"),
});
// WARNING: never send this message to clients.
export const ClientJoinMessageSchema = z.object({
type: z.literal("join"),
clientID: ID,
token: TokenSchema, // WARNING: PII
flag: FlagSchema,
gameID: ID,
lastTurn: z.number(), // The last turn the client saw.
username: UsernameSchema,
flag: FlagSchema,
pattern: PatternSchema,
token: TokenSchema, // WARNING: PII
type: z.literal("join"),
username: UsernameSchema,
});
export const ClientMessageSchema = z.discriminatedUnion("type", [
@@ -534,11 +534,11 @@ export const PlayerRecordSchema = PlayerSchema.extend({
export type PlayerRecord = z.infer<typeof PlayerRecordSchema>;
export const GameEndInfoSchema = GameStartInfoSchema.extend({
duration: z.number().nonnegative(),
end: z.number(),
num_turns: z.number(),
players: PlayerRecordSchema.array(),
start: z.number(),
end: z.number(),
duration: z.number().nonnegative(),
num_turns: z.number(),
winner: WinnerSchema,
});
export type GameEndInfo = z.infer<typeof GameEndInfoSchema>;
@@ -546,11 +546,11 @@ export type GameEndInfo = z.infer<typeof GameEndInfoSchema>;
const GitCommitSchema = z.string().regex(/^[0-9a-fA-F]{40}$/);
export const AnalyticsRecordSchema = z.object({
info: GameEndInfoSchema,
version: z.literal("v0.0.2"),
gitCommit: GitCommitSchema,
subdomain: z.string(),
domain: z.string(),
gitCommit: GitCommitSchema,
info: GameEndInfoSchema,
subdomain: z.string(),
version: z.literal("v0.0.2"),
});
export type AnalyticsRecord = z.infer<typeof AnalyticsRecordSchema>;
+13 -12
View File
@@ -88,6 +88,7 @@ export function calculateBoundingBox(
maxY = Math.max(maxY, cell.y);
});
// eslint-disable-next-line sort-keys
return { min: new Cell(minX, minY), max: new Cell(maxX, maxY) };
}
@@ -143,10 +144,10 @@ export function sanitize(name: string): string {
export function onlyImages(html: string) {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ["span", "img"],
ALLOWED_ATTR: ["src", "alt", "class", "style"],
ALLOWED_URI_REGEXP: /^https:\/\/cdn\.jsdelivr\.net\/gh\/twitter\/twemoji/,
ADD_ATTR: ["style"],
ALLOWED_ATTR: ["src", "alt", "class", "style"],
ALLOWED_TAGS: ["span", "img"],
ALLOWED_URI_REGEXP: /^https:\/\/cdn\.jsdelivr\.net\/gh\/twitter\/twemoji/,
});
}
@@ -171,21 +172,21 @@ export function createGameRecord(
(t) => t.intents.length !== 0 || t.hash !== undefined,
);
const record: GameRecord = {
domain,
gitCommit,
info: {
gameID,
config,
duration,
end,
gameID,
num_turns,
players,
start,
end,
duration,
num_turns,
winner,
},
version,
gitCommit,
subdomain,
domain,
turns,
version,
};
return record;
}
@@ -197,8 +198,8 @@ export function decompressGameRecord(gameRecord: GameRecord) {
while (lastTurnNum < turn.turnNumber - 1) {
lastTurnNum++;
turns.push({
turnNumber: lastTurnNum,
intents: [],
turnNumber: lastTurnNum,
});
}
turns.push(turn);
@@ -207,8 +208,8 @@ export function decompressGameRecord(gameRecord: GameRecord) {
const turnLength = turns.length;
for (let i = turnLength; i < gameRecord.info.num_turns; i++) {
turns.push({
turnNumber: i,
intents: [],
turnNumber: i,
});
}
gameRecord.turns = turns;
+1
View File
@@ -5,6 +5,7 @@ import lchPlugin from "colord/plugins/lch";
extend([lchPlugin]);
extend([labPlugin]);
/* eslint-disable sort-keys */
export const red = colord({ h: 0, s: 82, l: 56 });
export const blue = colord({ h: 224, s: 100, l: 58 });
export const teal = colord({ h: 172, s: 66, l: 50 });
+2
View File
@@ -367,6 +367,7 @@ export class DefaultConfig implements Config {
return 1_000_000;
}
/* eslint-disable sort-keys */
unitInfo(type: UnitType): UnitInfo {
switch (type) {
case UnitType.TransportShip:
@@ -488,6 +489,7 @@ export class DefaultConfig implements Config {
assertNever(type);
}
}
/* eslint-enable sort-keys */
private costWrapper(
type: UnitType,
+10
View File
@@ -17,6 +17,7 @@ export class PastelTheme implements Theme {
private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors);
private nationColorAllocator = new ColorAllocator(nationColors, nationColors);
/* eslint-disable sort-keys */
private background = colord({ r: 60, g: 60, b: 60 });
private shore = colord({ r: 204, g: 203, b: 158 });
private falloutColors = [
@@ -34,6 +35,7 @@ export class PastelTheme implements Theme {
private _enemyColor = colord({ r: 255, g: 0, b: 0 });
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
/* eslint-enable sort-keys */
teamColor(team: Team): Colord {
return this.teamColorAllocator.assignTeamColor(team);
@@ -59,20 +61,24 @@ export class PastelTheme implements Theme {
specialBuildingColor(player: PlayerView): Colord {
const tc = this.territoryColor(player).rgba;
/* eslint-disable sort-keys */
return colord({
r: Math.max(tc.r - 50, 0),
g: Math.max(tc.g - 50, 0),
b: Math.max(tc.b - 50, 0),
});
/* eslint-enable sort-keys */
}
railroadColor(player: PlayerView): Colord {
const tc = this.territoryColor(player).rgba;
/* eslint-disable sort-keys */
const color = colord({
r: Math.max(tc.r - 10, 0),
g: Math.max(tc.g - 10, 0),
b: Math.max(tc.b - 10, 0),
});
/* eslint-enable sort-keys */
return color;
}
@@ -81,16 +87,19 @@ export class PastelTheme implements Theme {
return this.borderColorCache.get(player.id())!;
}
const tc = this.territoryColor(player).rgba;
/* eslint-disable sort-keys */
const color = colord({
r: Math.max(tc.r - 40, 0),
g: Math.max(tc.g - 40, 0),
b: Math.max(tc.b - 40, 0),
});
/* eslint-enable sort-keys */
this.borderColorCache.set(player.id(), color);
return color;
}
/* eslint-disable sort-keys */
defendedBorderColors(player: PlayerView): { light: Colord; dark: Colord } {
return {
light: this.territoryColor(player).darken(0.2),
@@ -140,6 +149,7 @@ export class PastelTheme implements Theme {
});
}
}
/* eslint-enable sort-keys */
backgroundColor(): Colord {
return this.background;
@@ -17,6 +17,7 @@ export class PastelThemeDark implements Theme {
private teamColorAllocator = new ColorAllocator(humanColors, fallbackColors);
private nationColorAllocator = new ColorAllocator(nationColors, nationColors);
/* eslint-disable sort-keys */
private background = colord({ r: 0, g: 0, b: 0 });
private shore = colord({ r: 134, g: 133, b: 88 });
private falloutColors = [
@@ -34,6 +35,7 @@ export class PastelThemeDark implements Theme {
private _enemyColor = colord({ r: 255, g: 0, b: 0 });
private _spawnHighlightColor = colord({ r: 255, g: 213, b: 79 });
/* eslint-enable sort-keys */
teamColor(team: Team): Colord {
return this.teamColorAllocator.assignTeamColor(team);
@@ -57,6 +59,7 @@ export class PastelThemeDark implements Theme {
return player.type() === PlayerType.Human ? "#ffffff" : "#e6e6e6";
}
/* eslint-disable sort-keys */
specialBuildingColor(player: PlayerView): Colord {
const tc = this.territoryColor(player).rgba;
return colord({
@@ -142,6 +145,7 @@ export class PastelThemeDark implements Theme {
});
}
}
/* eslint-enable sort-keys */
backgroundColor(): Colord {
return this.background;
+2 -1
View File
@@ -184,9 +184,10 @@ export class NukeExecution implements Execution {
this.mg.config().defaultNukeTargetableRange() ** 2;
const allTiles: TileRef[] = this.pathFinder.allTiles();
for (const tile of allTiles) {
const targetable = this.isTargetable(target, tile, targetRangeSquared);
trajectoryTiles.push({
targetable,
tile,
targetable: this.isTargetable(target, tile, targetRangeSquared),
});
}
+3 -1
View File
@@ -18,6 +18,7 @@ export class RailroadExecution implements Execution {
return this.active;
}
/* eslint-disable sort-keys */
init(mg: Game, ticks: number): void {
this.mg = mg;
const tiles = this.railRoad.tiles;
@@ -48,6 +49,7 @@ export class RailroadExecution implements Execution {
: RailType.VERTICAL,
});
}
/* eslint-enable sort-keys */
private computeExtremityDirection(tile: TileRef, next: TileRef): RailType {
const x = this.mg.x(tile);
@@ -143,9 +145,9 @@ export class RailroadExecution implements Execution {
}
if (updatedRailTiles) {
this.mg.addUpdate({
type: GameUpdateType.RailroadEvent,
isActive: true,
railTiles: updatedRailTiles,
type: GameUpdateType.RailroadEvent,
});
}
}
@@ -98,6 +98,7 @@ class SAMTargetingSystem {
}
const interceptionTile = this.computeInterceptionTile(nuke.unit);
if (interceptionTile !== undefined) {
// eslint-disable-next-line sort-keys
targets.push({ unit: nuke.unit, tile: interceptionTile });
} else {
// Store unreachable nukes in order to prevent useless interception computation
+1 -1
View File
@@ -43,8 +43,8 @@ export class TradeShipExecution implements Execution {
return;
}
this.tradeShip = this.origOwner.buildUnit(UnitType.TradeShip, spawn, {
targetUnit: this._dstPort,
lastSetSafeFromPirates: ticks,
targetUnit: this._dstPort,
});
this.mg.stats().boatSendTrade(this.origOwner, this._dstPort.owner());
}
+1 -1
View File
@@ -119,8 +119,8 @@ export class TrainExecution implements Execution {
for (let i = 0; i < this.numCars; i++) {
this.cars.push(
this.player.buildUnit(UnitType.Train, tile, {
trainType: TrainType.Carriage,
loaded: this.hasCargo,
trainType: TrainType.Carriage,
}),
);
}
+1 -1
View File
@@ -113,7 +113,7 @@ export class WarshipExecution implements Execution {
continue;
}
}
potentialTargets.push({ unit: unit, distSquared });
potentialTargets.push({ distSquared, unit });
}
return potentialTargets.sort((a, b) => {
+3 -3
View File
@@ -31,10 +31,10 @@ export class AllianceRequestImpl implements AllianceRequest {
toUpdate(): AllianceRequestUpdate {
return {
type: GameUpdateType.AllianceRequest,
requestorID: this.requestor_.smallID(),
recipientID: this.recipient_.smallID(),
createdAt: this.tickCreated,
recipientID: this.recipient_.smallID(),
requestorID: this.requestor_.smallID(),
type: GameUpdateType.AllianceRequest,
};
}
}
+7 -7
View File
@@ -37,6 +37,13 @@ export class BinaryLoaderGameMapLoader implements GameMapLoader {
const fileName = key?.toLowerCase();
const mapData = {
manifest: this.createLazyLoader(() =>
(
import(
`../../../resources/maps/${fileName}/manifest.json`
) as Promise<NationMapModule>
).then((m) => m.default),
),
mapBin: this.createLazyLoader(() =>
(
import(
@@ -51,13 +58,6 @@ export class BinaryLoaderGameMapLoader implements GameMapLoader {
) as Promise<BinModule>
).then((m) => this.toUInt8Array(m.default)),
),
manifest: this.createLazyLoader(() =>
(
import(
`../../../resources/maps/${fileName}/manifest.json`
) as Promise<NationMapModule>
).then((m) => m.default),
),
webpPath: this.createLazyLoader(() =>
(
import(
+1 -1
View File
@@ -27,10 +27,10 @@ export class FetchGameMapLoader implements GameMapLoader {
}
const mapData = {
manifest: () => this.loadJsonFromUrl(this.url(fileName, "manifest.json")),
mapBin: () => this.loadBinaryFromUrl(this.url(fileName, "map.bin")),
miniMapBin: () =>
this.loadBinaryFromUrl(this.url(fileName, "mini_map.bin")),
manifest: () => this.loadJsonFromUrl(this.url(fileName, "manifest.json")),
webpPath: async () => this.url(fileName, "thumbnail.webp"),
} satisfies MapData;
+3
View File
@@ -44,6 +44,7 @@ export const Duos = "Duos" as const;
export const Trios = "Trios" as const;
export const Quads = "Quads" as const;
/* eslint-disable sort-keys */
export const ColoredTeams: Record<string, Team> = {
Red: "Red",
Blue: "Blue",
@@ -54,6 +55,7 @@ export const ColoredTeams: Record<string, Team> = {
Green: "Green",
Bot: "Bot",
} as const;
/* eslint-enable sort-keys */
export enum GameMapType {
World = "World",
@@ -118,6 +120,7 @@ export const mapCategories: Record<string, GameMapType[]> = {
GameMapType.Italia,
GameMapType.Yenisei,
],
// eslint-disable-next-line sort-keys
fantasy: [
GameMapType.Pangaea,
GameMapType.Pluto,
+27 -27
View File
@@ -292,9 +292,9 @@ export class GameImpl implements Game {
recipient.endTemporaryEmbargo(requestor.id());
this.addUpdate({
type: GameUpdateType.AllianceRequestReply,
request: request.toUpdate(),
accepted: true,
request: request.toUpdate(),
type: GameUpdateType.AllianceRequestReply,
});
}
@@ -306,9 +306,9 @@ export class GameImpl implements Game {
request,
);
this.addUpdate({
type: GameUpdateType.AllianceRequestReply,
request: request.toUpdate(),
accepted: false,
request: request.toUpdate(),
type: GameUpdateType.AllianceRequestReply,
});
}
@@ -358,9 +358,9 @@ export class GameImpl implements Game {
}
if (this.ticks() % 10 === 0) {
this.addUpdate({
type: GameUpdateType.Hash,
tick: this.ticks(),
hash: this.hash(),
tick: this.ticks(),
type: GameUpdateType.Hash,
});
}
this._ticks++;
@@ -573,9 +573,9 @@ export class GameImpl implements Game {
target(targeter: Player, target: Player) {
this.addUpdate({
type: GameUpdateType.TargetPlayer,
playerID: targeter.smallID(),
targetID: target.smallID(),
type: GameUpdateType.TargetPlayer,
});
}
@@ -604,9 +604,9 @@ export class GameImpl implements Game {
}
this.alliances_ = this.alliances_.filter((a) => a !== alliances[0]);
this.addUpdate({
type: GameUpdateType.BrokeAlliance,
traitorID: breaker.smallID(),
betrayedID: other.smallID(),
traitorID: breaker.smallID(),
type: GameUpdateType.BrokeAlliance,
});
}
@@ -623,24 +623,24 @@ export class GameImpl implements Game {
}
this.alliances_ = this.alliances_.filter((a) => a !== alliances[0]);
this.addUpdate({
type: GameUpdateType.AllianceExpired,
player1ID: alliance.requestor().smallID(),
player2ID: alliance.recipient().smallID(),
type: GameUpdateType.AllianceExpired,
});
}
sendEmojiUpdate(msg: EmojiMessage): void {
this.addUpdate({
type: GameUpdateType.Emoji,
emoji: msg,
type: GameUpdateType.Emoji,
});
}
setWinner(winner: Player | Team, allPlayersStats: AllPlayersStats): void {
this.addUpdate({
allPlayersStats,
type: GameUpdateType.Win,
winner: this.makeWinner(winner),
allPlayersStats,
});
}
@@ -683,12 +683,12 @@ export class GameImpl implements Game {
id = this.player(playerID).smallID();
}
this.addUpdate({
type: GameUpdateType.DisplayEvent,
goldAmount,
message,
messageType: type,
message: message,
params,
playerID: id,
goldAmount: goldAmount,
params: params,
type: GameUpdateType.DisplayEvent,
});
}
@@ -705,13 +705,13 @@ export class GameImpl implements Game {
id = this.player(playerID).smallID();
}
this.addUpdate({
type: GameUpdateType.DisplayChatEvent,
key: message,
category: category,
target: target,
playerID: id,
category,
isFrom,
recipient: recipient,
key: message,
playerID: id,
recipient,
target,
type: GameUpdateType.DisplayChatEvent,
});
}
@@ -724,11 +724,11 @@ export class GameImpl implements Game {
const id = this.player(playerID).smallID();
this.addUpdate({
type: GameUpdateType.UnitIncoming,
unitID: unitID,
message: message,
message,
messageType: type,
playerID: id,
type: GameUpdateType.UnitIncoming,
unitID,
});
}
@@ -889,10 +889,10 @@ export class GameImpl implements Game {
conqueror.addGold(gold);
conquered.removeGold(gold);
this.addUpdate({
type: GameUpdateType.ConquestEvent,
conquerorId: conqueror.id(),
conqueredId: conquered.id(),
conquerorId: conqueror.id(),
gold,
type: GameUpdateType.ConquestEvent,
});
// Record stats
+10 -7
View File
@@ -126,6 +126,7 @@ export class PlayerImpl implements Player {
);
const stats = this.mg.stats().getPlayerStats(this);
/* eslint-disable sort-keys */
return {
type: GameUpdateType.Player,
clientID: this.clientID(),
@@ -176,6 +177,7 @@ export class PlayerImpl implements Player {
hasSpawned: this.hasSpawned(),
betrayals: stats?.betrayals,
};
/* eslint-enable sort-keys */
}
smallID(): number {
@@ -508,6 +510,7 @@ export class PlayerImpl implements Player {
}
target(other: Player): void {
// eslint-disable-next-line sort-keys
this.targets_.push({ tick: this.mg.ticks(), target: other });
this.mg.target(this, other);
}
@@ -533,10 +536,10 @@ export class PlayerImpl implements Player {
throw Error(`Cannot send emoji to oneself: ${this}`);
}
const msg: EmojiMessage = {
message: emoji,
senderID: this.smallID(),
recipientID: recipient === AllPlayers ? recipient : recipient.smallID(),
createdAt: this.mg.ticks(),
message: emoji,
recipientID: recipient === AllPlayers ? recipient : recipient.smallID(),
senderID: this.smallID(),
};
this.outgoingEmojis_.push(msg);
this.mg.sendEmojiUpdate(msg);
@@ -716,11 +719,11 @@ export class PlayerImpl implements Player {
this._gold += toAdd;
if (tile) {
this.mg.addUpdate({
type: GameUpdateType.BonusEvent,
gold: Number(toAdd),
player: this.id(),
tile,
gold: Number(toAdd),
troops: 0,
type: GameUpdateType.BonusEvent,
});
}
}
@@ -837,12 +840,12 @@ export class PlayerImpl implements Player {
}
}
return {
type: u,
canBuild: this.mg.inSpawnPhase()
? false
: this.canBuild(u, tile, validTiles),
canUpgrade: canUpgrade,
cost: this.mg.config().unitInfo(u).cost(this),
type: u,
} as BuildableUnit;
});
}
@@ -1042,13 +1045,13 @@ export class PlayerImpl implements Player {
public playerProfile(): PlayerProfile {
const rel = {
alliances: this.alliances().map((a) => a.other(this).smallID()),
relations: Object.fromEntries(
this.allRelationsSorted().map(({ player, relation }) => [
player.smallID(),
relation,
]),
),
alliances: this.alliances().map((a) => a.other(this).smallID()),
};
return rel;
}
+2
View File
@@ -216,6 +216,7 @@ export class RailNetworkImpl implements RailNetwork {
const visited = new Set<TrainStation>();
const queue: Array<{ station: TrainStation; distance: number }> = [
// eslint-disable-next-line sort-keys
{ station: start, distance: 0 },
];
@@ -229,6 +230,7 @@ export class RailNetworkImpl implements RailNetwork {
for (const neighbor of station.neighbors()) {
if (neighbor === dest) return distance + 1;
if (!visited.has(neighbor)) {
// eslint-disable-next-line sort-keys
queue.push({ station: neighbor, distance: distance + 1 });
}
}
+2 -2
View File
@@ -12,13 +12,13 @@ export class Railroad {
delete(game: Game) {
const railTiles: RailTile[] = this.tiles.map((tile) => ({
tile,
railType: RailType.VERTICAL,
tile,
}));
game.addUpdate({
type: GameUpdateType.RailroadEvent,
isActive: false,
railTiles,
type: GameUpdateType.RailroadEvent,
});
this.from.getRailroads().delete(this);
this.to.getRailroads().delete(this);
+1 -1
View File
@@ -48,8 +48,8 @@ export async function loadTerrainMap(
await mapFiles.miniMapBin(),
);
const result = {
manifest: await mapFiles.manifest(),
gameMap: gameMap,
manifest: await mapFiles.manifest(),
miniGameMap: miniGameMap,
};
loadedMaps.set(map, result);
+2 -2
View File
@@ -119,13 +119,13 @@ export class TrainStation {
);
if (toRemove) {
const railTiles: RailTile[] = toRemove.tiles.map((tile) => ({
tile,
railType: RailType.VERTICAL,
tile,
}));
this.mg.addUpdate({
type: GameUpdateType.RailroadEvent,
isActive: false,
railTiles,
type: GameUpdateType.RailroadEvent,
});
this.railroads.delete(toRemove);
}
+2 -2
View File
@@ -183,10 +183,10 @@ export function candidateShoreTiles(
let bestByManhattan: TileRef | null = null;
const extremumTiles: Record<string, TileRef | null> = {
minX: null,
minY: null,
maxX: null,
maxY: null,
minX: null,
minY: null,
};
const borderShoreTiles = Array.from(player.borderTiles()).filter((t) =>
+2 -1
View File
@@ -113,7 +113,7 @@ export class UnitGrid {
gridY + Math.ceil((range - (cellSize - (y % cellSize))) / cellSize),
);
return { startGridX, endGridX, startGridY, endGridY };
return { endGridX, endGridY, startGridX, startGridY };
}
private squaredDistanceFromTile(
@@ -154,6 +154,7 @@ export class UnitGrid {
if (!unit.isActive()) continue;
const distSquared = this.squaredDistanceFromTile(unit, tile);
if (distSquared > rangeSquared) continue;
// eslint-disable-next-line sort-keys
const value = { unit, distSquared };
if (predicate !== undefined && !predicate(value)) continue;
nearby.push(value);
+2
View File
@@ -114,6 +114,7 @@ export class UnitImpl implements Unit {
return this._id;
}
/* eslint-disable sort-keys */
toUpdate(): UnitUpdate {
return {
type: GameUpdateType.Unit,
@@ -139,6 +140,7 @@ export class UnitImpl implements Unit {
loaded: this._loaded,
};
}
/* eslint-enable sort-keys */
type(): UnitType {
return this._type;
+2
View File
@@ -148,6 +148,7 @@ export class PathFinder {
}
if (this.game.manhattanDist(curr, dst) < dist) {
// eslint-disable-next-line sort-keys
return { type: PathFindResultType.Completed, node: curr };
}
@@ -164,6 +165,7 @@ export class PathFinder {
if (tile === undefined) {
throw new Error("missing tile");
}
// eslint-disable-next-line sort-keys
return { type: PathFindResultType.NextTile, node: tile };
}
}
+3
View File
@@ -49,6 +49,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
this.fwdGScore.set(startPoint, 0);
this.fwdOpenSet.add({
tile: startPoint,
// eslint-disable-next-line sort-keys
fScore: this.heuristic(startPoint, dst),
});
});
@@ -57,6 +58,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
this.bwdGScore.set(dst, 0);
this.bwdOpenSet.add({
tile: dst,
// eslint-disable-next-line sort-keys
fScore: this.heuristic(dst, this.findClosestSource(dst)),
});
}
@@ -145,6 +147,7 @@ export class SerialAStar<NodeType> implements AStar<NodeType> {
const fScore =
totalG +
this.heuristic(neighbor, isForward ? this.dst : this.closestSource);
// eslint-disable-next-line sort-keys
openSet.add({ tile: neighbor, fScore: fScore });
}
}
+4 -3
View File
@@ -50,33 +50,34 @@ export function validateUsername(username: string): {
error?: string;
} {
if (typeof username !== "string") {
// eslint-disable-next-line sort-keys
return { isValid: false, error: translateText("username.not_string") };
}
if (username.length < MIN_USERNAME_LENGTH) {
return {
isValid: false,
error: translateText("username.too_short", {
min: MIN_USERNAME_LENGTH,
}),
isValid: false,
};
}
if (username.length > MAX_USERNAME_LENGTH) {
return {
isValid: false,
error: translateText("username.too_long", {
max: MAX_USERNAME_LENGTH,
}),
isValid: false,
};
}
if (!validPattern.test(username)) {
return {
isValid: false,
error: translateText("username.invalid_chars", {
max: MAX_USERNAME_LENGTH,
}),
isValid: false,
};
}
+7 -7
View File
@@ -23,8 +23,8 @@ function gameUpdate(gu: GameUpdateViewData | ErrorUpdate) {
return;
}
sendMessage({
type: "game_update",
gameUpdate: gu,
type: "game_update",
});
}
@@ -48,8 +48,8 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
gameUpdate,
).then((gr) => {
sendMessage({
type: "initialized",
id: message.id,
type: "initialized",
} as InitializedMessage);
return gr;
});
@@ -85,9 +85,9 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
message.y,
);
sendMessage({
type: "player_actions_result",
id: message.id,
result: actions,
type: "player_actions_result",
} as PlayerActionsResultMessage);
} catch (error) {
console.error("Failed to check borders:", error);
@@ -102,9 +102,9 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
try {
const profile = (await gameRunner).playerProfile(message.playerID);
sendMessage({
type: "player_profile_result",
id: message.id,
result: profile,
type: "player_profile_result",
} as PlayerProfileResultMessage);
} catch (error) {
console.error("Failed to check borders:", error);
@@ -121,9 +121,9 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
message.playerID,
);
sendMessage({
type: "player_border_tiles_result",
id: message.id,
result: borderTiles,
type: "player_border_tiles_result",
} as PlayerBorderTilesResultMessage);
} catch (error) {
console.error("Failed to get border tiles:", error);
@@ -141,8 +141,8 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
message.attackID,
);
sendMessage({
type: "attack_average_position_result",
id: message.id,
type: "attack_average_position_result",
x: averagePosition ? averagePosition.x : null,
y: averagePosition ? averagePosition.y : null,
} as AttackAveragePositionResultMessage);
@@ -162,9 +162,9 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
message.targetTile,
);
sendMessage({
type: "transport_ship_spawn_result",
id: message.id,
result: spawnTile,
type: "transport_ship_spawn_result",
} as TransportShipSpawnResultMessage);
} catch (error) {
console.error("Failed to spawn transport ship:", error);
+16 -16
View File
@@ -66,10 +66,10 @@ export class WorkerClient {
});
this.worker.postMessage({
type: "init",
id: messageId,
gameStartInfo: this.gameStartInfo,
clientID: this.clientID,
gameStartInfo: this.gameStartInfo,
id: messageId,
type: "init",
});
// Add timeout for initialization
@@ -95,8 +95,8 @@ export class WorkerClient {
}
this.worker.postMessage({
type: "turn",
turn,
type: "turn",
});
}
@@ -125,9 +125,9 @@ export class WorkerClient {
});
this.worker.postMessage({
type: "player_profile",
id: messageId,
playerID: playerID,
playerID,
type: "player_profile",
});
});
}
@@ -151,9 +151,9 @@ export class WorkerClient {
});
this.worker.postMessage({
type: "player_border_tiles",
id: messageId,
playerID: playerID,
playerID,
type: "player_border_tiles",
});
});
}
@@ -181,9 +181,9 @@ export class WorkerClient {
});
this.worker.postMessage({
type: "player_actions",
id: messageId,
playerID: playerID,
playerID,
type: "player_actions",
x: x,
y: y,
});
@@ -217,10 +217,10 @@ export class WorkerClient {
});
this.worker.postMessage({
type: "attack_average_position",
attackID,
id: messageId,
playerID: playerID,
attackID: attackID,
playerID,
type: "attack_average_position",
});
});
}
@@ -247,10 +247,10 @@ export class WorkerClient {
});
this.worker.postMessage({
type: "transport_ship_spawn",
id: messageId,
playerID: playerID,
targetTile: targetTile,
playerID,
targetTile,
type: "transport_ship_spawn",
});
});
}
+2
View File
@@ -11,11 +11,13 @@ const log = logger.child({ component: "Archive" });
// R2 client configuration
const r2 = new S3({
region: "auto", // R2 ignores region, but it's required by the SDK
/* eslint-disable sort-keys */
endpoint: config.r2Endpoint(),
credentials: {
accessKeyId: config.r2AccessKey(),
secretAccessKey: config.r2SecretKey(),
},
/* eslint-disable sort-keys */
});
const bucket = config.r2Bucket();
+7 -7
View File
@@ -60,12 +60,12 @@ export class Cloudflare {
data?: any,
): Promise<T> {
const response = await fetch(url, {
method,
body: data ? JSON.stringify(data) : undefined,
headers: {
Authorization: `Bearer ${this.apiToken}`,
"Content-Type": "application/json",
},
body: data ? JSON.stringify(data) : undefined,
method,
});
if (!response.ok) {
@@ -178,7 +178,6 @@ export class Cloudflare {
log.info(`Created credentials file at: ${this.credsPath}`);
const tunnelConfig: CloudflaredConfig = {
tunnel: tunnelId,
"credentials-file": this.credsPath,
ingress: [
...Array.from(subdomainToService.entries()).map(
@@ -191,6 +190,7 @@ export class Cloudflare {
service: "http_status:404",
},
],
tunnel: tunnelId,
};
// Write config file
@@ -210,11 +210,11 @@ export class Cloudflare {
const recordId = existingRecords.result[0]?.id;
const dnsData = {
type: "CNAME",
name: subdomain,
content: `${tunnelId}.cfargotunnel.com`,
ttl: 1,
name: subdomain,
proxied: true,
ttl: 1,
type: "CNAME",
};
if (recordId) {
@@ -240,12 +240,12 @@ export class Cloudflare {
["tunnel", "--config", this.configPath, "--loglevel", "error", "run"],
{
detached: true,
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
// Set this to bypass origin cert requirement for named tunnels
TUNNEL_ORIGIN_CERT: "/dev/null",
},
stdio: ["ignore", "pipe", "pipe"],
},
);
+5 -5
View File
@@ -39,16 +39,16 @@ export class GameManager {
Date.now(),
this.config,
{
gameMap: GameMapType.World,
gameType: GameType.Private,
bots: 400,
difficulty: Difficulty.Medium,
disableNPCs: false,
disabledUnits: [],
gameMap: GameMapType.World,
gameMode: GameMode.FFA,
gameType: GameType.Private,
infiniteGold: false,
infiniteTroops: false,
instantBuild: false,
gameMode: GameMode.FFA,
bots: 400,
disabledUnits: [],
...gameConfig,
},
creatorClientID,
+26 -26
View File
@@ -123,15 +123,15 @@ export class GameServer {
// Log when lobby creator joins private game
if (client.clientID === this.LobbyCreatorID) {
this.log.info("Lobby creator joined", {
gameID: this.id,
creatorID: this.LobbyCreatorID,
gameID: this.id,
});
}
this.log.info("client (re)joining game", {
clientID: client.clientID,
persistentID: client.persistentID,
clientIP: ipAnonymize(client.ip),
isRejoin: lastTurn > 0,
persistentID: client.persistentID,
});
if (
@@ -210,9 +210,9 @@ export class GameServer {
});
client.ws.send(
JSON.stringify({
type: "error",
error,
message,
type: "error",
} satisfies ServerErrorMessage),
);
client.ws.close(1002, "ClientMessageSchema");
@@ -244,8 +244,8 @@ export class GameServer {
this.log.warn(`Only lobby creator can kick players`, {
clientID: authenticatedClientID,
creatorID: this.LobbyCreatorID,
target: clientMsg.intent.target,
gameID: this.id,
target: clientMsg.intent.target,
});
return;
}
@@ -261,9 +261,9 @@ export class GameServer {
// Log and execute the kick
this.log.info(`Lobby creator initiated kick of player`, {
creatorID: authenticatedClientID,
target: clientMsg.intent.target,
gameID: this.id,
kickMethod: "websocket",
target: clientMsg.intent.target,
});
this.kickClient(clientMsg.intent.target);
@@ -358,8 +358,8 @@ export class GameServer {
this._hasPrestarted = true;
const prestartMsg = ServerPrestartMessageSchema.safeParse({
type: "prestart",
gameMap: this.gameConfig.gameMap,
type: "prestart",
});
if (!prestartMsg.success) {
@@ -393,13 +393,13 @@ export class GameServer {
this.lastPingUpdate = Date.now();
const result = GameStartInfoSchema.safeParse({
gameID: this.id,
config: this.gameConfig,
gameID: this.id,
players: this.activeClients.map((c) => ({
username: c.username,
clientID: c.clientID,
pattern: c.pattern,
flag: c.flag,
pattern: c.pattern,
username: c.username,
})),
});
if (!result.success) {
@@ -430,9 +430,9 @@ export class GameServer {
try {
ws.send(
JSON.stringify({
type: "start",
turns: this.turns.slice(lastTurn),
gameStartInfo: this.gameStartInfo,
turns: this.turns.slice(lastTurn),
type: "start",
} satisfies ServerStartGameMessage),
);
} catch (error) {
@@ -447,8 +447,8 @@ export class GameServer {
private endTurn() {
const pastTurn: Turn = {
turnNumber: this.turns.length,
intents: this.intents,
turnNumber: this.turns.length,
};
this.turns.push(pastTurn);
this.intents = [];
@@ -457,8 +457,8 @@ export class GameServer {
this.checkDisconnectedStatus();
const msg = JSON.stringify({
type: "turn",
turn: pastTurn,
type: "turn",
} satisfies ServerTurnMessage);
this.activeClients.forEach((c) => {
c.ws.send(msg);
@@ -510,9 +510,9 @@ export class GameServer {
}
this.log.error("Error archiving game record details:", {
gameId: this.id,
errorType: typeof error,
error: errorDetails,
errorType: typeof error,
gameId: this.id,
});
}
}
@@ -587,12 +587,12 @@ export class GameServer {
public gameInfo(): GameInfo {
return {
gameID: this.id,
clients: this.activeClients.map((c) => ({
username: c.username,
clientID: c.clientID,
username: c.username,
})),
gameConfig: this.gameConfig,
gameID: this.id,
msUntilStart: this.isPublic()
? this.createdAt + this.config.gameCreationRate()
: undefined,
@@ -618,8 +618,8 @@ export class GameServer {
});
client.ws.send(
JSON.stringify({
type: "error",
error: "Kicked from game (you may have been playing on another tab)",
type: "error",
} satisfies ServerErrorMessage),
);
client.ws.close(1000, "Kicked from game");
@@ -660,9 +660,9 @@ export class GameServer {
private markClientDisconnected(clientID: string, isDisconnected: boolean) {
this.clientsDisconnectedStatus.set(clientID, isDisconnected);
this.addIntent({
type: "mark_disconnected",
clientID: clientID,
clientID,
isDisconnected: isDisconnected,
type: "mark_disconnected",
});
}
@@ -681,10 +681,10 @@ export class GameServer {
}
return {
clientID: player.clientID,
username: player.username,
persistentID:
this.allClients.get(player.clientID)?.persistentID ?? "",
stats,
username: player.username,
} satisfies PlayerRecord;
},
);
@@ -722,17 +722,17 @@ export class GameServer {
}
const serverDesync = ServerDesyncSchema.safeParse({
type: "desync",
turn: lastHashTurn,
correctHash: mostCommonHash,
clientsWithCorrectHash:
this.activeClients.length - outOfSyncClients.length,
correctHash: mostCommonHash,
totalActiveClients: this.activeClients.length,
turn: lastHashTurn,
type: "desync",
});
if (!serverDesync.success) {
this.log.warn("failed to create desync message", {
gameID: this.id,
error: serverDesync.error,
gameID: this.id,
});
return;
}
@@ -745,8 +745,8 @@ export class GameServer {
}
this.sentDesyncMessageClients.add(c.clientID);
this.log.info("sending desync to client", {
gameID: this.id,
clientID: c.clientID,
gameID: this.id,
persistentID: c.persistentID,
});
c.ws.send(desyncMsg);
+3
View File
@@ -29,6 +29,7 @@ if (config.otelEnabled()) {
// Add OTLP exporter for logs
const logExporter = new OTLPLogExporter({
url: `${config.otelEndpoint()}/v1/logs`,
// eslint-disable-next-line sort-keys
headers,
});
@@ -56,6 +57,7 @@ const addSeverityFormat = winston.format((info) => {
// Define your base/parent logger
const logger = winston.createLogger({
level: "info",
/* eslint-disable sort-keys */
format: winston.format.combine(
winston.format.timestamp(),
addSeverityFormat(),
@@ -65,6 +67,7 @@ const logger = winston.createLogger({
service: "openfront",
environment: process.env.GAME_ENV ?? "prod",
},
/* eslint-enable sort-keys */
transports: [
new winston.transports.Console(),
new OpenTelemetryTransportV3(),
+29 -29
View File
@@ -20,33 +20,33 @@ const config = getServerConfigFromServer();
// How many times each map should appear in the playlist.
// Note: The Partial should eventually be removed for better type safety.
const frequency: Partial<Record<GameMapName, number>> = {
World: 3,
Europe: 2,
Africa: 2,
Baikal: 2,
Australia: 1,
NorthAmerica: 1,
Britannia: 1,
GatewayToTheAtlantic: 1,
Iceland: 1,
SouthAmerica: 1,
DeglaciatedAntarctica: 1,
EuropeClassic: 1,
Mena: 1,
Pangaea: 1,
Asia: 1,
Australia: 1,
Baikal: 2,
BetweenTwoSeas: 1,
BlackSea: 1,
Britannia: 1,
DeglaciatedAntarctica: 1,
EastAsia: 1,
Europe: 2,
EuropeClassic: 1,
FalklandIslands: 1,
FaroeIslands: 1,
GatewayToTheAtlantic: 1,
Halkidiki: 1,
Iceland: 1,
Italia: 1,
Mars: 1,
MarsRevised: 1,
BetweenTwoSeas: 1,
EastAsia: 1,
BlackSea: 1,
FaroeIslands: 1,
FalklandIslands: 1,
Halkidiki: 1,
StraitOfGibraltar: 1,
Italia: 1,
Yenisei: 1,
Mena: 1,
NorthAmerica: 1,
Pangaea: 1,
Pluto: 1,
SouthAmerica: 1,
StraitOfGibraltar: 1,
World: 3,
Yenisei: 1,
};
interface MapWithMode {
@@ -77,18 +77,18 @@ export class MapPlaylist {
// Create the default public game config (from your GameManager)
return {
gameMap: map,
maxPlayers: config.lobbyMaxPlayers(map, mode, playerTeams),
gameType: GameType.Public,
bots: 400,
difficulty: Difficulty.Medium,
disableNPCs: mode === GameMode.Team,
disabledUnits: [],
gameMap: map,
gameMode: mode,
gameType: GameType.Public,
infiniteGold: false,
infiniteTroops: false,
instantBuild: false,
disableNPCs: mode === GameMode.Team,
gameMode: mode,
maxPlayers: config.lobbyMaxPlayers(map, mode, playerTeams),
playerTeams,
bots: 400,
disabledUnits: [],
} satisfies GameConfig;
}
+6 -6
View File
@@ -54,8 +54,8 @@ app.use(express.json());
app.set("trust proxy", 3);
app.use(
rateLimit({
windowMs: 1000, // 1 second
max: 20, // 20 requests per IP per second
windowMs: 1000, // 1 second
}),
);
@@ -180,10 +180,10 @@ app.post(
const response = await fetch(
`http://localhost:${config.workerPort(gameID)}/api/kick_player/${gameID}/${clientID}`,
{
method: "POST",
headers: {
[config.adminHeader()]: config.adminToken(),
},
method: "POST",
},
);
@@ -232,10 +232,10 @@ async function fetchLobbies(): Promise<number> {
.filter((result) => result !== null)
.map((gi: GameInfo) => {
return {
gameID: gi.gameID,
numClients: gi?.clients?.length ?? 0,
gameConfig: gi.gameConfig,
gameID: gi.gameID,
msUntilStart: (gi.msUntilStart ?? Date.now()) - Date.now(),
numClients: gi?.clients?.length ?? 0,
} as GameInfo;
});
@@ -283,12 +283,12 @@ async function schedulePublicGame(playlist: MapPlaylist) {
const response = await fetch(
`http://localhost:${config.workerPort(gameID)}/api/create_game/${gameID}`,
{
method: "POST",
body: JSON.stringify(playlist.gameConfig()),
headers: {
"Content-Type": "application/json",
[config.adminHeader()]: config.adminToken(),
},
body: JSON.stringify(playlist.gameConfig()),
method: "POST",
},
);
+2
View File
@@ -18,6 +18,7 @@ export function getOtelResource() {
export function getPromLabels() {
return {
"service.instance.id": process.env.HOSTNAME,
/* eslint-disable sort-keys */
"openfront.environment": config.env(),
"openfront.host": process.env.HOST,
"openfront.domain": process.env.DOMAIN,
@@ -25,5 +26,6 @@ export function getPromLabels() {
"openfront.component": process.env.WORKER_ID
? "Worker " + process.env.WORKER_ID
: "Master",
/* eslint-enable sort-keys */
};
}
+1 -1
View File
@@ -55,8 +55,8 @@ async function setupTunnels() {
if (!(await cloudflare.configAlreadyExists())) {
await cloudflare.createTunnel({
subdomain: config.subdomain(),
domain: config.domain(),
subdomain: config.subdomain(),
subdomainToService: domainToService,
} as TunnelConfig);
} else {
+9 -9
View File
@@ -85,8 +85,8 @@ export async function startWorker() {
app.use(express.static(path.join(__dirname, "../../out")));
app.use(
rateLimit({
windowMs: 1000, // 1 second
max: 20, // 20 requests per IP per second
windowMs: 1000, // 1 second
}),
);
@@ -232,9 +232,9 @@ export async function startWorker() {
if (!gameRecord) {
return res.status(404).json({
success: false,
error: "Game not found",
exists: false,
success: false,
});
}
@@ -246,20 +246,20 @@ export async function startWorker() {
`git commit mismatch for game ${req.params.id}, expected ${config.gitCommit()}, got ${gameRecord.gitCommit}`,
);
return res.status(409).json({
success: false,
details: {
actualCommit: gameRecord.gitCommit,
expectedCommit: config.gitCommit(),
},
error: "Version mismatch",
exists: true,
details: {
expectedCommit: config.gitCommit(),
actualCommit: gameRecord.gitCommit,
},
success: false,
});
}
return res.status(200).json({
success: true,
exists: true,
gameRecord: gameRecord,
success: true,
});
}),
);
@@ -324,8 +324,8 @@ export async function startWorker() {
log.warn("Error parsing client message", error);
ws.send(
JSON.stringify({
type: "error",
error: error.toString(),
type: "error",
} satisfies ServerErrorMessage),
);
ws.close(1002, "ClientJoinMessageSchema");
+3 -3
View File
@@ -25,20 +25,20 @@ export function initWorkerMetrics(gameManager: GameManager): void {
// Create metrics exporter
const metricExporter = new OTLPMetricExporter({
url: `${config.otelEndpoint()}/v1/metrics`,
headers,
url: `${config.otelEndpoint()}/v1/metrics`,
});
// Configure the metric reader
const metricReader = new PeriodicExportingMetricReader({
exporter: metricExporter,
exportIntervalMillis: 15000, // Export metrics every 15 seconds
exporter: metricExporter,
});
// Create a meter provider
const meterProvider = new MeterProvider({
resource,
readers: [metricReader],
resource,
});
// Get meter for creating metrics
+3 -2
View File
@@ -21,6 +21,7 @@ export async function verifyClientToken(
config: ServerConfig,
): Promise<TokenVerificationResult> {
if (PersistentIdSchema.safeParse(token).success) {
// eslint-disable-next-line sort-keys
return { persistentId: token, claims: null };
}
try {
@@ -29,8 +30,8 @@ export async function verifyClientToken(
const key = await config.jwkPublicKey();
const { payload, protectedHeader } = await jwtVerify(token, key, {
algorithms: ["EdDSA"],
issuer,
audience,
issuer,
});
const result = TokenPayloadSchema.safeParse(payload);
if (!result.success) {
@@ -40,7 +41,7 @@ export async function verifyClientToken(
}
const claims = result.data;
const persistentId = claims.sub;
return { persistentId, claims };
return { claims, persistentId };
} catch (e) {
return false;
}
+3 -3
View File
@@ -57,12 +57,12 @@ export async function setup(
// Configure the game
const serverConfig = new TestServerConfig();
const gameConfig: GameConfig = {
bots: 0,
difficulty: Difficulty.Medium,
disableNPCs: false,
gameMap: GameMapType.Asia,
gameMode: GameMode.FFA,
gameType: GameType.Singleplayer,
difficulty: Difficulty.Medium,
disableNPCs: false,
bots: 0,
infiniteGold: false,
infiniteTroops: false,
instantBuild: false,