mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-03 12:40:47 +00:00
store user's persistent id in bigquery
This commit is contained in:
+11
-16
@@ -12,21 +12,20 @@ import { WinCheckExecution } from "../core/execution/WinCheckExecution";
|
||||
import { SendAttackIntentEvent, SendSpawnIntentEvent, Transport } from "./Transport";
|
||||
import { createCanvas } from "./Utils";
|
||||
import { DisplayMessageEvent, MessageType } from "./graphics/layers/EventsDisplay";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { WorkerClient } from "../core/worker/WorkerClient";
|
||||
|
||||
|
||||
export interface LobbyConfig {
|
||||
gameType: GameType
|
||||
playerName: () => string
|
||||
gameID: GameID
|
||||
clientID: ClientID,
|
||||
playerID: PlayerID,
|
||||
persistentID: string,
|
||||
gameType: GameType
|
||||
gameID: GameID,
|
||||
map: GameMap | null
|
||||
difficulty: Difficulty | null
|
||||
}
|
||||
|
||||
export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => void {
|
||||
const clientID = generateID()
|
||||
const playerID = generateID()
|
||||
const eventBus = new EventBus()
|
||||
const config = getConfig()
|
||||
|
||||
@@ -40,14 +39,10 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
|
||||
}
|
||||
|
||||
const transport = new Transport(
|
||||
lobbyConfig.gameType == GameType.Singleplayer,
|
||||
lobbyConfig,
|
||||
gameConfig,
|
||||
eventBus,
|
||||
lobbyConfig.gameID,
|
||||
clientID,
|
||||
playerID,
|
||||
config,
|
||||
lobbyConfig.playerName
|
||||
)
|
||||
|
||||
const onconnect = () => {
|
||||
@@ -58,7 +53,7 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
|
||||
if (message.type == "start") {
|
||||
console.log('lobby: game started')
|
||||
onjoin()
|
||||
createClientGame(message.config, eventBus, transport, lobbyConfig.gameID, clientID).then(r => r.start())
|
||||
createClientGame(lobbyConfig, message.config, eventBus, transport).then(r => r.start())
|
||||
};
|
||||
}
|
||||
transport.connect(onconnect, onmessage)
|
||||
@@ -69,7 +64,7 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
|
||||
}
|
||||
|
||||
|
||||
export async function createClientGame(gameConfig: GameConfig, eventBus: EventBus, transport: Transport, gameID: GameID, clientID: ClientID): Promise<GameRunner> {
|
||||
export async function createClientGame(lobbyConfig: LobbyConfig, gameConfig: GameConfig, eventBus: EventBus, transport: Transport): Promise<GameRunner> {
|
||||
const config = getConfig()
|
||||
|
||||
const terrainMap = await loadTerrainMap(gameConfig.gameMap);
|
||||
@@ -82,18 +77,18 @@ export async function createClientGame(gameConfig: GameConfig, eventBus: EventBu
|
||||
await worker.initialize()
|
||||
console.log('inited path finder')
|
||||
const canvas = createCanvas()
|
||||
let gameRenderer = createRenderer(canvas, game, eventBus, clientID)
|
||||
let gameRenderer = createRenderer(canvas, game, eventBus, lobbyConfig.clientID)
|
||||
|
||||
|
||||
console.log(`creating private game got difficulty: ${gameConfig.difficulty}`)
|
||||
|
||||
return new GameRunner(
|
||||
clientID,
|
||||
lobbyConfig.clientID,
|
||||
eventBus,
|
||||
game,
|
||||
gameRenderer,
|
||||
new InputHandler(canvas, eventBus),
|
||||
new Executor(game, gameID, worker),
|
||||
new Executor(game, lobbyConfig.gameID, worker),
|
||||
transport,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Config } from "../core/configuration/Config";
|
||||
import { ClientID, ClientMessage, ClientMessageSchema, GameConfig, GameID, GameRecordSchema, Intent, PlayerRecord, ServerMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
|
||||
import { CreateGameRecord, generateID } from "../core/Util";
|
||||
import { LobbyConfig } from "./GameRunner";
|
||||
import { getPersistentIDFromCookie } from "./Main";
|
||||
|
||||
export class LocalServer {
|
||||
|
||||
@@ -11,10 +13,14 @@ export class LocalServer {
|
||||
|
||||
private endTurnIntervalID
|
||||
|
||||
private gameID: GameID
|
||||
|
||||
constructor(private clientID: ClientID, private config: Config, private gameConfig: GameConfig, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) {
|
||||
this.gameID = generateID()
|
||||
constructor(
|
||||
private config: Config,
|
||||
private gameConfig: GameConfig,
|
||||
private lobbyConfig: LobbyConfig,
|
||||
private clientConnect: () => void,
|
||||
private clientMessage: (message: ServerMessage) => void
|
||||
) {
|
||||
}
|
||||
|
||||
start() {
|
||||
@@ -38,7 +44,7 @@ export class LocalServer {
|
||||
private endTurn() {
|
||||
const pastTurn: Turn = {
|
||||
turnNumber: this.turns.length,
|
||||
gameID: this.gameID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
intents: this.intents
|
||||
}
|
||||
this.turns.push(pastTurn)
|
||||
@@ -54,9 +60,18 @@ export class LocalServer {
|
||||
clearInterval(this.endTurnIntervalID)
|
||||
const players: PlayerRecord[] = [{
|
||||
ip: null,
|
||||
clientID: this.clientID
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
username: this.lobbyConfig.playerName(),
|
||||
clientID: this.lobbyConfig.clientID
|
||||
}]
|
||||
const record = CreateGameRecord(this.gameID, this.gameConfig, players, this.turns, this.startedAt, Date.now())
|
||||
const record = CreateGameRecord(
|
||||
this.lobbyConfig.gameID,
|
||||
this.gameConfig,
|
||||
players,
|
||||
this.turns,
|
||||
this.startedAt,
|
||||
Date.now()
|
||||
)
|
||||
// Clear turns because beacon only supports up to 64kb
|
||||
record.turns = []
|
||||
// For unload events, sendBeacon is the only reliable method
|
||||
|
||||
+31
-1
@@ -9,6 +9,7 @@ import { UsernameInput } from "./UsernameInput";
|
||||
import { SinglePlayerModal } from "./SinglePlayerModal";
|
||||
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
|
||||
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
|
||||
import { generateID } from "../core/Util";
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +72,9 @@ class Client {
|
||||
gameType: event.detail.gameType,
|
||||
playerName: (): string => this.usernameInput.getCurrentUsername(),
|
||||
gameID: lobby.id,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
playerID: generateID(),
|
||||
clientID: generateID(),
|
||||
map: event.detail.map,
|
||||
difficulty: event.detail.difficulty,
|
||||
},
|
||||
@@ -105,4 +109,30 @@ function setFavicon(): void {
|
||||
link.rel = 'shortcut icon';
|
||||
link.href = favicon;
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
// WARNING: DO NOT EXPOSE THIS ID
|
||||
export function getPersistentIDFromCookie(): string {
|
||||
const COOKIE_NAME = 'player_persistent_id'
|
||||
|
||||
// Try to get existing cookie
|
||||
const cookies = document.cookie.split(';')
|
||||
for (let cookie of cookies) {
|
||||
const [cookieName, cookieValue] = cookie.split('=').map(c => c.trim())
|
||||
if (cookieName === COOKIE_NAME) {
|
||||
return cookieValue
|
||||
}
|
||||
}
|
||||
|
||||
// If no cookie exists, create new ID and set cookie
|
||||
const newId = crypto.randomUUID() // Using built-in UUID generator
|
||||
document.cookie = [
|
||||
`${COOKIE_NAME}=${newId}`,
|
||||
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
|
||||
'path=/',
|
||||
'SameSite=Strict',
|
||||
'Secure'
|
||||
].join(';')
|
||||
|
||||
return newId
|
||||
}
|
||||
|
||||
+35
-35
@@ -1,9 +1,9 @@
|
||||
import { Config } from "../core/configuration/Config"
|
||||
import { EventBus, GameEvent } from "../core/EventBus"
|
||||
import { AllianceRequest, AllPlayers, Cell, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game"
|
||||
import { AllianceRequest, AllPlayers, Cell, GameType, Player, PlayerID, PlayerType, Tile, UnitType } from "../core/game/Game"
|
||||
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig } from "../core/Schemas"
|
||||
import { LobbyConfig } from "./GameRunner"
|
||||
import { LocalServer } from "./LocalServer"
|
||||
import { getPersistentIDFromCookie } from "./Utils"
|
||||
|
||||
|
||||
export class SendAllianceRequestIntentEvent implements GameEvent {
|
||||
@@ -96,18 +96,17 @@ export class Transport {
|
||||
|
||||
|
||||
private pingInterval: number | null = null
|
||||
private lastPingTime: number | null = null
|
||||
private isLocal: boolean
|
||||
|
||||
constructor(
|
||||
private isLocal: boolean,
|
||||
private lobbyConfig: LobbyConfig,
|
||||
// gameConfig only set on private games
|
||||
private gameConfig: GameConfig | null,
|
||||
private eventBus: EventBus,
|
||||
private gameID: GameID,
|
||||
private clientID: ClientID,
|
||||
private playerID: PlayerID,
|
||||
private config: Config,
|
||||
private playerName: () => string,
|
||||
) {
|
||||
this.isLocal = lobbyConfig.gameType == GameType.Singleplayer
|
||||
|
||||
this.eventBus.on(SendAllianceRequestIntentEvent, (e) => this.onSendAllianceRequest(e))
|
||||
this.eventBus.on(SendAllianceReplyIntentEvent, (e) => this.onAllianceRequestReplyUIEvent(e))
|
||||
this.eventBus.on(SendBreakAllianceIntentEvent, (e) => this.onBreakAllianceRequestUIEvent(e))
|
||||
@@ -128,8 +127,8 @@ export class Transport {
|
||||
if (this.socket != null && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.sendMsg(JSON.stringify(ClientPingMessageSchema.parse({
|
||||
type: 'ping',
|
||||
clientID: this.clientID,
|
||||
gameID: this.gameID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
})))
|
||||
}
|
||||
}, 5 * 1000);
|
||||
@@ -152,7 +151,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void) {
|
||||
this.localServer = new LocalServer(this.clientID, this.config, this.gameConfig, onconnect, onmessage)
|
||||
this.localServer = new LocalServer(this.config, this.gameConfig, this.lobbyConfig, onconnect, onmessage)
|
||||
this.localServer.start()
|
||||
}
|
||||
|
||||
@@ -193,10 +192,11 @@ export class Transport {
|
||||
JSON.stringify(
|
||||
ClientJoinMessageSchema.parse({
|
||||
type: "join",
|
||||
gameID: this.gameID,
|
||||
clientID: this.clientID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
lastTurn: numTurns,
|
||||
persistentID: getPersistentIDFromCookie(),
|
||||
persistentID: this.lobbyConfig.persistentID,
|
||||
username: this.lobbyConfig.playerName()
|
||||
})
|
||||
)
|
||||
)
|
||||
@@ -221,7 +221,7 @@ export class Transport {
|
||||
private onSendAllianceRequest(event: SendAllianceRequestIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "allianceRequest",
|
||||
clientID: this.clientID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.requestor.id(),
|
||||
recipient: event.recipient.id(),
|
||||
})
|
||||
@@ -230,7 +230,7 @@ export class Transport {
|
||||
private onAllianceRequestReplyUIEvent(event: SendAllianceReplyIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "allianceRequestReply",
|
||||
clientID: this.clientID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.allianceRequest.requestor().id(),
|
||||
recipient: event.allianceRequest.recipient().id(),
|
||||
accept: event.accepted,
|
||||
@@ -240,7 +240,7 @@ export class Transport {
|
||||
private onBreakAllianceRequestUIEvent(event: SendBreakAllianceIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "breakAlliance",
|
||||
clientID: this.clientID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: event.requestor.id(),
|
||||
recipient: event.recipient.id(),
|
||||
})
|
||||
@@ -249,9 +249,9 @@ export class Transport {
|
||||
private onSendSpawnIntentEvent(event: SendSpawnIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "spawn",
|
||||
clientID: this.clientID,
|
||||
playerID: this.playerID,
|
||||
name: this.playerName(),
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
playerID: this.lobbyConfig.playerID,
|
||||
name: this.lobbyConfig.playerName(),
|
||||
playerType: PlayerType.Human,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y
|
||||
@@ -261,8 +261,8 @@ export class Transport {
|
||||
private onSendAttackIntent(event: SendAttackIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "attack",
|
||||
clientID: this.clientID,
|
||||
attackerID: this.playerID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
attackerID: this.lobbyConfig.playerID,
|
||||
targetID: event.targetID,
|
||||
troops: event.troops,
|
||||
sourceX: null,
|
||||
@@ -275,8 +275,8 @@ export class Transport {
|
||||
private onSendBoatAttackIntent(event: SendBoatAttackIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "boat",
|
||||
clientID: this.clientID,
|
||||
attackerID: this.playerID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
attackerID: this.lobbyConfig.playerID,
|
||||
targetID: event.targetID,
|
||||
troops: event.troops,
|
||||
x: event.cell.x,
|
||||
@@ -287,8 +287,8 @@ export class Transport {
|
||||
private onSendTargetPlayerIntent(event: SendTargetPlayerIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "targetPlayer",
|
||||
clientID: this.clientID,
|
||||
requestor: this.playerID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
requestor: this.lobbyConfig.playerID,
|
||||
target: event.targetID,
|
||||
})
|
||||
}
|
||||
@@ -296,8 +296,8 @@ export class Transport {
|
||||
private onSendEmojiIntent(event: SendEmojiIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "emoji",
|
||||
clientID: this.clientID,
|
||||
sender: this.playerID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
sender: this.lobbyConfig.playerID,
|
||||
recipient: event.recipient == AllPlayers ? AllPlayers : event.recipient.id(),
|
||||
emoji: event.emoji
|
||||
})
|
||||
@@ -306,7 +306,7 @@ export class Transport {
|
||||
private onSendDonateIntent(event: SendDonateIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "donate",
|
||||
clientID: this.clientID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
sender: event.sender.id(),
|
||||
recipient: event.recipient.id(),
|
||||
troops: event.troops,
|
||||
@@ -316,8 +316,8 @@ export class Transport {
|
||||
private onSendSetTargetTroopRatioEvent(event: SendSetTargetTroopRatioEvent) {
|
||||
this.sendIntent({
|
||||
type: "troop_ratio",
|
||||
clientID: this.clientID,
|
||||
player: this.playerID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
player: this.lobbyConfig.playerID,
|
||||
ratio: event.ratio,
|
||||
})
|
||||
}
|
||||
@@ -325,8 +325,8 @@ export class Transport {
|
||||
private onBuildUnitIntent(event: BuildUnitIntentEvent) {
|
||||
this.sendIntent({
|
||||
type: "build_unit",
|
||||
clientID: this.clientID,
|
||||
player: this.playerID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
player: this.lobbyConfig.playerID,
|
||||
unit: event.unit,
|
||||
x: event.cell.x,
|
||||
y: event.cell.y,
|
||||
@@ -337,8 +337,8 @@ export class Transport {
|
||||
if (this.isLocal || this.socket.readyState === WebSocket.OPEN) {
|
||||
const msg = ClientIntentMessageSchema.parse({
|
||||
type: "intent",
|
||||
clientID: this.clientID,
|
||||
gameID: this.gameID,
|
||||
clientID: this.lobbyConfig.clientID,
|
||||
gameID: this.lobbyConfig.gameID,
|
||||
intent: intent
|
||||
})
|
||||
this.sendMsg(JSON.stringify(msg))
|
||||
|
||||
@@ -35,28 +35,3 @@ export function createCanvas(): HTMLCanvasElement {
|
||||
return canvas
|
||||
}
|
||||
|
||||
// WARNING: DO NOT EXPOSE THIS ID
|
||||
export function getPersistentIDFromCookie(): string {
|
||||
const COOKIE_NAME = 'player_persistent_id';
|
||||
|
||||
// Try to get existing cookie
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let cookie of cookies) {
|
||||
const [cookieName, cookieValue] = cookie.split('=').map(c => c.trim());
|
||||
if (cookieName === COOKIE_NAME) {
|
||||
return cookieValue;
|
||||
}
|
||||
}
|
||||
|
||||
// If no cookie exists, create new ID and set cookie
|
||||
const newId = crypto.randomUUID(); // Using built-in UUID generator
|
||||
document.cookie = [
|
||||
`${COOKIE_NAME}=${newId}`,
|
||||
`max-age=${5 * 365 * 24 * 60 * 60}`, // 5 years
|
||||
'path=/',
|
||||
'SameSite=Strict',
|
||||
'Secure'
|
||||
].join(';');
|
||||
|
||||
return newId;
|
||||
}
|
||||
+7
-5
@@ -222,10 +222,12 @@ export const ClientIntentMessageSchema = ClientBaseMessageSchema.extend({
|
||||
intent: IntentSchema
|
||||
})
|
||||
|
||||
// WARNING: never send this message to clients.
|
||||
export const ClientJoinMessageSchema = ClientBaseMessageSchema.extend({
|
||||
type: z.literal('join'),
|
||||
persistentID: z.string(),
|
||||
lastTurn: z.number() // The last turn the client saw.
|
||||
persistentID: z.string(), // WARNING: persistent id is private.
|
||||
lastTurn: z.number(), // The last turn the client saw.
|
||||
username: z.string(),
|
||||
})
|
||||
|
||||
export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientIntentMessageSchema, ClientJoinMessageSchema]);
|
||||
@@ -233,12 +235,12 @@ export const ClientMessageSchema = z.union([ClientPingMessageSchema, ClientInten
|
||||
export const PlayerRecordSchema = z.object({
|
||||
clientID: z.string(),
|
||||
username: z.string(),
|
||||
ip: z.string().nullable(),
|
||||
ip: z.string().nullable(), // WARNING: PII
|
||||
persistentID: z.string(), // WARNING: PII
|
||||
})
|
||||
|
||||
export const GameRecordSchema = z.object({
|
||||
id: z.string(), // WARNING: PII
|
||||
persistentID: z.string(), // WARNING: PII
|
||||
id: z.string(),
|
||||
gameConfig: GameConfigSchema,
|
||||
players: z.array(PlayerRecordSchema),
|
||||
startTimestampMS: z.number(),
|
||||
|
||||
@@ -22,6 +22,8 @@ export async function archive(gameRecord: GameRecord) {
|
||||
players: gameRecord.players.map(p => ({
|
||||
username: p.username,
|
||||
ip: p.ip,
|
||||
persistentID: p.persistentID,
|
||||
clientID: p.clientID,
|
||||
})),
|
||||
};
|
||||
|
||||
@@ -32,8 +34,11 @@ export async function archive(gameRecord: GameRecord) {
|
||||
|
||||
console.log(`wrote game metadata to BigQuery: ${gameRecord.id}`);
|
||||
if (gameRecord.turns.length > 0) {
|
||||
// Players will see this so make sure to clear PII.
|
||||
gameRecord.players.forEach(p => p.ip = "REDACTED")
|
||||
// Players may see this so make sure to clear PII.
|
||||
gameRecord.players.forEach(p => {
|
||||
p.ip = "REDACTED"
|
||||
p.persistentID = "REDACTED"
|
||||
})
|
||||
console.log(`writing game ${gameRecord.id} to gcs`)
|
||||
const bucket = storage.bucket("openfront-games");
|
||||
const file = bucket.file(gameRecord.id);
|
||||
|
||||
@@ -10,6 +10,7 @@ export class Client {
|
||||
public readonly clientID: ClientID,
|
||||
public readonly persistentID: string,
|
||||
public readonly ip: string | null,
|
||||
public readonly username: string,
|
||||
public readonly ws: WebSocket,
|
||||
) { }
|
||||
}
|
||||
@@ -166,9 +166,19 @@ export class GameServer {
|
||||
const playerRecords: PlayerRecord[] = Array.from(this.allClients.values()).map(client => ({
|
||||
ip: client.ip,
|
||||
clientID: client.clientID,
|
||||
username: client.username,
|
||||
persistentID: client.persistentID,
|
||||
}));
|
||||
const record = CreateGameRecord(this.id, this.gameConfig, playerRecords, this.turns, this._startTime, Date.now())
|
||||
archive(record)
|
||||
archive(
|
||||
CreateGameRecord(
|
||||
this.id,
|
||||
this.gameConfig,
|
||||
playerRecords,
|
||||
this.turns,
|
||||
this._startTime,
|
||||
Date.now()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
console.log(`${this.id}: no clients joined, not archiving game`)
|
||||
}
|
||||
|
||||
@@ -115,6 +115,7 @@ wss.on('connection', (ws, req) => {
|
||||
clientMsg.clientID,
|
||||
clientMsg.persistentID,
|
||||
ip,
|
||||
clientMsg.username,
|
||||
ws
|
||||
),
|
||||
clientMsg.gameID,
|
||||
|
||||
Reference in New Issue
Block a user