/*
* The Mana Server
* Copyright (C) 2004-2010 The Mana World Development Team
*
* This file is part of The Mana Server.
*
* The Mana Server is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* any later version.
*
* The Mana Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with The Mana Server. If not, see .
*/
#include "game-server/state.h"
#include "common/configuration.h"
#include "game-server/accountconnection.h"
#include "game-server/effect.h"
#include "game-server/gamehandler.h"
#include "game-server/inventory.h"
#include "game-server/item.h"
#include "game-server/itemmanager.h"
#include "game-server/map.h"
#include "game-server/mapcomposite.h"
#include "game-server/mapmanager.h"
#include "game-server/monster.h"
#include "game-server/npc.h"
#include "game-server/trade.h"
#include "net/messageout.h"
#include "scripting/script.h"
#include "scripting/scriptmanager.h"
#include "utils/logger.h"
#include "utils/speedconv.h"
#include
enum
{
EVENT_REMOVE = 0,
EVENT_INSERT,
EVENT_WARP
};
/**
* Event expected to happen at next update.
*/
struct DelayedEvent
{
unsigned short type;
Point point;
MapComposite *map;
};
typedef std::map< Entity *, DelayedEvent > DelayedEvents;
/**
* The current world time in ticks since server start.
*/
static int currentTick;
/**
* List of delayed events.
*/
static DelayedEvents delayedEvents;
/**
* Cached persistent script variables
*/
static std::map< std::string, std::string > mScriptVariables;
/**
* Sets message fields describing character look.
*/
static void serializeLooks(Entity *ch, MessageOut &msg)
{
auto *characterComponent = ch->getComponent();
msg.writeInt8(characterComponent->getHairStyle());
msg.writeInt8(characterComponent->getHairColor());
const EquipData &equipData =
characterComponent->getPossessions().getEquipment();
const InventoryData &inventoryData =
characterComponent->getPossessions().getInventory();
// The map storing the info about the look changes to send
//{ slot type id, item id }
std::map lookChanges;
// Note that we can send several updates on the same slot type as different
// items may have been equipped.
for (EquipData::const_iterator it = equipData.begin(),
it_end = equipData.end(); it != it_end; ++it)
{
InventoryData::const_iterator itemIt = inventoryData.find(*it);
if (!itemManager->isEquipSlotVisible(itemIt->second.equipmentSlot))
continue;
lookChanges.insert(std::make_pair(
itemIt->second.equipmentSlot,
itemIt->second.itemId));
}
if (!lookChanges.empty())
{
// Number of look changes to send
msg.writeInt8(lookChanges.size());
for (std::map::const_iterator it =
lookChanges.begin(), it_end = lookChanges.end();
it != it_end; ++it)
{
msg.writeInt8(it->first);
msg.writeInt16(it->second);
}
}
}
/**
* Informs a player of what happened around the character.
*/
static void informPlayer(MapComposite *map, Entity *p)
{
MessageOut moveMsg(GPMSG_BEINGS_MOVE);
MessageOut damageMsg(GPMSG_BEINGS_DAMAGE);
const Point &pold = p->getComponent()->getOldPosition();
const Point &ppos = p->getComponent()->getPosition();
int pflags = p->getComponent()->getUpdateFlags();
int visualRange = Configuration::getValue("game_visualRange", 448);
// Inform client about activities of other beings near its character
for (BeingIterator it(map->getAroundBeingIterator(p, visualRange));
it; ++it)
{
Entity *o = *it;
const Point &oold =
o->getComponent()->getOldPosition();
const Point &opos = o->getComponent()->getPosition();
int otype = o->getType();
int oid = o->getComponent()->getPublicID();
int oflags = o->getComponent()->getUpdateFlags();
int flags = 0;
// Check if the character p and the moving object o are around.
bool wereInRange = pold.inRangeOf(oold, visualRange) &&
!((pflags | oflags) & UPDATEFLAG_NEW_ON_MAP);
bool willBeInRange = ppos.inRangeOf(opos, visualRange);
if (!wereInRange && !willBeInRange)
{
// Nothing to report: o and p are far away from each other.
continue;
}
if (wereInRange && willBeInRange)
{
// Send action change messages.
if ((oflags & UPDATEFLAG_ACTIONCHANGE))
{
MessageOut actionMsg(GPMSG_BEING_ACTION_CHANGE);
actionMsg.writeInt16(oid);
actionMsg.writeInt8(
o->getComponent()->getAction());
gameHandler->sendTo(p, actionMsg);
}
// Send looks change messages.
if (oflags & UPDATEFLAG_LOOKSCHANGE)
{
MessageOut looksMsg(GPMSG_BEING_LOOKS_CHANGE);
looksMsg.writeInt16(oid);
serializeLooks(o, looksMsg);
gameHandler->sendTo(p, looksMsg);
}
// Send emote messages.
if (oflags & UPDATEFLAG_EMOTE)
{
int emoteId =
o->getComponent()->getLastEmote();
if (emoteId > -1)
{
MessageOut emoteMsg(GPMSG_BEING_EMOTE);
emoteMsg.writeInt16(oid);
emoteMsg.writeInt16(emoteId);
gameHandler->sendTo(p, emoteMsg);
}
}
// Send direction change messages.
if (oflags & UPDATEFLAG_DIRCHANGE)
{
MessageOut dirMsg(GPMSG_BEING_DIR_CHANGE);
dirMsg.writeInt16(oid);
dirMsg.writeInt8(
o->getComponent()->getDirection());
gameHandler->sendTo(p, dirMsg);
}
// Send ability uses
if (oflags & UPDATEFLAG_ABILITY_ON_POINT)
{
MessageOut abilityMsg(GPMSG_BEING_ABILITY_POINT);
abilityMsg.writeInt16(oid);
auto *abilityComponent = o->getComponent();
const Point &point = abilityComponent->getLastTargetPoint();
abilityMsg.writeInt8(abilityComponent->getLastUsedAbilityId());
abilityMsg.writeInt16(point.x);
abilityMsg.writeInt16(point.y);
gameHandler->sendTo(p, abilityMsg);
}
if (oflags & UPDATEFLAG_ABILITY_ON_BEING)
{
MessageOut abilityMsg(GPMSG_BEING_ABILITY_BEING);
abilityMsg.writeInt16(oid);
auto *abilityComponent = o->getComponent();
abilityMsg.writeInt8(abilityComponent->getLastUsedAbilityId());
abilityMsg.writeInt16(
abilityComponent->getLastTargetBeingId());
gameHandler->sendTo(p, abilityMsg);
}
// Send damage messages.
if (o->canFight())
{
auto *beingComponent = o->getComponent();
const Hits &hits = beingComponent->getHitsTaken();
for (Hits::const_iterator j = hits.begin(),
j_end = hits.end(); j != j_end; ++j)
{
damageMsg.writeInt16(oid);
damageMsg.writeInt16(*j);
}
}
if (oold == opos)
{
// o does not move, nothing more to report.
continue;
}
}
if (!willBeInRange)
{
// o is no longer visible from p. Send leave message.
MessageOut leaveMsg(GPMSG_BEING_LEAVE);
leaveMsg.writeInt16(oid);
gameHandler->sendTo(p, leaveMsg);
continue;
}
if (!wereInRange)
{
// o is now visible by p. Send enter message.
MessageOut enterMsg(GPMSG_BEING_ENTER);
enterMsg.writeInt8(otype);
enterMsg.writeInt16(oid);
enterMsg.writeInt8(o->getComponent()->getAction());
enterMsg.writeInt16(opos.x);
enterMsg.writeInt16(opos.y);
enterMsg.writeInt8(
o->getComponent()->getDirection());
enterMsg.writeInt8(o->getComponent()->getGender());
switch (otype)
{
case OBJECT_CHARACTER:
{
enterMsg.writeString(
o->getComponent()->getName());
serializeLooks(o, enterMsg);
} break;
case OBJECT_MONSTER:
{
MonsterComponent *monsterComponent =
o->getComponent();
enterMsg.writeInt16(monsterComponent->getSpecy()->getId());
enterMsg.writeString(
o->getComponent()->getName());
} break;
case OBJECT_NPC:
{
NpcComponent *npcComponent =
o->getComponent();
enterMsg.writeInt16(npcComponent->getNpcId());
enterMsg.writeString(
o->getComponent()->getName());
} break;
default:
assert(false); // TODO
break;
}
gameHandler->sendTo(p, enterMsg);
}
if (opos != oold)
{
// Add position check coords every 5 seconds.
if (currentTick % 50 == 0)
flags |= MOVING_POSITION;
flags |= MOVING_DESTINATION;
}
// Send move messages.
moveMsg.writeInt16(oid);
moveMsg.writeInt8(flags);
if (flags & MOVING_POSITION)
{
moveMsg.writeInt16(oold.x);
moveMsg.writeInt16(oold.y);
}
if (flags & MOVING_DESTINATION)
{
moveMsg.writeInt16(opos.x);
moveMsg.writeInt16(opos.y);
// We multiply the sent speed (in tiles per second) by ten
// to get it within a byte with decimal precision.
// For instance, a value of 4.5 will be sent as 45.
auto *tpsSpeedAttribute = attributeManager->getAttributeInfo(ATTR_MOVE_SPEED_TPS);
moveMsg.writeInt8((unsigned short)
(o->getComponent()
->getModifiedAttribute(tpsSpeedAttribute) * 10));
}
}
// Do not send a packet if nothing happened in p's range.
if (moveMsg.getLength() > 2)
gameHandler->sendTo(p, moveMsg);
if (damageMsg.getLength() > 2)
gameHandler->sendTo(p, damageMsg);
// Inform client about status change.
p->getComponent()->sendStatus(*p);
// Inform client about health change of party members
for (CharacterIterator i(map->getWholeMapIterator()); i; ++i)
{
Entity *c = *i;
// Make sure its not the same character
if (c == p)
continue;
// make sure they are in the same party
if (c->getComponent()->getParty() ==
p->getComponent()->getParty())
{
int cflags = c->getComponent()->getUpdateFlags();
if (cflags & UPDATEFLAG_HEALTHCHANGE)
{
auto *beingComponent = c->getComponent();
MessageOut healthMsg(GPMSG_BEING_HEALTH_CHANGE);
healthMsg.writeInt16(
c->getComponent()->getPublicID());
auto *hpAttribute = attributeManager->getAttributeInfo(ATTR_HP);
healthMsg.writeInt16(
beingComponent->getModifiedAttribute(hpAttribute));
auto *maxHpAttribute = attributeManager->getAttributeInfo(ATTR_MAX_HP);
healthMsg.writeInt16(
beingComponent->getModifiedAttribute(maxHpAttribute));
gameHandler->sendTo(p, healthMsg);
}
}
}
// Inform client about items on the ground around its character
MessageOut itemMsg(GPMSG_ITEMS);
for (FixedActorIterator it(map->getAroundBeingIterator(p, visualRange));
it; ++it)
{
Entity *o = *it;
assert(o->getType() == OBJECT_ITEM ||
o->getType() == OBJECT_EFFECT);
Point opos = o->getComponent()->getPosition();
int oflags = o->getComponent()->getUpdateFlags();
bool willBeInRange = ppos.inRangeOf(opos, visualRange);
bool wereInRange = pold.inRangeOf(opos, visualRange) &&
!((pflags | oflags) & UPDATEFLAG_NEW_ON_MAP);
if (willBeInRange ^ wereInRange)
{
switch (o->getType())
{
case OBJECT_ITEM:
{
ItemComponent *item = o->getComponent();
ItemClass *itemClass = item->getItemClass();
if (oflags & UPDATEFLAG_NEW_ON_MAP)
{
/* Send a specific message to the client when an item appears
out of nowhere, so that a sound/animation can be performed. */
MessageOut appearMsg(GPMSG_ITEM_APPEAR);
appearMsg.writeInt16(itemClass->getDatabaseID());
appearMsg.writeInt16(opos.x);
appearMsg.writeInt16(opos.y);
gameHandler->sendTo(p, appearMsg);
}
else
{
itemMsg.writeInt16(willBeInRange ? itemClass->getDatabaseID() : 0);
itemMsg.writeInt16(opos.x);
itemMsg.writeInt16(opos.y);
}
}
break;
case OBJECT_EFFECT:
{
EffectComponent *e = o->getComponent();
e->setShown();
// Don't show old effects
if (!(oflags & UPDATEFLAG_NEW_ON_MAP))
break;
if (Entity *b = e->getBeing())
{
auto *actorComponent =
b->getComponent();
MessageOut effectMsg(GPMSG_CREATE_EFFECT_BEING);
effectMsg.writeInt16(e->getEffectId());
effectMsg.writeInt16(actorComponent->getPublicID());
gameHandler->sendTo(p, effectMsg);
} else {
MessageOut effectMsg(GPMSG_CREATE_EFFECT_POS);
effectMsg.writeInt16(e->getEffectId());
effectMsg.writeInt16(opos.x);
effectMsg.writeInt16(opos.y);
gameHandler->sendTo(p, effectMsg);
}
}
break;
default: break;
} // Switch
}
}
// Do not send a packet if nothing happened in p's range.
if (itemMsg.getLength() > 2)
gameHandler->sendTo(p, itemMsg);
}
#ifndef NDEBUG
static bool dbgLockObjects;
#endif
void GameState::update(int tick)
{
currentTick = tick;
#ifndef NDEBUG
dbgLockObjects = true;
#endif
ScriptManager::currentState()->update();
// Update game state (update AI, etc.)
const MapManager::Maps &maps = MapManager::getMaps();
for (MapManager::Maps::const_iterator m = maps.begin(),
m_end = maps.end(); m != m_end; ++m)
{
MapComposite *map = m->second;
if (!map->isActive())
continue;
map->update();
for (CharacterIterator p(map->getWholeMapIterator()); p; ++p)
{
informPlayer(map, *p);
}
for (ActorIterator it(map->getWholeMapIterator()); it; ++it)
{
Entity *a = *it;
a->getComponent()->clearUpdateFlags();
if (a->canFight())
{
a->getComponent()->clearHitsTaken();
}
}
}
# ifndef NDEBUG
dbgLockObjects = false;
# endif
// Take care of events that were delayed because of their side effects.
for (DelayedEvents::iterator it = delayedEvents.begin(),
it_end = delayedEvents.end(); it != it_end; ++it)
{
const DelayedEvent &e = it->second;
Entity *o = it->first;
switch (e.type)
{
case EVENT_REMOVE:
remove(o);
if (o->getType() == OBJECT_CHARACTER)
{
o->getComponent()->disconnected(*o);
gameHandler->kill(o);
}
delete o;
break;
case EVENT_INSERT:
insertOrDelete(o);
break;
case EVENT_WARP:
assert(o->getType() == OBJECT_CHARACTER);
warp(o, e.map, e.point);
break;
}
}
delayedEvents.clear();
}
bool GameState::insert(Entity *ptr)
{
assert(!dbgLockObjects);
MapComposite *map = ptr->getMap();
assert(map && map->isActive());
/* Non-visible objects have neither position nor public ID, so their
insertion cannot fail. Take care of them first. */
if (!ptr->isVisible())
{
map->insert(ptr);
ptr->signal_inserted.emit(ptr);
return true;
}
// Check that coordinates are actually valid.
Entity *obj = static_cast< Entity * >(ptr);
Map *mp = map->getMap();
Point pos = obj->getComponent()->getPosition();
if ((int)pos.x / mp->getTileWidth() >= mp->getWidth() ||
(int)pos.y / mp->getTileHeight() >= mp->getHeight())
{
LOG_ERROR("Tried to insert an actor at position " << pos.x << ','
<< pos.y << " outside map " << map->getID() << '.');
// Set an arbitrary small position.
pos = Point(100, 100);
obj->getComponent()->setPosition(*ptr, pos);
}
if (!map->insert(obj))
{
// The map is overloaded, no room to add a new actor
LOG_ERROR("Too many actors on map " << map->getID() << '.');
return false;
}
obj->signal_inserted.emit(obj);
// DEBUG INFO
switch (obj->getType())
{
case OBJECT_ITEM:
LOG_DEBUG("Item inserted: "
<< obj->getComponent()->getItemClass()->getDatabaseID());
break;
case OBJECT_NPC:
LOG_DEBUG("NPC inserted: " << obj->getComponent()->getNpcId());
break;
case OBJECT_CHARACTER:
LOG_DEBUG("Player inserted: "
<< obj->getComponent()->getName());
break;
case OBJECT_EFFECT:
LOG_DEBUG("Effect inserted: "
<< obj->getComponent()->getEffectId());
break;
case OBJECT_MONSTER:
{
MonsterComponent *monsterComponent =
obj->getComponent();
LOG_DEBUG("Monster inserted: "
<< monsterComponent->getSpecy()->getId());
break;
}
case OBJECT_ACTOR:
case OBJECT_OTHER:
default:
LOG_DEBUG("Entity inserted: " << obj->getType());
break;
}
obj->getComponent()->raiseUpdateFlags(
UPDATEFLAG_NEW_ON_MAP);
if (obj->getType() != OBJECT_CHARACTER)
return true;
/* Since the player does not know yet where in the world its character is,
we send a map-change message, even if it is the first time it
connects to this server. */
MessageOut mapChangeMessage(GPMSG_PLAYER_MAP_CHANGE);
mapChangeMessage.writeString(map->getName());
mapChangeMessage.writeInt16(pos.x);
mapChangeMessage.writeInt16(pos.y);
gameHandler->sendTo(ptr, mapChangeMessage);
// update the online state of the character
accountHandler->updateOnlineStatus(ptr->getComponent()
->getDatabaseID(), true);
return true;
}
int GameState::getCurrentTick()
{
return currentTick;
}
bool GameState::insertOrDelete(Entity *ptr)
{
if (insert(ptr)) return true;
delete ptr;
return false;
}
void GameState::remove(Entity *ptr)
{
assert(!dbgLockObjects);
MapComposite *map = ptr->getMap();
int visualRange = Configuration::getValue("game_visualRange", 448);
ptr->signal_removed.emit(ptr);
// DEBUG INFO
switch (ptr->getType())
{
case OBJECT_ITEM:
LOG_DEBUG("Item removed: "
<< ptr->getComponent()->getItemClass()->getDatabaseID());
break;
case OBJECT_NPC:
LOG_DEBUG("NPC removed: " << ptr->getComponent()->getNpcId());
break;
case OBJECT_CHARACTER:
LOG_DEBUG("Player removed: "
<< ptr->getComponent()->getName());
break;
case OBJECT_EFFECT:
LOG_DEBUG("Effect removed: "
<< ptr->getComponent()->getEffectId());
break;
case OBJECT_MONSTER:
{
MonsterComponent *monsterComponent =
ptr->getComponent();
LOG_DEBUG("Monster removed: "
<< monsterComponent->getSpecy()->getId());
break;
}
case OBJECT_ACTOR:
case OBJECT_OTHER:
default:
LOG_DEBUG("Entity removed: " << ptr->getType());
break;
}
if (ptr->canMove())
{
if (ptr->getType() == OBJECT_CHARACTER)
{
auto *characterComponent =
ptr->getComponent();
characterComponent->cancelTransaction();
// remove characters online status
accountHandler->updateOnlineStatus(
characterComponent->getDatabaseID(), false);
}
MessageOut msg(GPMSG_BEING_LEAVE);
msg.writeInt16(ptr->getComponent()->getPublicID());
Point objectPos = ptr->getComponent()->getPosition();
for (CharacterIterator p(map->getAroundActorIterator(ptr, visualRange));
p; ++p)
{
if (*p != ptr && objectPos.inRangeOf(
(*p)->getComponent()->getPosition(),
visualRange))
{
gameHandler->sendTo(*p, msg);
}
}
}
else if (ptr->getType() == OBJECT_ITEM)
{
Point pos = ptr->getComponent()->getPosition();
MessageOut msg(GPMSG_ITEMS);
msg.writeInt16(0);
msg.writeInt16(pos.x);
msg.writeInt16(pos.y);
for (CharacterIterator p(map->getAroundActorIterator(ptr, visualRange)); p; ++p)
{
const Point &point =
(*p)->getComponent()->getPosition();
if (pos.inRangeOf(point, visualRange))
{
gameHandler->sendTo(*p, msg);
}
}
}
map->remove(ptr);
}
void GameState::warp(Entity *ptr, MapComposite *map, const Point &point)
{
remove(ptr);
ptr->setMap(map);
ptr->getComponent()->setPosition(*ptr, point);
ptr->getComponent()->clearDestination(*ptr);
/* Force update of persistent data on map change, so that
characters can respawn at the start of the map after a death or
a disconnection. */
accountHandler->sendCharacterData(ptr);
auto *characterComponent =
ptr->getComponent();
// If the player has just left, The character pointer is also about
// to be deleted. So we don't have to do anything else.
if (!characterComponent->isConnected())
return;
if (map->isActive())
{
if (!insert(ptr))
{
characterComponent->disconnected(*ptr);
gameHandler->kill(ptr);
delete ptr;
}
}
else
{
MessageOut msg(GAMSG_REDIRECT);
msg.writeInt32(characterComponent->getDatabaseID());
accountHandler->send(msg);
gameHandler->prepareServerChange(ptr);
}
}
/**
* Enqueues an event. It will be executed at end of update.
*/
static void enqueueEvent(Entity *ptr, const DelayedEvent &e)
{
std::pair< DelayedEvents::iterator, bool > p =
delayedEvents.insert(std::make_pair(ptr, e));
// Delete events take precedence over other events.
if (!p.second && e.type == EVENT_REMOVE)
{
p.first->second.type = EVENT_REMOVE;
}
}
void GameState::enqueueInsert(Entity *ptr)
{
DelayedEvent event;
event.type = EVENT_INSERT;
event.map = 0;
enqueueEvent(ptr, event);
}
void GameState::enqueueRemove(Entity *ptr)
{
DelayedEvent event;
event.type = EVENT_REMOVE;
event.map = 0;
enqueueEvent(ptr, event);
}
void GameState::enqueueWarp(Entity *ptr,
MapComposite *map,
const Point &point)
{
// When the player has just disconnected, better not wait for the pointer
// to become invalid.
if (!ptr->getComponent()->isConnected())
{
warp(ptr, map, point);
return;
}
DelayedEvent event;
event.type = EVENT_WARP;
event.point = point;
event.map = map;
enqueueEvent(ptr, event);
}
void GameState::sayAround(Entity *entity, const std::string &text)
{
Point speakerPosition = entity->getComponent()->getPosition();
int visualRange = Configuration::getValue("game_visualRange", 448);
for (CharacterIterator i(entity->getMap()->getAroundActorIterator(entity, visualRange)); i; ++i)
{
const Point &point =
(*i)->getComponent()->getPosition();
if (speakerPosition.inRangeOf(point, visualRange))
{
sayTo(*i, entity, text);
}
}
}
void GameState::sayTo(Entity *destination, Entity *source, const std::string &text)
{
if (destination->getType() != OBJECT_CHARACTER)
return; //only characters will read it anyway
MessageOut msg(GPMSG_SAY);
if (source == nullptr)
{
msg.writeInt16(0);
}
else if (!source->canMove())
{
msg.writeInt16(65535);
}
else
{
msg.writeInt16(source->getComponent()->getPublicID());
}
msg.writeString(text);
gameHandler->sendTo(destination, msg);
}
void GameState::sayToAll(const std::string &text)
{
MessageOut msg(GPMSG_SAY);
// The message will be shown as if it was from the server
msg.writeInt16(0);
msg.writeString(text);
// Sends it to everyone connected to the game server
gameHandler->sendToEveryone(msg);
}
std::string GameState::getVariable(const std::string &key)
{
std::map::iterator iValue =
mScriptVariables.find(key);
if (iValue != mScriptVariables.end())
return iValue->second;
else
return std::string();
}
void GameState::setVariable(const std::string &key, const std::string &value)
{
if (mScriptVariables[key] == value)
return;
mScriptVariables[key] = value;
accountHandler->updateWorldVar(key, value);
callVariableCallbacks(key, value);
}
void GameState::setVariableFromDbserver(const std::string &key,
const std::string &value)
{
if (mScriptVariables[key] == value)
return;
mScriptVariables[key] = value;
callVariableCallbacks(key, value);
}
void GameState::callVariableCallbacks(const std::string &key,
const std::string &value)
{
const MapManager::Maps &maps = MapManager::getMaps();
for (MapManager::Maps::const_iterator m = maps.begin(),
m_end = maps.end(); m != m_end; ++m)
{
m->second->callWorldVariableCallback(key, value);
}
}