mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-08-08 03:29:44 +00:00
merge branch v0.17.3
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
build/
|
build/
|
||||||
node_modules/
|
node_modules/
|
||||||
out/
|
out/
|
||||||
|
static/
|
||||||
TODO.txt
|
TODO.txt
|
||||||
resources/images/.DS_Store
|
resources/images/.DS_Store
|
||||||
resources/.DS_Store
|
resources/.DS_Store
|
||||||
|
|||||||
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
[submodule "src/server/gatekeeper"]
|
[submodule "gatekeeper"]
|
||||||
path = src/server/gatekeeper
|
path = gatekeeper
|
||||||
url = https://github.com/openfrontio/gatekeeper.git
|
url = git@github.com:openfrontio/gatekeeper.git
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
npx lint-staged
|
# npx lint-staged
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
FROM node:18
|
FROM node:18
|
||||||
|
|
||||||
# Add environment variable
|
# Add environment variable
|
||||||
ARG GAME_ENV=preprod
|
ARG GAME_ENV=prod
|
||||||
ENV GAME_ENV=$GAME_ENV
|
ENV GAME_ENV=$GAME_ENV
|
||||||
|
|
||||||
# Install Nginx, Supervisor and Git (for Husky)
|
# Install Nginx, Supervisor and Git (for Husky)
|
||||||
|
|||||||
+150
@@ -30,6 +30,11 @@ map $uri $uri_path {
|
|||||||
default $uri;
|
default $uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Cache configuration
|
||||||
|
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:10m inactive=24h max_size=1g;
|
||||||
|
# API cache for frequently requested endpoints
|
||||||
|
proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=API_CACHE:10m inactive=60m max_size=100m;
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80 default_server;
|
listen 80 default_server;
|
||||||
|
|
||||||
@@ -37,6 +42,151 @@ server {
|
|||||||
access_log /var/log/nginx/access.log;
|
access_log /var/log/nginx/access.log;
|
||||||
error_log /var/log/nginx/error.log;
|
error_log /var/log/nginx/error.log;
|
||||||
|
|
||||||
|
# Static file handling with proper MIME types and consistent caching
|
||||||
|
location ~* ^/static/(.*)$ {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
|
||||||
|
# Include MIME types
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
|
||||||
|
# Cache configuration for static files
|
||||||
|
proxy_cache STATIC;
|
||||||
|
proxy_cache_valid 200 302 24h; # Cache successful responses for 24 hours
|
||||||
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
|
||||||
|
# Show cache status in response headers
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
|
||||||
|
# Standard proxy headers
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# Default cache policy for static files
|
||||||
|
add_header Cache-Control "public, max-age=86400"; # 24 hours
|
||||||
|
}
|
||||||
|
|
||||||
|
# /api/public_lobbies endpoint - Cache for 1 second to handle high request volume
|
||||||
|
location = /api/public_lobbies {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Cache configuration
|
||||||
|
proxy_cache API_CACHE;
|
||||||
|
proxy_cache_valid 200 1s; # Cache successful responses for 1 second
|
||||||
|
proxy_cache_use_stale updating error timeout http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
|
||||||
|
# Standard proxy headers
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# /api/env endpoint - Cache for 1 hour
|
||||||
|
location = /api/env {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
# Cache configuration
|
||||||
|
proxy_cache API_CACHE;
|
||||||
|
proxy_cache_valid 200 1h; # Cache successful responses for 1 hour
|
||||||
|
proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
|
||||||
|
# Standard proxy headers
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Binary files caching
|
||||||
|
location ~* \.(bin|dat|exe|dll|so|dylib)$ {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable"; # 1 year for binary files
|
||||||
|
|
||||||
|
proxy_cache STATIC;
|
||||||
|
proxy_cache_valid 200 302 24h;
|
||||||
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Specific file type caching rules (outside the /static/ location)
|
||||||
|
location ~* \.js$ {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
add_header Content-Type application/javascript;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable"; # 1 year for JS files
|
||||||
|
|
||||||
|
proxy_cache STATIC;
|
||||||
|
proxy_cache_valid 200 302 24h;
|
||||||
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.css$ {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
add_header Content-Type text/css;
|
||||||
|
add_header Cache-Control "public, max-age=31536000, immutable"; # 1 year for CSS files
|
||||||
|
|
||||||
|
proxy_cache STATIC;
|
||||||
|
proxy_cache_valid 200 302 24h;
|
||||||
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.html$ {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
add_header Content-Type text/html;
|
||||||
|
add_header Cache-Control "public, max-age=86400"; # 24 hours for HTML files
|
||||||
|
|
||||||
|
proxy_cache STATIC;
|
||||||
|
proxy_cache_valid 200 302 24h;
|
||||||
|
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||||
|
proxy_cache_lock on;
|
||||||
|
add_header X-Cache-Status $upstream_cache_status;
|
||||||
|
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Root location - make sure index.html is the default
|
||||||
|
location = / {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
# Main location
|
# Main location
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://127.0.0.1:3000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
|||||||
+665
-188
File diff suppressed because one or more lines are too long
+209
-93
File diff suppressed because one or more lines are too long
@@ -18,12 +18,13 @@ import {
|
|||||||
} from "../core/game/GameUpdates";
|
} from "../core/game/GameUpdates";
|
||||||
import { WorkerClient } from "../core/worker/WorkerClient";
|
import { WorkerClient } from "../core/worker/WorkerClient";
|
||||||
import { consolex, initRemoteSender } from "../core/Consolex";
|
import { consolex, initRemoteSender } from "../core/Consolex";
|
||||||
import { getConfig, getServerConfig } from "../core/configuration/Config";
|
import { getConfig, ServerConfig } from "../core/configuration/Config";
|
||||||
import { GameView, PlayerView } from "../core/game/GameView";
|
import { GameView, PlayerView } from "../core/game/GameView";
|
||||||
import { GameUpdateViewData } from "../core/game/GameUpdates";
|
import { GameUpdateViewData } from "../core/game/GameUpdates";
|
||||||
import { UserSettings } from "../core/game/UserSettings";
|
import { UserSettings } from "../core/game/UserSettings";
|
||||||
|
|
||||||
export interface LobbyConfig {
|
export interface LobbyConfig {
|
||||||
|
serverConfig: ServerConfig;
|
||||||
flag: () => string;
|
flag: () => string;
|
||||||
playerName: () => string;
|
playerName: () => string;
|
||||||
clientID: ClientID;
|
clientID: ClientID;
|
||||||
@@ -51,8 +52,6 @@ export function joinLobby(
|
|||||||
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`,
|
`joinging lobby: gameID: ${lobbyConfig.gameID}, clientID: ${lobbyConfig.clientID}, persistentID: ${lobbyConfig.persistentID}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const serverConfig = getServerConfig();
|
|
||||||
|
|
||||||
const userSettings: UserSettings = new UserSettings();
|
const userSettings: UserSettings = new UserSettings();
|
||||||
let gameConfig: GameConfig = null;
|
let gameConfig: GameConfig = null;
|
||||||
if (lobbyConfig.gameType == GameType.Singleplayer) {
|
if (lobbyConfig.gameType == GameType.Singleplayer) {
|
||||||
@@ -72,7 +71,7 @@ export function joinLobby(
|
|||||||
lobbyConfig,
|
lobbyConfig,
|
||||||
gameConfig,
|
gameConfig,
|
||||||
eventBus,
|
eventBus,
|
||||||
serverConfig,
|
lobbyConfig.serverConfig,
|
||||||
);
|
);
|
||||||
|
|
||||||
const onconnect = () => {
|
const onconnect = () => {
|
||||||
@@ -106,7 +105,7 @@ export async function createClientGame(
|
|||||||
transport: Transport,
|
transport: Transport,
|
||||||
userSettings: UserSettings,
|
userSettings: UserSettings,
|
||||||
): Promise<ClientGameRunner> {
|
): Promise<ClientGameRunner> {
|
||||||
const config = getConfig(gameConfig, userSettings);
|
const config = await getConfig(gameConfig, userSettings);
|
||||||
|
|
||||||
const gameMap = await loadTerrainMap(gameConfig.gameMap);
|
const gameMap = await loadTerrainMap(gameConfig.gameMap);
|
||||||
const worker = new WorkerClient(
|
const worker = new WorkerClient(
|
||||||
|
|||||||
@@ -249,6 +249,10 @@ export class HelpModal extends LitElement {
|
|||||||
<td>C</td>
|
<td>C</td>
|
||||||
<td>Center camera on player</td>
|
<td>Center camera on player</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>C</td>
|
||||||
|
<td>Center camera on player</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Q / E</td>
|
<td>Q / E</td>
|
||||||
<td>Zoom out/in</td>
|
<td>Zoom out/in</td>
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import { consolex } from "../core/Consolex";
|
|||||||
import "./components/Difficulties";
|
import "./components/Difficulties";
|
||||||
import { DifficultyDescription } from "./components/Difficulties";
|
import { DifficultyDescription } from "./components/Difficulties";
|
||||||
import "./components/Maps";
|
import "./components/Maps";
|
||||||
import { generateID } from "../core/Util";
|
|
||||||
import { getConfig, getServerConfig } from "../core/configuration/Config";
|
|
||||||
import randomMap from "../../resources/images/RandomMap.png";
|
import randomMap from "../../resources/images/RandomMap.png";
|
||||||
|
import { generateID } from "../core/Util";
|
||||||
|
import {
|
||||||
|
getConfig,
|
||||||
|
getServerConfigFromClient,
|
||||||
|
} from "../core/configuration/Config";
|
||||||
|
|
||||||
@customElement("host-lobby-modal")
|
@customElement("host-lobby-modal")
|
||||||
export class HostLobbyModal extends LitElement {
|
export class HostLobbyModal extends LitElement {
|
||||||
@@ -26,6 +29,8 @@ export class HostLobbyModal extends LitElement {
|
|||||||
@state() private useRandomMap: boolean = false;
|
@state() private useRandomMap: boolean = false;
|
||||||
|
|
||||||
private playersInterval = null;
|
private playersInterval = null;
|
||||||
|
// Add a new timer for debouncing bot changes
|
||||||
|
private botsUpdateTimer: number | null = null;
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
@@ -571,11 +576,18 @@ export class HostLobbyModal extends LitElement {
|
|||||||
clearInterval(this.playersInterval);
|
clearInterval(this.playersInterval);
|
||||||
this.playersInterval = null;
|
this.playersInterval = null;
|
||||||
}
|
}
|
||||||
|
// Clear any pending bot updates
|
||||||
|
if (this.botsUpdateTimer !== null) {
|
||||||
|
clearTimeout(this.botsUpdateTimer);
|
||||||
|
this.botsUpdateTimer = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleRandomMapToggle() {
|
private async handleRandomMapToggle() {
|
||||||
this.useRandomMap = true;
|
this.useRandomMap = true;
|
||||||
this.putGameConfig();
|
this.putGameConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleMapSelection(value: GameMapType) {
|
private async handleMapSelection(value: GameMapType) {
|
||||||
this.selectedMap = value;
|
this.selectedMap = value;
|
||||||
this.useRandomMap = false;
|
this.useRandomMap = false;
|
||||||
@@ -586,14 +598,28 @@ export class HostLobbyModal extends LitElement {
|
|||||||
this.putGameConfig();
|
this.putGameConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modified to include debouncing
|
||||||
private handleBotsChange(e: Event) {
|
private handleBotsChange(e: Event) {
|
||||||
const value = parseInt((e.target as HTMLInputElement).value);
|
const value = parseInt((e.target as HTMLInputElement).value);
|
||||||
if (isNaN(value) || value < 0 || value > 400) {
|
if (isNaN(value) || value < 0 || value > 400) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update the display value immediately
|
||||||
this.bots = value;
|
this.bots = value;
|
||||||
this.putGameConfig();
|
|
||||||
|
// Clear any existing timer
|
||||||
|
if (this.botsUpdateTimer !== null) {
|
||||||
|
clearTimeout(this.botsUpdateTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a new timer to call putGameConfig after 300ms of inactivity
|
||||||
|
this.botsUpdateTimer = window.setTimeout(() => {
|
||||||
|
this.putGameConfig();
|
||||||
|
this.botsUpdateTimer = null;
|
||||||
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleInstantBuildChange(e: Event) {
|
private handleInstantBuildChange(e: Event) {
|
||||||
this.instantBuild = Boolean((e.target as HTMLInputElement).checked);
|
this.instantBuild = Boolean((e.target as HTMLInputElement).checked);
|
||||||
this.putGameConfig();
|
this.putGameConfig();
|
||||||
@@ -614,8 +640,9 @@ export class HostLobbyModal extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async putGameConfig() {
|
private async putGameConfig() {
|
||||||
|
const config = await getServerConfigFromClient();
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${window.location.origin}/${getServerConfig().workerPath(this.lobbyId)}/game/${this.lobbyId}`,
|
`${window.location.origin}/${config.workerPath(this.lobbyId)}/api/game/${this.lobbyId}`,
|
||||||
{
|
{
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -650,8 +677,9 @@ export class HostLobbyModal extends LitElement {
|
|||||||
`Starting private game with map: ${GameMapType[this.selectedMap]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
|
`Starting private game with map: ${GameMapType[this.selectedMap]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
|
||||||
);
|
);
|
||||||
this.close();
|
this.close();
|
||||||
|
const config = await getServerConfigFromClient();
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${window.location.origin}/${getServerConfig().workerPath(this.lobbyId)}/start_game/${this.lobbyId}`,
|
`${window.location.origin}/${config.workerPath(this.lobbyId)}/api/start_game/${this.lobbyId}`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -677,15 +705,13 @@ export class HostLobbyModal extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async pollPlayers() {
|
private async pollPlayers() {
|
||||||
fetch(
|
const config = await getServerConfigFromClient();
|
||||||
`/${getServerConfig().workerPath(this.lobbyId)}/game/${this.lobbyId}`,
|
fetch(`/${config.workerPath(this.lobbyId)}/api/game/${this.lobbyId}`, {
|
||||||
{
|
method: "GET",
|
||||||
method: "GET",
|
headers: {
|
||||||
headers: {
|
"Content-Type": "application/json",
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
})
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data: GameInfo) => {
|
.then((data: GameInfo) => {
|
||||||
console.log(`got response: ${data}`);
|
console.log(`got response: ${data}`);
|
||||||
@@ -695,11 +721,11 @@ export class HostLobbyModal extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createLobby(): Promise<GameInfo> {
|
async function createLobby(): Promise<GameInfo> {
|
||||||
const serverConfig = getServerConfig();
|
const config = await getServerConfigFromClient();
|
||||||
try {
|
try {
|
||||||
const id = generateID();
|
const id = generateID();
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`/${serverConfig.workerPath(id)}/create_game/${id}`,
|
`/${config.workerPath(id)}/api/create_game/${id}`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { LitElement, css, html } from "lit";
|
import { LitElement, css, html } from "lit";
|
||||||
import { customElement, query, state } from "lit/decorators.js";
|
import { customElement, query, state } from "lit/decorators.js";
|
||||||
import { getServerConfig } from "../core/configuration/Config";
|
|
||||||
import { consolex } from "../core/Consolex";
|
import { consolex } from "../core/Consolex";
|
||||||
import { GameMapType, GameType } from "../core/game/Game";
|
import { GameMapType, GameType } from "../core/game/Game";
|
||||||
import { GameInfo } from "../core/Schemas";
|
import { GameInfo } from "../core/Schemas";
|
||||||
|
import { getServerConfigFromClient } from "../core/configuration/Config";
|
||||||
|
|
||||||
@customElement("join-private-lobby-modal")
|
@customElement("join-private-lobby-modal")
|
||||||
export class JoinPrivateLobbyModal extends LitElement {
|
export class JoinPrivateLobbyModal extends LitElement {
|
||||||
@@ -363,12 +363,13 @@ export class JoinPrivateLobbyModal extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private joinLobby() {
|
private async joinLobby() {
|
||||||
const lobbyId = this.lobbyIdInput.value;
|
const lobbyId = this.lobbyIdInput.value;
|
||||||
consolex.log(`Joining lobby with ID: ${lobbyId}`);
|
consolex.log(`Joining lobby with ID: ${lobbyId}`);
|
||||||
this.message = "Checking lobby..."; // Set initial message
|
this.message = "Checking lobby..."; // Set initial message
|
||||||
|
|
||||||
const url = `/${getServerConfig().workerPath(lobbyId)}/game/${lobbyId}/exists`;
|
const config = await getServerConfigFromClient();
|
||||||
|
const url = `/${config.workerPath(lobbyId)}/api/game/${lobbyId}/exists`;
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -406,9 +407,10 @@ export class JoinPrivateLobbyModal extends LitElement {
|
|||||||
|
|
||||||
private async pollPlayers() {
|
private async pollPlayers() {
|
||||||
if (!this.lobbyIdInput?.value) return;
|
if (!this.lobbyIdInput?.value) return;
|
||||||
|
const config = await getServerConfigFromClient();
|
||||||
|
|
||||||
fetch(
|
fetch(
|
||||||
`/${getServerConfig().workerPath(this.lobbyIdInput.value)}/game/${this.lobbyIdInput.value}`,
|
`/${config.workerPath(this.lobbyIdInput.value)}/api/game/${this.lobbyIdInput.value}`,
|
||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { Config, GameEnv, ServerConfig } from "../core/configuration/Config";
|
||||||
Config,
|
|
||||||
GameEnv,
|
|
||||||
getServerConfig,
|
|
||||||
ServerConfig,
|
|
||||||
} from "../core/configuration/Config";
|
|
||||||
import { consolex } from "../core/Consolex";
|
import { consolex } from "../core/Consolex";
|
||||||
import { GameEvent } from "../core/EventBus";
|
import { GameEvent } from "../core/EventBus";
|
||||||
import {
|
import {
|
||||||
@@ -130,7 +125,7 @@ export class LocalServer {
|
|||||||
const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], {
|
const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], {
|
||||||
type: "application/json",
|
type: "application/json",
|
||||||
});
|
});
|
||||||
const workerPath = getServerConfig().workerPath(this.lobbyConfig.gameID);
|
const workerPath = this.serverConfig.workerPath(this.lobbyConfig.gameID);
|
||||||
navigator.sendBeacon(`/${workerPath}/archive_singleplayer_game`, blob);
|
navigator.sendBeacon(`/${workerPath}/archive_singleplayer_game`, blob);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { DarkModeButton } from "./DarkModeButton";
|
|||||||
import "./GoogleAdElement";
|
import "./GoogleAdElement";
|
||||||
import { HelpModal } from "./HelpModal";
|
import { HelpModal } from "./HelpModal";
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
|
import { getServerConfigFromClient } from "../core/configuration/Config";
|
||||||
|
|
||||||
class Client {
|
class Client {
|
||||||
private gameStop: () => void;
|
private gameStop: () => void;
|
||||||
@@ -136,9 +137,11 @@ class Client {
|
|||||||
consolex.log("joining lobby, stopping existing game");
|
consolex.log("joining lobby, stopping existing game");
|
||||||
this.gameStop();
|
this.gameStop();
|
||||||
}
|
}
|
||||||
|
const config = await getServerConfigFromClient();
|
||||||
const gameType = event.detail.gameType;
|
const gameType = event.detail.gameType;
|
||||||
this.gameStop = joinLobby(
|
this.gameStop = joinLobby(
|
||||||
{
|
{
|
||||||
|
serverConfig: config,
|
||||||
gameType: gameType,
|
gameType: gameType,
|
||||||
flag: (): string =>
|
flag: (): string =>
|
||||||
this.flagInput.getCurrentFlag() == "xx"
|
this.flagInput.getCurrentFlag() == "xx"
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export class PublicLobby extends LitElement {
|
|||||||
private async fetchAndUpdateLobbies(): Promise<void> {
|
private async fetchAndUpdateLobbies(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const lobbies = await this.fetchLobbies();
|
const lobbies = await this.fetchLobbies();
|
||||||
|
console.log(`got lobbies: ${JSON.stringify(lobbies)}`);
|
||||||
this.lobbies = lobbies;
|
this.lobbies = lobbies;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
consolex.error("Error fetching lobbies:", error);
|
consolex.error("Error fetching lobbies:", error);
|
||||||
@@ -46,7 +47,7 @@ export class PublicLobby extends LitElement {
|
|||||||
|
|
||||||
async fetchLobbies(): Promise<GameInfo[]> {
|
async fetchLobbies(): Promise<GameInfo[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/public_lobbies`);
|
const response = await fetch(`/api/public_lobbies`);
|
||||||
if (!response.ok)
|
if (!response.ok)
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|||||||
@@ -228,7 +228,6 @@ export class WinModal extends LitElement implements Layer {
|
|||||||
this.won = false;
|
this.won = false;
|
||||||
this.show();
|
this.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => {
|
this.game.updatesSinceLastTick()[GameUpdateType.Win].forEach((wu) => {
|
||||||
const winner = this.game.playerBySmallID(wu.winnerID) as PlayerView;
|
const winner = this.game.playerBySmallID(wu.winnerID) as PlayerView;
|
||||||
this.eventBus.emit(new SendWinnerEvent(winner.clientID()));
|
this.eventBus.emit(new SendWinnerEvent(winner.clientID()));
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export async function createGameRunner(
|
|||||||
clientID: ClientID,
|
clientID: ClientID,
|
||||||
callBack: (gu: GameUpdateViewData) => void,
|
callBack: (gu: GameUpdateViewData) => void,
|
||||||
): Promise<GameRunner> {
|
): Promise<GameRunner> {
|
||||||
const config = getConfig(gameConfig, null);
|
const config = await getConfig(gameConfig, null);
|
||||||
const gameMap = await loadGameMap(gameConfig.gameMap);
|
const gameMap = await loadGameMap(gameConfig.gameMap);
|
||||||
const game = createGame(
|
const game = createGame(
|
||||||
gameMap.gameMap,
|
gameMap.gameMap,
|
||||||
|
|||||||
@@ -22,21 +22,24 @@ import { GameMap, TileRef } from "../game/GameMap";
|
|||||||
import { PlayerView } from "../game/GameView";
|
import { PlayerView } from "../game/GameView";
|
||||||
import { UserSettings } from "../game/UserSettings";
|
import { UserSettings } from "../game/UserSettings";
|
||||||
|
|
||||||
|
let cachedSC: ServerConfig = null;
|
||||||
|
|
||||||
export enum GameEnv {
|
export enum GameEnv {
|
||||||
Dev,
|
Dev,
|
||||||
Preprod,
|
Preprod,
|
||||||
Prod,
|
Prod,
|
||||||
}
|
}
|
||||||
export function getConfig(
|
|
||||||
|
export async function getConfig(
|
||||||
gameConfig: GameConfig,
|
gameConfig: GameConfig,
|
||||||
userSettings: UserSettings | null = null,
|
userSettings: UserSettings | null = null,
|
||||||
): Config {
|
): Promise<Config> {
|
||||||
const sc = getServerConfig();
|
const sc = await getServerConfigFromClient();
|
||||||
switch (process.env.GAME_ENV) {
|
switch (sc.env()) {
|
||||||
case "dev":
|
case GameEnv.Dev:
|
||||||
return new DevConfig(sc, gameConfig, userSettings);
|
return new DevConfig(sc, gameConfig, userSettings);
|
||||||
case "preprod":
|
case GameEnv.Preprod:
|
||||||
case "prod":
|
case GameEnv.Prod:
|
||||||
consolex.log("using prod config");
|
consolex.log("using prod config");
|
||||||
return new DefaultConfig(sc, gameConfig, userSettings);
|
return new DefaultConfig(sc, gameConfig, userSettings);
|
||||||
default:
|
default:
|
||||||
@@ -44,20 +47,43 @@ export function getConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getServerConfig(): ServerConfig {
|
export async function getServerConfigFromClient(): Promise<ServerConfig> {
|
||||||
switch (process.env.GAME_ENV) {
|
if (cachedSC) {
|
||||||
|
return cachedSC;
|
||||||
|
}
|
||||||
|
const response = await fetch("/api/env");
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to fetch server config: ${response.status} ${response.statusText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const config = await response.json();
|
||||||
|
// Log the retrieved configuration
|
||||||
|
console.log("Server config loaded:", config);
|
||||||
|
|
||||||
|
cachedSC = getServerConfig(config.game_env);
|
||||||
|
return cachedSC;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServerConfigFromServer(): ServerConfig {
|
||||||
|
const gameEnv = process.env.GAME_ENV;
|
||||||
|
return getServerConfig(gameEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getServerConfig(gameEnv: string) {
|
||||||
|
switch (gameEnv) {
|
||||||
case "dev":
|
case "dev":
|
||||||
consolex.log("using dev config");
|
consolex.log("using dev server config");
|
||||||
return new DevServerConfig();
|
return new DevServerConfig();
|
||||||
case "preprod":
|
case "staging":
|
||||||
consolex.log("using preprod config");
|
consolex.log("using preprod server config");
|
||||||
return preprodConfig;
|
return preprodConfig;
|
||||||
case "prod":
|
case "prod":
|
||||||
default:
|
consolex.log("using prod server config");
|
||||||
consolex.log("using prod config");
|
|
||||||
return prodConfig;
|
return prodConfig;
|
||||||
// default:
|
default:
|
||||||
// throw Error(`unsupported server configuration: ${process.env.GAME_ENV}`)
|
throw Error(`unsupported server configuration: ${gameEnv}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
|||||||
adminToken(): string {
|
adminToken(): string {
|
||||||
return process.env.ADMIN_TOKEN;
|
return process.env.ADMIN_TOKEN;
|
||||||
}
|
}
|
||||||
numWorkers(): number {
|
abstract numWorkers(): number;
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
abstract env(): GameEnv;
|
abstract env(): GameEnv;
|
||||||
abstract discordRedirectURI(): string;
|
abstract discordRedirectURI(): string;
|
||||||
turnIntervalMs(): number {
|
turnIntervalMs(): number {
|
||||||
@@ -38,9 +36,9 @@ export abstract class DefaultServerConfig implements ServerConfig {
|
|||||||
}
|
}
|
||||||
gameCreationRate(highTraffic: boolean): number {
|
gameCreationRate(highTraffic: boolean): number {
|
||||||
if (highTraffic) {
|
if (highTraffic) {
|
||||||
return 30 * 1000;
|
return 20 * 1000;
|
||||||
} else {
|
} else {
|
||||||
return 60 * 1000;
|
return 50 * 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lobbyLifetime(highTraffic: boolean): number {
|
lobbyLifetime(highTraffic: boolean): number {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ export class DevServerConfig extends DefaultServerConfig {
|
|||||||
discordRedirectURI(): string {
|
discordRedirectURI(): string {
|
||||||
return "http://localhost:3000/auth/callback";
|
return "http://localhost:3000/auth/callback";
|
||||||
}
|
}
|
||||||
|
numWorkers(): number {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DevConfig extends DefaultConfig {
|
export class DevConfig extends DefaultConfig {
|
||||||
|
|||||||
@@ -8,4 +8,7 @@ export const preprodConfig = new (class extends DefaultServerConfig {
|
|||||||
discordRedirectURI(): string {
|
discordRedirectURI(): string {
|
||||||
return "https://openfront.dev/auth/callback";
|
return "https://openfront.dev/auth/callback";
|
||||||
}
|
}
|
||||||
|
numWorkers(): number {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { GameEnv } from "./Config";
|
|||||||
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
|
import { DefaultConfig, DefaultServerConfig } from "./DefaultConfig";
|
||||||
|
|
||||||
export const prodConfig = new (class extends DefaultServerConfig {
|
export const prodConfig = new (class extends DefaultServerConfig {
|
||||||
|
numWorkers(): number {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
env(): GameEnv {
|
env(): GameEnv {
|
||||||
return GameEnv.Prod;
|
return GameEnv.Prod;
|
||||||
}
|
}
|
||||||
|
|||||||
+52
-51
@@ -1,27 +1,29 @@
|
|||||||
import { GameRecord, GameID } from "../core/Schemas";
|
import { GameRecord, GameID } from "../core/Schemas";
|
||||||
import { S3 } from "@aws-sdk/client-s3";
|
import { S3 } from "@aws-sdk/client-s3";
|
||||||
import { RedshiftData } from "@aws-sdk/client-redshift-data";
|
import { RedshiftData } from "@aws-sdk/client-redshift-data";
|
||||||
|
import {
|
||||||
|
GameEnv,
|
||||||
|
getServerConfigFromServer,
|
||||||
|
} from "../core/configuration/Config";
|
||||||
|
|
||||||
// Initialize AWS clients
|
const config = getServerConfigFromServer();
|
||||||
const s3 = new S3();
|
|
||||||
const bucket = "openfront-games";
|
|
||||||
const redshiftData = new RedshiftData({ region: "eu-west-1" });
|
|
||||||
|
|
||||||
// Redshift Serverless configuration
|
const s3 = new S3({ region: "eu-west-1" });
|
||||||
const REDSHIFT_WORKGROUP = "game-analytics";
|
|
||||||
const REDSHIFT_DATABASE = "game_archive";
|
const gameBucket = "openfront-games";
|
||||||
|
const analyticsBucket = "openfront-analytics";
|
||||||
|
|
||||||
export async function archive(gameRecord: GameRecord) {
|
export async function archive(gameRecord: GameRecord) {
|
||||||
try {
|
try {
|
||||||
// Archive to Redshift Serverless
|
// Archive to Redshift Serverless
|
||||||
await archiveToRedshift(gameRecord);
|
await archiveAnalyticsToS3(gameRecord);
|
||||||
|
|
||||||
// Archive to S3 if there are turns
|
// Archive to S3 if there are turns
|
||||||
if (gameRecord.turns.length > 0) {
|
if (gameRecord.turns.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
`${gameRecord.id}: game has more than zero turns, attempting to write to S3`,
|
`${gameRecord.id}: game has more than zero turns, attempting to write to full game to S3`,
|
||||||
);
|
);
|
||||||
await archiveToS3(gameRecord);
|
await archiveFullGameToS3(gameRecord);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`${gameRecord.id}: Final archive error: ${error}`, {
|
console.error(`${gameRecord.id}: Final archive error: ${error}`, {
|
||||||
@@ -33,55 +35,54 @@ export async function archive(gameRecord: GameRecord) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function archiveToRedshift(gameRecord: GameRecord) {
|
async function archiveAnalyticsToS3(gameRecord: GameRecord) {
|
||||||
const row = {
|
// Create analytics data object (similar to what was going to Redshift)
|
||||||
|
const analyticsData = {
|
||||||
id: gameRecord.id,
|
id: gameRecord.id,
|
||||||
start: new Date(gameRecord.startTimestampMS),
|
env: config.env(),
|
||||||
end: new Date(gameRecord.endTimestampMS),
|
start_time: new Date(gameRecord.startTimestampMS).toISOString(),
|
||||||
|
end_time: new Date(gameRecord.endTimestampMS).toISOString(),
|
||||||
duration_seconds: gameRecord.durationSeconds,
|
duration_seconds: gameRecord.durationSeconds,
|
||||||
number_turns: gameRecord.num_turns,
|
number_turns: gameRecord.num_turns,
|
||||||
game_mode: gameRecord.gameConfig.gameType,
|
game_mode: gameRecord.gameConfig.gameType,
|
||||||
winner: gameRecord.winner,
|
winner: gameRecord.winner,
|
||||||
difficulty: gameRecord.gameConfig.difficulty,
|
difficulty: gameRecord.gameConfig.difficulty,
|
||||||
map: gameRecord.gameConfig.gameMap,
|
mapType: gameRecord.gameConfig.gameMap,
|
||||||
players: JSON.stringify(
|
players: gameRecord.players.map((p) => ({
|
||||||
gameRecord.players.map((p) => ({
|
username: p.username,
|
||||||
username: p.username,
|
ip: p.ip,
|
||||||
ip: p.ip,
|
persistentID: p.persistentID,
|
||||||
persistentID: p.persistentID,
|
clientID: p.clientID,
|
||||||
clientID: p.clientID,
|
})),
|
||||||
})),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert the row to SQL parameters for insertion
|
try {
|
||||||
const params = {
|
// Store analytics data using just the game ID as the key
|
||||||
Sql: `
|
const analyticsKey = `${gameRecord.id}.json`;
|
||||||
INSERT INTO game_results (
|
|
||||||
id, start, end, duration_seconds, number_turns, game_mode,
|
|
||||||
winner, difficulty, map, players
|
|
||||||
) VALUES (
|
|
||||||
'${row.id}',
|
|
||||||
'${row.start.toISOString()}',
|
|
||||||
'${row.end.toISOString()}',
|
|
||||||
${row.duration_seconds},
|
|
||||||
${row.number_turns},
|
|
||||||
'${row.game_mode}',
|
|
||||||
'${row.winner}',
|
|
||||||
'${row.difficulty}',
|
|
||||||
'${row.map}',
|
|
||||||
JSON_PARSE('${row.players}')
|
|
||||||
)
|
|
||||||
`,
|
|
||||||
WorkgroupName: REDSHIFT_WORKGROUP,
|
|
||||||
Database: REDSHIFT_DATABASE,
|
|
||||||
};
|
|
||||||
|
|
||||||
await redshiftData.executeStatement(params);
|
await s3.putObject({
|
||||||
console.log(`${gameRecord.id}: wrote game metadata to Redshift`);
|
Bucket: analyticsBucket,
|
||||||
|
Key: analyticsKey,
|
||||||
|
Body: JSON.stringify(analyticsData),
|
||||||
|
ContentType: "application/json",
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${gameRecord.id}: successfully wrote game analytics to S3`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`${gameRecord.id}: Error writing game analytics to S3: ${error}`,
|
||||||
|
{
|
||||||
|
message: error?.message || error,
|
||||||
|
stack: error?.stack,
|
||||||
|
name: error?.name,
|
||||||
|
...(error && typeof error === "object" ? error : {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function archiveToS3(gameRecord: GameRecord) {
|
async function archiveFullGameToS3(gameRecord: GameRecord) {
|
||||||
// Create a deep copy to avoid modifying the original
|
// Create a deep copy to avoid modifying the original
|
||||||
const recordCopy = JSON.parse(JSON.stringify(gameRecord));
|
const recordCopy = JSON.parse(JSON.stringify(gameRecord));
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ async function archiveToS3(gameRecord: GameRecord) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await s3.putObject({
|
await s3.putObject({
|
||||||
Bucket: bucket,
|
Bucket: gameBucket,
|
||||||
Key: recordCopy.id,
|
Key: recordCopy.id,
|
||||||
Body: JSON.stringify(recordCopy),
|
Body: JSON.stringify(recordCopy),
|
||||||
ContentType: "application/json",
|
ContentType: "application/json",
|
||||||
@@ -110,7 +111,7 @@ export async function readGameRecord(gameId: GameID): Promise<GameRecord> {
|
|||||||
try {
|
try {
|
||||||
// Check if file exists and download in one operation
|
// Check if file exists and download in one operation
|
||||||
const response = await s3.getObject({
|
const response = await s3.getObject({
|
||||||
Bucket: bucket,
|
Bucket: gameBucket,
|
||||||
Key: gameId,
|
Key: gameId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ export async function readGameRecord(gameId: GameID): Promise<GameRecord> {
|
|||||||
export async function gameRecordExists(gameId: GameID): Promise<boolean> {
|
export async function gameRecordExists(gameId: GameID): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await s3.headObject({
|
await s3.headObject({
|
||||||
Bucket: bucket,
|
Bucket: gameBucket,
|
||||||
Key: gameId,
|
Key: gameId,
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+45
-10
@@ -4,7 +4,10 @@ import express from "express";
|
|||||||
import { GameMapType, GameType, Difficulty } from "../core/game/Game";
|
import { GameMapType, GameType, Difficulty } from "../core/game/Game";
|
||||||
import { generateID } from "../core/Util";
|
import { generateID } from "../core/Util";
|
||||||
import { PseudoRandom } from "../core/PseudoRandom";
|
import { PseudoRandom } from "../core/PseudoRandom";
|
||||||
import { GameEnv, getServerConfig } from "../core/configuration/Config";
|
import {
|
||||||
|
GameEnv,
|
||||||
|
getServerConfigFromServer,
|
||||||
|
} from "../core/configuration/Config";
|
||||||
import { GameInfo } from "../core/Schemas";
|
import { GameInfo } from "../core/Schemas";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit from "express-rate-limit";
|
||||||
@@ -12,7 +15,7 @@ import { fileURLToPath } from "url";
|
|||||||
import { isHighTrafficTime } from "./Util";
|
import { isHighTrafficTime } from "./Util";
|
||||||
import { gatekeeper, LimiterType } from "./Gatekeeper";
|
import { gatekeeper, LimiterType } from "./Gatekeeper";
|
||||||
|
|
||||||
const config = getServerConfig();
|
const config = getServerConfigFromServer();
|
||||||
const readyWorkers = new Set();
|
const readyWorkers = new Set();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -21,8 +24,25 @@ const server = http.createServer(app);
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
// Serve static files from the 'out' directory
|
app.use(
|
||||||
app.use(express.static(path.join(__dirname, "../../out")));
|
express.static(path.join(__dirname, "../../static"), {
|
||||||
|
maxAge: "1y", // Set max-age to 1 year for all static assets
|
||||||
|
setHeaders: (res, path) => {
|
||||||
|
// You can conditionally set different cache times based on file types
|
||||||
|
if (path.endsWith(".html")) {
|
||||||
|
// HTML files get shorter cache time
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=60");
|
||||||
|
} else if (path.match(/\.(js|css|svg)$/)) {
|
||||||
|
// JS, CSS, SVG get long cache with immutable
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
||||||
|
} else if (path.match(/\.(bin|dat|exe|dll|so|dylib)$/)) {
|
||||||
|
// Binary files also get long cache with immutable
|
||||||
|
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
||||||
|
}
|
||||||
|
// Other file types use the default maxAge setting
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
app.set("trust proxy", 3);
|
app.set("trust proxy", 3);
|
||||||
@@ -122,9 +142,19 @@ export async function startMaster() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.get(
|
||||||
|
"/api/env",
|
||||||
|
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
|
||||||
|
const envConfig = {
|
||||||
|
game_env: process.env.GAME_ENV || "prod",
|
||||||
|
};
|
||||||
|
res.json(envConfig);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Add lobbies endpoint to list public games for this worker
|
// Add lobbies endpoint to list public games for this worker
|
||||||
app.get(
|
app.get(
|
||||||
"/public_lobbies",
|
"/api/public_lobbies",
|
||||||
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
|
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
|
||||||
res.send(publicLobbiesJsonStr);
|
res.send(publicLobbiesJsonStr);
|
||||||
}),
|
}),
|
||||||
@@ -135,7 +165,7 @@ async function fetchLobbies(): Promise<void> {
|
|||||||
|
|
||||||
for (const gameID of publicLobbyIDs) {
|
for (const gameID of publicLobbyIDs) {
|
||||||
const port = config.workerPort(gameID);
|
const port = config.workerPort(gameID);
|
||||||
const promise = fetch(`http://localhost:${port}/game/${gameID}`)
|
const promise = fetch(`http://localhost:${port}/api/game/${gameID}`)
|
||||||
.then((resp) => resp.json())
|
.then((resp) => resp.json())
|
||||||
.then((json) => {
|
.then((json) => {
|
||||||
return json as GameInfo;
|
return json as GameInfo;
|
||||||
@@ -191,17 +221,18 @@ async function schedulePublicGame() {
|
|||||||
disableNPCs: false,
|
disableNPCs: false,
|
||||||
bots: 400,
|
bots: 400,
|
||||||
};
|
};
|
||||||
|
|
||||||
const workerPath = config.workerPath(gameID);
|
const workerPath = config.workerPath(gameID);
|
||||||
|
|
||||||
// Send request to the worker to start the game
|
// Send request to the worker to start the game
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`http://localhost:${config.workerPort(gameID)}/create_game/${gameID}`,
|
`http://localhost:${config.workerPort(gameID)}/api/create_game/${gameID}`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Internal-Request": "true",
|
"X-Internal-Request": "true", // Special header for internal requests
|
||||||
[config.adminHeader()]: config.adminToken(),
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
gameID: gameID,
|
gameID: gameID,
|
||||||
@@ -209,9 +240,11 @@ async function schedulePublicGame() {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to schedule public game: ${response.statusText}`);
|
throw new Error(`Failed to schedule public game: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -222,9 +255,11 @@ async function schedulePublicGame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Map rotation management (moved from GameManager)
|
||||||
let mapsPlaylist: GameMapType[] = [];
|
let mapsPlaylist: GameMapType[] = [];
|
||||||
const random = new PseudoRandom(123);
|
const random = new PseudoRandom(123);
|
||||||
|
|
||||||
|
// Get the next map in rotation
|
||||||
function getNextMap(): GameMapType {
|
function getNextMap(): GameMapType {
|
||||||
if (mapsPlaylist.length > 0) {
|
if (mapsPlaylist.length > 0) {
|
||||||
return mapsPlaylist.shift()!;
|
return mapsPlaylist.shift()!;
|
||||||
@@ -275,5 +310,5 @@ function sleep(ms: number): Promise<void> {
|
|||||||
|
|
||||||
// SPA fallback route
|
// SPA fallback route
|
||||||
app.get("*", function (req, res) {
|
app.get("*", function (req, res) {
|
||||||
res.sendFile(path.join(__dirname, "../../out/index.html"));
|
res.sendFile(path.join(__dirname, "../../static/index.html"));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Gatekeeper
|
||||||
|
|
||||||
|
Security module for botting, rate limiting, fingerprinting, etc.
|
||||||
+34
-13
@@ -4,7 +4,7 @@ import { WebSocketServer } from "ws";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { GameManager } from "./GameManager";
|
import { GameManager } from "./GameManager";
|
||||||
import { getServerConfig } from "../core/configuration/Config";
|
import { getServerConfigFromServer } from "../core/configuration/Config";
|
||||||
import { WebSocket } from "ws";
|
import { WebSocket } from "ws";
|
||||||
import { Client } from "./Client";
|
import { Client } from "./Client";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit from "express-rate-limit";
|
||||||
@@ -13,9 +13,9 @@ import { GameConfig, GameRecord, LogSeverity } from "../core/Schemas";
|
|||||||
import { slog } from "./StructuredLog";
|
import { slog } from "./StructuredLog";
|
||||||
import { GameType } from "../core/game/Game";
|
import { GameType } from "../core/game/Game";
|
||||||
import { archive } from "./Archive";
|
import { archive } from "./Archive";
|
||||||
import { LimiterType, gatekeeper } from "./Gatekeeper";
|
import { gatekeeper, LimiterType } from "./Gatekeeper";
|
||||||
|
|
||||||
const config = getServerConfig();
|
const config = getServerConfigFromServer();
|
||||||
|
|
||||||
// Worker setup
|
// Worker setup
|
||||||
export function startWorker() {
|
export function startWorker() {
|
||||||
@@ -69,7 +69,7 @@ export function startWorker() {
|
|||||||
|
|
||||||
// Endpoint to create a private lobby
|
// Endpoint to create a private lobby
|
||||||
app.post(
|
app.post(
|
||||||
"/create_game/:id",
|
"/api/create_game/:id",
|
||||||
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
|
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -79,9 +79,9 @@ export function startWorker() {
|
|||||||
// TODO: if game is public make sure request came from localhohst!!!
|
// TODO: if game is public make sure request came from localhohst!!!
|
||||||
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
|
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
|
||||||
const gc = req.body?.gameConfig as GameConfig;
|
const gc = req.body?.gameConfig as GameConfig;
|
||||||
if (gc?.gameType == GameType.Public && !isAdmin(req)) {
|
if (gc?.gameType == GameType.Public && !isLocalhost(req)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`cannot create public game ${id}, ip ${clientIP} not admin`,
|
`cannot create public game ${id}, ip ${clientIP} not localhost`,
|
||||||
);
|
);
|
||||||
return res.status(400);
|
return res.status(400);
|
||||||
}
|
}
|
||||||
@@ -106,7 +106,7 @@ export function startWorker() {
|
|||||||
|
|
||||||
// Add other endpoints from your original server
|
// Add other endpoints from your original server
|
||||||
app.post(
|
app.post(
|
||||||
"/start_game/:id",
|
"/api/start_game/:id",
|
||||||
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
|
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
|
||||||
console.log(`starting private lobby with id ${req.params.id}`);
|
console.log(`starting private lobby with id ${req.params.id}`);
|
||||||
const game = gm.game(req.params.id);
|
const game = gm.game(req.params.id);
|
||||||
@@ -126,7 +126,7 @@ export function startWorker() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
app.put(
|
app.put(
|
||||||
"/game/:id",
|
"/api/game/:id",
|
||||||
gatekeeper.httpHandler(LimiterType.Put, async (req, res) => {
|
gatekeeper.httpHandler(LimiterType.Put, async (req, res) => {
|
||||||
// TODO: only update public game if from local host
|
// TODO: only update public game if from local host
|
||||||
const lobbyID = req.params.id;
|
const lobbyID = req.params.id;
|
||||||
@@ -157,7 +157,7 @@ export function startWorker() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
"/game/:id/exists",
|
"/api/game/:id/exists",
|
||||||
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
|
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
|
||||||
const lobbyId = req.params.id;
|
const lobbyId = req.params.id;
|
||||||
res.json({
|
res.json({
|
||||||
@@ -167,7 +167,7 @@ export function startWorker() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
"/game/:id",
|
"/api/game/:id",
|
||||||
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
|
gatekeeper.httpHandler(LimiterType.Get, async (req, res) => {
|
||||||
const game = gm.game(req.params.id);
|
const game = gm.game(req.params.id);
|
||||||
if (game == null) {
|
if (game == null) {
|
||||||
@@ -179,7 +179,7 @@ export function startWorker() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
"/archive_singleplayer_game",
|
"/api/archive_singleplayer_game",
|
||||||
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
|
gatekeeper.httpHandler(LimiterType.Post, async (req, res) => {
|
||||||
const gameRecord: GameRecord = req.body;
|
const gameRecord: GameRecord = req.body;
|
||||||
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
|
const clientIP = req.ip || req.socket.remoteAddress || "unknown";
|
||||||
@@ -320,6 +320,27 @@ export function startWorker() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAdmin = (req: Request): boolean => {
|
const isLocalhost = (req: Request): boolean => {
|
||||||
return req.headers[config.adminHeader()] === config.adminToken();
|
// Get client IP address from various possible sources
|
||||||
|
const clientIP =
|
||||||
|
req.ip ||
|
||||||
|
req.socket.remoteAddress ||
|
||||||
|
(req.headers["x-forwarded-for"] as string)?.split(",").shift() ||
|
||||||
|
"unknown";
|
||||||
|
|
||||||
|
// Check if the request is from a loopback address
|
||||||
|
const isLoopbackIP =
|
||||||
|
// IPv4 localhost
|
||||||
|
clientIP === "127.0.0.1" ||
|
||||||
|
// IPv6 localhost
|
||||||
|
clientIP === "::1" ||
|
||||||
|
// Full loopback range
|
||||||
|
clientIP.startsWith("127.");
|
||||||
|
|
||||||
|
// Check hostname
|
||||||
|
const isLocalHostname =
|
||||||
|
req.hostname === "localhost" || req.headers.host?.startsWith("localhost:");
|
||||||
|
|
||||||
|
// Consider request local if either IP is loopback or hostname is localhost
|
||||||
|
return isLoopbackIP || isLocalHostname;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
# 1. Builds and uploads the Docker image to ECR with appropriate tag
|
# 1. Builds and uploads the Docker image to ECR with appropriate tag
|
||||||
# 2. Copies the update script to EC2 instance (staging or prod)
|
# 2. Copies the update script to EC2 instance (staging or prod)
|
||||||
# 3. Executes the update script on the EC2 instance
|
# 3. Executes the update script on the EC2 instance
|
||||||
|
|
||||||
set -e # Exit immediately if a command exits with a non-zero status
|
set -e # Exit immediately if a command exits with a non-zero status
|
||||||
|
|
||||||
# Function to print section headers
|
# Function to print section headers
|
||||||
@@ -64,8 +63,6 @@ if [ ! -f "$UPDATE_SCRIPT" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Step 1: Build and upload Docker image to ECR
|
# Step 1: Build and upload Docker image to ECR
|
||||||
print_header "STEP 1: Building and uploading Docker image to ECR"
|
print_header "STEP 1: Building and uploading Docker image to ECR"
|
||||||
echo "Environment: ${ENV}"
|
echo "Environment: ${ENV}"
|
||||||
@@ -73,7 +70,6 @@ echo "Using version tag: $VERSION_TAG"
|
|||||||
|
|
||||||
# Execute the build script with the version tag
|
# Execute the build script with the version tag
|
||||||
$BUILD_SCRIPT $VERSION_TAG
|
$BUILD_SCRIPT $VERSION_TAG
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "❌ Build and upload failed. Stopping deployment."
|
echo "❌ Build and upload failed. Stopping deployment."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -88,20 +84,17 @@ chmod +x $UPDATE_SCRIPT
|
|||||||
|
|
||||||
# Copy the update script to the EC2 instance
|
# Copy the update script to the EC2 instance
|
||||||
scp -i $EC2_KEY $UPDATE_SCRIPT $EC2_HOST:$REMOTE_UPDATE_SCRIPT
|
scp -i $EC2_KEY $UPDATE_SCRIPT $EC2_HOST:$REMOTE_UPDATE_SCRIPT
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "❌ Failed to copy update script to EC2 instance. Stopping deployment."
|
echo "❌ Failed to copy update script to EC2 instance. Stopping deployment."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ Update script successfully copied to EC2 instance."
|
echo "✅ Update script successfully copied to EC2 instance."
|
||||||
|
|
||||||
# Step 3: Execute the update script on the EC2 instance
|
# Step 3: Execute the update script on the EC2 instance
|
||||||
print_header "STEP 3: Executing update script on EC2 instance"
|
print_header "STEP 3: Executing update script on EC2 instance"
|
||||||
|
|
||||||
# Make the script executable on the remote server and execute it
|
# Make the script executable on the remote server and execute it with the environment parameter
|
||||||
ssh -i $EC2_KEY $EC2_HOST "chmod +x $REMOTE_UPDATE_SCRIPT && $REMOTE_UPDATE_SCRIPT"
|
ssh -i $EC2_KEY $EC2_HOST "chmod +x $REMOTE_UPDATE_SCRIPT && $REMOTE_UPDATE_SCRIPT $ENV"
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "❌ Failed to execute update script on EC2 instance."
|
echo "❌ Failed to execute update script on EC2 instance."
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Script to update Docker container
|
# Script to update Docker container
|
||||||
|
|
||||||
|
# Check if environment parameter is provided
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "Error: Environment parameter is required (prod or staging)"
|
||||||
|
echo "Usage: $0 <environment>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Set environment from parameter
|
||||||
|
ENV=$1
|
||||||
|
CONTAINER_NAME="openfront-${ENV}"
|
||||||
|
LOG_GROUP="/aws/ec2/docker-containers/${ENV}"
|
||||||
|
|
||||||
# Get AWS account ID
|
# Get AWS account ID
|
||||||
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
|
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
|
||||||
ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.eu-west-1.amazonaws.com/openfront:latest"
|
ECR_REPO="${AWS_ACCOUNT_ID}.dkr.ecr.eu-west-1.amazonaws.com/openfront:latest"
|
||||||
|
|
||||||
|
echo "Deploying to ${ENV} environment..."
|
||||||
echo "Logging in to ECR..."
|
echo "Logging in to ECR..."
|
||||||
aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.eu-west-1.amazonaws.com
|
aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.eu-west-1.amazonaws.com
|
||||||
|
|
||||||
@@ -13,29 +26,60 @@ docker pull $ECR_REPO
|
|||||||
|
|
||||||
echo "Checking for existing container..."
|
echo "Checking for existing container..."
|
||||||
# Check for running container
|
# Check for running container
|
||||||
RUNNING_CONTAINER=$(docker ps | grep openfront | awk '{print $1}')
|
RUNNING_CONTAINER=$(docker ps | grep ${CONTAINER_NAME} | awk '{print $1}')
|
||||||
if [ -n "$RUNNING_CONTAINER" ]; then
|
if [ -n "$RUNNING_CONTAINER" ]; then
|
||||||
echo "Stopping running container $RUNNING_CONTAINER..."
|
echo "Stopping running container $RUNNING_CONTAINER..."
|
||||||
docker stop $RUNNING_CONTAINER
|
docker stop $RUNNING_CONTAINER
|
||||||
|
echo "Waiting for container to fully stop and release resources..."
|
||||||
|
sleep 5 # Add a 5-second delay
|
||||||
docker rm $RUNNING_CONTAINER
|
docker rm $RUNNING_CONTAINER
|
||||||
echo "Container $RUNNING_CONTAINER stopped and removed."
|
echo "Container $RUNNING_CONTAINER stopped and removed."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Also check for stopped containers with the same name
|
# Also check for stopped containers with the same name
|
||||||
STOPPED_CONTAINER=$(docker ps -a | grep openfront | awk '{print $1}')
|
STOPPED_CONTAINER=$(docker ps -a | grep ${CONTAINER_NAME} | awk '{print $1}')
|
||||||
if [ -n "$STOPPED_CONTAINER" ]; then
|
if [ -n "$STOPPED_CONTAINER" ]; then
|
||||||
echo "Removing stopped container $STOPPED_CONTAINER..."
|
echo "Removing stopped container $STOPPED_CONTAINER..."
|
||||||
docker rm $STOPPED_CONTAINER
|
docker rm $STOPPED_CONTAINER
|
||||||
echo "Container $STOPPED_CONTAINER removed."
|
echo "Container $STOPPED_CONTAINER removed."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Starting new container..."
|
# Check if port 80 is still in use
|
||||||
|
echo "Checking if port 80 is still in use..."
|
||||||
|
if command -v lsof >/dev/null 2>&1; then
|
||||||
|
PORT_CHECK=$(lsof -i :80 | grep LISTEN)
|
||||||
|
elif command -v netstat >/dev/null 2>&1; then
|
||||||
|
PORT_CHECK=$(netstat -tuln | grep ":80 ")
|
||||||
|
else
|
||||||
|
PORT_CHECK=""
|
||||||
|
echo "Warning: Cannot check if port is in use (neither lsof nor netstat found)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$PORT_CHECK" ]; then
|
||||||
|
echo "Warning: Port 80 is still in use by another process:"
|
||||||
|
echo "$PORT_CHECK"
|
||||||
|
echo "Attempting to proceed anyway..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Starting new container for ${ENV} environment..."
|
||||||
docker run -d -p 80:80 \
|
docker run -d -p 80:80 \
|
||||||
|
--restart=always \
|
||||||
--log-driver=awslogs \
|
--log-driver=awslogs \
|
||||||
--log-opt awslogs-region=eu-west-1 \
|
--log-opt awslogs-region=eu-west-1 \
|
||||||
--log-opt awslogs-group=/aws/ec2/docker-containers \
|
--log-opt awslogs-group=${LOG_GROUP} \
|
||||||
--log-opt awslogs-create-group=true \
|
--log-opt awslogs-create-group=true \
|
||||||
--name openfront \
|
--env GAME_ENV=${ENV} \
|
||||||
|
--name ${CONTAINER_NAME} \
|
||||||
$ECR_REPO
|
$ECR_REPO
|
||||||
|
|
||||||
echo "Update complete! New container is running."
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "Update complete! New ${ENV} container is running."
|
||||||
|
# Final cleanup after successful deployment
|
||||||
|
echo "Performing final cleanup of unused Docker resources..."
|
||||||
|
echo "Removing unused images (not tagged and not referenced)..."
|
||||||
|
docker image prune -f
|
||||||
|
docker container prune -f
|
||||||
|
echo "Cleanup complete."
|
||||||
|
else
|
||||||
|
echo "Failed to start container"
|
||||||
|
fi
|
||||||
+65
-26
@@ -14,19 +14,25 @@ export default (env, argv) => {
|
|||||||
entry: "./src/client/Main.ts",
|
entry: "./src/client/Main.ts",
|
||||||
output: {
|
output: {
|
||||||
publicPath: "/",
|
publicPath: "/",
|
||||||
filename: "bundle.js",
|
filename: "js/[name].[contenthash].js", // Added content hash
|
||||||
path: path.resolve(__dirname, "out"),
|
path: path.resolve(__dirname, "static"),
|
||||||
clean: true,
|
clean: true,
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.bin$/,
|
test: /\.bin$/,
|
||||||
use: "raw-loader",
|
type: "asset/resource", // Changed from raw-loader
|
||||||
|
generator: {
|
||||||
|
filename: "binary/[name].[contenthash][ext]", // Added content hash
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.txt$/,
|
test: /\.txt$/,
|
||||||
use: "raw-loader",
|
type: "asset/resource", // Changed from raw-loader
|
||||||
|
generator: {
|
||||||
|
filename: "text/[name].[contenthash][ext]", // Added content hash
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.ts$/,
|
test: /\.ts$/,
|
||||||
@@ -57,7 +63,7 @@ export default (env, argv) => {
|
|||||||
test: /\.(png|jpe?g|gif)$/i,
|
test: /\.(png|jpe?g|gif)$/i,
|
||||||
type: "asset/resource",
|
type: "asset/resource",
|
||||||
generator: {
|
generator: {
|
||||||
filename: "images/[hash][ext][query]",
|
filename: "images/[name].[contenthash][ext]", // Added content hash
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -66,20 +72,17 @@ export default (env, argv) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
type: "asset/inline",
|
type: "asset/resource", // Changed from asset/inline for caching
|
||||||
|
generator: {
|
||||||
|
filename: "images/[name].[contenthash][ext]", // Added content hash
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||||
use: [
|
type: "asset/resource", // Changed from file-loader
|
||||||
{
|
generator: {
|
||||||
loader: "file-loader",
|
filename: "fonts/[name].[contenthash][ext]", // Added content hash and fixed path
|
||||||
options: {
|
},
|
||||||
name: "[name].[ext]",
|
|
||||||
outputPath: "fonts/",
|
|
||||||
publicPath: "../fonts/", // This is important
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -96,6 +99,17 @@ export default (env, argv) => {
|
|||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: "./src/client/index.html",
|
template: "./src/client/index.html",
|
||||||
filename: "index.html",
|
filename: "index.html",
|
||||||
|
// Add optimization for HTML
|
||||||
|
minify: isProduction
|
||||||
|
? {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeComments: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
removeScriptTypeAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
useShortDoctype: true,
|
||||||
|
}
|
||||||
|
: false,
|
||||||
}),
|
}),
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
"process.env.WEBSOCKET_URL": JSON.stringify(
|
"process.env.WEBSOCKET_URL": JSON.stringify(
|
||||||
@@ -106,18 +120,42 @@ export default (env, argv) => {
|
|||||||
"process.env.GAME_ENV": JSON.stringify(isProduction ? "prod" : "dev"),
|
"process.env.GAME_ENV": JSON.stringify(isProduction ? "prod" : "dev"),
|
||||||
}),
|
}),
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [{ from: "resources", to: path.resolve(__dirname, "out") }],
|
patterns: [
|
||||||
|
{
|
||||||
|
from: "resources",
|
||||||
|
to: ".", // Copy to the output directory (static)
|
||||||
|
// Add content hashing to copied files
|
||||||
|
transform: function (content, path) {
|
||||||
|
return content; // Return unmodified content
|
||||||
|
},
|
||||||
|
// Don't hash HTML files from resources
|
||||||
|
noErrorOnMissing: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
options: {
|
options: {
|
||||||
concurrency: 100,
|
concurrency: 100,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
optimization: {
|
||||||
|
// Add optimization configuration for better caching
|
||||||
|
runtimeChunk: "single",
|
||||||
|
splitChunks: {
|
||||||
|
cacheGroups: {
|
||||||
|
vendor: {
|
||||||
|
test: /[\\/]node_modules[\\/]/,
|
||||||
|
name: "vendors",
|
||||||
|
chunks: "all",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
devServer: isProduction
|
devServer: isProduction
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
devMiddleware: { writeToDisk: true },
|
devMiddleware: { writeToDisk: true },
|
||||||
static: {
|
static: {
|
||||||
directory: path.join(__dirname, "out"),
|
directory: path.join(__dirname, "static"),
|
||||||
},
|
},
|
||||||
historyApiFallback: true,
|
historyApiFallback: true,
|
||||||
compress: true,
|
compress: true,
|
||||||
@@ -184,14 +222,15 @@ export default (env, argv) => {
|
|||||||
// Original API endpoints
|
// Original API endpoints
|
||||||
{
|
{
|
||||||
context: [
|
context: [
|
||||||
"/public_lobbies",
|
"/api/env",
|
||||||
"/join_game",
|
"/api/game",
|
||||||
"/start_game",
|
"/api/public_lobbies",
|
||||||
"/create_game",
|
"/api/join_game",
|
||||||
"/archive_singleplayer_game",
|
"/api/start_game",
|
||||||
"/debug-ip",
|
"/api/create_game",
|
||||||
"/auth/callback",
|
"/api/archive_singleplayer_game",
|
||||||
"/auth/discord",
|
"/api/auth/callback",
|
||||||
|
"/api/auth/discord",
|
||||||
],
|
],
|
||||||
target: "http://localhost:3000",
|
target: "http://localhost:3000",
|
||||||
secure: false,
|
secure: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user