187 lines
5.0 KiB
JavaScript
187 lines
5.0 KiB
JavaScript
// Browser network role: talks to the authoritative Node server over a WebSocket
|
|
// and exposes high-level events so the screens do not care about the transport.
|
|
|
|
export const DEFAULT_PORT = 27015;
|
|
export const DEFAULT_PATH = "/ws";
|
|
|
|
export class Network {
|
|
constructor() {
|
|
this.socket = null;
|
|
this.isAdmin = false;
|
|
this.localPlayerId = 0;
|
|
this.players = {};
|
|
this._handlers = {};
|
|
this._loginFailure = "";
|
|
this._connectResolve = null;
|
|
this._connectReject = null;
|
|
}
|
|
|
|
on(event, handler) {
|
|
if (!this._handlers[event]) this._handlers[event] = [];
|
|
this._handlers[event].push(handler);
|
|
}
|
|
|
|
_emit(event, ...args) {
|
|
for (const handler of this._handlers[event] || []) handler(...args);
|
|
}
|
|
|
|
isClientConnected() {
|
|
return this.localPlayerId !== 0;
|
|
}
|
|
|
|
defaultServerUrl() {
|
|
const location = window.location;
|
|
if (location.protocol === "http:" || location.protocol === "https:") {
|
|
const scheme = location.protocol === "https:" ? "wss" : "ws";
|
|
return `${scheme}://${location.host}${DEFAULT_PATH}`;
|
|
}
|
|
return `ws://127.0.0.1:${DEFAULT_PORT}${DEFAULT_PATH}`;
|
|
}
|
|
|
|
// Accepts a bare hostname or a full ws(s):// or http(s):// URL.
|
|
normalizeUrl(host, port = DEFAULT_PORT) {
|
|
let url = String(host || "").trim().replace(/\/+$/, "");
|
|
if (url.startsWith("https://")) url = "wss://" + url.slice("https://".length);
|
|
else if (url.startsWith("http://")) url = "ws://" + url.slice("http://".length);
|
|
else if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
|
|
url = `ws://${url}:${port}`;
|
|
}
|
|
const afterScheme = url.slice(url.indexOf("://") + 3);
|
|
if (!afterScheme.includes("/")) url += DEFAULT_PATH;
|
|
return url;
|
|
}
|
|
|
|
connect(url) {
|
|
this.close();
|
|
this._loginFailure = "";
|
|
this.localPlayerId = 0;
|
|
this.isAdmin = false;
|
|
return new Promise((resolve, reject) => {
|
|
let socket;
|
|
try {
|
|
socket = new WebSocket(url);
|
|
} catch (error) {
|
|
reject(error);
|
|
return;
|
|
}
|
|
this.socket = socket;
|
|
this._connectResolve = resolve;
|
|
this._connectReject = reject;
|
|
socket.onopen = () => {
|
|
if (this._connectResolve) {
|
|
this._connectResolve();
|
|
this._connectResolve = null;
|
|
this._connectReject = null;
|
|
}
|
|
};
|
|
socket.onerror = () => {
|
|
if (this._connectReject) {
|
|
this._connectReject(new Error("connection failed"));
|
|
this._connectResolve = null;
|
|
this._connectReject = null;
|
|
}
|
|
};
|
|
socket.onclose = () => {
|
|
const failure = this._loginFailure;
|
|
this._loginFailure = "";
|
|
this.localPlayerId = 0;
|
|
this.isAdmin = false;
|
|
if (this._connectReject) {
|
|
this._connectReject(new Error("connection closed"));
|
|
this._connectResolve = null;
|
|
this._connectReject = null;
|
|
return;
|
|
}
|
|
if (failure) this._emit("session_failed", failure);
|
|
else this._emit("server_disconnected");
|
|
};
|
|
socket.onmessage = (event) => {
|
|
let message;
|
|
try {
|
|
message = JSON.parse(event.data);
|
|
} catch {
|
|
return;
|
|
}
|
|
this._handleMessage(message);
|
|
};
|
|
});
|
|
}
|
|
|
|
requestGameList(url) {
|
|
return this.connect(url).then(() => {
|
|
this._send({ t: "request_games" });
|
|
});
|
|
}
|
|
|
|
createGame(name, password) {
|
|
return this.connect(this.defaultServerUrl()).then(() => {
|
|
this._send({ t: "login", name, password });
|
|
});
|
|
}
|
|
|
|
joinGame(url, name, password) {
|
|
return this.connect(url).then(() => {
|
|
this._send({ t: "login", name, password });
|
|
});
|
|
}
|
|
|
|
sendGameSetup(setup) {
|
|
this._send({ t: "configure_game", setup });
|
|
}
|
|
|
|
sendOrder(order) {
|
|
this._send({ t: "order", order });
|
|
}
|
|
|
|
close() {
|
|
if (!this.socket) return;
|
|
const socket = this.socket;
|
|
this.socket = null;
|
|
socket.onopen = null;
|
|
socket.onerror = null;
|
|
socket.onclose = null;
|
|
socket.onmessage = null;
|
|
try {
|
|
socket.close();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
_send(message) {
|
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
|
this.socket.send(JSON.stringify(message));
|
|
}
|
|
}
|
|
|
|
_handleMessage(message) {
|
|
switch (message.t) {
|
|
case "assigned":
|
|
this.localPlayerId = message.playerId;
|
|
this.isAdmin = !!message.admin;
|
|
this._emit("session_started", message);
|
|
break;
|
|
case "players":
|
|
this.players = message.players || {};
|
|
this._emit("players_updated", this.players);
|
|
break;
|
|
case "game_list":
|
|
this._emit("game_list_received", message.games || []);
|
|
break;
|
|
case "login_rejected":
|
|
this._loginFailure = message.reason || "Login failed.";
|
|
this._emit("session_failed", this._loginFailure);
|
|
this.close();
|
|
break;
|
|
case "game_configured":
|
|
this._emit("game_configured");
|
|
break;
|
|
case "game_state":
|
|
this._emit("game_state_received", message.state);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|