UNPKG

@colyseus/core

Version:

Multiplayer Framework for Node.js.

262 lines (261 loc) 10 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var Server_exports = {}; __export(Server_exports, { Server: () => Server }); module.exports = __toCommonJS(Server_exports); var import_greeting_banner = __toESM(require("@colyseus/greeting-banner")); var import_Debug = require("./Debug.js"); var matchMaker = __toESM(require("./MatchMaker.js")); var import_Room = require("./Room.js"); var import_Utils = require("./utils/Utils.js"); var import_discovery = require("./discovery/index.js"); var import_LocalPresence = require("./presence/LocalPresence.js"); var import_LocalDriver = require("./matchmaker/driver/local/LocalDriver.js"); var import_Logger = require("./Logger.js"); var import_DevMode = require("./utils/DevMode.js"); class Server { constructor(options = {}) { //@ts-expect-error this._originalRoomOnMessage = null; this.onShutdownCallback = () => Promise.resolve(); this.onBeforeShutdownCallback = () => Promise.resolve(); const { gracefullyShutdown = true, greet = true } = options; (0, import_DevMode.setDevMode)(options.devMode === true); this.presence = options.presence || new import_LocalPresence.LocalPresence(); this.driver = options.driver || new import_LocalDriver.LocalDriver(); this.greet = greet; this.attach(options); matchMaker.setup( this.presence, this.driver, options.publicAddress, options.selectProcessIdToCreateRoom ); if (gracefullyShutdown) { (0, import_Utils.registerGracefulShutdown)((err) => this.gracefullyShutdown(true, err)); } if (options.logger) { (0, import_Logger.setLogger)(options.logger); } } attach(options) { if (options.pingInterval !== void 0 || options.pingMaxRetries !== void 0 || options.server !== void 0 || options.verifyClient !== void 0) { import_Logger.logger.warn("DEPRECATION WARNING: 'pingInterval', 'pingMaxRetries', 'server', and 'verifyClient' Server options will be permanently moved to WebSocketTransport on v0.15"); import_Logger.logger.warn(`new Server({ transport: new WebSocketTransport({ pingInterval: ..., pingMaxRetries: ..., server: ..., verifyClient: ... }) })`); import_Logger.logger.warn("\u{1F449} Documentation: https://docs.colyseus.io/server/transport/"); } const transport = options.transport || this.getDefaultTransport(options); this.transport = transport; if (this.transport.server) { this.transport.server.once("listening", () => this.registerProcessForDiscovery()); this.attachMatchMakingRoutes(this.transport.server); } } /** * Bind the server into the port specified. * * @param port * @param hostname * @param backlog * @param listeningListener */ async listen(port, hostname, backlog, listeningListener) { this.port = port; await matchMaker.accept(); if (this.greet) { console.log(import_greeting_banner.default); } return new Promise((resolve, reject) => { this.transport.server?.on("error", (err) => reject(err)); this.transport.listen(port, hostname, backlog, (err) => { if (listeningListener) { listeningListener(err); } if (err) { reject(err); } else { resolve(); } }); }); } async registerProcessForDiscovery() { await (0, import_discovery.registerNode)(this.presence, { port: this.port, processId: matchMaker.processId }); } define(nameOrHandler, handlerOrOptions, defaultOptions) { const name = typeof nameOrHandler === "string" ? nameOrHandler : nameOrHandler.name; const roomClass = typeof nameOrHandler === "string" ? handlerOrOptions : nameOrHandler; const options = typeof nameOrHandler === "string" ? defaultOptions : handlerOrOptions; return matchMaker.defineRoomType(name, roomClass, options); } /** * Remove a room definition from matchmaking. * This method does not destroy any room. It only dissallows matchmaking */ removeRoomType(name) { matchMaker.removeRoomType(name); } async gracefullyShutdown(exit = true, err) { if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) { return; } await (0, import_discovery.unregisterNode)(this.presence, { port: this.port, processId: matchMaker.processId }); try { await this.onBeforeShutdownCallback(); await matchMaker.gracefullyShutdown(); this.transport.shutdown(); this.presence.shutdown(); this.driver.shutdown(); await this.onShutdownCallback(); } catch (e) { (0, import_Debug.debugAndPrintError)(`error during shutdown: ${e}`); } finally { if (exit) { process.exit(err && !import_DevMode.isDevMode ? 1 : 0); } } } /** * Add simulated latency between client and server. * @param milliseconds round trip latency in milliseconds. */ simulateLatency(milliseconds) { if (milliseconds > 0) { import_Logger.logger.warn(`\u{1F4F6}\uFE0F\u2757 Colyseus latency simulation enabled \u2192 ${milliseconds}ms latency for round trip.`); } else { import_Logger.logger.warn(`\u{1F4F6}\uFE0F\u2757 Colyseus latency simulation disabled.`); } const halfwayMS = milliseconds / 2; this.transport.simulateLatency(halfwayMS); if (this._originalRoomOnMessage == null) { this._originalRoomOnMessage = import_Room.Room.prototype["_onMessage"]; } const originalOnMessage = this._originalRoomOnMessage; import_Room.Room.prototype["_onMessage"] = milliseconds <= Number.EPSILON ? originalOnMessage : function(client, buffer) { const cachedBuffer = Buffer.from(buffer); setTimeout(() => originalOnMessage.call(this, client, cachedBuffer), halfwayMS); }; } /** * Register a callback that is going to be executed before the server shuts down. * @param callback */ onShutdown(callback) { this.onShutdownCallback = callback; } onBeforeShutdown(callback) { this.onBeforeShutdownCallback = callback; } getDefaultTransport(_) { throw new Error("Please provide a 'transport' layer. Default transport not set."); } attachMatchMakingRoutes(server) { const listeners = server.listeners("request").slice(0); server.removeAllListeners("request"); server.on("request", (req, res) => { if (req.url.indexOf(`/${matchMaker.controller.matchmakeRoute}`) !== -1) { (0, import_Debug.debugMatchMaking)("received matchmake request: %s", req.url); this.handleMatchMakeRequest(req, res); } else { for (let i = 0, l = listeners.length; i < l; i++) { listeners[i].call(server, req, res); } } }); } async handleMatchMakeRequest(req, res) { if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) { res.writeHead(503, {}); res.end(); return; } const headers = Object.assign( {}, matchMaker.controller.DEFAULT_CORS_HEADERS, matchMaker.controller.getCorsHeaders.call(void 0, req) ); if (req.method === "OPTIONS") { res.writeHead(204, headers); res.end(); } else if (req.method === "POST") { const matchedParams = req.url.match(matchMaker.controller.allowedRoomNameChars); const matchmakeIndex = matchedParams.indexOf(matchMaker.controller.matchmakeRoute); const method = matchedParams[matchmakeIndex + 1]; const roomName = matchedParams[matchmakeIndex + 2] || ""; const data = []; req.on("data", (chunk) => data.push(chunk)); req.on("end", async () => { headers["Content-Type"] = "application/json"; res.writeHead(200, headers); try { const clientOptions = JSON.parse(Buffer.concat(data).toString()); const response = await matchMaker.controller.invokeMethod( method, roomName, clientOptions, { token: (0, import_Utils.getBearerToken)(req.headers["authorization"]), headers: req.headers, ip: req.headers["x-real-ip"] ?? req.headers["x-forwarded-for"] ?? req.socket.remoteAddress, req } ); if (this.transport.protocol !== void 0) { response.protocol = this.transport.protocol; } res.write(JSON.stringify(response)); } catch (e) { res.write(JSON.stringify({ code: e.code, error: e.message })); } res.end(); }); } else if (req.method === "GET") { res.writeHead(404, headers); res.end(); } } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Server });