store client ips in bigquery table

This commit is contained in:
Evan
2024-12-14 10:03:05 -08:00
parent c93bca059f
commit 2fc81c7d17
12 changed files with 90 additions and 95 deletions
+1
View File
@@ -238,6 +238,7 @@
* make boats work on oceania 12/12/2024 * make boats work on oceania 12/12/2024
* add capture alert DONE 12/13/2024 * add capture alert DONE 12/13/2024
* better emojis 🏳️🤦‍♂️🖕☮️🫡😡😈🤡 DONE 12/13/2024 * better emojis 🏳️🤦‍♂️🖕☮️🫡😡😈🤡 DONE 12/13/2024
* store ips in bigquery table DONE 12/14/2024
* bug: player names not updating sometimes * bug: player names not updating sometimes
* make player editeable configs * make player editeable configs
* games disconnects on multplayer after some time * games disconnects on multplayer after some time
+3 -5
View File
@@ -7,7 +7,7 @@ import { createRenderer, GameRenderer } from "./graphics/GameRenderer";
import { InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent } from "./InputHandler" import { InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent } from "./InputHandler"
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientMessageSchema, GameConfig, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn } from "../core/Schemas"; import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientMessageSchema, GameConfig, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn } from "../core/Schemas";
import { createMiniMap, loadTerrainMap, TerrainMapImpl } from "../core/game/TerrainMapLoader"; import { createMiniMap, loadTerrainMap, TerrainMapImpl } from "../core/game/TerrainMapLoader";
import { and, bfs, dist, manhattanDist } from "../core/Util"; import { and, bfs, dist, generateID, manhattanDist } from "../core/Util";
import { WinCheckExecution } from "../core/execution/WinCheckExecution"; import { WinCheckExecution } from "../core/execution/WinCheckExecution";
import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport"; import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport";
import { createCanvas } from "./graphics/Utils"; import { createCanvas } from "./graphics/Utils";
@@ -20,14 +20,13 @@ export interface LobbyConfig {
gameType: GameType gameType: GameType
playerName: () => string playerName: () => string
gameID: GameID gameID: GameID
ip: string | null
map: GameMap | null map: GameMap | null
difficulty: Difficulty | null difficulty: Difficulty | null
} }
export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => void { export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => void {
const clientID = uuidv4() const clientID = generateID()
const playerID = uuidv4() const playerID = generateID()
const eventBus = new EventBus() const eventBus = new EventBus()
const config = getConfig() const config = getConfig()
@@ -45,7 +44,6 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
gameConfig, gameConfig,
eventBus, eventBus,
lobbyConfig.gameID, lobbyConfig.gameID,
lobbyConfig.ip,
clientID, clientID,
playerID, playerID,
config, config,
+9 -5
View File
@@ -1,6 +1,6 @@
import { Config } from "../core/configuration/Config"; import { Config } from "../core/configuration/Config";
import { ClientMessage, ClientMessageSchema, GameConfig, GameID, GameRecordSchema, Intent, ServerMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas"; import { ClientID, ClientMessage, ClientMessageSchema, GameConfig, GameID, GameRecordSchema, Intent, PlayerRecord, ServerMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
import { CreateGameRecord, generateGameID } from "../core/Util"; import { CreateGameRecord, generateID } from "../core/Util";
export class LocalServer { export class LocalServer {
@@ -13,8 +13,8 @@ export class LocalServer {
private gameID: GameID private gameID: GameID
constructor(private config: Config, private gameConfig: GameConfig, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) { constructor(private clientID: ClientID, private config: Config, private gameConfig: GameConfig, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) {
this.gameID = generateGameID() this.gameID = generateID()
} }
start() { start() {
@@ -52,7 +52,11 @@ export class LocalServer {
public endGame() { public endGame() {
console.log('local server ending game') console.log('local server ending game')
clearInterval(this.endTurnIntervalID) clearInterval(this.endTurnIntervalID)
const record = CreateGameRecord(this.gameID, this.gameConfig, this.turns, this.startedAt, Date.now()) const players: PlayerRecord[] = [{
ip: null,
clientID: this.clientID
}]
const record = CreateGameRecord(this.gameID, this.gameConfig, players, this.turns, this.startedAt, Date.now())
// Clear turns because beacon only supports up to 64kb // Clear turns because beacon only supports up to 64kb
record.turns = [] record.turns = []
// For unload events, sendBeacon is the only reliable method // For unload events, sendBeacon is the only reliable method
-37
View File
@@ -16,8 +16,6 @@ import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
class Client { class Client {
private gameStop: () => void private gameStop: () => void
private ip: Promise<string | null> = null
private usernameInput: UsernameInput | null = null; private usernameInput: UsernameInput | null = null;
private joinModal: JoinPrivateLobbyModal private joinModal: JoinPrivateLobbyModal
@@ -37,7 +35,6 @@ class Client {
}); });
setFavicon() setFavicon()
this.ip = getClientIP()
document.addEventListener('join-lobby', this.handleJoinLobby.bind(this)); document.addEventListener('join-lobby', this.handleJoinLobby.bind(this));
document.addEventListener('leave-lobby', this.handleLeaveLobby.bind(this)); document.addEventListener('leave-lobby', this.handleLeaveLobby.bind(this));
document.addEventListener('single-player', this.handleSinglePlayer.bind(this)); document.addEventListener('single-player', this.handleSinglePlayer.bind(this));
@@ -65,8 +62,6 @@ class Client {
private async handleJoinLobby(event: CustomEvent) { private async handleJoinLobby(event: CustomEvent) {
const lobby = event.detail.lobby const lobby = event.detail.lobby
console.log(`joining lobby ${lobby.id}`) console.log(`joining lobby ${lobby.id}`)
const clientIP = await this.ip
console.log(`got ip ${clientIP}`)
if (this.gameStop != null) { if (this.gameStop != null) {
console.log('joining lobby, stopping existing game') console.log('joining lobby, stopping existing game')
this.gameStop() this.gameStop()
@@ -76,7 +71,6 @@ class Client {
gameType: event.detail.gameType, gameType: event.detail.gameType,
playerName: (): string => this.usernameInput.getCurrentUsername(), playerName: (): string => this.usernameInput.getCurrentUsername(),
gameID: lobby.id, gameID: lobby.id,
ip: clientIP,
map: event.detail.map, map: event.detail.map,
difficulty: event.detail.difficulty, difficulty: event.detail.difficulty,
}, },
@@ -98,37 +92,6 @@ class Client {
} }
} }
async function getClientIP(timeoutMs: number = 1000): Promise<string | null> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response: Response = await fetch('https://api.ipify.org?format=json', {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: { ip: string } = await response.json();
return data.ip;
} catch (error) {
if (error instanceof Error) {
if (error.name === 'AbortError') {
console.error('Request timed out');
} else {
console.error('Error fetching IP:', error.message);
}
} else {
console.error('An unknown error occurred');
}
return null;
} finally {
clearTimeout(timeoutId);
}
}
// Initialize the client when the DOM is loaded // Initialize the client when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
new Client().initialize(); new Client().initialize();
+2 -2
View File
@@ -1,7 +1,7 @@
import { LitElement, html, css } from 'lit'; import { LitElement, html, css } from 'lit';
import { customElement, property, state } from 'lit/decorators.js'; import { customElement, property, state } from 'lit/decorators.js';
import { Difficulty, GameMap, GameType } from '../core/game/Game'; import { Difficulty, GameMap, GameType } from '../core/game/Game';
import { generateGameID as generateGameID } from '../core/Util'; import { generateID as generateID } from '../core/Util';
@customElement('single-player-modal') @customElement('single-player-modal')
export class SinglePlayerModal extends LitElement { export class SinglePlayerModal extends LitElement {
@@ -128,7 +128,7 @@ export class SinglePlayerModal extends LitElement {
detail: { detail: {
gameType: GameType.Singleplayer, gameType: GameType.Singleplayer,
lobby: { lobby: {
id: generateGameID(), id: generateID(),
}, },
map: this.selectedMap, map: this.selectedMap,
difficulty: this.selectedDifficulty difficulty: this.selectedDifficulty
+1 -3
View File
@@ -102,7 +102,6 @@ export class Transport {
private gameConfig: GameConfig | null, private gameConfig: GameConfig | null,
private eventBus: EventBus, private eventBus: EventBus,
private gameID: GameID, private gameID: GameID,
private clientIP: string | null,
private clientID: ClientID, private clientID: ClientID,
private playerID: PlayerID, private playerID: PlayerID,
private config: Config, private config: Config,
@@ -152,7 +151,7 @@ export class Transport {
} }
private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void) { private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void) {
this.localServer = new LocalServer(this.config, this.gameConfig, onconnect, onmessage) this.localServer = new LocalServer(this.clientID, this.config, this.gameConfig, onconnect, onmessage)
this.localServer.start() this.localServer.start()
} }
@@ -195,7 +194,6 @@ export class Transport {
type: "join", type: "join",
gameID: this.gameID, gameID: this.gameID,
clientID: this.clientID, clientID: this.clientID,
clientIP: this.clientIP,
lastTurn: numTurns lastTurn: numTurns
}) })
) )
+9 -8
View File
@@ -42,6 +42,7 @@ export type ClientPingMessage = z.infer<typeof ClientPingMessageSchema>
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema> export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>
export type ClientJoinMessage = z.infer<typeof ClientJoinMessageSchema> export type ClientJoinMessage = z.infer<typeof ClientJoinMessageSchema>
export type PlayerRecord = z.infer<typeof PlayerRecordSchema>
export type GameRecord = z.infer<typeof GameRecordSchema> export type GameRecord = z.infer<typeof GameRecordSchema>
const PlayerTypeSchema = z.nativeEnum(PlayerType); const PlayerTypeSchema = z.nativeEnum(PlayerType);
@@ -105,11 +106,6 @@ export const BoatAttackIntentSchema = BaseIntentSchema.extend({
y: z.number(), y: z.number(),
}) })
export const UpdateNameIntentSchema = BaseIntentSchema.extend({
type: z.literal('updateName'),
name: z.string(),
})
export const AllianceRequestIntentSchema = BaseIntentSchema.extend({ export const AllianceRequestIntentSchema = BaseIntentSchema.extend({
type: z.literal('allianceRequest'), type: z.literal('allianceRequest'),
requestor: z.string(), requestor: z.string(),
@@ -228,20 +224,25 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({
export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({ export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
type: z.literal('join'), type: z.literal('join'),
clientIP: z.string().nullable(),
lastTurn: z.number() // The last turn the client saw. lastTurn: z.number() // The last turn the client saw.
}) })
export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientIntentMessageSchema, ClientJoinMessageSchema]); export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientIntentMessageSchema, ClientJoinMessageSchema]);
export const PlayerRecordSchema = z.object({
clientID: z.string(),
username: z.string(),
ip: z.string().nullable(),
})
export const GameRecordSchema = z.object({ export const GameRecordSchema = z.object({
id: z.string(), id: z.string(),
gameConfig: GameConfigSchema, gameConfig: GameConfigSchema,
players: z.array(PlayerRecordSchema),
startTimestampMS: z.number(), startTimestampMS: z.number(),
endTimestampMS: z.number(), endTimestampMS: z.number(),
durationSeconds: z.number(), durationSeconds: z.number(),
date: z.string(), date: z.string(),
usernames: z.array(z.string()),
num_turns: z.number(), num_turns: z.number(),
turns: z.array(TurnSchema) turns: z.array(TurnSchema)
}) })
+19 -7
View File
@@ -5,7 +5,7 @@ import DOMPurify from 'dompurify';
import { Cell, Game, Player, TerraNullius, Tile, Unit } from "./game/Game"; import { Cell, Game, Player, TerraNullius, Tile, Unit } from "./game/Game";
import { number } from 'zod'; import { number } from 'zod';
import { GameConfig, GameID, GameRecord, Turn } from './Schemas'; import { GameConfig, GameID, GameRecord, PlayerRecord, Turn } from './Schemas';
import { customAlphabet, nanoid } from 'nanoid'; import { customAlphabet, nanoid } from 'nanoid';
export function manhattanDist(c1: Cell, c2: Cell): number { export function manhattanDist(c1: Cell, c2: Cell): number {
@@ -232,7 +232,15 @@ export function onlyImages(html: string) {
}); });
} }
export function CreateGameRecord(id: GameID, gameConfig: GameConfig, turns: Turn[], start: number, end: number): GameRecord { export function CreateGameRecord(
id: GameID,
gameConfig: GameConfig,
// username does not need to be set.
players: PlayerRecord[],
turns: Turn[],
start: number,
end: number
): GameRecord {
const record: GameRecord = { const record: GameRecord = {
id: id, id: id,
gameConfig: gameConfig, gameConfig: gameConfig,
@@ -241,18 +249,22 @@ export function CreateGameRecord(id: GameID, gameConfig: GameConfig, turns: Turn
date: new Date().toISOString().split('T')[0], date: new Date().toISOString().split('T')[0],
turns: [] turns: []
} }
const usernames = new Set<string>()
for (const turn of turns) { for (const turn of turns) {
if (turn.intents.length != 0) { if (turn.intents.length != 0) {
record.turns.push(turn) record.turns.push(turn)
for (const intent of turn.intents) { for (const intent of turn.intents) {
if (intent.type == 'spawn') { if (intent.type == "spawn") {
usernames.add(intent.name) for (const playerRecord of players) {
if (playerRecord.clientID == intent.clientID) {
playerRecord.username = intent.name
}
}
} }
} }
} }
} }
record.usernames = Array.from(usernames) record.players = players
record.durationSeconds = Math.floor((record.endTimestampMS - record.startTimestampMS) / 1000) record.durationSeconds = Math.floor((record.endTimestampMS - record.startTimestampMS) / 1000)
record.num_turns = turns.length record.num_turns = turns.length
return record; return record;
@@ -262,7 +274,7 @@ export function assertNever(x: never): never {
throw new Error('Unexpected value: ' + x); throw new Error('Unexpected value: ' + x);
} }
export function generateGameID(): GameID { export function generateID(): GameID {
const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 8) const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', 8)
return nanoid() return nanoid()
} }
+6 -1
View File
@@ -15,11 +15,14 @@ export async function archive(gameRecord: GameRecord) {
end: new Date(gameRecord.endTimestampMS), end: new Date(gameRecord.endTimestampMS),
duration_seconds: gameRecord.durationSeconds, duration_seconds: gameRecord.durationSeconds,
number_turns: gameRecord.num_turns, number_turns: gameRecord.num_turns,
usernames: gameRecord.usernames,
game_mode: gameRecord.gameConfig.gameType, game_mode: gameRecord.gameConfig.gameType,
winner: null, winner: null,
difficulty: gameRecord.gameConfig.difficulty, difficulty: gameRecord.gameConfig.difficulty,
map: gameRecord.gameConfig.gameMap, map: gameRecord.gameConfig.gameMap,
players: gameRecord.players.map(p => ({
username: p.username,
ip: p.ip,
})),
}; };
await bigquery await bigquery
@@ -29,6 +32,8 @@ export async function archive(gameRecord: GameRecord) {
console.log(`wrote game metadata to BigQuery: ${gameRecord.id}`); console.log(`wrote game metadata to BigQuery: ${gameRecord.id}`);
if (gameRecord.turns.length > 0) { if (gameRecord.turns.length > 0) {
// Players will see this so make sure to clear PII.
gameRecord.players.forEach(p => p.ip = "REDACTED")
console.log(`writing game ${gameRecord.id} to gcs`) console.log(`writing game ${gameRecord.id} to gcs`)
const bucket = storage.bucket("openfront-games"); const bucket = storage.bucket("openfront-games");
const file = bucket.file(gameRecord.id); const file = bucket.file(gameRecord.id);
+3 -3
View File
@@ -4,7 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
import { Client } from "./Client"; import { Client } from "./Client";
import { GamePhase, GameServer } from "./GameServer"; import { GamePhase, GameServer } from "./GameServer";
import { Difficulty, GameMap, GameType } from "../core/game/Game"; import { Difficulty, GameMap, GameType } from "../core/game/Game";
import { generateGameID } from "../core/Util"; import { generateID } from "../core/Util";
@@ -39,7 +39,7 @@ export class GameManager {
} }
createPrivateGame(): string { createPrivateGame(): string {
const id = generateGameID() const id = generateID()
this.games.push(new GameServer( this.games.push(new GameServer(
id, id,
Date.now(), Date.now(),
@@ -79,7 +79,7 @@ export class GameManager {
if (now > this.lastNewLobby + this.config.gameCreationRate()) { if (now > this.lastNewLobby + this.config.gameCreationRate()) {
this.lastNewLobby = now this.lastNewLobby = now
lobbies.push(new GameServer( lobbies.push(new GameServer(
generateGameID(), generateID(),
now, now,
true, true,
this.config, this.config,
+26 -17
View File
@@ -1,10 +1,10 @@
import { ClientMessage, ClientMessageSchema, GameConfig, GameRecordSchema, Intent, ServerPingMessageSchema, ServerStartGameMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas"; import { ClientID, ClientMessage, ClientMessageSchema, GameConfig, GameRecordSchema, Intent, PlayerRecord, ServerPingMessageSchema, ServerStartGameMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
import { Config } from "../core/configuration/Config"; import { Config } from "../core/configuration/Config";
import { Client } from "./Client"; import { Client } from "./Client";
import WebSocket from 'ws'; import WebSocket from 'ws';
import { slog } from "./StructuredLog"; import { slog } from "./StructuredLog";
import { Storage } from '@google-cloud/storage'; import { Storage } from '@google-cloud/storage';
import { CreateGameRecord, CreateGameRecord as ProcessRecord } from "../core/Util"; import { CreateGameRecord } from "../core/Util";
import { archive } from "./Archive"; import { archive } from "./Archive";
import { arc } from "d3"; import { arc } from "d3";
@@ -22,7 +22,9 @@ export class GameServer {
private turns: Turn[] = [] private turns: Turn[] = []
private intents: Intent[] = [] private intents: Intent[] = []
private clients: Client[] = [] private activeClients: Client[] = []
// Used for record record keeping
private allClients: Map<ClientID, Client> = new Map()
private _hasStarted = false private _hasStarted = false
private _startTime: number = null private _startTime: number = null
@@ -35,7 +37,7 @@ export class GameServer {
private config: Config, private config: Config,
private gameConfig: GameConfig, private gameConfig: GameConfig,
) {} ) { }
public updateGameConfig(gameConfig: GameConfig): void { public updateGameConfig(gameConfig: GameConfig): void {
if (gameConfig.gameMap != null) { if (gameConfig.gameMap != null) {
@@ -55,13 +57,16 @@ export class GameServer {
isRejoin: lastTurn > 0 isRejoin: lastTurn > 0
}) })
// Remove stale client if this is a reconnect // Remove stale client if this is a reconnect
const existing = this.clients.find(c => c.id == client.id) const existing = this.activeClients.find(c => c.id == client.id)
if (existing != null) { if (existing != null) {
existing.ws.removeAllListeners('message') existing.ws.removeAllListeners('message')
} }
this.clients = this.clients.filter(c => c.id != client.id) this.activeClients = this.activeClients.filter(c => c.id != client.id)
this.clients.push(client) this.activeClients.push(client)
client.lastPing = Date.now() client.lastPing = Date.now()
this.allClients.set(client.id, client)
client.ws.on('message', (message: string) => { client.ws.on('message', (message: string) => {
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message)) const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
if (clientMsg.type == "intent") { if (clientMsg.type == "intent") {
@@ -77,7 +82,7 @@ export class GameServer {
}) })
client.ws.on('close', () => { client.ws.on('close', () => {
console.log(`client ${client.id} disconnected`) console.log(`client ${client.id} disconnected`)
this.clients = this.clients.filter(c => c.id != client.id) this.activeClients = this.activeClients.filter(c => c.id != client.id)
}) })
// In case a client joined the game late and missed the start message. // In case a client joined the game late and missed the start message.
@@ -87,7 +92,7 @@ export class GameServer {
} }
public numClients(): number { public numClients(): number {
return this.clients.length return this.activeClients.length
} }
public startTime(): number { public startTime(): number {
@@ -104,7 +109,7 @@ export class GameServer {
this._startTime = Date.now() this._startTime = Date.now()
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs()); this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs());
this.clients.forEach(c => { this.activeClients.forEach(c => {
console.log(`game ${this.id} sending start message to ${c.id}`) console.log(`game ${this.id} sending start message to ${c.id}`)
this.sendStartGameMsg(c.ws, 0) this.sendStartGameMsg(c.ws, 0)
}) })
@@ -139,7 +144,7 @@ export class GameServer {
turn: pastTurn turn: pastTurn
} }
)) ))
this.clients.forEach(c => { this.activeClients.forEach(c => {
c.ws.send(msg) c.ws.send(msg)
}) })
} }
@@ -147,7 +152,7 @@ export class GameServer {
async endGame() { async endGame() {
// Close all WebSocket connections // Close all WebSocket connections
clearInterval(this.endTurnIntervalID); clearInterval(this.endTurnIntervalID);
this.clients.forEach(client => { this.activeClients.forEach(client => {
client.ws.removeAllListeners('message'); // TODO: remove this? client.ws.removeAllListeners('message'); // TODO: remove this?
if (client.ws.readyState === WebSocket.OPEN) { if (client.ws.readyState === WebSocket.OPEN) {
client.ws.close(1000, "game has ended"); client.ws.close(1000, "game has ended");
@@ -156,7 +161,11 @@ export class GameServer {
console.log(`ending game ${this.id} with ${this.turns.length} turns`) console.log(`ending game ${this.id} with ${this.turns.length} turns`)
try { try {
if (this.turns.length > 100) { if (this.turns.length > 100) {
const record = CreateGameRecord(this.id, this.gameConfig, this.turns, this._startTime, Date.now()) const playerRecords: PlayerRecord[] = Array.from(this.allClients.values()).map(client => ({
ip: client.ip,
clientID: client.id,
}));
const record = CreateGameRecord(this.id, this.gameConfig, playerRecords, this.turns, this._startTime, Date.now())
archive(record) archive(record)
} }
} catch (error) { } catch (error) {
@@ -167,7 +176,7 @@ export class GameServer {
phase(): GamePhase { phase(): GamePhase {
const now = Date.now() const now = Date.now()
const alive = [] const alive = []
for (const client of this.clients) { for (const client of this.activeClients) {
if (now - client.lastPing > 60_000) { if (now - client.lastPing > 60_000) {
console.log(`no pings from ${client.id}, terminating connection`) console.log(`no pings from ${client.id}, terminating connection`)
if (client.ws.readyState === WebSocket.OPEN) { if (client.ws.readyState === WebSocket.OPEN) {
@@ -177,14 +186,14 @@ export class GameServer {
alive.push(client) alive.push(client)
} }
} }
this.clients = alive this.activeClients = alive
if (now > this.createdAt + this.config.lobbyLifetime() + this.maxGameDuration) { if (now > this.createdAt + this.config.lobbyLifetime() + this.maxGameDuration) {
console.warn(`game past max duration ${this.id}`) console.warn(`game past max duration ${this.id}`)
return GamePhase.Finished return GamePhase.Finished
} }
if (!this.isPublic) { if (!this.isPublic) {
if (this._hasStarted) { if (this._hasStarted) {
if (this.clients.length == 0) { if (this.activeClients.length == 0) {
console.log(`private game: ${this.id} complete`) console.log(`private game: ${this.id} complete`)
return GamePhase.Finished return GamePhase.Finished
} else { } else {
@@ -199,7 +208,7 @@ export class GameServer {
return GamePhase.Lobby return GamePhase.Lobby
} }
if (this.clients.length == 0 && now > this.createdAt + this.config.lobbyLifetime() + 30 * 60) { // wait at least 30s before ending game if (this.activeClients.length == 0 && now > this.createdAt + this.config.lobbyLifetime() + 30 * 60) { // wait at least 30s before ending game
return GamePhase.Finished return GamePhase.Finished
} }
+11 -7
View File
@@ -43,17 +43,18 @@ app.post('/private_lobby', (req, res) => {
}); });
}); });
app.post('/archive_singleplayer_game', (req, res) => { app.post('/archive_singleplayer_game', (req, res) => {
try { try {
const gameRecord = req.body const gameRecord: GameRecord = req.body
const clientIP = req.ip || req.socket.remoteAddress || 'unknown'; // Added this line
if (!gameRecord) { if (!gameRecord) {
console.log('game record not found in request') console.log('game record not found in request')
res.status(404).json({ error: 'Game record not found' }); res.status(404).json({ error: 'Game record not found' });
return; return;
} }
gameRecord.players.forEach(p => p.ip = clientIP)
GameRecordSchema.parse(gameRecord); GameRecordSchema.parse(gameRecord);
console.log(`archiving singleplayer game ${gameRecord.id}`)
archive(gameRecord) archive(gameRecord)
res.json({ res.json({
success: true, success: true,
@@ -91,17 +92,20 @@ app.get('/private_lobby/:id', (req, res) => {
}); });
}); });
wss.on('connection', (ws) => { wss.on('connection', (ws, req) => {
ws.on('message', (message: string) => { ws.on('message', (message: string) => {
const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message)) const clientMsg: ClientMessage = ClientMessageSchema.parse(JSON.parse(message))
slog('websocket_msg', 'server received websocket message', clientMsg, LogSeverity.DEBUG) slog('websocket_msg', 'server received websocket message', clientMsg, LogSeverity.DEBUG)
if (clientMsg.type == "join") { if (clientMsg.type == "join") {
gm.addClient(new Client(clientMsg.clientID, clientMsg.clientIP, ws), clientMsg.gameID, clientMsg.lastTurn) const forwarded = req.headers['x-forwarded-for']
const ip = Array.isArray(forwarded)
? forwarded[0] // Get the first IP if it's an array
: forwarded || req.socket.remoteAddress;
gm.addClient(new Client(clientMsg.clientID, ip, ws), clientMsg.gameID, clientMsg.lastTurn)
} }
// TODO: send error message // TODO: send error message
}) })
}); });
function runGame() { function runGame() {