mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-17 05:34:37 +00:00
store client ips in bigquery table
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user