mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:40:44 +00:00
record games in gcs
This commit is contained in:
@@ -211,14 +211,17 @@
|
||||
* add cities DONE 12/4/2024
|
||||
* write multiplayer games to GCS DONE 12/6/2024
|
||||
* write single player games to GCS
|
||||
* bugfix: private game host game doesn't start
|
||||
* record game winner
|
||||
* record game difficulty
|
||||
* bufix: mini map doesn't load in time
|
||||
* standardize game ids
|
||||
* record commit hash of game
|
||||
* store metadata in BigQuery
|
||||
* replay stored games
|
||||
* max price for units
|
||||
* when player dies, don't remove atom bombs
|
||||
* remove alliance when player dies
|
||||
* record and replay games for debugging purposes
|
||||
* add bug report button in game
|
||||
* bugfix: destroyers can't find path to dst and freeze
|
||||
|
||||
Generated
+25
-6
@@ -28,6 +28,7 @@
|
||||
"jimp": "^0.22.12",
|
||||
"lit": "^3.2.1",
|
||||
"msgpack5": "^6.0.2",
|
||||
"nanoid": "^5.0.9",
|
||||
"node-addon-api": "^8.1.0",
|
||||
"node-gyp": "^10.2.0",
|
||||
"priority-queue-typescript": "^1.0.1",
|
||||
@@ -10719,10 +10720,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"dev": true,
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||
"integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -10731,10 +10731,10 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
@@ -11598,6 +11598,25 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postcss/node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-error": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz",
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
"jimp": "^0.22.12",
|
||||
"lit": "^3.2.1",
|
||||
"msgpack5": "^6.0.2",
|
||||
"nanoid": "^5.0.9",
|
||||
"node-addon-api": "^8.1.0",
|
||||
"node-gyp": "^10.2.0",
|
||||
"priority-queue-typescript": "^1.0.1",
|
||||
|
||||
@@ -30,8 +30,19 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
|
||||
const playerID = uuidv4()
|
||||
const eventBus = new EventBus()
|
||||
const config = getConfig()
|
||||
|
||||
let gameConfig: GameConfig = null
|
||||
if (lobbyConfig.gameType == GameType.Singleplayer) {
|
||||
gameConfig = {
|
||||
gameType: GameType.Singleplayer,
|
||||
gameMap: lobbyConfig.map,
|
||||
difficulty: lobbyConfig.difficulty,
|
||||
}
|
||||
}
|
||||
|
||||
const transport = new Transport(
|
||||
lobbyConfig.gameType == GameType.Singleplayer,
|
||||
gameConfig,
|
||||
eventBus,
|
||||
lobbyConfig.gameID,
|
||||
lobbyConfig.ip,
|
||||
@@ -49,12 +60,7 @@ export function joinLobby(lobbyConfig: LobbyConfig, onjoin: () => void): () => v
|
||||
if (message.type == "start") {
|
||||
console.log('lobby: game started')
|
||||
onjoin()
|
||||
const gameConfig = {
|
||||
gameMap: message.config?.gameMap || lobbyConfig.map,
|
||||
difficulty: message.config?.difficulty || lobbyConfig.difficulty,
|
||||
gameType: lobbyConfig.gameType
|
||||
}
|
||||
createClientGame(gameConfig, eventBus, transport, lobbyConfig.gameID, clientID).then(r => r.start())
|
||||
createClientGame(message.config, eventBus, transport, lobbyConfig.gameID, clientID).then(r => r.start())
|
||||
};
|
||||
}
|
||||
transport.connect(onconnect, onmessage)
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import {Config} from "../core/configuration/Config";
|
||||
import {ClientMessage, ClientMessageSchema, Intent, ServerMessage, ServerTurnMessageSchema, Turn} from "../core/Schemas";
|
||||
import { Config } from "../core/configuration/Config";
|
||||
import { ClientMessage, ClientMessageSchema, GameConfig, GameID, GameRecordSchema, Intent, ServerMessage, ServerStartGameMessageSchema, ServerTurnMessageSchema, Turn } from "../core/Schemas";
|
||||
import { CreateGameRecord, generateGameID } from "../core/Util";
|
||||
|
||||
export class LocalServer {
|
||||
|
||||
private gameID = "LOCAL"
|
||||
|
||||
|
||||
private turns: Turn[] = []
|
||||
private intents: Intent[] = []
|
||||
private startedAt: number
|
||||
|
||||
private endTurnIntervalID
|
||||
|
||||
constructor(private config: Config, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) {
|
||||
private gameID: GameID
|
||||
|
||||
constructor(private config: Config, private gameConfig: GameConfig, private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void) {
|
||||
this.gameID = generateGameID()
|
||||
}
|
||||
|
||||
start() {
|
||||
this.startedAt = Date.now()
|
||||
this.endTurnIntervalID = setInterval(() => this.endTurn(), this.config.turnIntervalMs());
|
||||
this.clientConnect()
|
||||
this.clientMessage({
|
||||
this.clientMessage(ServerStartGameMessageSchema.parse({
|
||||
type: "start",
|
||||
config: this.gameConfig,
|
||||
turns: [],
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
onMessage(message: string) {
|
||||
@@ -43,4 +48,15 @@ export class LocalServer {
|
||||
turn: pastTurn
|
||||
})
|
||||
}
|
||||
|
||||
public endGame() {
|
||||
console.log('local server ending game')
|
||||
clearInterval(this.endTurnIntervalID)
|
||||
const record = CreateGameRecord(this.gameID, this.gameConfig, this.turns, this.startedAt, Date.now())
|
||||
// For unload events, sendBeacon is the only reliable method
|
||||
const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], {
|
||||
type: 'application/json'
|
||||
});
|
||||
navigator.sendBeacon('/archive_singleplayer_game', blob);
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -29,11 +29,10 @@ class Client {
|
||||
if (!this.usernameInput) {
|
||||
console.warn('Username input element not found');
|
||||
}
|
||||
const s = this.stopGame
|
||||
window.addEventListener('beforeunload', function (event) {
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
console.log('Browser is closing');
|
||||
if (s != null) {
|
||||
s()
|
||||
if (this.gameStop != null) {
|
||||
this.gameStop()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ export class PublicLobby extends LitElement {
|
||||
@click=${() => this.lobbyClicked(lobby)}
|
||||
class="lobby-button ${this.isLobbyHighlighted ? 'highlighted' : ''}"
|
||||
>
|
||||
<div class="lobby-name">Game ${lobby.id.substring(0, 3)}</div>
|
||||
<div class="lobby-name">Game ${lobby.id}</div>
|
||||
<div class="lobby-timer">Starts in: ${timeRemaining}s</div>
|
||||
<div class="player-count">Players: ${lobby.numClients}</div>
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { Difficulty, GameMap, GameType } from '../core/game/Game';
|
||||
import { generateGameID as generateGameID } from '../core/Util';
|
||||
|
||||
@customElement('single-player-modal')
|
||||
export class SinglePlayerModal extends LitElement {
|
||||
@@ -127,7 +128,7 @@ export class SinglePlayerModal extends LitElement {
|
||||
detail: {
|
||||
gameType: GameType.Singleplayer,
|
||||
lobby: {
|
||||
id: "LOCAL",
|
||||
id: generateGameID(),
|
||||
},
|
||||
map: this.selectedMap,
|
||||
difficulty: this.selectedDifficulty
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema } from "../core/Schemas"
|
||||
import { ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ClientPingMessageSchema, GameConfig } from "../core/Schemas"
|
||||
import { LocalServer } from "./LocalServer"
|
||||
|
||||
|
||||
@@ -99,6 +99,7 @@ export class Transport {
|
||||
|
||||
constructor(
|
||||
private isLocal: boolean,
|
||||
private gameConfig: GameConfig | null,
|
||||
private eventBus: EventBus,
|
||||
private gameID: GameID,
|
||||
private clientIP: string | null,
|
||||
@@ -150,7 +151,7 @@ export class Transport {
|
||||
}
|
||||
|
||||
private connectLocal(onconnect: () => void, onmessage: (message: ServerMessage) => void) {
|
||||
this.localServer = new LocalServer(this.config, onconnect, onmessage)
|
||||
this.localServer = new LocalServer(this.config, this.gameConfig, onconnect, onmessage)
|
||||
this.localServer.start()
|
||||
}
|
||||
|
||||
@@ -208,6 +209,7 @@ export class Transport {
|
||||
|
||||
leaveGame() {
|
||||
if (this.isLocal) {
|
||||
this.localServer.endGame()
|
||||
return
|
||||
}
|
||||
this.stopPing()
|
||||
|
||||
+20
-13
@@ -5,7 +5,8 @@ import DOMPurify from 'dompurify';
|
||||
|
||||
import { Cell, Game, Player, TerraNullius, Tile, Unit } from "./game/Game";
|
||||
import { number } from 'zod';
|
||||
import { GameRecord } from './Schemas';
|
||||
import { GameConfig, GameID, GameRecord, Turn } from './Schemas';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export function manhattanDist(c1: Cell, c2: Cell): number {
|
||||
return Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y);
|
||||
@@ -223,13 +224,19 @@ export function onlyImages(html: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function ProcessGameRecord(record: GameRecord): GameRecord {
|
||||
const packed: GameRecord = structuredClone(record);
|
||||
packed.turns = []
|
||||
export function CreateGameRecord(id: GameID, gameConfig: GameConfig, turns: Turn[], start: number, end: number): GameRecord {
|
||||
const record: GameRecord = {
|
||||
id: id,
|
||||
gameConfig: gameConfig,
|
||||
startTimestampMS: start,
|
||||
endTimestampMS: end,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
turns: []
|
||||
}
|
||||
const usernames = new Set<string>()
|
||||
for (const turn of record.turns) {
|
||||
for (const turn of turns) {
|
||||
if (turn.intents.length != 0) {
|
||||
packed.turns.push(turn)
|
||||
record.turns.push(turn)
|
||||
for (const intent of turn.intents) {
|
||||
if (intent.type == 'spawn') {
|
||||
usernames.add(intent.name)
|
||||
@@ -237,15 +244,15 @@ export function ProcessGameRecord(record: GameRecord): GameRecord {
|
||||
}
|
||||
}
|
||||
}
|
||||
packed.usernames = Array.from(usernames)
|
||||
packed.durationSeconds = Math.floor((record.endTimestampMS - record.startTimestampMS) / 1000)
|
||||
return packed;
|
||||
}
|
||||
|
||||
export function ToBigQuery(record: GameRecord) {
|
||||
|
||||
record.usernames = Array.from(usernames)
|
||||
record.durationSeconds = Math.floor((record.endTimestampMS - record.startTimestampMS) / 1000)
|
||||
return record;
|
||||
}
|
||||
|
||||
export function assertNever(x: never): never {
|
||||
throw new Error('Unexpected value: ' + x);
|
||||
}
|
||||
|
||||
export function generateGameID(): string {
|
||||
return nanoid(8)
|
||||
}
|
||||
@@ -65,7 +65,6 @@ export interface Config {
|
||||
defensePostDefenseBonus(): number
|
||||
falloutDefenseModifier(): number
|
||||
maxUnitCost(): number
|
||||
gameStorageBucketName(): string
|
||||
}
|
||||
|
||||
export interface Theme {
|
||||
|
||||
@@ -20,9 +20,6 @@ export class DefaultConfig implements Config {
|
||||
return 2
|
||||
}
|
||||
|
||||
gameStorageBucketName(): string {
|
||||
return "openfront-games"
|
||||
}
|
||||
defensePostRange(): number {
|
||||
return 30
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { GameConfig, GameID, GameRecord, GameRecordSchema, Turn } from "../core/Schemas";
|
||||
import { Storage } from '@google-cloud/storage';
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
export async function archive(gameRecord: GameRecord) {
|
||||
console.log(`writing game ${gameRecord.id} to gcs`)
|
||||
const bucket = storage.bucket("openfront-games");
|
||||
const file = bucket.file(gameRecord.id);
|
||||
await file.save(JSON.stringify(GameRecordSchema.parse(gameRecord)), {
|
||||
contentType: 'application/json'
|
||||
});
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { Client } from "./Client";
|
||||
import { GamePhase, GameServer } from "./GameServer";
|
||||
import { Difficulty, GameMap, GameType } from "../core/game/Game";
|
||||
import { generateGameID } from "../core/Util";
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +39,7 @@ export class GameManager {
|
||||
}
|
||||
|
||||
createPrivateGame(): string {
|
||||
const id = genSmallGameID()
|
||||
const id = generateGameID()
|
||||
this.games.push(new GameServer(
|
||||
id,
|
||||
Date.now(),
|
||||
@@ -77,9 +78,8 @@ export class GameManager {
|
||||
const now = Date.now()
|
||||
if (now > this.lastNewLobby + this.config.gameCreationRate()) {
|
||||
this.lastNewLobby = now
|
||||
const id = uuidv4()
|
||||
lobbies.push(new GameServer(
|
||||
id,
|
||||
generateGameID(),
|
||||
now,
|
||||
true,
|
||||
this.config,
|
||||
@@ -97,15 +97,4 @@ export class GameManager {
|
||||
finished.map(g => g.endGame()); // Fire and forget
|
||||
this.games = [...lobbies, ...active]
|
||||
}
|
||||
}
|
||||
|
||||
function genSmallGameID(): string {
|
||||
// Generate a UUID
|
||||
const uuid: string = uuidv4();
|
||||
|
||||
// Convert UUID to base64
|
||||
const base64: string = btoa(uuid);
|
||||
|
||||
// Take the first 4 characters of the base64 string
|
||||
return base64.slice(0, 4);
|
||||
}
|
||||
+11
-18
@@ -4,9 +4,10 @@ import { Client } from "./Client";
|
||||
import WebSocket from 'ws';
|
||||
import { slog } from "./StructuredLog";
|
||||
import { Storage } from '@google-cloud/storage';
|
||||
import { ProcessGameRecord as ProcessRecord } from "../core/Util";
|
||||
import { CreateGameRecord, CreateGameRecord as ProcessRecord } from "../core/Util";
|
||||
import { archive } from "./Archive";
|
||||
import { arc } from "d3";
|
||||
|
||||
const storage = new Storage();
|
||||
|
||||
export enum GamePhase {
|
||||
Lobby = 'LOBBY',
|
||||
@@ -90,7 +91,12 @@ export class GameServer {
|
||||
}
|
||||
|
||||
public startTime(): number {
|
||||
return this._startTime
|
||||
if (this._startTime > 0) {
|
||||
return this._startTime
|
||||
} else {
|
||||
//game hasn't started yet, only works for public games
|
||||
return this.createdAt + this.config.lobbyLifetime()
|
||||
}
|
||||
}
|
||||
|
||||
public start() {
|
||||
@@ -150,21 +156,8 @@ export class GameServer {
|
||||
console.log(`ending game ${this.id} with ${this.turns.length} turns`)
|
||||
try {
|
||||
if (this.turns.length > 100) {
|
||||
console.log(`writing game ${this.id} to gcs`)
|
||||
const bucket = storage.bucket(this.config.gameStorageBucketName());
|
||||
const file = bucket.file(this.id);
|
||||
const game = {
|
||||
id: this.id,
|
||||
gameConfig: this.gameConfig,
|
||||
startTimestampMS: this._startTime,
|
||||
endTimestampMS: Date.now(),
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
turns: this.turns
|
||||
}
|
||||
const processed = ProcessRecord(game)
|
||||
await file.save(JSON.stringify(GameRecordSchema.parse(processed)), {
|
||||
contentType: 'application/json'
|
||||
});
|
||||
const record = CreateGameRecord(this.id, this.gameConfig, this.turns, this._startTime, Date.now())
|
||||
archive(record)
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('error writing game to gcs: ' + error)
|
||||
|
||||
+8
-54
@@ -9,9 +9,7 @@ import { getConfig } from '../core/configuration/Config';
|
||||
import { LogSeverity, slog } from './StructuredLog';
|
||||
import { Client } from './Client';
|
||||
import { GamePhase, GameServer } from './GameServer';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { z } from 'zod';
|
||||
import { ProcessGameRecord } from '../core/Util';
|
||||
import { archive } from './Archive';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@@ -25,7 +23,6 @@ app.use(express.static(path.join(__dirname, '../../out')));
|
||||
app.use(express.json())
|
||||
|
||||
const gm = new GameManager(getConfig())
|
||||
const privateGames = new Map<string, GameRecord>()
|
||||
|
||||
// New GET endpoint to list lobbies
|
||||
app.get('/lobbies', (req, res) => {
|
||||
@@ -46,66 +43,23 @@ app.post('/private_lobby', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/new_private_game_record', (req, res) => {
|
||||
|
||||
app.post('/archive_singleplayer_game', (req, res) => {
|
||||
try {
|
||||
// Validate the complete game record sent by client
|
||||
const gameRecord = GameRecordSchema.parse(req.body);
|
||||
privateGames.set(gameRecord.id, gameRecord);
|
||||
|
||||
slog('new_private_game_record', 'Created new private game record', { id: gameRecord.id }, LogSeverity.DEBUG);
|
||||
res.json({ id: gameRecord.id });
|
||||
} catch (error) {
|
||||
slog('new_private_game_record', 'Failed to create new private game record', { error }, LogSeverity.ERROR);
|
||||
res.status(400).json({ error: 'Invalid game record format' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/add_single_player_game_turn', (req, res) => {
|
||||
const { gameId, turns } = req.body;
|
||||
|
||||
try {
|
||||
const gameRecord = privateGames.get(gameId);
|
||||
const gameRecord = req.body
|
||||
if (!gameRecord) {
|
||||
console.log('game record not found in request')
|
||||
res.status(404).json({ error: 'Game record not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the array of turns
|
||||
const validatedTurns = z.array(TurnSchema).parse(turns);
|
||||
|
||||
// Add the turns to the game record's turns
|
||||
gameRecord.turns.push(...validatedTurns);
|
||||
privateGames.set(gameId, gameRecord);
|
||||
|
||||
res.json({ success: true, numTurns: validatedTurns.length });
|
||||
} catch (error) {
|
||||
slog('add_single_player_game_turn', 'Failed to add turns', { error, gameId }, LogSeverity.ERROR);
|
||||
res.status(400).json({ error: 'Invalid turns format' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/complete_single_player_game_record/:id', (req, res) => {
|
||||
const gameId = req.params.id;
|
||||
try {
|
||||
let gameRecord = privateGames.get(gameId);
|
||||
if (!gameRecord) {
|
||||
res.status(404).json({ error: 'Game record not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
gameRecord.endTimestampMS = Date.now();
|
||||
|
||||
gameRecord = ProcessGameRecord(gameRecord)
|
||||
// TODO: send to gcs
|
||||
|
||||
GameRecordSchema.parse(gameRecord);
|
||||
|
||||
console.log(`archiving singleplayer game ${gameRecord.id}`)
|
||||
archive(gameRecord)
|
||||
res.json({
|
||||
success: true,
|
||||
durationSeconds: gameRecord.durationSeconds
|
||||
});
|
||||
} catch (error) {
|
||||
slog('complete_single_player_game_record', 'Failed to complete game record', { error, gameId }, LogSeverity.ERROR);
|
||||
slog('complete_single_player_game_record', 'Failed to complete game record', { error }, LogSeverity.ERROR);
|
||||
res.status(400).json({ error: 'Invalid game record format' });
|
||||
}
|
||||
})
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import {fileURLToPath} from 'url';
|
||||
import { fileURLToPath } from 'url';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import webpack from 'webpack';
|
||||
|
||||
@@ -95,7 +95,7 @@ export default (env, argv) => {
|
||||
ws: true,
|
||||
},
|
||||
{
|
||||
context: ['/lobbies', '/join_game', '/join_lobby', '/private_lobby', '/start_private_lobby', '/lobby'],
|
||||
context: ['/lobbies', '/join_game', '/join_lobby', '/private_lobby', '/start_private_lobby', '/lobby', '/archive_singleplayer_game'],
|
||||
target: 'http://localhost:3000',
|
||||
secure: false,
|
||||
changeOrigin: true,
|
||||
|
||||
Reference in New Issue
Block a user