diff --git a/Dockerfile b/Dockerfile
index fb6cad03b..686fb8937 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,6 +16,6 @@ COPY . .
# Build the client-side application
RUN npm run build-prod
# Expose the port the app runs on
-EXPOSE 3000
+EXPOSE 3000 9229
# Define the command to run the app
CMD ["npm", "run", "start:server"]
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index e9cb94d28..a3e36cf8b 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,9 +1,10 @@
-version: '3'
+version: "3"
services:
game-server:
build: .
ports:
- "3000:3000"
+ - "9229:9229"
environment:
- NODE_ENV=production
nginx:
@@ -15,4 +16,4 @@ services:
- ./nginx.conf:/etc/nginx/nginx.conf
- /etc/letsencrypt:/etc/letsencrypt
depends_on:
- - game-server
\ No newline at end of file
+ - game-server
diff --git a/package.json b/package.json
index df0b0c8b7..08c4fbba6 100644
--- a/package.json
+++ b/package.json
@@ -5,12 +5,12 @@
"build-dev": "webpack --config webpack.config.js --mode development",
"build-prod": "webpack --config webpack.config.js --mode production",
"start:client": "webpack serve --open --node-env development",
- "start:server": "node --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts",
+ "start:server": "node --inspect=0.0.0.0:9229 --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts",
"start:server-dev": "GAME_ENV=dev node --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts",
"dev": "GAME_ENV=dev concurrently \"npm run start:client\" \"npm run start:server-dev\"",
"tunnel": "npm run build-prod && npm run start:server",
"test": "jest",
- "tailwind": "tailwindcss build -i ./src/client/tailwind.css -o public/tailwind.css"
+ "start:server-dev-profile": "GAME_ENV=dev node --inspect --trace-gc --loader ts-node/esm --experimental-specifier-resolution=node src/server/Server.ts"
},
"devDependencies": {
"@babel/core": "^7.25.2",
diff --git a/resources/.DS_Store b/resources/.DS_Store
new file mode 100644
index 000000000..a81528f49
Binary files /dev/null and b/resources/.DS_Store differ
diff --git a/src/client/index.html b/src/client/index.html
index 79b6b7083..4633aab18 100644
--- a/src/client/index.html
+++ b/src/client/index.html
@@ -35,7 +35,7 @@
OpenFront.io
diff --git a/src/core/configuration/Config.ts b/src/core/configuration/Config.ts
index f2991967d..39a5730c8 100644
--- a/src/core/configuration/Config.ts
+++ b/src/core/configuration/Config.ts
@@ -1,4 +1,17 @@
-import { Difficulty, GameType, Gold, Player, PlayerID, PlayerInfo, TerraNullius, Tick, Tile, Unit, UnitInfo, UnitType } from "../game/Game";
+import {
+ Difficulty,
+ GameType,
+ Gold,
+ Player,
+ PlayerID,
+ PlayerInfo,
+ TerraNullius,
+ Tick,
+ Tile,
+ Unit,
+ UnitInfo,
+ UnitType,
+} from "../game/Game";
import { Colord, colord } from "colord";
import { preprodConfig } from "./PreprodConfig";
import { prodConfig } from "./ProdConfig";
@@ -8,100 +21,110 @@ import { DefaultConfig } from "./DefaultConfig";
import { DevConfig, DevServerConfig } from "./DevConfig";
export enum GameEnv {
- Dev,
- Prod
+ Dev,
+ Prod,
}
export function getConfig(gameConfig: GameConfig): Config {
- const sc = getServerConfig()
- switch (process.env.GAME_ENV) {
- case 'dev':
- return new DevConfig(sc, gameConfig)
- case 'preprod':
- case 'prod':
- consolex.log('using prod config')
- return new DefaultConfig(sc, gameConfig)
- default:
- throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`)
- }
+ const sc = getServerConfig();
+ switch (process.env.GAME_ENV) {
+ case "dev":
+ return new DevConfig(sc, gameConfig);
+ case "preprod":
+ case "prod":
+ consolex.log("using prod config");
+ return new DefaultConfig(sc, gameConfig);
+ default:
+ throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`);
+ }
}
export function getServerConfig(): ServerConfig {
- switch (process.env.GAME_ENV) {
- case 'dev':
- consolex.log('using dev config')
- return new DevServerConfig()
- case 'preprod':
- consolex.log('using preprod config')
- return preprodConfig
- case 'prod':
- consolex.log('using prod config')
- return prodConfig
- default:
- throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`)
- }
+ switch (process.env.GAME_ENV) {
+ case "dev":
+ consolex.log("using dev config");
+ return new DevServerConfig();
+ case "preprod":
+ consolex.log("using preprod config");
+ return preprodConfig;
+ case "prod":
+ default:
+ consolex.log("using prod config");
+ return prodConfig;
+ // default:
+ // throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`)
+ }
}
export interface ServerConfig {
- turnIntervalMs(): number
- gameCreationRate(): number
- lobbyLifetime(): number
+ turnIntervalMs(): number;
+ gameCreationRate(): number;
+ lobbyLifetime(): number;
}
export interface Config {
- serverConfig(): ServerConfig
- gameConfig(): GameConfig
- theme(): Theme;
- percentageTilesOwnedToWin(): number
- numBots(): number
- spawnNPCs(): boolean
- numSpawnPhaseTurns(gameType: GameType): number
+ serverConfig(): ServerConfig;
+ gameConfig(): GameConfig;
+ theme(): Theme;
+ percentageTilesOwnedToWin(): number;
+ numBots(): number;
+ spawnNPCs(): boolean;
+ numSpawnPhaseTurns(gameType: GameType): number;
- startManpower(playerInfo: PlayerInfo): number
- populationIncreaseRate(player: Player): number
- goldAdditionRate(player: Player): number
- troopAdjustmentRate(player: Player): number
- attackTilesPerTick(attckTroops: number, attacker: Player, defender: Player | TerraNullius, numAdjacentTilesWithEnemy: number): number
- attackLogic(attackTroops: number, attacker: Player, defender: Player | TerraNullius, tileToConquer: Tile): {
- attackerTroopLoss: number,
- defenderTroopLoss: number,
- tilesPerTickUsed: number
- }
- attackAmount(attacker: Player, defender: Player | TerraNullius): number
- maxPopulation(player: Player): number
- cityPopulationIncrease(): number
- boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number
- boatMaxDistance(): number
- boatMaxNumber(): number
- allianceDuration(): Tick
- allianceRequestCooldown(): Tick
- targetDuration(): Tick
- targetCooldown(): Tick
- emojiMessageCooldown(): Tick
- emojiMessageDuration(): Tick
- donateCooldown(): Tick
- defaultDonationAmount(sender: Player): number
- unitInfo(type: UnitType): UnitInfo
- tradeShipGold(src: Unit, dst: Unit): Gold
- tradeShipSpawnRate(): number
- defensePostRange(): number
- defensePostDefenseBonus(): number
- falloutDefenseModifier(): number
- difficultyModifier(difficulty: Difficulty): number
+ startManpower(playerInfo: PlayerInfo): number;
+ populationIncreaseRate(player: Player): number;
+ goldAdditionRate(player: Player): number;
+ troopAdjustmentRate(player: Player): number;
+ attackTilesPerTick(
+ attckTroops: number,
+ attacker: Player,
+ defender: Player | TerraNullius,
+ numAdjacentTilesWithEnemy: number
+ ): number;
+ attackLogic(
+ attackTroops: number,
+ attacker: Player,
+ defender: Player | TerraNullius,
+ tileToConquer: Tile
+ ): {
+ attackerTroopLoss: number;
+ defenderTroopLoss: number;
+ tilesPerTickUsed: number;
+ };
+ attackAmount(attacker: Player, defender: Player | TerraNullius): number;
+ maxPopulation(player: Player): number;
+ cityPopulationIncrease(): number;
+ boatAttackAmount(attacker: Player, defender: Player | TerraNullius): number;
+ boatMaxDistance(): number;
+ boatMaxNumber(): number;
+ allianceDuration(): Tick;
+ allianceRequestCooldown(): Tick;
+ targetDuration(): Tick;
+ targetCooldown(): Tick;
+ emojiMessageCooldown(): Tick;
+ emojiMessageDuration(): Tick;
+ donateCooldown(): Tick;
+ defaultDonationAmount(sender: Player): number;
+ unitInfo(type: UnitType): UnitInfo;
+ tradeShipGold(src: Unit, dst: Unit): Gold;
+ tradeShipSpawnRate(): number;
+ defensePostRange(): number;
+ defensePostDefenseBonus(): number;
+ falloutDefenseModifier(): number;
+ difficultyModifier(difficulty: Difficulty): number;
}
export interface Theme {
- playerInfoColor(id: PlayerID): Colord;
- territoryColor(playerInfo: PlayerInfo): Colord;
- borderColor(playerInfo: PlayerInfo): Colord;
- defendedBorderColor(playerInfo: PlayerInfo): Colord;
- terrainColor(tile: Tile): Colord;
- backgroundColor(): Colord;
- falloutColor(): Colord
- font(): string;
- // unit color for alternate view
- selfColor(): Colord
- allyColor(): Colord
- enemyColor(): Colord
- spawnHighlightColor(): Colord
+ playerInfoColor(id: PlayerID): Colord;
+ territoryColor(playerInfo: PlayerInfo): Colord;
+ borderColor(playerInfo: PlayerInfo): Colord;
+ defendedBorderColor(playerInfo: PlayerInfo): Colord;
+ terrainColor(tile: Tile): Colord;
+ backgroundColor(): Colord;
+ falloutColor(): Colord;
+ font(): string;
+ // unit color for alternate view
+ selfColor(): Colord;
+ allyColor(): Colord;
+ enemyColor(): Colord;
+ spawnHighlightColor(): Colord;
}
-
diff --git a/src/server/Server.ts b/src/server/Server.ts
index 80b79b6b0..583a5b60e 100644
--- a/src/server/Server.ts
+++ b/src/server/Server.ts
@@ -1,17 +1,27 @@
-import express, { json } from 'express';
-import http from 'http';
-import { WebSocketServer } from 'ws';
-import path from 'path';
-import { fileURLToPath } from 'url';
-import { GameManager } from './GameManager';
-import { ClientMessage, ClientMessageSchema, GameRecord, GameRecordSchema, LogSeverity } from '../core/Schemas';
-import { getConfig, getServerConfig } from '../core/configuration/Config';
-import { slog } from './StructuredLog';
-import { Client } from './Client';
-import { GamePhase, GameServer } from './GameServer';
-import { archive } from './Archive';
-import { DiscordBot } from './DiscordBot';
-import { sanitizeUsername, validateUsername } from "../core/validations/username";
+import express, { json } from "express";
+import http from "http";
+import { WebSocketServer } from "ws";
+import path from "path";
+import { fileURLToPath } from "url";
+import { GameManager } from "./GameManager";
+import {
+ ClientMessage,
+ ClientMessageSchema,
+ GameRecord,
+ GameRecordSchema,
+ LogSeverity,
+} from "../core/Schemas";
+import { getConfig, getServerConfig } from "../core/configuration/Config";
+import { slog } from "./StructuredLog";
+import { Client } from "./Client";
+import { GamePhase, GameServer } from "./GameServer";
+import { archive } from "./Archive";
+import { DiscordBot } from "./DiscordBot";
+import {
+ sanitizeUsername,
+ validateUsername,
+} from "../core/validations/username";
+import { Request, Response } from "express";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -21,170 +31,180 @@ const server = http.createServer(app);
const wss = new WebSocketServer({ server });
// Serve static files from the 'out' directory
-app.use(express.static(path.join(__dirname, '../../out')));
-app.use(express.json())
+app.use(express.static(path.join(__dirname, "../../out")));
+app.use(express.json());
-const gm = new GameManager(getServerConfig())
+const gm = new GameManager(getServerConfig());
const bot = new DiscordBot();
try {
- await bot.start();
+ await bot.start();
} catch (error) {
- console.error('Failed to start bot:', error);
+ console.error("Failed to start bot:", error);
}
+let lobbiesString = "";
+
// New GET endpoint to list lobbies
-app.get('/lobbies', (req, res) => {
- const now = Date.now()
- res.json({
- lobbies: gm.gamesByPhase(GamePhase.Lobby)
- .filter(g => g.isPublic)
- .map(g => ({ id: g.id, msUntilStart: g.startTime() - now, numClients: g.numClients() }))
- .sort((a, b) => a.msUntilStart - b.msUntilStart),
- });
+app.get("/lobbies", (req: Request, res: Response) => {
+ res.send(lobbiesString);
});
-app.post('/private_lobby', (req, res) => {
- const id = gm.createPrivateGame()
- console.log('creating private lobby with id ${id}')
- res.json({
- id: id
- });
+app.post("/private_lobby", (req, res) => {
+ const id = gm.createPrivateGame();
+ console.log("creating private lobby with id ${id}");
+ res.json({
+ id: id,
+ });
});
-app.post('/archive_singleplayer_game', (req, res) => {
+app.post("/archive_singleplayer_game", (req, res) => {
+ try {
+ const gameRecord: GameRecord = req.body;
+ const clientIP = req.ip || req.socket.remoteAddress || "unknown"; // Added this line
+
+ if (!gameRecord) {
+ console.log("game record not found in request");
+ res.status(404).json({ error: "Game record not found" });
+ return;
+ }
+ gameRecord.players.forEach((p) => (p.ip = clientIP));
+ GameRecordSchema.parse(gameRecord);
+ archive(gameRecord);
+ res.json({
+ success: true,
+ });
+ } catch (error) {
+ slog({
+ logKey: "complete_single_player_game_record",
+ msg: `Failed to complete game record: ${error}`,
+ severity: LogSeverity.Error,
+ });
+ res.status(400).json({ error: "Invalid game record format" });
+ }
+});
+
+app.post("/start_private_lobby/:id", (req, res) => {
+ console.log(`starting private lobby with id ${req.params.id}`);
+ gm.startPrivateGame(req.params.id);
+});
+
+app.put("/private_lobby/:id", (req, res) => {
+ const lobbyID = req.params.id;
+ gm.updateGameConfig(lobbyID, {
+ gameMap: req.body.gameMap,
+ difficulty: req.body.difficulty,
+ });
+});
+
+app.get("/lobby/:id/exists", (req, res) => {
+ const lobbyId = req.params.id;
+ console.log(`checking lobby ${lobbyId} exists`);
+ const lobbyExists = gm.hasActiveGame(lobbyId);
+
+ res.json({
+ exists: lobbyExists,
+ });
+});
+
+app.get("/lobby/:id", (req, res) => {
+ const game = gm.game(req.params.id);
+ if (game == null) {
+ console.log(`lobby ${req.params.id} not found`);
+ return res.status(404).json({ error: "Game not found" });
+ }
+ res.json({
+ players: game.activeClients.map((c) => ({
+ username: c.username,
+ clientID: c.clientID,
+ })),
+ });
+});
+
+app.get("/private_lobby/:id", (req, res) => {
+ res.json({
+ hi: "5",
+ });
+});
+
+wss.on("connection", (ws, req) => {
+ ws.on("message", (message: string) => {
try {
- const gameRecord: GameRecord = req.body
- const clientIP = req.ip || req.socket.remoteAddress || 'unknown'; // Added this line
-
-
- if (!gameRecord) {
- console.log('game record not found in request')
- res.status(404).json({ error: 'Game record not found' });
- return;
+ const clientMsg: ClientMessage = ClientMessageSchema.parse(
+ JSON.parse(message)
+ );
+ if (clientMsg.type == "join") {
+ const forwarded = req.headers["x-forwarded-for"];
+ let ip = Array.isArray(forwarded)
+ ? forwarded[0] // Get the first IP if it's an array
+ : forwarded || req.socket.remoteAddress;
+ if (Array.isArray(ip)) {
+ ip = ip[0];
}
- gameRecord.players.forEach(p => p.ip = clientIP)
- GameRecordSchema.parse(gameRecord);
- archive(gameRecord)
- res.json({
- success: true,
- });
- } catch (error) {
+ const { isValid, error } = validateUsername(clientMsg.username);
+ if (!isValid) {
+ console.log(
+ `game ${clientMsg.gameID}, client ${clientMsg.clientID} received invalid username, ${error}`
+ );
+ return;
+ }
+ clientMsg.username = sanitizeUsername(clientMsg.username);
+ gm.addClient(
+ new Client(
+ clientMsg.clientID,
+ clientMsg.persistentID,
+ ip,
+ clientMsg.username,
+ ws
+ ),
+ clientMsg.gameID,
+ clientMsg.lastTurn
+ );
+ }
+ if (clientMsg.type == "log") {
slog({
- logKey: 'complete_single_player_game_record',
- msg: `Failed to complete game record: ${error}`,
- severity: LogSeverity.Error,
+ logKey: "client_console_log",
+ msg: clientMsg.log,
+ severity: clientMsg.severity,
+ clientID: clientMsg.clientID,
+ gameID: clientMsg.gameID,
+ persistentID: clientMsg.persistentID,
});
- res.status(400).json({ error: 'Invalid game record format' });
+ }
+ } catch (error) {
+ console.log(`errror handling websocket message: ${error}`);
}
-})
-
-app.post('/start_private_lobby/:id', (req, res) => {
- console.log(`starting private lobby with id ${req.params.id}`)
- gm.startPrivateGame(req.params.id)
-});
-
-app.put('/private_lobby/:id', (req, res) => {
- const lobbyID = req.params.id
- gm.updateGameConfig(lobbyID, { gameMap: req.body.gameMap, difficulty: req.body.difficulty })
-});
-
-app.get('/lobby/:id/exists', (req, res) => {
- const lobbyId = req.params.id;
- console.log(`checking lobby ${lobbyId} exists`)
- const lobbyExists = gm.hasActiveGame(lobbyId);
-
- res.json({
- exists: lobbyExists
- });
-});
-
-app.get('/lobby/:id', (req, res) => {
- const game = gm.game(req.params.id)
- if (game == null) {
- console.log(`lobby ${req.params.id} not found`)
- return res.status(404).json({ error: 'Game not found' });
- }
- res.json({
- players: game.activeClients.map(c => ({
- username: c.username,
- clientID: c.clientID
- }))
- });
-});
-
-
-app.get('/private_lobby/:id', (req, res) => {
- res.json({
- hi: '5'
- });
-});
-
-wss.on('connection', (ws, req) => {
- ws.on('message', (message: string) => {
- try {
- const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
- slog({
- logKey: 'websocket_msg',
- msg: 'server received websocket message',
- data: clientMsg,
- severity: LogSeverity.Debug
- })
- if (clientMsg.type == "join") {
- const forwarded = req.headers['x-forwarded-for']
- let ip = Array.isArray(forwarded)
- ? forwarded[0] // Get the first IP if it's an array
- : forwarded || req.socket.remoteAddress;
- if (Array.isArray(ip)) {
- ip = ip[0]
- }
- const { isValid, error } = validateUsername(clientMsg.username);
- if (!isValid) {
- console.log(`game ${clientMsg.gameID}, client ${clientMsg.clientID} received invalid username, ${error}`)
- return;
- }
- clientMsg.username = sanitizeUsername(clientMsg.username)
- gm.addClient(
- new Client(
- clientMsg.clientID,
- clientMsg.persistentID,
- ip,
- clientMsg.username,
- ws
- ),
- clientMsg.gameID,
- clientMsg.lastTurn
- )
- }
- if (clientMsg.type == "log") {
- slog({
- logKey: "client_console_log",
- msg: clientMsg.log,
- severity: clientMsg.severity,
- clientID: clientMsg.clientID,
- gameID: clientMsg.gameID,
- persistentID: clientMsg.persistentID,
- })
- }
- } catch (error) {
- console.log(`errror handling websocket message: ${error}`)
- }
- })
+ });
});
function runGame() {
- setInterval(() => tick(), 1000);
+ setInterval(() => tick(), 1000);
+ setInterval(() => updateLobbies(), 100);
}
function tick() {
- gm.tick()
+ gm.tick();
+}
+
+function updateLobbies() {
+ lobbiesString = JSON.stringify({
+ lobbies: gm
+ .gamesByPhase(GamePhase.Lobby)
+ .filter((g) => g.isPublic)
+ .map((g) => ({
+ id: g.id,
+ msUntilStart: g.startTime() - Date.now(),
+ numClients: g.numClients(),
+ }))
+ .sort((a, b) => a.msUntilStart - b.msUntilStart),
+ });
}
const PORT = process.env.PORT || 3000;
console.log(`Server will try to run on http://localhost:${PORT}`);
server.listen(PORT, () => {
- console.log(`Server is running on http://localhost:${PORT}`);
+ console.log(`Server is running on http://localhost:${PORT}`);
});
-runGame()
+runGame();
diff --git a/update-deploy.sh b/update-deploy.sh
index 190316a6e..ca46381f5 100755
--- a/update-deploy.sh
+++ b/update-deploy.sh
@@ -18,20 +18,20 @@ fi
if [[ "$ENV" == "dev" ]]; then
INSTANCE_NAME="openfrontio-dev-instance"
TAG="dev"
- GAME_ENV="preprod" # Set game environment to preprod for dev
+ GAME_ENV="preprod"
echo "[DEV] Deploying to openfront.dev"
else
INSTANCE_NAME="openfrontio-instance"
TAG="latest"
- GAME_ENV="prod" # Set game environment to prod for prod
+ GAME_ENV="prod"
echo "[PROD] Deploying to openfront.io"
fi
# Ensure you're authenticated with Google Cloud
gcloud auth configure-docker us-central1-docker.pkg.dev
-# Build the new Docker image with GAME_ENV build argument
-docker build --build-arg GAME_ENV=$GAME_ENV -t openfrontio .
+# Build the new Docker image with platform specification and GAME_ENV build argument
+docker build --platform linux/amd64 --build-arg GAME_ENV=$GAME_ENV -t openfrontio .
# Tag the new image
docker tag openfrontio us-central1-docker.pkg.dev/openfrontio/openfrontio/game-server:$TAG