use node cluster to shard server

This commit is contained in:
Evan
2025-03-02 09:39:09 -08:00
parent 5d163a765c
commit a029b4277f
20 changed files with 1007 additions and 717 deletions
+55 -45
View File
@@ -1,11 +1,13 @@
import { LitElement, html, css } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { Lobby } from "../core/Schemas";
import { GameConfig, GameInfo } from "../core/Schemas";
import { consolex } from "../core/Consolex";
import "./components/Difficulties";
import { DifficultyDescription } from "./components/Difficulties";
import "./components/Maps";
import { generateID } from "../core/Util";
import { getConfig, getServerConfig } from "../core/configuration/Config";
@customElement("host-lobby-modal")
export class HostLobbyModal extends LitElement {
@@ -412,7 +414,7 @@ export class HostLobbyModal extends LitElement {
step="1"
@input=${this.handleBotsChange}
@change=${this.handleBotsChange}
.value="${this.bots}"
.value="${String(this.bots)}"
/>
<div class="option-card-title">
Bots: ${this.bots == 0 ? "Disabled" : this.bots}
@@ -508,7 +510,7 @@ export class HostLobbyModal extends LitElement {
public open() {
createLobby()
.then((lobby) => {
this.lobbyId = lobby.id;
this.lobbyId = lobby.gameID;
// join lobby
})
.then(() => {
@@ -517,7 +519,7 @@ export class HostLobbyModal extends LitElement {
detail: {
gameType: GameType.Private,
lobby: {
id: this.lobbyId,
gameID: this.lobbyId,
},
map: this.selectedMap,
difficulty: this.selectedDifficulty,
@@ -582,21 +584,24 @@ export class HostLobbyModal extends LitElement {
}
private async putGameConfig() {
const response = await fetch(`/private_lobby/${this.lobbyId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
const response = await fetch(
`${getServerConfig().workerPath(this.lobbyId)}/game/${this.lobbyId}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
gameMap: this.selectedMap,
difficulty: this.selectedDifficulty,
disableNPCs: this.disableNPCs,
bots: this.bots,
infiniteGold: this.infiniteGold,
infiniteTroops: this.infiniteTroops,
instantBuild: this.instantBuild,
} as GameConfig),
},
body: JSON.stringify({
gameMap: this.selectedMap,
difficulty: this.selectedDifficulty,
disableNPCs: this.disableNPCs,
bots: this.bots,
infiniteGold: this.infiniteGold,
infiniteTroops: this.infiniteTroops,
instantBuild: this.instantBuild,
}),
});
);
}
private async startGame() {
@@ -604,12 +609,15 @@ export class HostLobbyModal extends LitElement {
`Starting private game with map: ${GameMapType[this.selectedMap]}`,
);
this.close();
const response = await fetch(`/start_private_lobby/${this.lobbyId}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
const response = await fetch(
`${getServerConfig().workerPath(this.lobbyId)}/start_game/${this.lobbyId}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
},
});
);
}
private async copyToClipboard() {
@@ -623,34 +631,42 @@ export class HostLobbyModal extends LitElement {
this.copySuccess = false;
}, 2000);
} catch (err) {
consolex.error("Failed to copy text: ", err);
consolex.error(`Failed to copy text: ${err}`);
}
}
private async pollPlayers() {
fetch(`/lobby/${this.lobbyId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
fetch(
`/${getServerConfig().workerPath(this.lobbyId)}/game/${this.lobbyId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
})
)
.then((response) => response.json())
.then((data) => {
.then((data: GameInfo) => {
console.log(`got response: ${data}`);
this.players = data.players.map((p) => p.username);
this.players = data.clients.map((p) => p.username);
});
}
}
async function createLobby(): Promise<Lobby> {
async function createLobby(): Promise<GameInfo> {
const serverConfig = getServerConfig();
try {
const response = await fetch("/private_lobby", {
method: "POST",
headers: {
"Content-Type": "application/json",
const id = generateID();
const response = await fetch(
`/${serverConfig.workerPath(id)}/create_game/${id}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
// body: JSON.stringify(data), // Include this if you need to send data
},
// body: JSON.stringify(data), // Include this if you need to send data
});
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
@@ -659,13 +675,7 @@ async function createLobby(): Promise<Lobby> {
const data = await response.json();
consolex.log("Success:", data);
// Assuming the server returns an object with an 'id' property
const lobby: Lobby = {
id: data.id,
// Add other properties as needed
};
return lobby;
return data as GameInfo;
} catch (error) {
consolex.error("Error creating lobby:", error);
throw error; // Re-throw the error so the caller can handle it
+18 -10
View File
@@ -2,6 +2,8 @@ import { LitElement, html, css } from "lit";
import { customElement, property, state, query } from "lit/decorators.js";
import { GameMapType, GameType } from "../core/game/Game";
import { consolex } from "../core/Consolex";
import { getConfig, getServerConfig } from "../core/configuration/Config";
import { GameInfo } from "../core/Schemas";
@customElement("join-private-lobby-modal")
export class JoinPrivateLobbyModal extends LitElement {
@@ -358,13 +360,16 @@ export class JoinPrivateLobbyModal extends LitElement {
consolex.log(`Joining lobby with ID: ${lobbyId}`);
this.message = "Checking lobby..."; // Set initial message
fetch(`/lobby/${lobbyId}/exists`, {
const url = `${window.location.origin}/${getServerConfig().workerPath(lobbyId)}/game/${lobbyId}/exists`;
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((response) => {
return response.json();
})
.then((data) => {
if (data.exists) {
this.message = "Joined successfully! Waiting for game to start...";
@@ -372,7 +377,7 @@ export class JoinPrivateLobbyModal extends LitElement {
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: {
lobby: { id: lobbyId },
lobby: { gameID: lobbyId },
gameType: GameType.Private,
map: GameMapType.World,
},
@@ -394,15 +399,18 @@ export class JoinPrivateLobbyModal extends LitElement {
private async pollPlayers() {
if (!this.lobbyIdInput?.value) return;
fetch(`/lobby/${this.lobbyIdInput.value}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
fetch(
`${getServerConfig().workerPath(this.lobbyIdInput.value)}/lobby/${this.lobbyIdInput.value}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
})
)
.then((response) => response.json())
.then((data) => {
this.players = data.players.map((p) => p.username);
.then((data: GameInfo) => {
this.players = data.clients.map((p) => p.username);
})
.catch((error) => {
consolex.error("Error polling players:", error);
+8 -2
View File
@@ -1,4 +1,9 @@
import { Config, GameEnv, ServerConfig } from "../core/configuration/Config";
import {
Config,
GameEnv,
getServerConfig,
ServerConfig,
} from "../core/configuration/Config";
import { consolex } from "../core/Consolex";
import { GameEvent } from "../core/EventBus";
import {
@@ -125,6 +130,7 @@ export class LocalServer {
const blob = new Blob([JSON.stringify(GameRecordSchema.parse(record))], {
type: "application/json",
});
navigator.sendBeacon("/archive_singleplayer_game", blob);
const workerPath = getServerConfig().workerPath(this.lobbyConfig.gameID);
navigator.sendBeacon(`/${workerPath}/archive_singleplayer_game`, blob);
}
}
+2 -10
View File
@@ -65,10 +65,6 @@ class Client {
setFavicon();
document.addEventListener("join-lobby", this.handleJoinLobby.bind(this));
document.addEventListener("leave-lobby", this.handleLeaveLobby.bind(this));
document.addEventListener(
"single-player",
this.handleSinglePlayer.bind(this),
);
const spModal = document.querySelector(
"single-player-modal",
@@ -148,7 +144,7 @@ class Client {
? ""
: this.flagInput.getCurrentFlag(),
playerName: (): string => this.usernameInput.getCurrentUsername(),
gameID: lobby.id,
gameID: lobby.gameID,
persistentID: getPersistentIDFromCookie(),
playerID: generateID(),
clientID: generateID(),
@@ -164,7 +160,7 @@ class Client {
this.joinModal.close();
this.publicLobby.stop();
if (gameType != GameType.Singleplayer) {
window.history.pushState({}, "", `/join/${lobby.id}`);
window.history.pushState({}, "", `/join/${lobby.gameID}`);
sessionStorage.setItem("inLobby", "true");
}
},
@@ -180,10 +176,6 @@ class Client {
this.gameStop = null;
this.publicLobby.leaveLobby();
}
private async handleSinglePlayer(event: CustomEvent) {
alert("coming soon");
}
}
// Initialize the client when the DOM is loaded
+10 -6
View File
@@ -1,16 +1,16 @@
import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { Lobby } from "../core/Schemas";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { consolex } from "../core/Consolex";
import { getMapsImage } from "./utilities/Maps";
import { GameInfo } from "../core/Schemas";
@customElement("public-lobby")
export class PublicLobby extends LitElement {
@state() private lobbies: Lobby[] = [];
@state() private lobbies: GameInfo[] = [];
@state() public isLobbyHighlighted: boolean = false;
private lobbiesInterval: number | null = null;
private currLobby: Lobby = null;
private currLobby: GameInfo = null;
createRenderRoot() {
return this;
@@ -36,15 +36,16 @@ export class PublicLobby extends LitElement {
private async fetchAndUpdateLobbies(): Promise<void> {
try {
const lobbies = await this.fetchLobbies();
console.log(`got lobbies: ${JSON.stringify(lobbies)}`);
this.lobbies = lobbies;
} catch (error) {
consolex.error("Error fetching lobbies:", error);
}
}
async fetchLobbies(): Promise<Lobby[]> {
async fetchLobbies(): Promise<GameInfo[]> {
try {
const response = await fetch("/lobbies");
const response = await fetch("/public_lobbies");
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
@@ -67,6 +68,9 @@ export class PublicLobby extends LitElement {
if (this.lobbies.length === 0) return html``;
const lobby = this.lobbies[0];
if (!lobby?.gameConfig) {
return;
}
const timeRemaining = Math.max(0, Math.floor(lobby.msUntilStart / 1000));
// Format time to show minutes and seconds
@@ -121,7 +125,7 @@ export class PublicLobby extends LitElement {
this.currLobby = null;
}
private lobbyClicked(lobby: Lobby) {
private lobbyClicked(lobby: GameInfo) {
if (this.currLobby == null) {
this.isLobbyHighlighted = true;
this.currLobby = lobby;
+1 -1
View File
@@ -424,7 +424,7 @@ export class SinglePlayerModal extends LitElement {
detail: {
gameType: GameType.Singleplayer,
lobby: {
id: generateID(),
gameID: generateID(),
},
map: this.selectedMap,
difficulty: this.selectedDifficulty,
+3 -2
View File
@@ -228,9 +228,10 @@ export class Transport {
) {
this.startPing();
this.maybeKillSocket();
const wsHost = process.env.WEBSOCKET_URL || window.location.host;
const wsHost = window.location.host;
const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:";
this.socket = new WebSocket(`${wsProtocol}//${wsHost}`);
const workerPath = this.serverConfig.workerPath(this.lobbyConfig.gameID);
this.socket = new WebSocket(`${wsProtocol}//${wsHost}/${workerPath}`);
this.onconnect = onconnect;
this.onmessage = onmessage;
this.socket.onopen = () => {