UNPKG

@colyseus/core

Version:

Multiplayer Framework for Node.js.

225 lines (224 loc) 7.51 kB
// packages/core/src/Server.ts import { greet } from "@colyseus/greeting-banner"; import { debugAndPrintError } from "./Debug.mjs"; import * as matchMaker from "./MatchMaker.mjs"; import { RegisteredHandler } from "./matchmaker/RegisteredHandler.mjs"; import { Room } from "./Room.mjs"; import { Deferred, registerGracefulShutdown, dynamicImport } from "./utils/Utils.mjs"; import { setTransport } from "./Transport.mjs"; import { logger, setLogger } from "./Logger.mjs"; import { setDevMode, isDevMode } from "./utils/DevMode.mjs"; import { bindRouterToTransport } from "./router/index.mjs"; import "@colyseus/shared-types"; import { getDefaultRouter } from "./router/default_routes.mjs"; var Server = class { constructor(options = {}) { this._onTransportReady = new Deferred(); this._originalRoomOnMessage = null; this.onShutdownCallback = () => Promise.resolve(); this.onBeforeShutdownCallback = () => Promise.resolve(); const { gracefullyShutdown: gracefullyShutdown2 = true, greet: greet2 = true } = options; setDevMode(options.devMode === true); this.options = options; this.greet = greet2; this.attach(options); matchMaker.setup( options.presence, options.driver, options.publicAddress, options.selectProcessIdToCreateRoom ).then(() => { this.presence = matchMaker.presence; this.driver = matchMaker.driver; }); if (gracefullyShutdown2) { registerGracefulShutdown((err) => this.gracefullyShutdown(true, err)); } if (options.logger) { setLogger(options.logger); } } async attach(options) { this.transport = options.transport || await this.getDefaultTransport(options); if (options.express && this.transport.getExpressApp) { const expressApp = await this.transport.getExpressApp(); await options.express(expressApp); } this._onTransportReady.resolve(this.transport); } /** * Bind the server into the port specified. * * @param port - Port number or Unix socket path * @param hostname * @param backlog * @param listeningListener */ async listen(port, hostname, backlog, listeningListener) { if (this.options.beforeListen) { await this.options.beforeListen(); } if (process.env.COLYSEUS_CLOUD !== void 0) { if (typeof hostname === "number") { hostname = void 0; } else { try { return (await dynamicImport("@colyseus/tools")).listen(this); } catch (error) { const err = new Error("Please install @colyseus/tools to be able to host on Colyseus Cloud."); err.cause = error; throw err; } } } this.port = port; await matchMaker.accept(this.options.isStandaloneMatchMaker); if (this.greet) { greet(); } await this._onTransportReady; return new Promise((resolve, reject) => { setTransport(this.transport); this.transport.listen(port, hostname, backlog, (err) => { if (this.transport.server) { this.transport.server.on("error", (err2) => reject(err2)); } if (!this.router) { this.router = getDefaultRouter(); } else { this.router = this.router.extend({ ...getDefaultRouter().endpoints }); } bindRouterToTransport(this.transport, this.router, this.options.express !== void 0); if (listeningListener) { listeningListener(err); } if (err) { reject(err); } else { resolve(); } }); }); } 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; } try { await this.onBeforeShutdownCallback(); await matchMaker.gracefullyShutdown(); this.transport.shutdown(); this.presence?.shutdown(); await this.driver?.shutdown(); await this.onShutdownCallback(); } catch (e) { debugAndPrintError(`error during shutdown: ${e}`); } finally { if (exit) { process.exit(err && !isDevMode ? 1 : 0); } } } /** * Add simulated latency between client and server. * @param milliseconds round trip latency in milliseconds. */ simulateLatency(milliseconds) { if (milliseconds > 0) { logger.warn(`\u{1F4F6}\uFE0F\u2757 Colyseus latency simulation enabled \u2192 ${milliseconds}ms latency for round trip.`); } else { logger.warn(`\u{1F4F6}\uFE0F\u2757 Colyseus latency simulation disabled.`); } const halfwayMS = milliseconds / 2; this.transport.simulateLatency(halfwayMS); if (this._originalRoomOnMessage == null) { this._originalRoomOnMessage = Room.prototype["_onMessage"]; } const originalOnMessage = this._originalRoomOnMessage; 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; } async getDefaultTransport(options) { try { const module = await dynamicImport("@colyseus/ws-transport"); const WebSocketTransport = module.WebSocketTransport; return new WebSocketTransport(options); } catch (error) { this._onTransportReady.reject(error); throw new Error("Please provide a 'transport' layer. Default transport not set."); } } }; function isRegisteredHandler(value) { return value instanceof RegisteredHandler || typeof value === "object" && value !== null && "klass" in value; } function registerRoomDefinitions(rooms) { const roomNames = []; for (const [name, value] of Object.entries(rooms)) { if (isRegisteredHandler(value)) { value.name = name; matchMaker.addRoomType(value); } else { matchMaker.defineRoomType(name, value); } roomNames.push(name); } return roomNames; } function unregisterRoomDefinitions(roomNames) { for (const roomName of roomNames) { matchMaker.removeRoomType(roomName); } } function defineServer(options) { const { rooms, routes, ...serverOptions } = options; if (isDevMode) { return { options: serverOptions, router: routes, "~rooms": rooms }; } const server = new Server(serverOptions); server.router = routes; registerRoomDefinitions(rooms); return server; } function defineRoom(roomKlass, defaultOptions) { return new RegisteredHandler(roomKlass, defaultOptions); } export { Server, defineRoom, defineServer, registerRoomDefinitions, unregisterRoomDefinitions };