1 // Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
2 // Copyright (C) 2010 Winch Gate Property Limited
4 // This source file has been modified by the following contributors:
5 // Copyright (C) 2013 Laszlo KIS-ADAM (dfighter) <dfighter1985@gmail.com>
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/>.
22 #include "ingame_database_manager.h"
23 #include "game_share/generic_xml_msg_mngr.h"
25 #include "net_manager.h"
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()
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
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()
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
));
161 static const char *skillLeavesToMonitor
[2] = {"SKILL", "BaseSKILL"};
162 IngameDbMngr
.addBranchObserver( IngameDbMngr
.getNodePtr(), "CHARACTER_INFO:SKILLS", MissionPrerequisitEventObserver
, skillLeavesToMonitor
, getArraySize(skillLeavesToMonitor
));
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
));
169 IngameDbMngr
.addBranchObserver( "INVENTORY:HAND", &MissionPrerequisitEventObserver
);
170 IngameDbMngr
.addBranchObserver( "INVENTORY:EQUIP", &MissionPrerequisitEventObserver
);
173 IngameDbMngr
.addBranchObserver( "BRICK_FAMILY", &MissionPrerequisitEventObserver
);
175 // For other events, search for calls of onEventForMissionAvailabilityForThisChar()
178 void CNPCIconCache::removeObservers()
185 IngameDbMngr
.getNodePtr()->removeBranchObserver("MISSIONS", MissionStartStopObserver
);
186 IngameDbMngr
.getNodePtr()->removeBranchObserver("MISSIONS", MissionNpcAliasObserver
);
189 IngameDbMngr
.getNodePtr()->removeBranchObserver("CHARACTER_INFO:SKILLS", MissionPrerequisitEventObserver
);
192 IngameDbMngr
.getNodePtr()->removeBranchObserver("INVENTORY:BAG", MissionPrerequisitEventObserver
);
195 IngameDbMngr
.getNodePtr()->removeBranchObserver("INVENTORY:HAND", MissionPrerequisitEventObserver
);
196 IngameDbMngr
.getNodePtr()->removeBranchObserver("INVENTORY:EQUIP", MissionPrerequisitEventObserver
);
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()
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()))
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
)
254 // if (giver.getState() == NPC_ICON::ListHasAlreadyTakenMissions)
255 // continue; // commented out because it would not refresh in case an auto mission becomes available
259 // Don't refresh desscriptions already awaiting an update
260 if (giver
.isDescTransient())
263 // Don't refresh NPCs having data more recent than specified
264 NLMISC::TGameCycle informationAge
= NetMngr
.getCurrentServerTick() - giver
.getLastUpdateTimestamp();
265 if (informationAge
<= olderThan
)
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
))
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();
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())
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)
310 for (CSmallKeyList::const_iterator ikl
=_DescriptionsToRequest
.begin(); ikl
!=_DescriptionsToRequest
.end(); ++ikl
)
312 TNPCIconCacheKey key
= *ikl
;
316 //nldebug("%u: Pushing %hu NPC desc requests", NetMngr.getCurrentServerTick(), nb8);
317 _DescriptionsToRequest
.clear();
319 _LastRequestTimestamp
= NetMngr
.getCurrentServerTick();
323 void CNPCIconCache::onEventForMissionInProgress()
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()
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
)
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)
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;
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
));
454 std::string
CNPCMissionGiverDesc::getDump() const
456 return toString("%u [%u s ago]", _MissionGiverState
, (NetMngr
.getCurrentServerTick()-_LastUpdateTimestamp
)/10);
459 void CNPCIconCache::setEnabled(bool 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
472 refreshIconsOfScene(true);
476 #ifndef FINAL_VERSION
477 #error FINAL_VERSION should be defined (0 or 1)
482 NLMISC_COMMAND(dumpNPCIconCache
, "Display descriptions of NPCs", "")
484 log
.displayNL(CNPCIconCache::getInstance().getDump().c_str());
488 NLMISC_COMMAND(queryMissionGiverData
, "Query mission giver data for the specified alias", "<alias>")
493 NLMISC::fromString(args
[0], alias
);
495 CNPCIconCache::getInstance().queryMissionGiverData(alias
);
496 //giver.setDescTransient();
502 //#pragma optimize ("", on)