mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-05 14:12:03 +00:00
ee459b7410
## Description: This PR implements a major refactoring of how map data is stored and loaded, as described in #1242. Previously, map data (`.bin` files) was bundled directly into the client-side JavaScript by Webpack using `binary-loader`. This approach led to data duplication and increased bundle/image sizes. This refactoring changes the strategy entirely: - `GameMapLoader` interface has been introduced to decouple the map loading mechanism from the components that use it. - New `FetchGameMapLoader` implementation loads map data by fetching it from static server endpoint. - Webpack configuration and `Dockerfile` have been updated to serve the map files as static assets and to remove the source `resources/maps` directory from the final image, thus eliminating data duplication. This leads to several key improvements: - Docker image size is reduced from ~750 MB to ~600 MB. - Build time is decreased. On my local machine, the docker image build time went from 48s to 43s. Most of this speed-up comes from faster Webpack builds (reduced from 16s to 11s), as it no longer needs to process large binary files. This performance gain will be noticeable for all developers during local development, not just in the CI workflow. ## 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 - [X] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: aaa4xu
256 lines
6.9 KiB
TypeScript
256 lines
6.9 KiB
TypeScript
import { placeName } from "../client/graphics/NameBoxCalculator";
|
|
import { getConfig } from "./configuration/ConfigLoader";
|
|
import { Executor } from "./execution/ExecutionManager";
|
|
import { WinCheckExecution } from "./execution/WinCheckExecution";
|
|
import {
|
|
AllPlayers,
|
|
Cell,
|
|
Game,
|
|
GameUpdates,
|
|
NameViewData,
|
|
Nation,
|
|
Player,
|
|
PlayerActions,
|
|
PlayerBorderTiles,
|
|
PlayerID,
|
|
PlayerInfo,
|
|
PlayerProfile,
|
|
PlayerType,
|
|
} from "./game/Game";
|
|
import { createGame } from "./game/GameImpl";
|
|
import { TileRef } from "./game/GameMap";
|
|
import { GameMapLoader } from "./game/GameMapLoader";
|
|
import {
|
|
ErrorUpdate,
|
|
GameUpdateType,
|
|
GameUpdateViewData,
|
|
} from "./game/GameUpdates";
|
|
import { loadTerrainMap as loadGameMap } from "./game/TerrainMapLoader";
|
|
import { PseudoRandom } from "./PseudoRandom";
|
|
import { ClientID, GameStartInfo, Turn } from "./Schemas";
|
|
import { sanitize, simpleHash } from "./Util";
|
|
import { fixProfaneUsername } from "./validations/username";
|
|
|
|
export async function createGameRunner(
|
|
gameStart: GameStartInfo,
|
|
clientID: ClientID,
|
|
mapLoader: GameMapLoader,
|
|
callBack: (gu: GameUpdateViewData) => void,
|
|
): Promise<GameRunner> {
|
|
const config = await getConfig(gameStart.config, null);
|
|
const gameMap = await loadGameMap(gameStart.config.gameMap, mapLoader);
|
|
const random = new PseudoRandom(simpleHash(gameStart.gameID));
|
|
|
|
const humans = gameStart.players.map(
|
|
(p) =>
|
|
new PlayerInfo(
|
|
p.clientID === clientID
|
|
? sanitize(p.username)
|
|
: fixProfaneUsername(sanitize(p.username)),
|
|
PlayerType.Human,
|
|
p.clientID,
|
|
random.nextID(),
|
|
),
|
|
);
|
|
|
|
const nations = gameStart.config.disableNPCs
|
|
? []
|
|
: gameMap.manifest.nations.map(
|
|
(n) =>
|
|
new Nation(
|
|
new Cell(n.coordinates[0], n.coordinates[1]),
|
|
n.strength,
|
|
new PlayerInfo(n.name, PlayerType.FakeHuman, null, random.nextID()),
|
|
),
|
|
);
|
|
|
|
const game: Game = createGame(
|
|
humans,
|
|
nations,
|
|
gameMap.gameMap,
|
|
gameMap.miniGameMap,
|
|
config,
|
|
);
|
|
|
|
const gr = new GameRunner(
|
|
game,
|
|
new Executor(game, gameStart.gameID, clientID),
|
|
callBack,
|
|
);
|
|
gr.init();
|
|
return gr;
|
|
}
|
|
|
|
export class GameRunner {
|
|
private turns: Turn[] = [];
|
|
private currTurn = 0;
|
|
private isExecuting = false;
|
|
|
|
private playerViewData: Record<PlayerID, NameViewData> = {};
|
|
|
|
constructor(
|
|
public game: Game,
|
|
private execManager: Executor,
|
|
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
|
|
) {}
|
|
|
|
init() {
|
|
if (this.game.config().bots() > 0) {
|
|
this.game.addExecution(
|
|
...this.execManager.spawnBots(this.game.config().numBots()),
|
|
);
|
|
}
|
|
if (this.game.config().spawnNPCs()) {
|
|
this.game.addExecution(...this.execManager.fakeHumanExecutions());
|
|
}
|
|
this.game.addExecution(new WinCheckExecution());
|
|
}
|
|
|
|
public addTurn(turn: Turn): void {
|
|
this.turns.push(turn);
|
|
}
|
|
|
|
public executeNextTick() {
|
|
if (this.isExecuting) {
|
|
return;
|
|
}
|
|
if (this.currTurn >= this.turns.length) {
|
|
return;
|
|
}
|
|
this.isExecuting = true;
|
|
|
|
this.game.addExecution(
|
|
...this.execManager.createExecs(this.turns[this.currTurn]),
|
|
);
|
|
this.currTurn++;
|
|
|
|
let updates: GameUpdates;
|
|
|
|
try {
|
|
updates = this.game.executeNextTick();
|
|
} catch (error: unknown) {
|
|
if (error instanceof Error) {
|
|
console.error("Game tick error:", error.message);
|
|
this.callBack({
|
|
errMsg: error.message,
|
|
stack: error.stack,
|
|
} as ErrorUpdate);
|
|
} else {
|
|
console.error("Game tick error:", error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (this.game.inSpawnPhase() && this.game.ticks() % 2 === 0) {
|
|
this.game
|
|
.players()
|
|
.filter(
|
|
(p) =>
|
|
p.type() === PlayerType.Human || p.type() === PlayerType.FakeHuman,
|
|
)
|
|
.forEach(
|
|
(p) => (this.playerViewData[p.id()] = placeName(this.game, p)),
|
|
);
|
|
}
|
|
|
|
if (this.game.ticks() < 3 || this.game.ticks() % 30 === 0) {
|
|
this.game.players().forEach((p) => {
|
|
this.playerViewData[p.id()] = placeName(this.game, p);
|
|
});
|
|
}
|
|
|
|
// Many tiles are updated to pack it into an array
|
|
const packedTileUpdates = updates[GameUpdateType.Tile].map((u) => u.update);
|
|
updates[GameUpdateType.Tile] = [];
|
|
|
|
this.callBack({
|
|
tick: this.game.ticks(),
|
|
packedTileUpdates: new BigUint64Array(packedTileUpdates),
|
|
updates: updates,
|
|
playerNameViewData: this.playerViewData,
|
|
});
|
|
this.isExecuting = false;
|
|
}
|
|
|
|
public playerActions(
|
|
playerID: PlayerID,
|
|
x: number,
|
|
y: number,
|
|
): PlayerActions {
|
|
const player = this.game.player(playerID);
|
|
const tile = this.game.ref(x, y);
|
|
const actions = {
|
|
canAttack: player.canAttack(tile),
|
|
buildableUnits: player.buildableUnits(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),
|
|
};
|
|
const alliance = player.allianceWith(other as Player);
|
|
if (alliance) {
|
|
actions.interaction.allianceExpiresAt = alliance.expiresAt();
|
|
}
|
|
}
|
|
|
|
return actions;
|
|
}
|
|
|
|
public playerProfile(playerID: number): PlayerProfile {
|
|
const player = this.game.playerBySmallID(playerID);
|
|
if (!player.isPlayer()) {
|
|
throw new Error(`player with id ${playerID} not found`);
|
|
}
|
|
return player.playerProfile();
|
|
}
|
|
public playerBorderTiles(playerID: PlayerID): PlayerBorderTiles {
|
|
const player = this.game.player(playerID);
|
|
if (!player.isPlayer()) {
|
|
throw new Error(`player with id ${playerID} not found`);
|
|
}
|
|
return {
|
|
borderTiles: player.borderTiles(),
|
|
} as PlayerBorderTiles;
|
|
}
|
|
|
|
public attackAveragePosition(
|
|
playerID: number,
|
|
attackID: string,
|
|
): Cell | null {
|
|
const player = this.game.playerBySmallID(playerID);
|
|
if (!player.isPlayer()) {
|
|
throw new Error(`player with id ${playerID} not found`);
|
|
}
|
|
|
|
const condition = (a) => a.id() === attackID;
|
|
const attack =
|
|
player.outgoingAttacks().find(condition) ??
|
|
player.incomingAttacks().find(condition);
|
|
if (attack === undefined) {
|
|
return null;
|
|
}
|
|
|
|
return attack.averagePosition();
|
|
}
|
|
|
|
public bestTransportShipSpawn(
|
|
playerID: PlayerID,
|
|
targetTile: TileRef,
|
|
): TileRef | false {
|
|
const player = this.game.player(playerID);
|
|
if (!player.isPlayer()) {
|
|
throw new Error(`player with id ${playerID} not found`);
|
|
}
|
|
return player.bestTransportShipSpawn(targetTile);
|
|
}
|
|
}
|