/* * 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 #include "game-server/state.hpp" #include "point.h" #include "common/configuration.hpp" #include "game-server/accountconnection.hpp" #include "game-server/gamehandler.hpp" #include "game-server/inventory.hpp" #include "game-server/item.hpp" #include "game-server/itemmanager.hpp" #include "game-server/effect.hpp" #include "game-server/map.hpp" #include "game-server/mapcomposite.hpp" #include "game-server/mapmanager.hpp" #include "game-server/monster.hpp" #include "game-server/npc.hpp" #include "game-server/trade.hpp" #include "net/messageout.hpp" #include "scripting/script.hpp" #include "utils/logger.h" #include "utils/speedconv.hpp" enum { EVENT_REMOVE = 0, EVENT_INSERT, EVENT_WARP }; /** * Event expected to happen at next update. */ struct DelayedEvent { unsigned short type, x, y; MapComposite *map; }; typedef std::map< Actor *, DelayedEvent > DelayedEvents; /** * List of delayed events. */ static DelayedEvents delayedEvents; /** * Updates object states on the map. */ static void updateMap(MapComposite *map) { // 1. update object status. const std::vector< Thing * > &things = map->getEverything(); for (std::vector< Thing * >::const_iterator i = things.begin(), i_end = things.end(); i != i_end; ++i) { (*i)->update(); } // 2. run scripts. if (Script *s = map->getScript()) { s->update(); } // 3. perform actions. for (BeingIterator i(map->getWholeMapIterator()); i; ++i) { (*i)->perform(); } // 4. move objects around and update zones. for (BeingIterator i(map->getWholeMapIterator()); i; ++i) { (*i)->move(); } map->update(); } /** * Sets message fields describing character look. */ static void serializeLooks(Character *ch, MessageOut &msg, bool full) { const Possessions &poss = ch->getPossessions(); unsigned int nb_slots = itemManager->getVisibleSlotCount(); // Bitmask describing the changed entries. int changed = (1 << nb_slots) - 1; if (!full) { // TODO: do not assume the whole equipment changed, when an update is asked for. changed = (1 << nb_slots) - 1; } std::vector items; items.resize(nb_slots, 0); // Partially build both kinds of packet, to get their sizes. unsigned int mask_full = 0, mask_diff = 0; unsigned int nb_full = 0, nb_diff = 0; std::map::const_iterator it = poss.equipSlots.begin(); for (unsigned int i = 0; i < nb_slots; ++i) { if (changed & (1 << i)) { // Skip slots that have not changed, when sending an update. ++nb_diff; mask_diff |= 1 << i; } if (it == poss.equipSlots.end() || it->first > i) continue; ItemClass *eq; items[i] = it->first && (eq = itemManager->getItem(it->first)) ? eq->getSpriteID() : 0; if (items[i]) { /* If we are sending the whole equipment, only filled slots have to be accounted for, as the other ones will be automatically cleared. */ ++nb_full; mask_full |= 1 << i; } } // Choose the smaller payload. if (nb_full <= nb_diff) full = true; /* Bitmask enumerating the sent slots. Setting the upper bit tells the client to clear the slots beforehand. */ int mask = full ? mask_full | (1 << 7) : mask_diff; msg.writeByte(mask); for (unsigned int i = 0; i < nb_slots; ++i) { if (mask & (1 << i)) msg.writeShort(items[i]); } } /** * Informs a player of what happened around the character. */ static void informPlayer(MapComposite *map, Character *p) { MessageOut moveMsg(GPMSG_BEINGS_MOVE); MessageOut damageMsg(GPMSG_BEINGS_DAMAGE); const Point &pold = p->getOldPosition(), ppos = p->getPosition(); int pid = p->getPublicID(), pflags = p->getUpdateFlags(); int visualRange = Configuration::getValue("game_visualRange", 448); // Inform client about activities of other beings near its character for (BeingIterator i(map->getAroundBeingIterator(p, visualRange)); i; ++i) { Being *o = *i; const Point &oold = o->getOldPosition(), opos = o->getPosition(); int otype = o->getType(); int oid = o->getPublicID(), oflags = o->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 attack messages. if ((oflags & UPDATEFLAG_ATTACK) && oid != pid) { MessageOut AttackMsg(GPMSG_BEING_ATTACK); AttackMsg.writeShort(oid); AttackMsg.writeByte(o->getDirection()); AttackMsg.writeByte(static_cast< Being * >(o)->getAttackType()); gameHandler->sendTo(p, AttackMsg); } // Send action change messages. if ((oflags & UPDATEFLAG_ACTIONCHANGE)) { MessageOut ActionMsg(GPMSG_BEING_ACTION_CHANGE); ActionMsg.writeShort(oid); ActionMsg.writeByte(static_cast< Being * >(o)->getAction()); gameHandler->sendTo(p, ActionMsg); } // Send looks change messages. if (oflags & UPDATEFLAG_LOOKSCHANGE) { MessageOut LooksMsg(GPMSG_BEING_LOOKS_CHANGE); LooksMsg.writeShort(oid); Character * c = static_cast(o); serializeLooks(c, LooksMsg, false); LooksMsg.writeShort(c->getHairStyle()); LooksMsg.writeShort(c->getHairColor()); LooksMsg.writeShort(c->getGender()); gameHandler->sendTo(p, LooksMsg); } // Send direction change messages. if (oflags & UPDATEFLAG_DIRCHANGE) { MessageOut DirMsg(GPMSG_BEING_DIR_CHANGE); DirMsg.writeShort(oid); DirMsg.writeByte(o->getDirection()); gameHandler->sendTo(p, DirMsg); } // Send damage messages. if (o->canFight()) { Being *victim = static_cast< Being * >(o); const Hits &hits = victim->getHitsTaken(); for (Hits::const_iterator j = hits.begin(), j_end = hits.end(); j != j_end; ++j) { damageMsg.writeShort(oid); damageMsg.writeShort(*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.writeShort(oid); gameHandler->sendTo(p, leaveMsg); continue; } if (!wereInRange) { // o is now visible by p. Send enter message. MessageOut enterMsg(GPMSG_BEING_ENTER); enterMsg.writeByte(otype); enterMsg.writeShort(oid); enterMsg.writeByte(static_cast< Being *>(o)->getAction()); enterMsg.writeShort(opos.x); enterMsg.writeShort(opos.y); switch (otype) { case OBJECT_CHARACTER: { Character *q = static_cast< Character * >(o); enterMsg.writeString(q->getName()); enterMsg.writeByte(q->getHairStyle()); enterMsg.writeByte(q->getHairColor()); enterMsg.writeByte(q->getGender()); serializeLooks(q, enterMsg, true); } break; case OBJECT_MONSTER: { Monster *q = static_cast< Monster * >(o); enterMsg.writeShort(q->getSpecy()->getType()); enterMsg.writeString(q->getName()); } break; case OBJECT_NPC: { NPC *q = static_cast< NPC * >(o); enterMsg.writeShort(q->getNPC()); enterMsg.writeString(q->getName()); } break; default: assert(false); // TODO } gameHandler->sendTo(p, enterMsg); } if (opos != oold) { flags |= MOVING_POSITION; } // Send move messages. moveMsg.writeShort(oid); moveMsg.writeByte(flags); if (flags & MOVING_POSITION) { moveMsg.writeShort(opos.x); moveMsg.writeShort(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. moveMsg.writeByte((unsigned short) (o->getModifiedAttribute(ATTR_MOVE_SPEED_TPS) * 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->sendStatus(); // Inform client about health change of party members for (CharacterIterator i(map->getWholeMapIterator()); i; ++i) { Character *c = *i; // Make sure its not the same character if (c == p) continue; // make sure they are in the same party if (c->getParty() == p->getParty()) { int cflags = c->getUpdateFlags(); if (cflags & UPDATEFLAG_HEALTHCHANGE) { MessageOut healthMsg(GPMSG_BEING_HEALTH_CHANGE); healthMsg.writeShort(c->getPublicID()); healthMsg.writeShort(c->getModifiedAttribute(ATTR_HP)); healthMsg.writeShort(c->getModifiedAttribute(ATTR_MAX_HP)); gameHandler->sendTo(p, healthMsg); } } } // Inform client about items on the ground around its character MessageOut itemMsg(GPMSG_ITEMS); for (FixedActorIterator i(map->getAroundBeingIterator(p, visualRange)); i; ++i) { assert((*i)->getType() == OBJECT_ITEM || (*i)->getType() == OBJECT_EFFECT); Actor *o = *i; Point opos = o->getPosition(); int oflags = o->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: { Item *o = static_cast< Item * >(*i); 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.writeShort(o->getItemClass()->getDatabaseID()); appearMsg.writeShort(opos.x); appearMsg.writeShort(opos.y); gameHandler->sendTo(p, appearMsg); } else { itemMsg.writeShort(willBeInRange ? o->getItemClass()->getDatabaseID() : 0); itemMsg.writeShort(opos.x); itemMsg.writeShort(opos.y); } } break; case OBJECT_EFFECT: { Effect *o = static_cast< Effect * >(*i); o->show(); // Don't show old effects if (!(oflags & UPDATEFLAG_NEW_ON_MAP)) break; Being *b = o->getBeing(); if (b) { MessageOut effectMsg(GPMSG_CREATE_EFFECT_BEING); effectMsg.writeShort(o->getEffectId()); effectMsg.writeShort(b->getPublicID()); gameHandler->sendTo(p, effectMsg); } else { MessageOut effectMsg(GPMSG_CREATE_EFFECT_POS); effectMsg.writeShort(o->getEffectId()); effectMsg.writeShort(opos.x); effectMsg.writeShort(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 worldTime) { # ifndef NDEBUG dbgLockObjects = true; # endif // 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; } updateMap(map); for (CharacterIterator p(map->getWholeMapIterator()); p; ++p) { informPlayer(map, *p); /* sending the whole character is overhead for the database, it should be replaced by a syncbuffer. see: game-server/accountconnection: AccountConnection::syncChanges() if (worldTime % 2000 == 0) { accountHandler->sendCharacterData(*p); } */ } for (ActorIterator i(map->getWholeMapIterator()); i; ++i) { Actor *a = *i; a->clearUpdateFlags(); if (a->canFight()) { static_cast< Being * >(a)->clearHitsTaken(); } } } # ifndef NDEBUG dbgLockObjects = false; # endif // Take care of events that were delayed because of their side effects. for (DelayedEvents::iterator i = delayedEvents.begin(), i_end = delayedEvents.end(); i != i_end; ++i) { const DelayedEvent &e = i->second; Actor *o = i->first; switch (e.type) { case EVENT_REMOVE: remove(o); if (o->getType() == OBJECT_CHARACTER) { Character *ch = static_cast< Character * >(o); ch->disconnected(); gameHandler->kill(ch); } delete o; break; case EVENT_INSERT: insertSafe(o); break; case EVENT_WARP: assert(o->getType() == OBJECT_CHARACTER); warp(static_cast< Character * >(o), e.map, e.x, e.y); break; } } delayedEvents.clear(); } bool GameState::insert(Thing *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->inserted(); return true; } // Check that coordinates are actually valid. Actor *obj = static_cast< Actor * >(ptr); Map *mp = map->getMap(); Point pos = obj->getPosition(); if (pos.x / 32 >= (unsigned)mp->getWidth() || pos.y / 32 >= (unsigned)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->setPosition(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->inserted(); // DEBUG INFO switch (obj->getType()) { case OBJECT_ITEM: LOG_DEBUG("Item inserted: " << static_cast(obj)->getItemClass()->getDatabaseID()); break; case OBJECT_NPC: LOG_DEBUG("NPC inserted: " << static_cast(obj)->getNPC()); break; case OBJECT_CHARACTER: LOG_DEBUG("Player inserted: " << static_cast(obj)->getName()); break; case OBJECT_EFFECT: LOG_DEBUG("Effect inserted: " << static_cast(obj)->getEffectId()); break; case OBJECT_MONSTER: LOG_DEBUG("Monster inserted: " << static_cast(obj)->getSpecy()->getType()); break; case OBJECT_ACTOR: case OBJECT_OTHER: default: LOG_DEBUG("Thing inserted: " << obj->getType()); } obj->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.writeShort(pos.x); mapChangeMessage.writeShort(pos.y); gameHandler->sendTo(static_cast< Character * >(obj), mapChangeMessage); // update the online state of the character accountHandler->updateOnlineStatus( static_cast< Character * >(obj)->getDatabaseID(), true); return true; } bool GameState::insertSafe(Thing *ptr) { if (insert(ptr)) return true; delete ptr; return false; } void GameState::remove(Thing *ptr) { assert(!dbgLockObjects); MapComposite *map = ptr->getMap(); int visualRange = Configuration::getValue("game_visualRange", 448); ptr->removed(); // DEBUG INFO switch (ptr->getType()) { case OBJECT_ITEM: LOG_DEBUG("Item removed: " << static_cast(ptr)->getItemClass()->getDatabaseID()); break; case OBJECT_NPC: LOG_DEBUG("NPC removed: " << static_cast(ptr)->getNPC()); break; case OBJECT_CHARACTER: LOG_DEBUG("Player removed: " << static_cast(ptr)->getName()); break; case OBJECT_EFFECT: LOG_DEBUG("Effect removed: " << static_cast(ptr)->getEffectId()); break; case OBJECT_MONSTER: LOG_DEBUG("Monster removed: " << static_cast(ptr)->getSpecy()->getType()); break; case OBJECT_ACTOR: case OBJECT_OTHER: default: LOG_DEBUG("Thing removed: " << ptr->getType()); } if (ptr->canMove()) { if (ptr->getType() == OBJECT_CHARACTER) { static_cast< Character * >(ptr)->cancelTransaction(); // remove characters online status accountHandler->updateOnlineStatus( static_cast< Character * >(ptr)->getDatabaseID(), false); } Actor *obj = static_cast< Actor * >(ptr); MessageOut msg(GPMSG_BEING_LEAVE); msg.writeShort(obj->getPublicID()); Point objectPos = obj->getPosition(); for (CharacterIterator p(map->getAroundActorIterator(obj, visualRange)); p; ++p) { if (*p != obj && objectPos.inRangeOf((*p)->getPosition(), visualRange)) { gameHandler->sendTo(*p, msg); } } } else if (ptr->getType() == OBJECT_ITEM) { Item *obj = static_cast< Item * >(ptr); Point pos = obj->getPosition(); MessageOut msg(GPMSG_ITEMS); msg.writeShort(0); msg.writeShort(pos.x); msg.writeShort(pos.y); for (CharacterIterator p(map->getAroundActorIterator(obj, visualRange)); p; ++p) { if (pos.inRangeOf((*p)->getPosition(), visualRange)) { gameHandler->sendTo(*p, msg); } } } map->remove(ptr); } void GameState::warp(Character *ptr, MapComposite *map, int x, int y) { remove(ptr); ptr->setMap(map); ptr->setPosition(Point(x, y)); ptr->clearDestination(); /* 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); if (map->isActive()) { if (!insert(ptr)) { ptr->disconnected(); gameHandler->kill(ptr); delete ptr; } } else { MessageOut msg(GAMSG_REDIRECT); msg.writeLong(ptr->getDatabaseID()); accountHandler->send(msg); gameHandler->prepareServerChange(ptr); } } /** * Enqueues an event. It will be executed at end of update. */ static void enqueueEvent(Actor *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(Actor *ptr) { DelayedEvent e = { EVENT_INSERT, 0, 0, 0 }; enqueueEvent(ptr, e); } void GameState::enqueueRemove(Actor *ptr) { DelayedEvent e = { EVENT_REMOVE, 0, 0, 0 }; enqueueEvent(ptr, e); } void GameState::enqueueWarp(Character *ptr, MapComposite *m, int x, int y) { DelayedEvent e = { EVENT_WARP, x, y, m }; enqueueEvent(ptr, e); } void GameState::sayAround(Actor *obj, const std::string &text) { Point speakerPosition = obj->getPosition(); int visualRange = Configuration::getValue("game_visualRange", 448); for (CharacterIterator i(obj->getMap()->getAroundActorIterator(obj, visualRange)); i; ++i) { if (speakerPosition.inRangeOf((*i)->getPosition(), visualRange)) { sayTo(*i, obj, text); } } } void GameState::sayTo(Actor *destination, Actor *source, const std::string &text) { if (destination->getType() != OBJECT_CHARACTER) return; //only characters will read it anyway MessageOut msg(GPMSG_SAY); if (source == NULL) { msg.writeShort(0); } else if (!source->canMove()) { msg.writeShort(65535); } else { msg.writeShort(static_cast< Actor * >(source)->getPublicID()); } msg.writeString(text); gameHandler->sendTo(static_cast< Character * >(destination), msg); } void GameState::sayToAll(const std::string &text) { MessageOut msg(GPMSG_SAY); // message will show as from server msg.writeShort(0); msg.writeString(text); // sends to everyone connected to the game server gameHandler->sendToEveryone(msg); }