Merge branch '164-crash-on-patching-and-possibly-right-after-login' into main/gingo...
[ryzomcore.git] / ryzom / client / src / npc_icon.cpp
blobaa85082f2517c85cb6178c2009eaf236901d8219
1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010 Winch Gate Property Limited
3 //
4 // This source file has been modified by the following contributors:
5 // Copyright (C) 2013 Laszlo KIS-ADAM (dfighter) <dfighter1985@gmail.com>
6 //
7 // This program is free software: you can redistribute it and/or modify
8 // it under the terms of the GNU Affero General Public License as
9 // published by the Free Software Foundation, either version 3 of the
10 // License, or (at your option) any later version.
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 // GNU Affero General Public License for more details.
17 // You should have received a copy of the GNU Affero General Public License
18 // along with this program. If not, see <http://www.gnu.org/licenses/>.
20 #include "stdpch.h"
21 #include "npc_icon.h"
22 #include "ingame_database_manager.h"
23 #include "game_share/generic_xml_msg_mngr.h"
24 #include "entities.h"
25 #include "net_manager.h"
27 using namespace std;
28 using namespace NLMISC;
30 CNPCIconCache* CNPCIconCache::_Instance = NULL;
32 // 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)
33 NLMISC::TGameCycle CNPCIconCache::_CacheRefreshTimerDelay = NPC_ICON::DefaultClientNPCIconRefreshTimerDelayGC;
35 // Time between updates of the "catchall timer"
36 NLMISC::TGameCycle CNPCIconCache::_CatchallTimerPeriod = NPC_ICON::DefaultClientNPCIconRefreshTimerDelayGC;
38 extern CEntityManager EntitiesMngr;
39 extern CGenericXmlMsgHeaderManager GenericMsgHeaderMngr;
40 extern CNetManager NetMngr;
43 // #pragma optimize ("", off)
46 CNPCIconCache::CNPCIconCache() : _LastRequestTimestamp(0), _LastTimerUpdateTimestamp(0), _Enabled(true)
48 _Icons[NPC_ICON::IconNone].init("", "");
49 _Icons[NPC_ICON::IconNotAMissionGiver].init("", "");
50 _Icons[NPC_ICON::IconListHasOutOfReachMissions].init("mission_available.tga", ""); //"MP_Blood.tga"
51 _Icons[NPC_ICON::IconListHasAlreadyTakenMissions].init("ICO_Task_Generic.tga", "r2ed_tool_redo");
52 _Icons[NPC_ICON::IconListHasAvailableMission].init("mission_available.tga", "", CViewRadar::MissionList); //"MP_Wood.tga"
53 _Icons[NPC_ICON::IconAutoHasUnavailableMissions].init("spe_com.tga", "");
54 _Icons[NPC_ICON::IconAutoHasAvailableMission].init("spe_com.tga", "", CViewRadar::MissionAuto); //"MP_Oil.tga"
55 _Icons[NPC_ICON::IconStepMission].init("mission_step.tga", "", CViewRadar::MissionStep); //"MP_Shell.tga"
57 _DescriptionsToRequest.reserve(256);
60 void CNPCIconCache::release()
62 if (_Instance)
64 delete _Instance;
65 _Instance = NULL;
69 const CNPCIconCache::CNPCIconDesc& CNPCIconCache::getNPCIcon(const CEntityCL *entity, bool bypassEnabled)
71 // Not applicable? Most entities (creatures, characters) have a null key here.
72 BOMB_IF(!entity, "NULL entity in getNPCIcon", return _Icons[NPC_ICON::IconNone]);
73 TNPCIconCacheKey npcIconCacheKey = CNPCIconCache::entityToKey(entity);
74 if (npcIconCacheKey == 0)
75 return _Icons[NPC_ICON::IconNone];
77 // Is system disabled?
78 if ((!enabled()) && !bypassEnabled)
79 return _Icons[NPC_ICON::IconNone];
81 // This method must be reasonably fast, because it constantly gets called by the radar view
82 H_AUTO(GetNPCIconWithKey);
84 // Not applicable (more checks)?
85 if (!entity->canHaveMissionIcon())
86 return _Icons[NPC_ICON::IconNone];
87 if (!entity->isFriend()) // to display icons in the radar, we need the Contextual property to be received as soon as possible
88 return _Icons[NPC_ICON::IconNone];
90 // Temporarily not shown if the player is in interaction with the NPC
91 if (UserEntity->interlocutor() != CLFECOMMON::INVALID_SLOT)
93 CEntityCL *interlocutorEntity = EntitiesMngr.entity(UserEntity->interlocutor());
94 if (interlocutorEntity && (entityToKey(interlocutorEntity) == npcIconCacheKey))
95 return _Icons[NPC_ICON::IconNone];
97 if (UserEntity->trader() != CLFECOMMON::INVALID_SLOT)
99 CEntityCL *traderEntity = EntitiesMngr.entity(UserEntity->trader());
100 if (traderEntity && (entityToKey(traderEntity) == npcIconCacheKey))
101 return _Icons[NPC_ICON::IconNone];
104 // 1. Test if the NPC is involved in a current goal
105 if (isNPCaCurrentGoal(npcIconCacheKey))
106 return _Icons[NPC_ICON::IconStepMission];
108 // 2. Compute "has mission to take": take from cache, or query the server
109 H_AUTO(GetNPCIcon_GIVER);
110 CMissionGiverMap::iterator img = _MissionGivers.find(npcIconCacheKey);
111 if (img != _MissionGivers.end())
113 CNPCMissionGiverDesc& giver = (*img).second;
114 if (giver.getState() != NPC_ICON::AwaitingFirstData)
116 // Ask the server to refresh the state if the information is old
117 // but only known mission givers that have a chance to propose new missions
118 if ((giver.getState() != NPC_ICON::NotAMissionGiver) &&
119 // (giver.getState() != NPC_ICON::ListHasAlreadyTakenMissions) && // commented out because it would not refresh in case an auto mission become available
120 (!giver.isDescTransient()))
122 NLMISC::TGameCycle informationAge = NetMngr.getCurrentServerTick() - giver.getLastUpdateTimestamp();
123 if (informationAge > _CacheRefreshTimerDelay)
125 queryMissionGiverData(npcIconCacheKey);
126 giver.setDescTransient();
130 // Return the icon depending on the state in the cache
131 return _Icons[giver.getState()]; // TNPCIconId maps TNPCMissionGiverState
134 else
136 // Create mission giver entry and query the server
137 CNPCMissionGiverDesc giver;
138 CMissionGiverMap::iterator itg = _MissionGivers.insert(make_pair(npcIconCacheKey, giver)).first;
139 queryMissionGiverData(npcIconCacheKey);
140 //(*itg).second.setDescTransient(); // already made transient by constructor
143 return _Icons[NPC_ICON::IconNone];
146 #define getArraySize(a) (sizeof(a)/sizeof(a[0]))
148 void CNPCIconCache::addObservers()
150 // Disabled?
151 if (!enabled())
152 return;
154 // Mission Journal
155 static const char *missionStartStopLeavesToMonitor [2] = {"TITLE", "FINISHED"};
156 IngameDbMngr.addBranchObserver( IngameDbMngr.getNodePtr(), "MISSIONS", MissionStartStopObserver, missionStartStopLeavesToMonitor, getArraySize(missionStartStopLeavesToMonitor));
157 static const char *missionNpcAliasLeavesToMonitor [1] = {"NPC_ALIAS"};
158 IngameDbMngr.addBranchObserver( IngameDbMngr.getNodePtr(), "MISSIONS", MissionNpcAliasObserver, missionNpcAliasLeavesToMonitor, getArraySize(missionNpcAliasLeavesToMonitor));
160 // Skills
161 static const char *skillLeavesToMonitor [2] = {"SKILL", "BaseSKILL"};
162 IngameDbMngr.addBranchObserver( IngameDbMngr.getNodePtr(), "CHARACTER_INFO:SKILLS", MissionPrerequisitEventObserver, skillLeavesToMonitor, getArraySize(skillLeavesToMonitor));
164 // Owned Items
165 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
166 IngameDbMngr.addBranchObserver( IngameDbMngr.getNodePtr(), "INVENTORY:BAG", MissionPrerequisitEventObserver, bagLeavesToMonitor, getArraySize(bagLeavesToMonitor));
168 // Worn Items
169 IngameDbMngr.addBranchObserver( "INVENTORY:HAND", &MissionPrerequisitEventObserver);
170 IngameDbMngr.addBranchObserver( "INVENTORY:EQUIP", &MissionPrerequisitEventObserver);
172 // Known Bricks
173 IngameDbMngr.addBranchObserver( "BRICK_FAMILY", &MissionPrerequisitEventObserver);
175 // For other events, search for calls of onEventForMissionAvailabilityForThisChar()
178 void CNPCIconCache::removeObservers()
180 // Disabled?
181 if (!enabled())
182 return;
184 // Mission Journal
185 IngameDbMngr.getNodePtr()->removeBranchObserver("MISSIONS", MissionStartStopObserver);
186 IngameDbMngr.getNodePtr()->removeBranchObserver("MISSIONS", MissionNpcAliasObserver);
188 // Skills
189 IngameDbMngr.getNodePtr()->removeBranchObserver("CHARACTER_INFO:SKILLS", MissionPrerequisitEventObserver);
191 // Owned Items
192 IngameDbMngr.getNodePtr()->removeBranchObserver("INVENTORY:BAG", MissionPrerequisitEventObserver);
194 // Worn Items
195 IngameDbMngr.getNodePtr()->removeBranchObserver("INVENTORY:HAND", MissionPrerequisitEventObserver);
196 IngameDbMngr.getNodePtr()->removeBranchObserver("INVENTORY:EQUIP", MissionPrerequisitEventObserver);
198 // Known Bricks
199 IngameDbMngr.getNodePtr()->removeBranchObserver("BRICK_FAMILY", MissionPrerequisitEventObserver);
202 void CNPCIconCache::CMissionStartStopObserver::update(ICDBNode* node)
204 // Every time a mission in progress is started or stopped, refresh the icon for visible NPCs (including mission giver information)
205 CNPCIconCache::getInstance().onEventForMissionInProgress();
208 void CNPCIconCache::CMissionNpcAliasObserver::update(ICDBNode* node)
210 CNPCIconCache::getInstance().onNpcAliasChangedInMissionGoals();
213 void CNPCIconCache::CMissionPrerequisitEventObserver::update(ICDBNode* node)
215 // Every time a mission in progress changes, refresh the icon for the related npc
216 CNPCIconCache::getInstance().onEventForMissionAvailabilityForThisChar();
219 void CNPCIconCache::onEventForMissionAvailabilityForThisChar()
221 // Disabled?
222 if (!enabled())
223 return;
225 queryAllVisibleMissionGiverData(0);
228 void CNPCIconCache::queryMissionGiverData(TNPCIconCacheKey npcIconCacheKey)
230 _DescriptionsToRequest.push_back(npcIconCacheKey);
231 //static set<TNPCIconCacheKey> requests1;
232 //requests1.insert(npcIconCacheKey);
233 //nldebug("%u: queryMissionGiverData %u (total %u)", NetMngr.getCurrentServerTick(), npcIconCacheKey, requests1.size());
237 void CNPCIconCache::queryAllVisibleMissionGiverData(NLMISC::TGameCycle olderThan)
239 // Request an update for all npcs (qualifying, i.e. that have missions) in vision
240 for (uint i=0; i<EntitiesMngr.entities().size(); ++i)
242 CEntityCL *entity = EntitiesMngr.entity(i);
243 if (!entity || !(entity->canHaveMissionIcon() && entity->isFriend()))
244 continue;
245 TNPCIconCacheKey npcIconCacheKey = CNPCIconCache::entityToKey(entity);
246 CMissionGiverMap::iterator img = _MissionGivers.find(npcIconCacheKey);
247 if (img == _MissionGivers.end())
248 continue; // if the NPC does not have an entry yet, it will be created by getNPCIcon()
250 // Refresh only known mission givers that have a chance to propose new missions
251 CNPCMissionGiverDesc& giver = (*img).second;
252 if (giver.getState() == NPC_ICON::NotAMissionGiver)
253 continue;
254 // if (giver.getState() == NPC_ICON::ListHasAlreadyTakenMissions)
255 // continue; // commented out because it would not refresh in case an auto mission becomes available
257 if (olderThan != 0)
259 // Don't refresh desscriptions already awaiting an update
260 if (giver.isDescTransient())
261 continue;
263 // Don't refresh NPCs having data more recent than specified
264 NLMISC::TGameCycle informationAge = NetMngr.getCurrentServerTick() - giver.getLastUpdateTimestamp();
265 if (informationAge <= olderThan)
266 continue;
268 // Don't refresh NPC being involved in a mission goal (the step icon has higher priority over the giver icon)
269 // If later the NPC is no more involved before the information is considered old, it will show
270 // the same giver state until the information is considered old. That's why we let refresh
271 // the NPC when triggered by an event (olderThan == 0).
272 if (isNPCaCurrentGoal(npcIconCacheKey))
273 continue;
276 _DescriptionsToRequest.push_back(npcIconCacheKey);
277 giver.setDescTransient();
279 //static set<TNPCIconCacheKey> requests2;
280 //requests2.insert(npcIconCacheKey);
281 //nldebug("%u: queryAllVisibleMissionGiverData %u (total %u)", NetMngr.getCurrentServerTick(), npcIconCacheKey, requests2.size());
285 void CNPCIconCache::update()
287 // Every CatchallTimerPeriod, browse visible entities and refresh the ones with outdated state
288 // (e.g. the ones not displayed in radar).
289 if (NetMngr.getCurrentServerTick() > _LastTimerUpdateTimestamp + _CatchallTimerPeriod)
291 _LastTimerUpdateTimestamp = NetMngr.getCurrentServerTick();
293 // Disabled?
294 if (!enabled())
295 return;
297 queryAllVisibleMissionGiverData(_CacheRefreshTimerDelay);
300 // Every tick update at most (2 cycles actually, cf. server<->client communication frequency),
301 // send all pending requests in a single message.
302 if (NetMngr.getCurrentServerTick() > _LastRequestTimestamp)
304 if (!_DescriptionsToRequest.empty())
306 CBitMemStream out;
307 GenericMsgHeaderMngr.pushNameToStream("NPC_ICON:GET_DESC", out);
308 uint8 nb8 = uint8(_DescriptionsToRequest.size() & 0xff); // up to vision size (255 i.e. 256 minus user)
309 out.serial(nb8);
310 for (CSmallKeyList::const_iterator ikl=_DescriptionsToRequest.begin(); ikl!=_DescriptionsToRequest.end(); ++ikl)
312 TNPCIconCacheKey key = *ikl;
313 out.serial(key);
315 NetMngr.push(out);
316 //nldebug("%u: Pushing %hu NPC desc requests", NetMngr.getCurrentServerTick(), nb8);
317 _DescriptionsToRequest.clear();
319 _LastRequestTimestamp = NetMngr.getCurrentServerTick();
323 void CNPCIconCache::onEventForMissionInProgress()
325 // Disabled?
326 if (!enabled())
327 return;
329 // Immediately reflect the mission journal (Step icons)
330 refreshIconsOfScene(true);
332 // Ask the server to update availability status (will refresh icons if there is at least one change)
333 onEventForMissionAvailabilityForThisChar();
336 void CNPCIconCache::onNpcAliasChangedInMissionGoals()
338 // Disabled?
339 if (!enabled())
340 return;
342 // Update the storage of keys having a current mission goal.
343 storeKeysOfCurrentGoals();
345 // Immediately reflect the mission journal (Step icons)
346 refreshIconsOfScene(true);
349 bool CNPCIconCache::isNPCaCurrentGoal(TNPCIconCacheKey npcIconCacheKey) const
351 // There aren't many simultaneous goals, we can safely browse the vector
352 for (CSmallKeyList::const_iterator ikl=_KeysOfCurrentGoals.begin(); ikl!=_KeysOfCurrentGoals.end(); ++ikl)
354 if ((*ikl) == npcIconCacheKey)
355 return true;
357 return false;
360 void CNPCIconCache::storeKeysOfCurrentGoals()
362 // This event is very unfrequent, and the number of elements of _KeysOfCurrentGoals is usually very small
363 // (typically 0 to 3, while theoretical max is 15*20) so we don't mind rebuilding the list.
364 _KeysOfCurrentGoals.clear();
365 CCDBNodeBranch *missionNode = safe_cast<CCDBNodeBranch*>(IngameDbMngr.getNodePtr()->getNode(ICDBNode::CTextId("MISSIONS")));
366 BOMB_IF (!missionNode, "MISSIONS node missing in DB", return);
367 uint nbCurrentMissionSlots = missionNode->getNbNodes();
368 for (uint i=0; i!=nbCurrentMissionSlots; ++i)
370 ICDBNode *missionEntry = missionNode->getNode((uint16)i);
371 ICDBNode::CTextId titleNode("TITLE");
372 if (missionEntry->getProp(titleNode) == 0)
373 continue;
375 CCDBNodeBranch *stepsToDoNode = safe_cast<CCDBNodeBranch*>(missionEntry->getNode(ICDBNode::CTextId("GOALS")));
376 BOMB_IF(!stepsToDoNode, "GOALS node missing in MISSIONS DB", return);
377 uint nbGoals = stepsToDoNode->getNbNodes();
378 for (uint j=0; j!=nbGoals; ++j)
380 ICDBNode *stepNode = stepsToDoNode->getNode((uint16)j);
381 CCDBNodeLeaf *aliasNode = safe_cast<CCDBNodeLeaf*>(stepNode->getNode(ICDBNode::CTextId("NPC_ALIAS")));
382 BOMB_IF(!aliasNode, "NPC_ALIAS node missing in MISSIONS DB", return);
383 TNPCIconCacheKey npcIconCacheKey = (TNPCIconCacheKey)aliasNode->getValue32();
384 if (npcIconCacheKey != 0)
385 _KeysOfCurrentGoals.push_back(npcIconCacheKey);
390 void CNPCIconCache::refreshIconsOfScene(bool force)
392 // Browse all NPCs in vision, and refresh their inscene interface
393 for (uint i=0; i<EntitiesMngr.entities().size(); ++i)
395 CEntityCL *entity = EntitiesMngr.entity(i);
396 if (!entity) continue;
398 CMissionGiverMap::iterator it = _MissionGivers.find(CNPCIconCache::entityToKey(entity));
399 if ((it!=_MissionGivers.end()) && ((*it).second.hasChanged() || force))
401 EntitiesMngr.refreshInsceneInterfaceOfFriendNPC(i);
402 (*it).second.setChanged(false);
407 bool CNPCIconCache::onReceiveMissionAvailabilityForThisChar(TNPCIconCacheKey npcIconCacheKey, NPC_ICON::TNPCMissionGiverState state)
409 CMissionGiverMap::iterator img = _MissionGivers.find(npcIconCacheKey);
410 BOMB_IF(img == _MissionGivers.end(), "Mission Giver " << npcIconCacheKey << "not found", return false);
412 //if (state != NPC_ICON::NotAMissionGiver)
414 // static set<TNPCIconCacheKey> qualifs;
415 // qualifs.insert(npcIconCacheKey);
416 // nldebug("NPC %u qualifies (total=%u)", npcIconCacheKey, qualifs.size());
419 return (*img).second.updateMissionAvailabilityForThisChar(state);
422 bool CNPCMissionGiverDesc::updateMissionAvailabilityForThisChar(NPC_ICON::TNPCMissionGiverState state)
424 _HasChanged = (state != _MissionGiverState);
425 _MissionGiverState = state;
426 _LastUpdateTimestamp = NetMngr.getCurrentServerTick();
427 _IsDescTransient = false;
428 return _HasChanged;
431 void CNPCIconCache::setMissionGiverTimer(NLMISC::TGameCycle delay)
433 _CacheRefreshTimerDelay = delay;
434 _CatchallTimerPeriod = delay;
437 std::string CNPCIconCache::getDump() const
439 string s = toString("System %s\nCurrent timers: %u %u\n", _Enabled?"enabled":"disabled", _CacheRefreshTimerDelay, _CatchallTimerPeriod);
440 s += toString("%u NPCs in mission giver map:\n", _MissionGivers.size());
441 for (CMissionGiverMap::const_iterator img=_MissionGivers.begin(); img!=_MissionGivers.end(); ++img)
443 const CNPCMissionGiverDesc& giver = (*img).second;
444 s += toString("NPC %u: ", (*img).first) + giver.getDump() + "\n";
446 s += "Current NPC goals:\n";
447 for (CSmallKeyList::const_iterator ikl=_KeysOfCurrentGoals.begin(); ikl!=_KeysOfCurrentGoals.end(); ++ikl)
449 s += toString("NPC %u", (*ikl));
451 return s;
454 std::string CNPCMissionGiverDesc::getDump() const
456 return toString("%u [%u s ago]", _MissionGiverState, (NetMngr.getCurrentServerTick()-_LastUpdateTimestamp)/10);
459 void CNPCIconCache::setEnabled(bool b)
461 if (!_Enabled && b)
463 _Enabled = b;
464 addObservers(); // with _Enabled true
465 storeKeysOfCurrentGoals(); // import from the DB
466 refreshIconsOfScene(true);
468 else if (_Enabled && !b)
470 removeObservers(); // with _Enabled true
471 _Enabled = b;
472 refreshIconsOfScene(true);
476 #ifndef FINAL_VERSION
477 #error FINAL_VERSION should be defined (0 or 1)
478 #endif
480 #if !FINAL_VERSION
482 NLMISC_COMMAND(dumpNPCIconCache, "Display descriptions of NPCs", "")
484 log.displayNL(CNPCIconCache::getInstance().getDump().c_str());
485 return true;
488 NLMISC_COMMAND(queryMissionGiverData, "Query mission giver data for the specified alias", "<alias>")
490 if (args.empty())
491 return false;
492 uint32 alias;
493 NLMISC::fromString(args[0], alias);
495 CNPCIconCache::getInstance().queryMissionGiverData(alias);
496 //giver.setDescTransient();
497 return true;
500 #endif
502 //#pragma optimize ("", on)