// Ryzom - MMORPG Framework // Copyright (C) 2010 Winch Gate Property Limited // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program 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 Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include "stdpch.h" #include "npc_icon.h" #include "ingame_database_manager.h" #include "game_share/generic_xml_msg_mngr.h" #include "entities.h" #include "net_manager.h" using namespace std; using namespace NLMISC; CNPCIconCache* CNPCIconCache::_Instance = NULL; // Time after which the state of a NPC is considered obsolete and must be refreshed (because it's in gamecycle, actual time increases if server slows down, to avoid server congestion) NLMISC::TGameCycle CNPCIconCache::_CacheRefreshTimerDelay = NPC_ICON::DefaultClientNPCIconRefreshTimerDelayGC; // Time between updates of the "catchall timer" NLMISC::TGameCycle CNPCIconCache::_CatchallTimerPeriod = NPC_ICON::DefaultClientNPCIconRefreshTimerDelayGC; extern CEntityManager EntitiesMngr; extern CGenericXmlMsgHeaderManager GenericMsgHeaderMngr; extern CNetManager NetMngr; // #pragma optimize ("", off) CNPCIconCache::CNPCIconCache() : _LastRequestTimestamp(0), _LastTimerUpdateTimestamp(0), _Enabled(true) { _Icons[NPC_ICON::IconNone].init("", ""); _Icons[NPC_ICON::IconNotAMissionGiver].init("", ""); _Icons[NPC_ICON::IconListHasOutOfReachMissions].init("mission_available.tga", ""); //"MP_Blood.tga" _Icons[NPC_ICON::IconListHasAlreadyTakenMissions].init("ICO_Task_Generic.tga", "r2ed_tool_redo"); _Icons[NPC_ICON::IconListHasAvailableMission].init("mission_available.tga", "", CViewRadar::MissionList); //"MP_Wood.tga" _Icons[NPC_ICON::IconAutoHasUnavailableMissions].init("spe_com.tga", ""); _Icons[NPC_ICON::IconAutoHasAvailableMission].init("spe_com.tga", "", CViewRadar::MissionAuto); //"MP_Oil.tga" _Icons[NPC_ICON::IconStepMission].init("mission_step.tga", "", CViewRadar::MissionStep); //"MP_Shell.tga" _DescriptionsToRequest.reserve(256); } void CNPCIconCache::release() { if (_Instance) { delete _Instance; _Instance = NULL; } } const CNPCIconCache::CNPCIconDesc& CNPCIconCache::getNPCIcon(const CEntityCL *entity, bool bypassEnabled) { // Not applicable? Most entities (creatures, characters) have a null key here. BOMB_IF(!entity, "NULL entity in getNPCIcon", return _Icons[NPC_ICON::IconNone]); TNPCIconCacheKey npcIconCacheKey = CNPCIconCache::entityToKey(entity); if (npcIconCacheKey == 0) return _Icons[NPC_ICON::IconNone]; // Is system disabled? if ((!enabled()) && !bypassEnabled) return _Icons[NPC_ICON::IconNone]; // This method must be reasonably fast, because it constantly gets called by the radar view H_AUTO(GetNPCIconWithKey); // Not applicable (more checks)? if (!entity->canHaveMissionIcon()) return _Icons[NPC_ICON::IconNone]; if (!entity->isFriend()) // to display icons in the radar, we need the Contextual property to be received as soon as possible return _Icons[NPC_ICON::IconNone]; // Temporarily not shown if the player is in interaction with the NPC if (UserEntity->interlocutor() != CLFECOMMON::INVALID_SLOT) { CEntityCL *interlocutorEntity = EntitiesMngr.entity(UserEntity->interlocutor()); if (interlocutorEntity && (entityToKey(interlocutorEntity) == npcIconCacheKey)) return _Icons[NPC_ICON::IconNone]; } if (UserEntity->trader() != CLFECOMMON::INVALID_SLOT) { CEntityCL *traderEntity = EntitiesMngr.entity(UserEntity->trader()); if (traderEntity && (entityToKey(traderEntity) == npcIconCacheKey)) return _Icons[NPC_ICON::IconNone]; } // 1. Test if the NPC is involved in a current goal if (isNPCaCurrentGoal(npcIconCacheKey)) return _Icons[NPC_ICON::IconStepMission]; // 2. Compute "has mission to take": take from cache, or query the server H_AUTO(GetNPCIcon_GIVER); CMissionGiverMap::iterator img = _MissionGivers.find(npcIconCacheKey); if (img != _MissionGivers.end()) { CNPCMissionGiverDesc& giver = (*img).second; if (giver.getState() != NPC_ICON::AwaitingFirstData) { // Ask the server to refresh the state if the information is old // but only known mission givers that have a chance to propose new missions if ((giver.getState() != NPC_ICON::NotAMissionGiver) && // (giver.getState() != NPC_ICON::ListHasAlreadyTakenMissions) && // commented out because it would not refresh in case an auto mission become available (!giver.isDescTransient())) { NLMISC::TGameCycle informationAge = NetMngr.getCurrentServerTick() - giver.getLastUpdateTimestamp(); if (informationAge > _CacheRefreshTimerDelay) { queryMissionGiverData(npcIconCacheKey); giver.setDescTransient(); } } // Return the icon depending on the state in the cache return _Icons[giver.getState()]; // TNPCIconId maps TNPCMissionGiverState } } else { // Create mission giver entry and query the server CNPCMissionGiverDesc giver; CMissionGiverMap::iterator itg = _MissionGivers.insert(make_pair(npcIconCacheKey, giver)).first; queryMissionGiverData(npcIconCacheKey); //(*itg).second.setDescTransient(); // already made transient by constructor } return _Icons[NPC_ICON::IconNone]; } #define getArraySize(a) (sizeof(a)/sizeof(a[0])) void CNPCIconCache::addObservers() { // Disabled? if (!enabled()) return; // Mission Journal static const char *missionStartStopLeavesToMonitor [2] = {"TITLE", "FINISHED"}; IngameDbMngr.getNodePtr()->addBranchObserver("MISSIONS", MissionStartStopObserver, missionStartStopLeavesToMonitor, getArraySize(missionStartStopLeavesToMonitor)); static const char *missionNpcAliasLeavesToMonitor [1] = {"NPC_ALIAS"}; IngameDbMngr.getNodePtr()->addBranchObserver("MISSIONS", MissionNpcAliasObserver, missionNpcAliasLeavesToMonitor, getArraySize(missionNpcAliasLeavesToMonitor)); // Skills static const char *skillLeavesToMonitor [2] = {"SKILL", "BaseSKILL"}; IngameDbMngr.getNodePtr()->addBranchObserver("CHARACTER_INFO:SKILLS", MissionPrerequisitEventObserver, skillLeavesToMonitor, getArraySize(skillLeavesToMonitor)); // Owned Items static const char *bagLeavesToMonitor [1] = {"SHEET"}; // just saves 2000 bytes or so (500 * observer pointer entry in vector) compared to one observer per bag slot IngameDbMngr.getNodePtr()->addBranchObserver("INVENTORY:BAG", MissionPrerequisitEventObserver, bagLeavesToMonitor, getArraySize(bagLeavesToMonitor)); // Worn Items IngameDbMngr.getNodePtr()->addBranchObserver("INVENTORY:HAND", MissionPrerequisitEventObserver); IngameDbMngr.getNodePtr()->addBranchObserver("INVENTORY:EQUIP", MissionPrerequisitEventObserver); // Known Bricks IngameDbMngr.getNodePtr()->addBranchObserver("BRICK_FAMILY", MissionPrerequisitEventObserver); // For other events, search for calls of onEventForMissionAvailabilityForThisChar() } void CNPCIconCache::removeObservers() { // Disabled? if (!enabled()) return; // Mission Journal IngameDbMngr.getNodePtr()->removeBranchObserver("MISSIONS", MissionStartStopObserver); IngameDbMngr.getNodePtr()->removeBranchObserver("MISSIONS", MissionNpcAliasObserver); // Skills IngameDbMngr.getNodePtr()->removeBranchObserver("CHARACTER_INFO:SKILLS", MissionPrerequisitEventObserver); // Owned Items IngameDbMngr.getNodePtr()->removeBranchObserver("INVENTORY:BAG", MissionPrerequisitEventObserver); // Worn Items IngameDbMngr.getNodePtr()->removeBranchObserver("INVENTORY:HAND", MissionPrerequisitEventObserver); IngameDbMngr.getNodePtr()->removeBranchObserver("INVENTORY:EQUIP", MissionPrerequisitEventObserver); // Known Bricks IngameDbMngr.getNodePtr()->removeBranchObserver("BRICK_FAMILY", MissionPrerequisitEventObserver); } void CNPCIconCache::CMissionStartStopObserver::update(ICDBNode* node) { // Every time a mission in progress is started or stopped, refresh the icon for visible NPCs (including mission giver information) CNPCIconCache::getInstance().onEventForMissionInProgress(); } void CNPCIconCache::CMissionNpcAliasObserver::update(ICDBNode* node) { CNPCIconCache::getInstance().onNpcAliasChangedInMissionGoals(); } void CNPCIconCache::CMissionPrerequisitEventObserver::update(ICDBNode* node) { // Every time a mission in progress changes, refresh the icon for the related npc CNPCIconCache::getInstance().onEventForMissionAvailabilityForThisChar(); } void CNPCIconCache::onEventForMissionAvailabilityForThisChar() { // Disabled? if (!enabled()) return; queryAllVisibleMissionGiverData(0); } void CNPCIconCache::queryMissionGiverData(TNPCIconCacheKey npcIconCacheKey) { _DescriptionsToRequest.push_back(npcIconCacheKey); //static set requests1; //requests1.insert(npcIconCacheKey); //nldebug("%u: queryMissionGiverData %u (total %u)", NetMngr.getCurrentServerTick(), npcIconCacheKey, requests1.size()); } void CNPCIconCache::queryAllVisibleMissionGiverData(NLMISC::TGameCycle olderThan) { // Request an update for all npcs (qualifying, i.e. that have missions) in vision for (uint i=0; icanHaveMissionIcon() && entity->isFriend())) continue; TNPCIconCacheKey npcIconCacheKey = CNPCIconCache::entityToKey(entity); CMissionGiverMap::iterator img = _MissionGivers.find(npcIconCacheKey); if (img == _MissionGivers.end()) continue; // if the NPC does not have an entry yet, it will be created by getNPCIcon() // Refresh only known mission givers that have a chance to propose new missions CNPCMissionGiverDesc& giver = (*img).second; if (giver.getState() == NPC_ICON::NotAMissionGiver) continue; // if (giver.getState() == NPC_ICON::ListHasAlreadyTakenMissions) // continue; // commented out because it would not refresh in case an auto mission becomes available if (olderThan != 0) { // Don't refresh desscriptions already awaiting an update if (giver.isDescTransient()) continue; // Don't refresh NPCs having data more recent than specified NLMISC::TGameCycle informationAge = NetMngr.getCurrentServerTick() - giver.getLastUpdateTimestamp(); if (informationAge <= olderThan) continue; // Don't refresh NPC being involved in a mission goal (the step icon has higher priority over the giver icon) // If later the NPC is no more involved before the information is considered old, it will show // the same giver state until the information is considered old. That's why we let refresh // the NPC when triggered by an event (olderThan == 0). if (isNPCaCurrentGoal(npcIconCacheKey)) continue; } _DescriptionsToRequest.push_back(npcIconCacheKey); giver.setDescTransient(); //static set requests2; //requests2.insert(npcIconCacheKey); //nldebug("%u: queryAllVisibleMissionGiverData %u (total %u)", NetMngr.getCurrentServerTick(), npcIconCacheKey, requests2.size()); } } void CNPCIconCache::update() { // Every CatchallTimerPeriod, browse visible entities and refresh the ones with outdated state // (e.g. the ones not displayed in radar). if (NetMngr.getCurrentServerTick() > _LastTimerUpdateTimestamp + _CatchallTimerPeriod) { _LastTimerUpdateTimestamp = NetMngr.getCurrentServerTick(); // Disabled? if (!enabled()) return; queryAllVisibleMissionGiverData(_CacheRefreshTimerDelay); } // Every tick update at most (2 cycles actually, cf. server<->client communication frequency), // send all pending requests in a single message. if (NetMngr.getCurrentServerTick() > _LastRequestTimestamp) { if (!_DescriptionsToRequest.empty()) { CBitMemStream out; GenericMsgHeaderMngr.pushNameToStream("NPC_ICON:GET_DESC", out); uint8 nb8 = uint8(_DescriptionsToRequest.size() & 0xff); // up to vision size (255 i.e. 256 minus user) out.serial(nb8); for (CSmallKeyList::const_iterator ikl=_DescriptionsToRequest.begin(); ikl!=_DescriptionsToRequest.end(); ++ikl) { TNPCIconCacheKey key = *ikl; out.serial(key); } NetMngr.push(out); //nldebug("%u: Pushing %hu NPC desc requests", NetMngr.getCurrentServerTick(), nb8); _DescriptionsToRequest.clear(); } _LastRequestTimestamp = NetMngr.getCurrentServerTick(); } } void CNPCIconCache::onEventForMissionInProgress() { // Disabled? if (!enabled()) return; // Immediately reflect the mission journal (Step icons) refreshIconsOfScene(true); // Ask the server to update availability status (will refresh icons if there is at least one change) onEventForMissionAvailabilityForThisChar(); } void CNPCIconCache::onNpcAliasChangedInMissionGoals() { // Disabled? if (!enabled()) return; // Update the storage of keys having a current mission goal. storeKeysOfCurrentGoals(); // Immediately reflect the mission journal (Step icons) refreshIconsOfScene(true); } bool CNPCIconCache::isNPCaCurrentGoal(TNPCIconCacheKey npcIconCacheKey) const { // There aren't many simultaneous goals, we can safely browse the vector for (CSmallKeyList::const_iterator ikl=_KeysOfCurrentGoals.begin(); ikl!=_KeysOfCurrentGoals.end(); ++ikl) { if ((*ikl) == npcIconCacheKey) return true; } return false; } void CNPCIconCache::storeKeysOfCurrentGoals() { // This event is very unfrequent, and the number of elements of _KeysOfCurrentGoals is usually very small // (typically 0 to 3, while theoretical max is 15*20) so we don't mind rebuilding the list. _KeysOfCurrentGoals.clear(); CCDBNodeBranch *missionNode = safe_cast(IngameDbMngr.getNodePtr()->getNode(ICDBNode::CTextId("MISSIONS"))); BOMB_IF (!missionNode, "MISSIONS node missing in DB", return); uint nbCurrentMissionSlots = missionNode->getNbNodes(); for (uint i=0; i!=nbCurrentMissionSlots; ++i) { ICDBNode *missionEntry = missionNode->getNode((uint16)i); ICDBNode::CTextId titleNode("TITLE"); if (missionEntry->getProp(titleNode) == 0) continue; CCDBNodeBranch *stepsToDoNode = safe_cast(missionEntry->getNode(ICDBNode::CTextId("GOALS"))); BOMB_IF(!stepsToDoNode, "GOALS node missing in MISSIONS DB", return); uint nbGoals = stepsToDoNode->getNbNodes(); for (uint j=0; j!=nbGoals; ++j) { ICDBNode *stepNode = stepsToDoNode->getNode((uint16)j); CCDBNodeLeaf *aliasNode = safe_cast(stepNode->getNode(ICDBNode::CTextId("NPC_ALIAS"))); BOMB_IF(!aliasNode, "NPC_ALIAS node missing in MISSIONS DB", return); TNPCIconCacheKey npcIconCacheKey = (TNPCIconCacheKey)aliasNode->getValue32(); if (npcIconCacheKey != 0) _KeysOfCurrentGoals.push_back(npcIconCacheKey); } } } void CNPCIconCache::refreshIconsOfScene(bool force) { // Browse all NPCs in vision, and refresh their inscene interface for (uint i=0; i qualifs; // qualifs.insert(npcIconCacheKey); // nldebug("NPC %u qualifies (total=%u)", npcIconCacheKey, qualifs.size()); //} return (*img).second.updateMissionAvailabilityForThisChar(state); } bool CNPCMissionGiverDesc::updateMissionAvailabilityForThisChar(NPC_ICON::TNPCMissionGiverState state) { _HasChanged = (state != _MissionGiverState); _MissionGiverState = state; _LastUpdateTimestamp = NetMngr.getCurrentServerTick(); _IsDescTransient = false; return _HasChanged; } void CNPCIconCache::setMissionGiverTimer(NLMISC::TGameCycle delay) { _CacheRefreshTimerDelay = delay; _CatchallTimerPeriod = delay; } std::string CNPCIconCache::getDump() const { string s = toString("System %s\nCurrent timers: %u %u\n", _Enabled?"enabled":"disabled", _CacheRefreshTimerDelay, _CatchallTimerPeriod); s += toString("%u NPCs in mission giver map:\n", _MissionGivers.size()); for (CMissionGiverMap::const_iterator img=_MissionGivers.begin(); img!=_MissionGivers.end(); ++img) { const CNPCMissionGiverDesc& giver = (*img).second; s += toString("NPC %u: ", (*img).first) + giver.getDump() + "\n"; } s += "Current NPC goals:\n"; for (CSmallKeyList::const_iterator ikl=_KeysOfCurrentGoals.begin(); ikl!=_KeysOfCurrentGoals.end(); ++ikl) { s += toString("NPC %u", (*ikl)); } return s; } std::string CNPCMissionGiverDesc::getDump() const { return toString("%u [%u s ago]", _MissionGiverState, (NetMngr.getCurrentServerTick()-_LastUpdateTimestamp)/10); } void CNPCIconCache::setEnabled(bool b) { if (!_Enabled && b) { _Enabled = b; addObservers(); // with _Enabled true storeKeysOfCurrentGoals(); // import from the DB refreshIconsOfScene(true); } else if (_Enabled && !b) { removeObservers(); // with _Enabled true _Enabled = b; refreshIconsOfScene(true); } } #ifndef FINAL_VERSION #error FINAL_VERSION should be defined (0 or 1) #endif #if !FINAL_VERSION NLMISC_COMMAND(dumpNPCIconCache, "Display descriptions of NPCs", "") { log.displayNL(CNPCIconCache::getInstance().getDump().c_str()); return true; } NLMISC_COMMAND(queryMissionGiverData, "Query mission giver data for the specified alias", "") { if (args.size() == 0) return false; uint32 alias; NLMISC::fromString(args[0], alias); CNPCIconCache::getInstance().queryMissionGiverData(alias); //giver.setDescTransient(); return true; } #endif //#pragma optimize ("", on)