/* * 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/abilitycomponent.h" #include "game-server/accountconnection.h" #include "game-server/being.h" #include "game-server/charactercomponent.h" #include "game-server/effect.h" #include "game-server/gamehandler.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 "net/messageout.h" #include "scripting/script.h" #include "scripting/scriptmanager.h" #include "utils/logger.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; }; using DelayedEvents = std::map; /** * 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 (int it : equipData) { auto 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 (auto lookChange : lookChanges) { msg.writeInt8(lookChange.first); msg.writeInt16(lookChange.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 && o != p) { 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); } if (oflags & UPDATEFLAG_ABILITY_ON_DIRECTION) { MessageOut abilityMsg(GPMSG_BEING_ABILITY_DIRECTION); abilityMsg.writeInt16(oid); auto *abilityComponent = o->getComponent(); abilityMsg.writeInt8(abilityComponent->getLastUsedAbilityId()); abilityMsg.writeInt8( abilityComponent->getLastTargetDirection()); gameHandler->sendTo(p, abilityMsg); } // Send damage messages. if (o->canFight()) { auto *beingComponent = o->getComponent(); const Hits &hits = beingComponent->getHitsTaken(); for (unsigned int hit : hits) { damageMsg.writeInt16(oid); damageMsg.writeInt16(hit); } } 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: { auto monsterComponent = o->getComponent(); enterMsg.writeInt16(monsterComponent->getSpecy()->getId()); enterMsg.writeString( o->getComponent()->getName()); } break; case OBJECT_NPC: { auto 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: { auto 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: { auto e = o->getComponent(); // 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 (auto m : maps) { 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 (auto &delayedEvent : delayedEvents) { const DelayedEvent &e = delayedEvent.second; Entity *o = delayedEvent.first; switch (e.type) { case EVENT_REMOVE: remove(o); if (o->getType() == OBJECT_CHARACTER) { o->getComponent()->disconnected(*o); gameHandler->detachClient(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. auto 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: { auto monsterComponent = obj->getComponent(); LOG_DEBUG("Monster inserted: " << monsterComponent->getSpecy()->getId()); break; } 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: { auto monsterComponent = ptr->getComponent(); LOG_DEBUG("Monster removed: " << monsterComponent->getSpecy()->getId()); break; } 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->detachClient(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 = nullptr; enqueueEvent(ptr, event); } void GameState::enqueueRemove(Entity *ptr) { DelayedEvent event; event.type = EVENT_REMOVE; event.map = nullptr; 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) { auto 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 (auto map : maps) map.second->callWorldVariableCallback(key, value); }