UNPKG

@bitzonegaming/roleplay-engine-framework

Version:
313 lines (312 loc) 11.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RPServer = void 0; const roleplay_engine_sdk_1 = require("@bitzonegaming/roleplay-engine-sdk"); const event_emitter_1 = require("../core/bus/event-emitter"); const hook_bus_1 = require("../core/bus/hook-bus"); const logger_1 = require("../core/logger"); const socket_1 = require("./socket/socket"); const service_1 = require("./domains/session/service"); const context_1 = require("./core/context"); const service_2 = require("./domains/account/service"); const service_3 = require("./domains/configuration/service"); const service_4 = require("./domains/localization/service"); const service_5 = require("./domains/world/service"); const service_6 = require("./domains/reference/service"); const api_1 = require("./api"); const api_controller_1 = require("./domains/account/api.controller"); const health_controller_1 = require("./api/controllers/health.controller"); const api_controller_2 = require("./domains/session/api.controller"); /** * Main roleplay server class that orchestrates all server functionality. * * This class provides: * - Singleton server instance management * - Complete server lifecycle (start, stop) * - Service registration and dependency injection * - WebSocket connection management * - Integration with roleplay engine APIs * - Event handling and server-to-client communication * * The server follows a singleton pattern and must be created using the static * create() method before use. It automatically registers all core services * (Account, Session, World, Configuration, Localization, Reference, etc.) and * manages their initialization. * * @example * ```typescript * // Create and configure the server * const server = RPServer.create({ * serverId: 'my-roleplay-server', * apiUrl: 'https://api.eu-central-nova.bitzone.com', * socketUrl: 'wss://socket.eu-central-nova.bitzone.com', * apiKeyId: 'your-api-key-id', * apiKeySecret: 'your-api-key-secret', * timeout: 15000 * }, { * s2cEventsAdapter: new MyS2CEventsAdapter() * }); * * // Start the server * await server.start(); * * // Access services through the context * const context = server.getContext(); * const accountService = context.getService(AccountService); * * // Stop the server when done * server.stop(); * ``` */ class RPServer { /** * Private constructor for singleton pattern. * Use RPServer.create() to create an instance. * * @private * @param options - Server configuration options * @param natives - Native integrations and adapters */ constructor(options, natives) { /** Registered API controllers */ this.apiControllers = []; /** Flag to track if shutdown handlers are registered */ this.shutdownHandlersRegistered = false; const logger = options.logger ?? logger_1.defaultLogger; const engineClient = new roleplay_engine_sdk_1.EngineClient({ apiUrl: options.apiUrl, serverId: options.serverId, timeout: options.timeout, applicationName: 'gamemode', }, new roleplay_engine_sdk_1.ApiKeyAuthorization(options.apiKeyId, options.apiKeySecret)); const eventEmitter = new event_emitter_1.RPEventEmitter(); const hookBus = new hook_bus_1.RPHookBus(); this.socket = new socket_1.EngineSocket({ url: options.socketUrl, serverId: options.serverId, apiKeyId: options.apiKeyId, apiKeySecret: options.apiKeySecret, }, eventEmitter, logger); const contextType = natives.customContext?.type ?? context_1.RPServerContext; const contextOptions = { engineClient, eventEmitter, hookBus, logger, ...natives?.customContext?.options, }; this.context = context_1.RPServerContext.create(contextType, contextOptions); this.context .addService(service_3.ConfigurationService) .addService(service_4.LocalizationService) .addService(service_5.WorldService) .addService(service_1.SessionService) .addService(service_6.ReferenceService) .addService(service_2.AccountService); this.apiServer = new api_1.ApiServer(this.context, options.api); this.registerController(health_controller_1.HealthController) .registerController(api_controller_1.AccountController) .registerController(api_controller_2.SessionController); } /** * Creates a new roleplay server instance. * * This factory method creates and configures a new server instance with all * necessary services and connections. The server follows a singleton pattern, * so subsequent calls will replace the previous instance. * * @param config - Server configuration including API endpoints and credentials * @param natives - Native integrations required for game engine communication * @returns A new configured server instance * * @example * ```typescript * const server = RPServer.create({ * serverId: 'my-server', * apiUrl: 'https://api.eu-central-nova.bitzone.com', * socketUrl: 'wss://socket.eu-central-nova.bitzone.com', * apiKeyId: 'your-key-id', * apiKeySecret: 'your-key-secret' * }, { * s2cEventsAdapter: new MyS2CEventsAdapter() * }); * ``` */ static create(config, natives) { this.instance = new RPServer(config, natives); return this.instance; } /** * Gets the singleton server instance. * * Returns the previously created server instance. The server must be created * using RPServer.create() before calling this method. * * @returns The singleton server instance * @throws {Error} When no server instance has been created * * @example * ```typescript * // Somewhere in your application after RPServer.create() * const server = RPServer.get(); * const context = server.getContext(); * ``` */ static get() { if (!RPServer.instance) { throw new Error('RPServer instance is not created. Use RPServer.create() first.'); } return RPServer.instance; } /** * Registers an API controller with the server. * Controllers must be registered before the server starts. * * @param controllerCtor - The controller class constructor * @returns The server instance for method chaining * * @example * ```typescript * server * .registerController(HealthController) * .registerController(SessionController); * ``` */ registerController(controllerCtor) { if (!this.apiServer) { throw new Error('API server is not configured. Set api options in RPServerOptions.'); } this.apiControllers.push(controllerCtor); return this; } /** * Starts the roleplay server. * * This method initializes the WebSocket connection to the roleplay engine * and starts all registered services. It also registers process signal handlers * for graceful shutdown. The server will be ready to handle events and API * requests after this method completes. * * @returns Promise that resolves when the server is fully started * * @example * ```typescript * const server = RPServer.create(config, natives); * await server.start(); * console.log('Server is ready!'); * ``` */ async start() { await this.socket.start(); await this.context.init(); if (this.apiServer) { for (const controller of this.apiControllers) { this.apiServer.registerController(controller); } await this.apiServer.start(); } this.registerShutdownHandlers(); } /** * Stops the roleplay server gracefully. * * This method performs a complete graceful shutdown by: * 1. Stopping the API server if configured * 2. Disposing all services in reverse order * 3. Closing the WebSocket connection to the roleplay engine * 4. Cleaning up all resources * * @returns Promise that resolves when the server is fully stopped * * @example * ```typescript * // Gracefully shutdown the server * await server.stop(); * console.log('Server stopped gracefully'); * ``` */ async stop() { try { if (this.apiServer) { await this.apiServer.stop(); } await this.context.dispose(); } catch (error) { this.context.logger.error('Error during service disposal:', error); } this.socket.close(1000, 'Normal closure'); } /** * Gets the server context for accessing services. * * The context provides dependency injection and service management. * Use this to access any of the registered services (Account, Session, * World, Configuration, etc.). * * @template C - The context type (for custom contexts) * @returns The server context instance * * @example * ```typescript * const context = server.getContext(); * const accountService = context.getService(AccountService); * const sessionService = context.getService(SessionService); * * // For custom contexts * const customContext = server.getContext<MyCustomContext>(); * ``` */ getContext() { return this.context; } /** * Gets the API server instance if configured. * * @returns The API server instance or undefined if not configured */ getApiServer() { return this.apiServer; } /** * Registers process signal handlers for graceful shutdown. * * This method sets up listeners for SIGTERM, SIGINT, and SIGHUP signals * to ensure the server shuts down gracefully when receiving system signals. * This is especially important in containerized environments. * * @private */ registerShutdownHandlers() { if (this.shutdownHandlersRegistered) { return; } const gracefulShutdown = async (signal) => { this.context.logger.info(`Received ${signal}, initiating graceful shutdown...`); try { await this.stop(); this.context.logger.info('Server shutdown completed successfully'); process.exit(0); } catch (error) { this.context.logger.error('Error during graceful shutdown:', error); process.exit(1); } }; // Handle graceful shutdown signals process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGHUP', () => gracefulShutdown('SIGHUP')); // Handle uncaught exceptions and unhandled rejections process.on('uncaughtException', (error) => { this.context.logger.error('Uncaught exception:', error); void gracefulShutdown('uncaughtException'); }); process.on('unhandledRejection', (reason, promise) => { this.context.logger.error(`Unhandled rejection at promise: ${String(promise)}, reason:`, reason); void gracefulShutdown('unhandledRejection'); }); this.shutdownHandlersRegistered = true; } } exports.RPServer = RPServer;