reldens
Version:
Reldens - MMORPG Platform
448 lines (426 loc) • 17.3 kB
JavaScript
/**
*
* Reldens - RoomsManager
*
* Manages room definitions, configurations, and lifecycle for the game server.
*
*/
const { RoomGame } = require('./game');
const { RoomScene } = require('./scene');
const { RoomsConst } = require('../constants');
const { GameConst } = require('../../game/constants');
const { ErrorManager, Logger, sc } = require('@reldens/utils');
/**
* @typedef {import('@reldens/utils').EventsManager} EventsManager
* @typedef {import('@reldens/storage').BaseDataServer} BaseDataServer
* @typedef {import('../../game/server/config-manager').ConfigManager} ConfigManager
* @typedef {import('@colyseus/core').Server} Server
* @typedef {import('../../../generated-entities/models/objection-js/rooms-model').RoomsModel} RoomsModel
*/
class RoomsManager
{
/**
* @param {Object} props
*/
constructor(props)
{
/** @type {EventsManager|boolean} */
this.events = sc.get(props, 'events', false);
if(!this.events){
Logger.error('EventsManager undefined in RoomsManager.');
}
/** @type {BaseDataServer|boolean} */
this.dataServer = sc.get(props, 'dataServer', false);
if(!this.dataServer){
Logger.error('DataServer undefined in RoomsManager.');
}
/** @type {ConfigManager|boolean} */
this.config = sc.get(props, 'config', false);
if(!this.config){
Logger.error('Config undefined in RoomsManager.');
}
/** @type {Array<Object>|boolean} */
this.loadedRooms = false;
/** @type {Object<number, Object>|boolean} */
this.loadedRoomsById = false;
/** @type {Object<string, Object>|boolean} */
this.loadedRoomsByName = false;
/** @type {Array<Object>} */
this.defineExtraRooms = [];
/** @type {Object<string, Object>} */
this.definedRooms = {};
/** @type {Object<string, boolean>} */
this.creatingInstances = {};
/** @type {Object<string, Object>} */
this.createdInstances = {};
/** @type {Object<string, string>} */
this.instanceIdByName = {};
/** @type {Object<string, Object>} */
this.availableRoomsGuest = {};
/** @type {Array<Object>} */
this.registrationAvailableRooms = {};
/** @type {Array<Object>} */
this.registrationAvailableRoomsGuest = {};
/** @type {Array<Object>} */
this.loginAvailableRooms = {};
/** @type {Array<Object>} */
this.loginAvailableRoomsGuest = {};
this.setupConfiguration();
}
/**
* @returns {boolean|void}
*/
setupConfiguration()
{
if(!this.config){
return false;
}
this.selectionConfig = this.config.get('client/rooms/selection', {});
this.allowGuestOnRooms = this.config.getWithoutLogs('server/players/guestUser/allowOnRooms', true);
}
/**
* @param {Server} gameServer
* @param {Object} props
* @returns {Promise<Object>}
*/
async defineRoomsInGameServer(gameServer, props)
{
await this.events.emit('reldens.roomsDefinition', this.defineExtraRooms);
if(!this.defineExtraRooms.length){
Logger.info('None extra rooms to be defined.');
}
// dispatch event to get the global message actions (that will be listened by every room):
let globalMessageActions = {};
await this.events.emit('reldens.roomsMessageActionsGlobal', globalMessageActions);
// lobby room:
await this.defineRoom(gameServer, GameConst.ROOM_GAME, RoomGame, props, globalMessageActions);
Logger.info('Loaded game room using stored configuration.');
let counter = await this.defineExtraRoomsInGameServer(gameServer, props, globalMessageActions);
let rooms = await this.loadRooms();
// register room-scenes from storage:
counter = await this.defineRoomsFromModels(rooms, props, gameServer, globalMessageActions, counter);
Logger.info('Total rooms loaded: '+counter);
// save rooms lists data for clients:
if(this.config.client?.rooms?.selection){
this.config.client.rooms.selection.availableRooms = {
registration: this.registrationAvailableRooms,
registrationGuest: this.registrationAvailableRoomsGuest,
login: this.loginAvailableRooms,
loginGuest: this.loginAvailableRoomsGuest
};
}
await this.events.emit('reldens.defineRoomsInGameServerDone', this);
return this.definedRooms;
}
/**
* @param {Array<Object>} rooms
* @param {Object} props
* @param {Server} gameServer
* @param {Object} globalMessageActions
* @param {number} counter
* @returns {Promise<number>}
*/
async defineRoomsFromModels(rooms, props, gameServer, globalMessageActions, counter)
{
if(0 === rooms.length){
return counter;
}
for(let roomModel of rooms){
let roomClass = RoomScene;
if(roomModel.roomClassPath){
let roomClassDefinition = props.config.get('server/customClasses/roomsClass/'+roomModel.roomClassPath);
if(!roomClassDefinition){
Logger.error('Custom room class not found for room ID "'+roomModel.roomId+'".');
continue;
}
roomClass = roomClassDefinition;
}
await this.defineRoom(gameServer, roomModel.roomName, roomClass, props, globalMessageActions, roomModel);
counter++;
Logger.info('Loaded room: '+roomModel.roomName);
}
return counter;
}
/**
* @param {Server} gameServer
* @param {Object} props
* @param {Object} globalMessageActions
* @returns {Promise<number>}
*/
async defineExtraRoomsInGameServer(gameServer, props, globalMessageActions)
{
if(0 === this.defineExtraRooms.length){
return 0;
}
let counter = 0;
for(let roomData of this.defineExtraRooms){
await this.defineRoom(gameServer, roomData.roomName, roomData.room, props, globalMessageActions);
counter++;
Logger.info('Loaded extra room: '+roomData.roomName+(roomData.serverUrl ? '('+roomData.serverUrl+')' : ''));
}
return counter;
}
/**
* @param {Server} gameServer
* @param {string} roomName
* @param {Function} roomClass
* @param {Object} props
* @param {Object} globalMessageActions
* @param {Object|boolean} [roomModel]
* @returns {Promise<void>}
*/
async defineRoom(gameServer, roomName, roomClass, props, globalMessageActions, roomModel = false)
{
let roomMessageActions = Object.assign({}, globalMessageActions);
// run message actions event for each room:
await this.events.emit('reldens.roomsMessageActionsByRoom', roomMessageActions, roomName);
let roomProps = {
loginManager: props.loginManager,
config: props.config,
messageActions: roomMessageActions,
events: this.events,
roomsManager: this,
dataServer: this.dataServer,
featuresManager: props.featuresManager
};
if(roomModel){
roomProps.roomData = roomModel;
}
gameServer.define(roomName, roomClass, roomProps);
this.definedRooms[roomName] = {roomClass, roomProps};
}
/**
* @returns {Promise<Array<Object>>}
*/
async loadRooms()
{
// @TODO - BETA - This will change when hot-plug is introduced.
if(this.loadedRooms){
return this.loadedRooms;
}
let roomsModels = await this.dataServer.getEntity('rooms').loadAllWithRelations([
'related_rooms_change_points_room.related_rooms_next_room',
'related_rooms_return_points_room.related_rooms_from_room'
]);
if(!roomsModels){
ErrorManager.error('None rooms found in the database. A room is required to run.');
}
let rooms = [];
let roomsById = {};
let roomsByName = {};
for(let room of roomsModels){
let roomModel = this.generateRoomModel(room);
if(!roomModel){
Logger.error('Room model could not be generated.', room);
continue;
}
rooms.push(roomModel);
roomsById[room.id] = roomModel;
roomsByName[room.name] = roomModel;
}
this.loadedRooms = rooms;
this.loadedRoomsById = roomsById;
this.loadedRoomsByName = roomsByName;
this.availableRoomsGuest = this.filterGuestRooms(roomsByName);
let registrationRooms = this.filterRooms(true);
this.registrationAvailableRooms = this.extractRoomDataForSelector(registrationRooms);
this.registrationAvailableRoomsGuest = this.extractRoomDataForSelector(this.fetchGuestRooms(registrationRooms));
let loginRooms = this.filterRooms(false);
this.loginAvailableRooms = this.extractRoomDataForSelector(loginRooms);
this.loginAvailableRoomsGuest = this.extractRoomDataForSelector(this.fetchGuestRooms(loginRooms));
return this.loadedRooms;
}
/**
* @param {number} roomId
* @returns {Promise<Object|boolean>}
*/
async loadRoomById(roomId)
{
return this.loadRoomBy('id', roomId);
}
/**
* @param {string} roomName
* @returns {Promise<Object|boolean>}
*/
async loadRoomByName(roomName)
{
return this.loadRoomBy('name', roomName);
}
/**
* @param {string} property
* @param {string|number} value
* @returns {Promise<Object|boolean>}
*/
async loadRoomBy(property, value)
{
try {
let propertyLabel = property.charAt(0).toUpperCase()+property.slice(1);
//Logger.debug('Load rooms by: '+propertyLabel);
let managerProperty = 'loadedRoomsBy'+propertyLabel; // see this.loadedById and this.loadedByName
// first try to fetch a loaded room from the loadedRoomsBy properties:
if(this[managerProperty][value]){
return this[managerProperty][value];
}
// if the room was not preloaded when it was defined, then reloaded here:
let room = await this.dataServer.getEntity('rooms').loadBy(property, value);
if(!room){
Logger.critical('Load room by "'+property+'" with value "'+value+'", not found.');
return false;
}
let roomModel = this.generateRoomModel(room);
if(!roomModel){
Logger.critical('Loading room by "'+property+'" with value "'+value+'", model could not be generated.');
return false;
}
this.loadedRooms.push(roomModel);
this.loadedRoomsById[room.id] = roomModel;
this.loadedRoomsByName[room.name] = roomModel;
// @TODO - BETA - When moving rooms selection config to rooms.customData include new rooms ("roomModel") in:
// - registrationAvailableRooms
// - registrationAvailableRoomsGuest
// - loginAvailableRooms
// - loginAvailableRoomsGuest
return roomModel;
} catch (error) {
Logger.critical('Load room by "'+property+'" with value "'+value+'" failed.', error);
}
return false;
}
/**
* @param {RoomsModel} room
* @returns {Object|boolean}
*/
generateRoomModel(room)
{
if(!sc.isObject(room) || 0 === Object.keys(room).length){
Logger.critical('Room not available.', room);
return false;
}
let roomDataModel = {
roomId: room.id,
roomName: room.name,
roomTitle: room.title,
serverUrl: room.server_url,
// @NOTE: the roomMap is the map file name without the extension (the extension is only used in the admin).
roomMap: (room?.map_filename || '').toString().replace(/^.*[\\/]/, '').replace(/\.[^.]*$/, ''),
sceneImages: room.scene_images.split(','),
changePoints: [],
returnPoints: [],
roomClassPath: room.room_class_key,
returnPointDefault: false,
customData: sc.toJson(room.customData, {})
};
for(let changePoint of room.related_rooms_change_points_room){
let changePointData = {};
changePointData[RoomsConst.TILE_INDEX] = changePoint.tile_index;
changePointData[RoomsConst.NEXT_SCENE] = changePoint.related_rooms_next_room.name;
roomDataModel.changePoints.push(changePointData);
}
for(let returnPosition of room.related_rooms_return_points_room){
let fromRoom = returnPosition.related_rooms_from_room
? returnPosition.related_rooms_from_room.name
: false;
let position = {
[RoomsConst.RETURN_POINT_KEYS.DIRECTION]: returnPosition.direction,
[RoomsConst.RETURN_POINT_KEYS.X]: returnPosition.x,
[RoomsConst.RETURN_POINT_KEYS.Y]: returnPosition.y,
[RoomsConst.RETURN_POINT_KEYS.PREVIOUS]: fromRoom
};
if(sc.hasOwn(returnPosition, 'is_default') && returnPosition.is_default){
position[RoomsConst.RETURN_POINT_KEYS.DEFAULT] = returnPosition.is_default;
roomDataModel.returnPointDefault = position;
}
roomDataModel.returnPoints.push(position);
}
if(0 === roomDataModel.returnPoints.length){
Logger.warning('Return points found for room: '+roomDataModel.roomName+'. Room ID: '+roomDataModel.roomId);
}
if(!roomDataModel.returnPointDefault){
roomDataModel.returnPointDefault = roomDataModel.returnPoints[0];
}
return roomDataModel;
}
/**
* @param {boolean} forRegistration
* @returns {Object<string, Object>|Array<Object>}
*/
filterRooms(forRegistration)
{
// @TODO - BETA - Move rooms selection configuration to rooms.customData.
let configuredRooms = this.fetchConfiguredRoomsList(forRegistration);
if('*' === configuredRooms){
return this.loadedRoomsByName;
}
return this.filterValidRooms(configuredRooms.split(','), this.loadedRoomsByName);
}
/**
* @param {boolean} forRegistration
* @returns {string}
*/
fetchConfiguredRoomsList(forRegistration)
{
if(forRegistration){
return this.selectionConfig.registrationAvailableRooms;
}
return this.selectionConfig.loginAvailableRooms;
}
/**
* @param {Array<string>} configuredRooms
* @param {Object<string, Object>} createdRooms
* @returns {Array<Object>}
*/
filterValidRooms(configuredRooms, createdRooms)
{
let validRooms = [];
for(let roomName of configuredRooms){
if(sc.hasOwn(createdRooms, roomName)){
validRooms.push(configuredRooms[roomName]);
}
}
return validRooms;
}
/**
* @param {Object<string, Object>|Array<Object>} availableRooms
* @returns {Object<string, Object>|Array<Object>}
*/
fetchGuestRooms(availableRooms)
{
if(this.allowGuestOnRooms){
return availableRooms;
}
return this.filterGuestRooms(availableRooms);
}
/**
* @param {Object<string, Object>} availableRooms
* @returns {Object<string, Object>}
*/
filterGuestRooms(availableRooms)
{
if(!sc.isObject(availableRooms)){
Logger.debug('The provided "availableRooms" is not an object.', availableRooms);
return {};
}
let validRooms = {};
for(let i of Object.keys(availableRooms)){
let room = availableRooms[i];
let customData = room.customData || {};
if(sc.get(customData, 'allowGuest')){
validRooms[room.roomName] = room;
}
}
return validRooms;
}
/**
* @param {Object<string, Object>|Array<Object>} availableRooms
* @returns {Array<Object>}
*/
extractRoomDataForSelector(availableRooms)
{
let titles = [];
for(let i of Object.keys(availableRooms)){
titles.push({name: availableRooms[i].roomName, title: availableRooms[i].roomTitle});
}
return titles;
}
}
module.exports.RoomsManager = RoomsManager;