@bitzonegaming/roleplay-engine-framework
Version:
Roleplay Engine Framework
313 lines (312 loc) • 11.7 kB
JavaScript
;
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;