reldens
Version:
Reldens - MMORPG Platform
1,029 lines (973 loc) • 43.7 kB
JavaScript
/**
*
* Reldens - RoomScene
*
*/
const { RoomLogin } = require('./login');
const { State } = require('./state');
const { WorldConfig } = require('./world-config');
const { P2world } = require('../../world/server/p2world');
const { CollisionsManager } = require('../../world/server/collisions-manager');
const { WorldPointsValidator } = require('../../world/world-points-validator');
const { WorldTimer } = require('../../world/world-timer');
const { ObjectsManager } = require('../../objects/server/manager');
const { DropObject } = require('../../objects/server/object/type/drop-object');
const { ObjectsConst } = require('../../objects/constants');
const { JoinedSceneRoomEvent } = require('./events/joined-scene-room-event');
const { RandomPlayerState } = require('./random-player-state');
const { RoomsConst } = require('../constants');
const { WorldConst } = require('../../world/constants');
const { GameConst } = require('../../game/constants');
const { ErrorManager, Logger, sc } = require('@reldens/utils');
class RoomScene extends RoomLogin
{
async onCreate(options)
{
super.onCreate(options);
Logger.notice('Creation process started for RoomScene "'+this.roomName+'" (ID: '+this.roomId+').');
this.autoDispose = sc.get(options.roomData, 'autoDispose', false);
this.disposeTimeoutMs = this.config.getWithoutLogs('server/rooms/disposeTimeout', 300000);
this.disposeTimeoutTimer = false;
this.roomWorld = {};
this.messageActions = {};
this.movementInterval = {};
this.playersLastStateTimers = {};
this.worldTimerCallback = false;
this.roomType = RoomsConst.ROOM_TYPE_SCENE;
this.validateRoomData = true;
this.sceneId = this.roomId;
// @NOTE: we create an instance of the objects manager for each room-scene, this is on purpose so all the
// related object instances will be removed when the room is disposed.
let objectsManagerConfig = Object.assign(
{events: this.events, roomName: this.roomName, roomId: this.roomId},
options
);
this.objectsManager = new ObjectsManager(objectsManagerConfig);
await this.objectsManager.loadObjectsByRoomId(options.roomData.roomId);
if(this.objectsManager.roomObjectsData){
await this.objectsManager.generateObjects();
//Logger.debug('Generated objects keys: '+JSON.stringify((Object.keys(this.objectsManager.roomObjectsData))));
}
// append objects that will listen messages:
if(this.objectsManager.listenMessages){
Object.assign(this.messageActions, this.objectsManager.listenMessagesObjects);
}
this.lastCallTime = Date.now() / 1000;
this.paused = false;
this.customData = options.roomData.customData || {};
this.joinInRandomPlace = Boolean(this.customData.joinInRandomPlace || false);
this.joinInRandomPlaceAlways = Boolean(this.customData.joinInRandomPlaceAlways || false);
this.joinInRandomPlaceGuestAlways = Boolean(this.customData.joinInRandomPlaceGuestAlways || false);
if(this.customData.patchRate){
//Logger.debug('Setting custom patch rate: '+this.customData.patchRate);
this.setPatchRate(this.customData.patchRate);
}
WorldConfig.mapWorldConfigValues(this, this.config);
this.allowSimultaneous = this.config.get('client/general/controls/allowSimultaneousKeys', true);
await this.createWorld(options.roomData, this.objectsManager);
this.applyPlayersLastStateHandler = Boolean(this.config.getWithoutLogs('server/players/stateHandler/enabled'));
this.applyStateOnZeroAffectedProperty = this.config.getWithoutLogs(
'server/players/state/forZeroAffectedProperty',
GameConst.STATUS.DEATH
);
this.playersAffectedProperty = this.config.get('client/actions/skills/affectedProperty');
this.playerBodyWidth = this.config.get('client/players/physicalBody/width');
this.playerBodyHeight = this.config.get('client/players/physicalBody/height');
this.playersStateHandlerTimeOut = Number(
this.config.getWithoutLogs('server/players/stateHandler/timeOut', 10000)
);
// the collisions manager has to be initialized after the world was created:
this.collisionsManager = new CollisionsManager(this);
// if the room has message actions those are specified here in the room-scene:
if(options.messageActions){
Object.assign(this.messageActions, options.messageActions);
}
// @NOTE: as you can see not all the scene information is being sent to the client, this is because we have
// hidden information to be discovered (hidden objects are only active on the server side).
this.roomData = options.roomData;
// append public objects to the room data:
this.roomData.preloadAssets = this.objectsManager.preloadAssets;
// append dynamic animations data to the room data:
this.roomData.objectsAnimationsData = this.objectsManager.objectsAnimationsData;
Object.assign(this.worldConfig, this.roomData.customData);
Object.assign(this.roomData, {worldConfig: this.worldConfig});
// room data is saved on the state:
let roomState = new State(this.roomData);
// after we set the state it will be automatically sync by the game-server:
this.setState(roomState);
this.randomPlayerState = this.joinInRandomPlace ? new RandomPlayerState(this) : false;
await this.events.emit('reldens.sceneRoomOnCreate', this);
Logger.notice(
'Created RoomScene: '+this.roomName+' ('+this.roomId+') - AutoDispose: '+(this.autoDispose ? 'Yes' : 'No')
);
delete options.roomsManager.creatingInstances[this.roomName];
}
async onJoin(client, options, userModel)
{
if(this.disposeTimeoutTimer){
//Logger.debug('Cleared dispose timeout on room "'+this.roomName+'".');
clearTimeout(this.disposeTimeoutTimer);
this.disposeTimeoutTimer = false;
}
await this.events.emit('reldens.joinRoomStart', this, client, options, userModel);
if(sc.hasOwn(options, 'selectedPlayer')){
userModel.selectedPlayer = options.selectedPlayer;
userModel.player = this.getPlayerByIdFromArray(userModel.players, options.selectedPlayer);
}
let isGuest = -1 !== userModel.email.indexOf(this.guestEmailDomain);
if(this.validateRoomData){
if(!this.validateRoom(userModel.player.state.scene, isGuest)){
await this.events.emit('reldens.joinRoomInvalid', this, client, options, userModel, isGuest);
return false;
}
}
let loggedPlayer = await this.createPlayerOnScene(client, userModel, isGuest);
// we do not need to create a player entity since we only need the name for the chat:
this.loginManager.activePlayers.add(userModel, client, this);
await this.events.emit(
'reldens.joinRoomEnd',
new JoinedSceneRoomEvent(this, client, options, userModel, loggedPlayer, isGuest)
);
}
async disconnectBySessionId(sessionId, client, userModel)
{
let player = this.playerBySessionIdFromState(sessionId);
if(!player){
//Logger.debug('Player not found for disconnection by session ID "'+sessionId+'".');
return false;
}
await this.events.emit('reldens.disconnectLoggedBefore', {room: this, player, client, userModel});
await this.savePlayedTime(player);
let savedStats = await this.savePlayerStats(player);
let savedState = await this.savePlayerState(sessionId);
if(savedState && savedStats){
await this.removePlayer(sessionId);
}
return true;
}
playerBySessionIdFromState(sessionId)
{
// @TODO - BETA - Refactor and extract Colyseus into a driver.
return this.state.players.get(sessionId);
}
playerByPlayerIdFromState(playerId)
{
if(0 === this.playersCountInState()){
return false;
}
for(let i of this.playersKeysFromState()){
let tmp = this.playerBySessionIdFromState(i);
if(tmp.player_id === playerId){
return tmp;
}
}
return false;
}
playersCountInState()
{
// @TODO - BETA - Refactor and extract Colyseus into a driver.
return this.state.players.size;
}
playersKeysFromState()
{
// @TODO - BETA - Refactor and extract Colyseus into a driver.
return Array.from(this.state.players.keys());
}
async createPlayerOnScene(client, userModel, isGuest)
{
await this.events.emit('reldens.createPlayerBefore', client, userModel, this);
let currentPlayer = this.state.createPlayerSchema(userModel, client.sessionId);
this.handlePlayerLastState(currentPlayer);
if(this.randomPlayerState){
this.randomPlayerState.randomizeLocation(currentPlayer, isGuest);
}
// @TODO - BETA - Remove this callbacks and provide a player container that will executed the required methods.
currentPlayer.persistData = async (params) => {
await this.events.emit('reldens.playerPersistDataBefore', client, userModel, currentPlayer, params, this);
await this.savePlayedTime(currentPlayer);
await this.savePlayerState(currentPlayer.sessionId);
await this.savePlayerStats(currentPlayer, client);
await this.events.emit('reldens.playerPersistDataAfter', client, userModel, currentPlayer, params, this);
};
await this.events.emit('reldens.createdPlayerSchema', client, userModel, currentPlayer, this);
currentPlayer.playStartTime = Date.now();
this.state.addPlayerToState(currentPlayer, client.sessionId);
// @TODO - BETA - Create player body using a new pack in the world package.
currentPlayer.physicalBody = this.roomWorld.createPlayerBody({
id: client.sessionId,
width: this.playerBodyWidth,
height: this.playerBodyHeight,
bodyState: currentPlayer.state,
speed: Number(currentPlayer.stats?.speed || 0)
});
await this.events.emit('reldens.createPlayerAfter', client, userModel, currentPlayer, this);
return currentPlayer;
}
handlePlayerLastState(currentPlayer)
{
if(!this.applyPlayersLastStateHandler){
return false;
}
// @TODO - BETA - Include a timer and a configuration to avoid player been attacked automatically after login.
if(!this.playersAffectedProperty){
return false;
}
let hasLife = 0 < Number(sc.get(currentPlayer.stats, this.playersAffectedProperty, 0));
if(hasLife){
return false;
}
currentPlayer.state.inState = this.applyStateOnZeroAffectedProperty;
this.playersLastStateTimers[currentPlayer.player_id] = setTimeout(
() => {
currentPlayer.state.inState = GameConst.STATUS.ACTIVE;
},
this.playersStateHandlerTimeOut
);
return false;
}
async onLeave(client, consented)
{
let playerSchema = this.playerBySessionIdFromState(client.sessionId);
if(!playerSchema){
// this is expected when the player is automatically disconnected on the second login:
// Logger.info('Save error, schema not found for session ID: '+client.sessionId, client.auth.username);
return;
}
await this.savePlayedTime(playerSchema);
let savedStats = await this.savePlayerStats(playerSchema);
if(!savedStats){
Logger.error('Player save stats error.', playerSchema);
}
let savedState = await this.savePlayerState(client.sessionId);
if(!savedState){
Logger.error('Player save state error.', playerSchema);
}
let isChangingScene = playerSchema.physicalBody?.isChangingScene;
let removed = await this.removePlayer(client.sessionId);
if(!removed){
Logger.error('Player remove error.', playerSchema);
}
clearTimeout(this.playersLastStateTimers[playerSchema.player_id]);
this.removeActivePlayer(playerSchema, client, isChangingScene);
if(0 === this.clients.length){
//Logger.debug('Setting dispose timeout ('+this.disposeTimeoutMs+'ms) on room "'+this.roomName+'".');
this.disposeTimeoutTimer = setTimeout(() => {
this.disconnect();
}, this.disposeTimeoutMs);
}
}
async handleReceivedMessage(client, messageData)
{
if(!messageData){
Logger.error('Empty message data on room "'+this.roomName+'" ID "'+this.roomId+'".', client, messageData);
return;
}
// only process the message if the player exists and has a body:
let playerSchema = this.playerBySessionIdFromState(client.sessionId);
await this.executeSceneMessageActions(client, messageData, playerSchema);
await this.executeMovePlayerActions(playerSchema, messageData);
this.executePlayerStatsAction(messageData, client, playerSchema);
}
executePlayerStatsAction(messageData, client, playerSchema)
{
// @NOTE:
// - Player states must be requested since are private user data that we can share with other players
// or broadcast to the rooms.
// - Considering base value could be changed temporally by a skill or item modifier will be really hard to
// identify which calls and cases would require only the stat data or the statBase, so we will always send
// both values. This could be improved in the future but for now it doesn't have a considerable impact.
if(GameConst.PLAYER_STATS !== messageData.act){
return false;
}
client.send('*', {
act: GameConst.PLAYER_STATS,
stats: playerSchema.stats,
statsBase: playerSchema.statsBase
});
}
async executeMovePlayerActions(playerSchema, messageData)
{
let bodyToMove = sc.get(playerSchema, 'physicalBody', false);
if(!bodyToMove){
Logger.warning('Player ID "'+playerSchema.player_id+'" body not found.');
return false;
}
if(playerSchema.isDeath() || playerSchema.isDisabled()){
this.clearMovementIntervals(playerSchema.player_id);
bodyToMove.stopMove();
return false;
}
if(GameConst.STOP === messageData.act){
this.clearMovementIntervals(playerSchema.player_id);
bodyToMove.stopMove();
}
let bodyCanMove = !bodyToMove.isChangingScene && !bodyToMove.isBlocked;
if(!bodyCanMove){
return false;
}
if(sc.hasOwn(messageData, 'dir')){
// @TODO - BETA - Create a single class/point for setIntervals or timeOuts instances creation.
let playerMovementIntervalKey = playerSchema.player_id+'_'+messageData.dir;
if(!bodyToMove.world.timeStep){
return false;
}
this.movementInterval[playerMovementIntervalKey] = setInterval(
() => {
bodyToMove.initMove(messageData.dir);
},
bodyToMove.world.timeStep
);
return true;
}
let isPointer = GameConst.POINTER === messageData.act && this.config.get('client/players/tapMovement/enabled');
let hasColumnAndRow = sc.hasOwn(messageData, 'column') && sc.hasOwn(messageData, 'row');
if(isPointer && hasColumnAndRow){
this.clearMovementIntervals(playerSchema.player_id);
bodyToMove.stopMove();
messageData = this.pointsValidator.makeValidPoints(messageData);
bodyToMove.moveToPoint(messageData);
}
}
activatePlayer(playerSchema, playerNewState)
{
//Logger.debug('Activate player "'+playerSchema.player_id+'".');
playerSchema.state.inState = playerNewState;
let bodyToMove = sc.get(playerSchema, 'physicalBody', false);
this.activateBody(bodyToMove, playerSchema.player_id, playerNewState);
}
activateBody(bodyToMove, playerId, playerNewState)
{
if(!bodyToMove){
//Logger.warning('Activate Player ID "'+playerId+'" body not found.');
return false;
}
bodyToMove.isBlocked = false;
bodyToMove.bodyState.inState = playerNewState;
return true;
}
deactivatePlayer(playerSchema, playerNewState)
{
Logger.debug('Deactivate player "'+playerSchema.player_id+'", new inState value "'+playerNewState+'".');
this.clearMovementIntervals(playerSchema.player_id);
playerSchema.state.inState = playerNewState;
this.deactivateBody(sc.get(playerSchema, 'physicalBody', false), playerSchema, playerNewState);
}
deactivateBody(bodyToMove, playerId, playerNewState)
{
if(!bodyToMove){
Logger.warning('Deactivate Player ID "'+playerId+'" body not found.');
return false;
}
bodyToMove.stopFull();
bodyToMove.resetAuto();
bodyToMove.isBlocked = true;
bodyToMove.bodyState.inState = playerNewState;
return true;
}
clearMovementIntervals(playerId)
{
let intervalKeys = Object.keys(this.movementInterval);
for(let i of intervalKeys){
if(playerId && 0 !== i.indexOf(playerId+'_')){
continue;
}
clearInterval(this.movementInterval[i]);
delete this.movementInterval[i];
// Logger.debug('Cleared movement interval: '+i);
}
}
async executeSceneMessageActions(client, messageData, playerSchema)
{
let event = {room: this, client, messageData, playerSchema, canContinue: true};
await this.events.emit('reldens.beforeSceneExecuteMessages', event);
if(!event.canContinue){
return;
}
let messageActionsKeys = Object.keys(this.messageActions);
if(0 === messageActionsKeys.length){
return;
}
if(!this.isAllowedAction(client, messageData, playerSchema)){
return;
}
for(let i of messageActionsKeys){
let messageObserver = this.messageActions[i];
if('function' !== typeof messageObserver.executeMessageActions){
Logger.warning('Invalid message observer.', messageObserver);
continue;
}
await messageObserver.executeMessageActions(client, messageData, this, playerSchema);
}
}
isAllowedAction(client, messageData, playerSchema)
{
if(this.customData.allActionsDisabled){
return false;
}
let disallowedActions = sc.get(this.customData, 'disallowedActions', {});
if(0 === Object.keys(disallowedActions).length){
return true;
}
if(sc.inArray(messageData['act'], disallowedActions)){
return false;
}
let playerSkill = playerSchema.skillsServer.classPath.currentSkills[messageData.type] || {};
let skillTypeClass = playerSkill?.constructor?.name;
if(skillTypeClass && sc.inArray(skillTypeClass, disallowedActions)){
return false;
}
let target = messageData.target?.type;
if(!target){
return true;
}
if(sc.inArray('target-'+target, disallowedActions)){
return false;
}
if(sc.inArray('target-'+target+'-'+messageData['act'], disallowedActions)){
return false;
}
return !sc.inArray('target-'+target+'-'+skillTypeClass, disallowedActions);
}
// @TODO - BETA - Extract all world or bodies creation functions into a new physics driver.
async createWorld(roomData, objectsManager)
{
await this.events.emit('reldens.createWorld', roomData, objectsManager, this);
this.roomWorld = this.createWorldInstance({
sceneName: this.roomName,
roomId: roomData.roomId,
roomMap: roomData.roomMap,
config: this.config,
objectsManager: objectsManager,
events: this.events,
allowSimultaneous: this.allowSimultaneous,
worldConfig: this.worldConfig
});
this.pointsValidator = new WorldPointsValidator(this.roomWorld.worldWidth, this.roomWorld.worldHeight);
this.roomWorld.createLimits();
this.validateWorldContents(roomData.roomMap);
await this.roomWorld.createWorldContent(roomData);
this.initializeWorldTimer();
Logger.info('World created in Room: ' + this.roomName);
}
validateWorldContents(roomMap)
{
if(!sc.get(this.config.server, 'validateMaps', true)){
return;
}
let mapJson = sc.get(this.config.server.maps, roomMap, false);
if(!mapJson){
Logger.critical('Undefined map JSON on config server maps: "'+this.sceneTiledMapFile+'"');
return;
}
let contentsLayers = Object.keys(this.objectsManager.roomObjectsByLayer);
let mapLayers = mapJson.layers.map((layer) => layer.name);
let missingLayers = contentsLayers.filter(layer => !mapLayers.includes(layer));
if(0 < missingLayers.length){
Logger.warning(
'There are objects assigned to the room but that contain invalid layer names.',
missingLayers
);
for(let missingLayer of missingLayers){
let layerObjectsIds = Object.keys(this.objectsManager.roomObjectsByLayer[missingLayers]);
Logger.warning('Layer "'+missingLayer+'" invalid objects ID(s): '+JSON.stringify(layerObjectsIds));
}
}
}
initializeWorldTimer()
{
this.worldTimerCallback = () => {
if(!this.roomWorld){
Logger.error('Room World not longer exists.', this.roomWorld);
return;
}
this.roomWorld.removeBodiesFromWorld();
};
this.worldTimer = new WorldTimer({
// @TODO - BETA - Create a single class/point for setIntervals or timeOuts instances creation.
// clockInstance: this.clock,
world: this.roomWorld,
callbacks: [this.worldTimerCallback]
});
this.worldTimer.startWorldSteps(this.roomWorld);
}
createWorldInstance(data)
{
return new P2world(data);
}
async nextSceneInitialPosition(client, data, playerBody)
{
let currentPlayer = this.playerBySessionIdFromState(client.sessionId);
if(!currentPlayer){
Logger.error('Player not found by session ID:' + client.sessionId);
return;
}
// @TODO - BETA - Avoid sending the player next scene to all the players.
this.broadcast('*', {
act: GameConst.CHANGING_SCENE,
id: client.sessionId,
scene: currentPlayer.state.scene,
prev: data.prev,
});
let nextRoom = await this.loginManager.roomsManager.loadRoomByName(data.next);
if(!nextRoom){
Logger.error('Player room change error. Next room not found: ' + data.next);
playerBody.isChangingScene = false;
return;
}
let newPosition = this.fetchNewPosition(nextRoom, data.prev);
if(!newPosition){
Logger.error('Can not find next room: '+nextRoom.roomName, {data, nextRoom, newPosition});
playerBody.isChangingScene = false;
return;
}
currentPlayer.state.scene = data.next;
currentPlayer.state.room_id = nextRoom.roomId;
currentPlayer.state.x = newPosition[RoomsConst.RETURN_POINT_KEYS.X];
currentPlayer.state.y = newPosition[RoomsConst.RETURN_POINT_KEYS.Y];
currentPlayer.state.dir = newPosition[RoomsConst.RETURN_POINT_KEYS.DIRECTION];
let stateSaved = await this.savePlayerState(client.sessionId);
if(!stateSaved){
Logger.error('Save state error: ' + client.sessionId);
playerBody.isChangingScene = false;
return;
}
this.broadcastSceneChange(client, currentPlayer, data);
}
fetchNewPosition(nextRoom, previousRoom)
{
if(0 === nextRoom.returnPoints.length){
Logger.warning('Next room "'+nextRoom.roomName+'" has no return points.', nextRoom);
return false;
}
for(let newPosition of nextRoom.returnPoints){
// @NOTE: P === false means there's only one room that would lead to this one. If there's more than one
// possible return point then validate the previous room.
// validate if previous room:
if(
newPosition[RoomsConst.RETURN_POINT_KEYS.PREVIOUS]
&& newPosition[RoomsConst.RETURN_POINT_KEYS.PREVIOUS] !== previousRoom
){
continue;
}
return newPosition;
}
return false;
}
broadcastSceneChange(client, currentPlayer, data)
{
// @NOTE: we need to broadcast the current player scene change to be removed or added in other players.
let showPlayedTimeConfig = this.config.getWithoutLogs(
'client/players/playedTime/show',
GameConst.SHOW_PLAYER_TIME.NONE
);
this.broadcast('*', {
act: GameConst.CHANGED_SCENE,
id: client.sessionId,
scene: currentPlayer.state.scene,
prev: data.prev,
x: currentPlayer.state.x,
y: currentPlayer.state.y,
dir: currentPlayer.state.dir,
playerName: currentPlayer.playerName,
playedTime: GameConst.SHOW_PLAYER_TIME.ALL_PLAYERS === showPlayedTimeConfig ? currentPlayer.playedTime : -1,
avatarKey: currentPlayer.avatarKey
});
let bodyToRemove = currentPlayer.physicalBody;
if(sc.isFunction(this.roomWorld.removeBody)){
this.roomWorld.removeBody(bodyToRemove);
}
// reconnect is to create the player in the new scene:
client.send('*', {act: GameConst.RECONNECT, player: currentPlayer, prev: data.prev});
}
async removePlayer(sessionId)
{
let playerSchema = this.playerBySessionIdFromState(sessionId);
let stateObject = {isRemoveReady: true};
await this.events.emit('reldens.removePlayerBefore', {
room: this,
playerSchema,
stateObject
});
if(playerSchema && stateObject.isRemoveReady){
this.removeAllPlayerReferences(playerSchema, sessionId);
Logger.debug('Removed player: '+playerSchema.playerName+' - Session ID: '+sessionId);
return playerSchema;
}
ErrorManager.error('Player not found, session ID: ' + sessionId);
return false;
}
removeAllPlayerReferences(playerSchema, sessionId)
{
this.events.offByMasterKey(playerSchema.eventsPrefix);
let bodyToRemove = playerSchema.physicalBody;
if(sc.isFunction(this.roomWorld.removeBody) && bodyToRemove){
this.roomWorld.removeBody(bodyToRemove);
}
playerSchema.physicalBody = null;
let itemsList = playerSchema?.inventory?.manager?.items || {};
let itemsKeys = Object.keys(itemsList);
if(0 < itemsKeys.length){
for(let i of itemsKeys){
let item = itemsList[i];
if(item.useTimer){
clearTimeout(item.useTimer);
}
if(item.execTimer){
clearTimeout(item.execTimer);
}
}
}
this.clearEntityActions(playerSchema);
// these contain references to the room:
delete playerSchema.skillsServer?.client;
delete playerSchema.inventory?.client;
playerSchema.executePhysicalSkill = null;
playerSchema.persistData = null;
playerSchema.skillsServer = null;
playerSchema.inventory = null;
let playerDeathTimer = playerSchema.getPrivate('playerDeathTimer');
if(playerDeathTimer){
clearTimeout(playerDeathTimer);
}
this.state.removePlayer(sessionId);
}
async savePlayerState(sessionId)
{
let playerSchema = this.playerBySessionIdFromState(sessionId);
let {room_id, x, y, dir} = playerSchema.state;
let playerId = playerSchema.player_id;
let updatePatch = {room_id, x: parseInt(x), y: parseInt(y), dir};
let updateReady = {continueUpdate: true};
this.events.emitSync('reldens.onSavePlayerStateBefore', {
room: this,
playerSchema,
playerId,
updatePatch,
updateReady
});
if(!updateReady.continueUpdate){
return playerSchema;
}
let updateResult = false;
let errorMessage = '';
try {
updateResult = await this.loginManager.usersManager.updateUserStateByPlayerId(playerId, updatePatch);
} catch (error) {
errorMessage = error.message;
}
if(!updateResult){
Logger.error('Player with ID "'+playerId+'" update error. ' + errorMessage);
return false;
}
return playerSchema;
}
async savePlayerStats(playerSchema, client)
{
// @TODO - BETA - For now we are always updating all the stats but this can be improved to save only the ones
// that changed.
let objectState = {updateReady: true};
this.events.emitSync('reldens.onSavePlayerStatsBefore', {room: this, playerSchema, client, objectState});
if(!objectState.updateReady){
return false;
}
for(let i of Object.keys(playerSchema.stats)){
let statId = this.config.client.players.initialStats[i].id;
let statPatch = {
value: playerSchema.stats[i],
base_value: playerSchema.statsBase[i]
};
await this.loginManager.usersManager.updatePlayerStatByIds(playerSchema.player_id, statId, statPatch);
}
if(client){
// @TODO - BETA - Convert all events in constants and consolidate them in a single file with descriptions.
await this.events.emit('reldens.savePlayerStatsUpdateClient', client, playerSchema, this);
client.send('*', {
act: GameConst.PLAYER_STATS,
stats: playerSchema.stats,
statsBase: playerSchema.statsBase
});
}
return true;
}
async savePlayedTime(playerSchema)
{
return this.loginManager.usersManager.updatePlayedTimeAndLogoutDate(playerSchema);
}
getClientById(clientId)
{
let client = this.activePlayerBySessionId(clientId, this.roomId)?.client;
if(client){
return client;
}
Logger.debug('Fetching client "'+clientId+'" from RoomScene.clients.');
if(!this.clients){
return false;
}
for(let client of this.clients){
if(client.sessionId === clientId){
return client;
}
}
Logger.debug('Expected when disconnects, client "'+clientId+'" not found in RoomScene.clients.');
return false;
}
async createDropObjectInRoom(dropObjectData, worldObjectData)
{
let roomObject = await this.objectsManager.generateObjectFromObjectData(dropObjectData);
let { layerName, tileIndex, tileWidth, tileHeight, x, y } = worldObjectData;
await this.roomWorld.createRoomObjectBody({ name: layerName }, tileIndex, tileWidth, tileHeight, x, y);
if(!roomObject){
Logger.error('Object body could not be created.', {dropObjectData, worldObjectData});
return false;
}
this.addObjectStateSceneData(roomObject);
this.setObjectAutoDestroyTime(roomObject);
if(roomObject.objectBody){
Logger.debug('Creating drop object with body and collision group: "'+WorldConst.COLLISIONS.DROP+'".');
roomObject.objectBody.originalCollisionGroup = WorldConst.COLLISIONS.DROP;
for(let shape of roomObject.objectBody.shapes){
shape.collisionGroup = WorldConst.COLLISIONS.DROP;
shape.collisionMask = roomObject.objectBody.world.collisionsGroupsByType[WorldConst.COLLISIONS.DROP];
}
}
return roomObject;
}
setObjectAutoDestroyTime(roomObject)
{
let objectLifeTime = Number(this.config.getWithoutLogs('server/objects/drops/disappearTime', 0));
if(0 === objectLifeTime){
return false;
}
return setTimeout(() => {
if(!this.objectsManager){
return false;
}
if(!this.objectsManager.getObjectData(roomObject.objectIndex)){
return false;
}
this.removeObject(roomObject);
}, objectLifeTime);
}
removeObject(roomObject)
{
if(sc.isFunction(this.roomWorld.removeBody)){
this.roomWorld.removeBody(roomObject.objectBody);
}
this.objectsManager.removeObjectData(roomObject);
this.deleteObjectSceneData(roomObject);
if(!this.hasActiveDroppedObjects()){
this.enableAutoDispose();
}
this.broadcast('*', {act: ObjectsConst.DROPS.REMOVE, id: roomObject.objectIndex});
}
hasActiveDroppedObjects()
{
for(let roomObject of Object.values(this.objectsManager.roomObjects)){
if(roomObject instanceof DropObject){
return true
}
}
return false;
}
onDispose()
{
if(!this.loginManager){
// expected when a room throws a CREATING_ROOM_AWAIT error and the room can be disposed automatically:
Logger.debug('Expected, LoginManager was not set as the created instance.', this.roomName, this.roomId);
return false;
}
let createdRoomId = this.loginManager.roomsManager.instanceIdByName[this.roomName];
if(!createdRoomId){
Logger.warning('Room ID was not set on the created instances.', this.roomName, this.roomId);
}
if(createdRoomId && this.roomId !== createdRoomId){
Logger.debug('Avoiding dispose for awaiting room creation.', this.roomName, this.roomId)
return true;
}
return new Promise((resolve) => {
try {
Logger.info('Starting dispose of RoomScene "'+this.roomName+'" (ID: '+this.roomId+')');
this.logEventsData('before');
this.clearMovementIntervals();
this.clearWorldTimers();
this.cleanUpRoomWorld();
this.clearPlayersTimers();
// @TODO - BETA - Replace the following two methods to use a single master key related to the room ID.
this.handleRespawnOnRoomDispose();
this.handleObjectsManagerOnRoomDispose();
delete this.objectsManager;
delete this.roomWorld;
this.roomWorld = {};
this.events.offByMasterKey(this.roomName + '-' + this.roomId);
super.onDispose();
this.logEventsData('after');
} catch (error) {
resolve({confirm: false, error});
}
Logger.info('Disposed RoomScene "'+this.roomName+'" (ID: '+this.roomId+').');
resolve({confirm: true});
});
}
clearPlayersTimers()
{
if(0 === this.playersCountInState()){
return false;
}
for(let i of this.playersKeysFromState()){
let playerSchema = this.playerBySessionIdFromState(i);
clearTimeout(playerSchema?.actions?.pvp?.playerReviveTimer);
}
}
cleanUpRoomWorld()
{
this.roomWorld.removeBodies.push(this.objectBody);
this.roomWorld._listeners['postBroadphase'] = [];
this.roomWorld._listeners['preSolve'] = [];
this.roomWorld._listeners['beginContact'] = [];
this.roomWorld._listeners['endContact'] = [];
this.roomWorld.clear();
}
clearWorldTimers()
{
clearInterval(this.roomWorld.worldDateTimeInterval);
this.worldTimer.callbacks = [];
this.worldTimer.clockInstance ? this.worldTimer.worldTimer.clear() : clearInterval(this.worldTimer.worldTimer);
clearInterval(this.worldTimer.worldTimer);
delete this.worldTimerCallback;
this.worldTimerCallback = () => {
// this serves as notification in case the room didn't clear the timers or events correctly:
Logger.warning('World timer callback still executed for room "'+this.roomName+'".');
};
Logger.debug('Cleared world timer and world date time intervals.');
this.worldTimer.world = null;
this.worldTimer = null;
delete this.worldTimer;
}
handleObjectsManagerOnRoomDispose()
{
if(!this.objectsManager.roomObjects){
Logger.debug('None roomObjects defined for room: '+this.roomName);
return;
}
for(let i of Object.keys(this.objectsManager.roomObjects)){
let roomObject = this.objectsManager.roomObjects[i];
roomObject.postBroadPhaseListener = [];
this.clearEntityActions(roomObject);
if(roomObject.battle){
clearTimeout(roomObject.battle.battleTimer);
clearTimeout(roomObject.battle.respawnStateTimer);
roomObject.battle.targetObject = null;
}
clearTimeout(roomObject?.objectBody?.moveToOriginalPointTimer);
clearTimeout(roomObject.respawnTimer);
clearInterval(roomObject.respawnTimerInterval);
clearTimeout(roomObject.isCasting);
Logger.debug('Cleared timers for: ' + roomObject.key + ' / ' + i);
delete this.objectsManager.roomObjects[i];
}
}
clearEntityActions(entityInstance)
{
let actions = entityInstance?.actions || {};
let actionsKeys = Object.keys(actions);
if(0 === actionsKeys.length){
return;
}
for(let i of actionsKeys){
if(actions[i].room){
actions[i].room = null;
}
}
}
handleRespawnOnRoomDispose()
{
if(!this.roomWorld.respawnAreas){
Logger.debug('None respawn areas defined for room: '+this.roomName);
return;
}
this.removeRespawnObjectsSubscribers();
this.deleteRespawnObjectInstances();
}
deleteRespawnObjectInstances()
{
if(!this.roomWorld.respawnAreas){
return;
}
for(let rI of Object.keys(this.roomWorld.respawnAreas)){
let respawnInstancesCreated = this.roomWorld.respawnAreas[rI].instancesCreated;
for(let i of Object.keys(respawnInstancesCreated)){
delete respawnInstancesCreated[i];
Logger.debug('Deleted respawn instances created: ' + i);
}
delete this.roomWorld.respawnAreas[rI];
Logger.debug('Deleted respawn area created: ' + rI);
}
delete this.roomWorld.respawnAreas;
Logger.debug('Deleted respawn areas for room: ' + this.roomName);
}
removeRespawnObjectsSubscribers()
{
if(!this.roomWorld.respawnAreas){
return;
}
for(let rI of Object.keys(this.roomWorld.respawnAreas)){
let respawnInstancesCreated = this.roomWorld.respawnAreas[rI].instancesCreated;
for(let i of Object.keys(respawnInstancesCreated)){
let respawnInstance = respawnInstancesCreated[i];
for(let respawnObject of respawnInstance){
Logger.debug('Setting "battleEnd" listener off: ' + respawnObject.uid);
this.events.offByMasterKey(respawnObject.uid);
}
}
}
}
logEventsData(eventKey)
{
let endSubscribersCount = 0;
let keysData = {};
for(let i of Object.keys(this.events._events)){
endSubscribersCount += this.events._events[i].length;
keysData[i] = {keys: []};
for(let event of this.events._events[i]){
keysData[i].keys.push((event.fn.name || 'anonymous').replace('bound ', ''));
}
keysData[i].keys = keysData[i].keys.join(', ');
}
Logger.debug('Subscribers count '+eventKey+':', endSubscribersCount, keysData);
}
addObjectStateSceneData(object)
{
// @TODO - BETA - Refactor, remove scene.roomData or scene.state.roomData or rename.
let updateData = sc.get(this.state.roomData.objectsAnimationsData, object.objectIndex);
if(!updateData){
return;
}
this.state.roomData.objectsAnimationsData[object.objectIndex] = updateData;
for(let objectAsset of object.objects_assets){
this.state.roomData.preloadAssets[this.objectAssetId(objectAsset)] = objectAsset;
}
this.state.mapRoomData();
}
deleteObjectSceneData(object)
{
// @TODO - BETA - Refactor, remove scene.roomData or scene.state.roomData or rename.
delete this.state.roomData.objectsAnimationsData[object.objectIndex];
for(let objectAsset of object.objects_assets){
delete this.state.roomData.preloadAssets[this.objectAssetId(objectAsset)];
}
this.state.mapRoomData();
}
objectAssetId(objectAsset)
{
return (objectAsset.object_id || '') + (objectAsset.object_asset_id || '');
}
disableAutoDispose()
{
this.autoDispose = false;
}
enableAutoDispose()
{
this.autoDispose = true;
}
}
module.exports.RoomScene = RoomScene;