summaryrefslogtreecommitdiff
path: root/src/game-server/charactercomponent.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/game-server/charactercomponent.cpp')
-rw-r--r--src/game-server/charactercomponent.cpp583
1 files changed, 583 insertions, 0 deletions
diff --git a/src/game-server/charactercomponent.cpp b/src/game-server/charactercomponent.cpp
new file mode 100644
index 00000000..8a50d7cd
--- /dev/null
+++ b/src/game-server/charactercomponent.cpp
@@ -0,0 +1,583 @@
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "game-server/charactercomponent.h"
+
+#include "common/configuration.h"
+#include "game-server/accountconnection.h"
+#include "game-server/attributemanager.h"
+#include "game-server/buysell.h"
+#include "game-server/inventory.h"
+#include "game-server/item.h"
+#include "game-server/itemmanager.h"
+#include "game-server/gamehandler.h"
+#include "game-server/map.h"
+#include "game-server/mapcomposite.h"
+#include "game-server/mapmanager.h"
+#include "game-server/state.h"
+#include "game-server/trade.h"
+#include "scripting/scriptmanager.h"
+#include "net/messagein.h"
+#include "net/messageout.h"
+
+#include "utils/logger.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cmath>
+#include <limits.h>
+
+Script::Ref CharacterComponent::mDeathCallback;
+Script::Ref CharacterComponent::mDeathAcceptedCallback;
+Script::Ref CharacterComponent::mLoginCallback;
+
+static bool executeCallback(Script::Ref function, Entity &entity)
+{
+ if (!function.isValid())
+ return false;
+
+ Script *script = ScriptManager::currentState();
+ script->prepare(function);
+ script->push(&entity);
+ script->execute(entity.getMap());
+ return true;
+}
+
+
+CharacterComponent::CharacterComponent(Entity &entity, MessageIn &msg):
+ mClient(nullptr),
+ mConnected(true),
+ mTransactionHandler(nullptr),
+ mDatabaseID(-1),
+ mHairStyle(0),
+ mHairColor(0),
+ mSendAttributePointsStatus(false),
+ mAttributePoints(0),
+ mCorrectionPoints(0),
+ mSendAbilityCooldown(false),
+ mParty(0),
+ mTransaction(TRANS_NONE),
+ mTalkNpcId(0),
+ mNpcThread(0),
+ mBaseEntity(&entity)
+{
+ auto *beingComponent = entity.getComponent<BeingComponent>();
+
+ auto &attributeScope = attributeManager->getAttributeScope(CharacterScope);
+ LOG_DEBUG("Character creation: initialisation of "
+ << attributeScope.size() << " attributes.");
+ for (auto &attribute : attributeScope)
+ beingComponent->createAttribute(attribute);
+
+ auto *actorComponent = entity.getComponent<ActorComponent>();
+ actorComponent->setWalkMask(Map::BLOCKMASK_WALL);
+ actorComponent->setBlockType(BLOCKTYPE_CHARACTER);
+ actorComponent->setSize(16);
+
+
+ auto *abilityComponent = new AbilityComponent();
+ entity.addComponent(abilityComponent);
+ abilityComponent->signal_ability_changed.connect(
+ sigc::mem_fun(this, &CharacterComponent::abilityStatusChanged));
+ abilityComponent->signal_global_cooldown_activated.connect(
+ sigc::mem_fun(this,
+ &CharacterComponent::abilityCooldownActivated));
+
+ // Get character data.
+ mDatabaseID = msg.readInt32();
+ beingComponent->setName(msg.readString());
+
+ deserialize(entity, msg);
+
+ Inventory(&entity, mPossessions).initialize();
+ modifiedAllAttributes(entity);;
+
+ beingComponent->signal_attribute_changed.connect(sigc::mem_fun(
+ this, &CharacterComponent::attributeChanged));
+
+ for (auto &abilityIt : abilityComponent->getAbilities())
+ mModifiedAbilities.insert(abilityIt.first);
+}
+
+CharacterComponent::~CharacterComponent()
+{
+ delete mNpcThread;
+}
+
+void CharacterComponent::deserialize(Entity &entity, MessageIn &msg)
+{
+ auto *beingComponent = entity.getComponent<BeingComponent>();
+
+ // general character properties
+ setAccountLevel(msg.readInt8());
+ beingComponent->setGender(ManaServ::getGender(msg.readInt8()));
+ setHairStyle(msg.readInt8());
+ setHairColor(msg.readInt8());
+ setAttributePoints(msg.readInt16());
+ setCorrectionPoints(msg.readInt16());
+
+ // character attributes
+ unsigned attrSize = msg.readInt16();
+ for (unsigned i = 0; i < attrSize; ++i)
+ {
+ unsigned id = msg.readInt16();
+ double base = msg.readDouble();
+ auto *attributeInfo = attributeManager->getAttributeInfo(id);
+ if (attributeInfo)
+ beingComponent->setAttribute(entity, attributeInfo, base);
+ }
+
+ // status effects currently affecting the character
+ int statusSize = msg.readInt16();
+
+ for (int i = 0; i < statusSize; i++)
+ {
+ int status = msg.readInt16();
+ int time = msg.readInt16();
+ beingComponent->applyStatusEffect(status, time);
+ }
+
+ // location
+ auto *map = MapManager::getMap(msg.readInt16());
+ entity.setMap(map);
+
+ Point temporaryPoint;
+ temporaryPoint.x = msg.readInt16();
+ temporaryPoint.y = msg.readInt16();
+ entity.getComponent<ActorComponent>()->setPosition(entity, temporaryPoint);
+
+ // kill count
+ int killSize = msg.readInt16();
+ for (int i = 0; i < killSize; i++)
+ {
+ int monsterId = msg.readInt16();
+ int kills = msg.readInt32();
+ setKillCount(monsterId, kills);
+ }
+
+ // character abilities
+ int abilitiesSize = msg.readInt16();
+ for (int i = 0; i < abilitiesSize; i++)
+ {
+ const int id = msg.readInt32();
+ entity.getComponent<AbilityComponent>()->giveAbility(id);
+ }
+
+
+ Possessions &poss = getPossessions();
+
+ // Loads inventory - must be last because size isn't transmitted
+ InventoryData inventoryData;
+ EquipData equipmentData;
+ while (msg.getUnreadLength())
+ {
+ InventoryItem i;
+ i.slot = msg.readInt16();
+ i.itemId = msg.readInt16();
+ i.amount = msg.readInt16();
+ bool isEquipped = msg.readInt8() != 0;
+ inventoryData.insert(std::make_pair(i.slot, i));
+ if (isEquipped)
+ equipmentData.insert(i.slot);
+ }
+ poss.setInventory(inventoryData);
+ poss.setEquipment(equipmentData);
+}
+
+void CharacterComponent::serialize(Entity &entity, MessageOut &msg)
+{
+ auto *beingComponent = entity.getComponent<BeingComponent>();
+
+ // general character properties
+ msg.writeInt8(getAccountLevel());
+ msg.writeInt8(beingComponent->getGender());
+ msg.writeInt8(getHairStyle());
+ msg.writeInt8(getHairColor());
+ msg.writeInt16(getAttributePoints());
+ msg.writeInt16(getCorrectionPoints());
+
+
+ const AttributeMap &attributes = beingComponent->getAttributes();
+ std::map<const AttributeInfo *, const Attribute *> attributesToSend;
+ for (auto &attributeIt : attributes)
+ {
+ if (attributeIt.first->persistent)
+ attributesToSend.insert(std::make_pair(attributeIt.first,
+ &attributeIt.second));
+ }
+ msg.writeInt16(attributesToSend.size());
+ for (auto &attributeIt : attributesToSend)
+ {
+ msg.writeInt16(attributeIt.first->id);
+ msg.writeDouble(attributeIt.second->getBase());
+ msg.writeDouble(attributeIt.second->getModifiedAttribute());
+ }
+
+ // status effects currently affecting the character
+ auto &statusEffects = beingComponent->getStatusEffects();
+ msg.writeInt16(statusEffects.size());
+ for (auto &statusIt : statusEffects)
+ {
+ msg.writeInt16(statusIt.first);
+ msg.writeInt16(statusIt.second.time);
+ }
+
+ // location
+ msg.writeInt16(entity.getMap()->getID());
+ const Point &pos = entity.getComponent<ActorComponent>()->getPosition();
+ msg.writeInt16(pos.x);
+ msg.writeInt16(pos.y);
+
+ // kill count
+ msg.writeInt16(getKillCountSize());
+ for (auto &killCountIt : mKillCount)
+ {
+ msg.writeInt16(killCountIt.first);
+ msg.writeInt32(killCountIt.second);
+ }
+
+ // character abilities
+ auto &abilities = entity.getComponent<AbilityComponent>()->getAbilities();
+ msg.writeInt16(abilities.size());
+ for (auto &abilityIt : abilities) {
+ msg.writeInt32(abilityIt.first);
+ }
+
+ // inventory - must be last because size isn't transmitted
+ const Possessions &poss = getPossessions();
+ const EquipData &equipData = poss.getEquipment();
+
+ const InventoryData &inventoryData = poss.getInventory();
+ for (InventoryData::const_iterator itemIt = inventoryData.begin(),
+ itemIt_end = inventoryData.end(); itemIt != itemIt_end; ++itemIt)
+ {
+ msg.writeInt16(itemIt->first); // slot id
+ msg.writeInt16(itemIt->second.itemId); // item id
+ msg.writeInt16(itemIt->second.amount); // amount
+ if (equipData.find(itemIt->first) != equipData.end())
+ msg.writeInt8(1); // equipped
+ else
+ msg.writeInt8(0); // not equipped
+ }
+}
+
+void CharacterComponent::update(Entity &entity)
+{
+ // Dead character: don't regenerate anything else
+ if (entity.getComponent<BeingComponent>()->getAction() == DEAD)
+ return;
+
+ if (!mModifiedAbilities.empty())
+ sendAbilityUpdate(entity);
+
+ if (mSendAbilityCooldown)
+ sendAbilityCooldownUpdate(entity);
+
+ if (mSendAttributePointsStatus)
+ sendAttributePointsStatus(entity);
+}
+
+void CharacterComponent::characterDied(Entity *being)
+{
+ executeCallback(mDeathCallback, *being);
+}
+
+void CharacterComponent::respawn(Entity &entity)
+{
+ auto *beingComponent = entity.getComponent<BeingComponent>();
+
+ if (beingComponent->getAction() != DEAD)
+ {
+ LOG_WARN("Character \"" << beingComponent->getName()
+ << "\" tried to respawn without being dead");
+ return;
+ }
+
+ // Make it alive again
+ beingComponent->setAction(entity, STAND);
+
+ // Execute respawn callback when set
+ if (executeCallback(mDeathAcceptedCallback, entity))
+ return;
+
+ // No script respawn callback set - fall back to hardcoded logic
+ const double maxHp = beingComponent->getModifiedAttribute(
+ attributeManager->getAttributeInfo(ATTR_MAX_HP));
+ beingComponent->setAttribute(entity,
+ attributeManager->getAttributeInfo(ATTR_HP),
+ maxHp);
+ // Warp back to spawn point.
+ int spawnMap = Configuration::getValue("char_respawnMap", 1);
+ int spawnX = Configuration::getValue("char_respawnX", 1024);
+ int spawnY = Configuration::getValue("char_respawnY", 1024);
+
+ GameState::enqueueWarp(&entity, MapManager::getMap(spawnMap),
+ Point(spawnX, spawnY));
+}
+
+void CharacterComponent::abilityStatusChanged(int id)
+{
+ mModifiedAbilities.insert(id);
+}
+
+void CharacterComponent::abilityCooldownActivated()
+{
+ mSendAbilityCooldown = true;
+}
+
+void CharacterComponent::sendAbilityUpdate(Entity &entity)
+{
+ auto &abilities = entity.getComponent<AbilityComponent>()->getAbilities();
+
+ MessageOut msg(GPMSG_ABILITY_STATUS);
+ for (unsigned id : mModifiedAbilities)
+ {
+ auto it = abilities.find(id);
+ if (it == abilities.end())
+ continue; // got deleted
+
+ msg.writeInt8(id);
+ msg.writeInt32(it->second.rechargeTimeout.remaining());
+ }
+
+ mModifiedAbilities.clear();
+ gameHandler->sendTo(mClient, msg);
+}
+
+void CharacterComponent::sendAbilityCooldownUpdate(Entity &entity)
+{
+ MessageOut msg(GPMSG_ABILITY_COOLDOWN);
+ auto *abilityComponent = entity.getComponent<AbilityComponent>();
+ msg.writeInt16(abilityComponent->globalCooldown());
+ gameHandler->sendTo(mClient, msg);
+ mSendAbilityCooldown = false;
+}
+
+void CharacterComponent::sendAttributePointsStatus(Entity &entity)
+{
+ MessageOut msg(GPMSG_ATTRIBUTE_POINTS_STATUS);
+ msg.writeInt16(mAttributePoints);
+ msg.writeInt16(mCorrectionPoints);
+ gameHandler->sendTo(mClient, msg);
+ mSendAttributePointsStatus = false;
+}
+
+void CharacterComponent::cancelTransaction()
+{
+ TransactionType t = mTransaction;
+ mTransaction = TRANS_NONE;
+ switch (t)
+ {
+ case TRANS_TRADE:
+ static_cast< Trade * >(mTransactionHandler)->cancel();
+ break;
+ case TRANS_BUYSELL:
+ static_cast< BuySell * >(mTransactionHandler)->cancel();
+ break;
+ case TRANS_NONE:
+ return;
+ }
+}
+
+Trade *CharacterComponent::getTrading() const
+{
+ return mTransaction == TRANS_TRADE
+ ? static_cast< Trade * >(mTransactionHandler) : nullptr;
+}
+
+BuySell *CharacterComponent::getBuySell() const
+{
+ return mTransaction == TRANS_BUYSELL
+ ? static_cast< BuySell * >(mTransactionHandler) : nullptr;
+}
+
+void CharacterComponent::setTrading(Trade *t)
+{
+ if (t)
+ {
+ cancelTransaction();
+ mTransactionHandler = t;
+ mTransaction = TRANS_TRADE;
+ }
+ else
+ {
+ assert(mTransaction == TRANS_NONE || mTransaction == TRANS_TRADE);
+ mTransaction = TRANS_NONE;
+ }
+}
+
+void CharacterComponent::setBuySell(BuySell *t)
+{
+ if (t)
+ {
+ cancelTransaction();
+ mTransactionHandler = t;
+ mTransaction = TRANS_BUYSELL;
+ }
+ else
+ {
+ assert(mTransaction == TRANS_NONE || mTransaction == TRANS_BUYSELL);
+ mTransaction = TRANS_NONE;
+ }
+}
+
+void CharacterComponent::sendStatus(Entity &entity)
+{
+ auto *beingComponent = entity.getComponent<BeingComponent>();
+ MessageOut attribMsg(GPMSG_PLAYER_ATTRIBUTE_CHANGE);
+ for (AttributeInfo *attribute : mModifiedAttributes)
+ {
+ attribMsg.writeInt16(attribute->id);
+ attribMsg.writeInt32(beingComponent->getAttributeBase(attribute) * 256);
+ attribMsg.writeInt32(beingComponent->getModifiedAttribute(attribute) * 256);
+ }
+ if (attribMsg.getLength() > 2) gameHandler->sendTo(mClient, attribMsg);
+ mModifiedAttributes.clear();
+}
+
+void CharacterComponent::modifiedAllAttributes(Entity &entity)
+{
+ auto *beingComponent = entity.getComponent<BeingComponent>();
+
+ LOG_DEBUG("Marking all attributes as changed, requiring recalculation.");
+ for (auto attribute : beingComponent->getAttributes())
+ {
+ beingComponent->recalculateBaseAttribute(entity, attribute.first);
+ mModifiedAttributes.insert(attribute.first);
+ }
+}
+
+void CharacterComponent::attributeChanged(Entity *entity,
+ AttributeInfo *attribute)
+{
+ auto *beingComponent = entity->getComponent<BeingComponent>();
+
+ // Inform the client of this attribute modification.
+ accountHandler->updateAttributes(getDatabaseID(), attribute->id,
+ beingComponent->getAttributeBase(attribute),
+ beingComponent->getModifiedAttribute(attribute));
+ mModifiedAttributes.insert(attribute);
+}
+
+void CharacterComponent::incrementKillCount(int monsterType)
+{
+ std::map<int, int>::iterator i = mKillCount.find(monsterType);
+ if (i == mKillCount.end())
+ {
+ // Character has never murdered this species before
+ mKillCount[monsterType] = 1;
+ }
+ else
+ {
+ // Character is a repeated offender
+ mKillCount[monsterType] ++;
+ }
+}
+
+int CharacterComponent::getKillCount(int monsterType) const
+{
+ std::map<int, int>::const_iterator i = mKillCount.find(monsterType);
+ if (i != mKillCount.end())
+ return i->second;
+ return 0;
+}
+
+AttribmodResponseCode CharacterComponent::useCharacterPoint(Entity &entity,
+ AttributeInfo *attribute)
+{
+ auto *beingComponent = entity.getComponent<BeingComponent>();
+
+ if (!attribute->modifiable)
+ return ATTRIBMOD_INVALID_ATTRIBUTE;
+ if (!mAttributePoints)
+ return ATTRIBMOD_NO_POINTS_LEFT;
+
+ setAttributePoints(mAttributePoints - 1);
+
+ const double base = beingComponent->getAttributeBase(attribute);
+ beingComponent->setAttribute(entity, attribute, base + 1);
+ beingComponent->updateDerivedAttributes(entity, attribute);
+ return ATTRIBMOD_OK;
+}
+
+AttribmodResponseCode CharacterComponent::useCorrectionPoint(Entity &entity,
+ AttributeInfo *attribute)
+{
+ auto *beingComponent = entity.getComponent<BeingComponent>();
+
+ if (!attribute->modifiable)
+ return ATTRIBMOD_INVALID_ATTRIBUTE;
+ if (!mCorrectionPoints)
+ return ATTRIBMOD_NO_POINTS_LEFT;
+ if (beingComponent->getAttributeBase(attribute) <= 1)
+ return ATTRIBMOD_DENIED;
+
+ setCorrectionPoints(mCorrectionPoints - 1);
+ setAttributePoints(mAttributePoints + 1);
+
+ const double base = beingComponent->getAttributeBase(attribute);
+ beingComponent->setAttribute(entity, attribute, base - 1);
+ return ATTRIBMOD_OK;
+}
+
+void CharacterComponent::startNpcThread(Script::Thread *thread, int npcId)
+{
+ if (mNpcThread)
+ delete mNpcThread;
+
+ mNpcThread = thread;
+ mTalkNpcId = npcId;
+
+ resumeNpcThread();
+}
+
+void CharacterComponent::resumeNpcThread()
+{
+ Script *script = ScriptManager::currentState();
+
+ assert(script->getCurrentThread() == mNpcThread);
+
+ if (script->resume())
+ {
+ MessageOut msg(GPMSG_NPC_CLOSE);
+ msg.writeInt16(mTalkNpcId);
+ gameHandler->sendTo(mClient, msg);
+
+ mTalkNpcId = 0;
+ mNpcThread = 0;
+ }
+}
+
+void CharacterComponent::disconnected(Entity &entity)
+{
+ mConnected = false;
+
+ // Make the dead characters respawn, even in case of disconnection.
+ if (entity.getComponent<BeingComponent>()->getAction() == DEAD)
+ respawn(entity);
+ else
+ GameState::remove(&entity);
+
+ signal_disconnected.emit(entity);
+}
+void CharacterComponent::triggerLoginCallback(Entity &entity)
+{
+ executeCallback(mLoginCallback, entity);
+}