You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
ryzom-core/ryzom/client/src/npc_icon.cpp

503 lines
18 KiB
C++

// Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
// Copyright (C) 2010 Winch Gate Property Limited
//
// This source file has been modified by the following contributors:
// Copyright (C) 2013 Laszlo KIS-ADAM (dfighter) <dfighter1985@gmail.com>
//
// 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 <http://www.gnu.org/licenses/>.
#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.addBranchObserver( IngameDbMngr.getNodePtr(), "MISSIONS", MissionStartStopObserver, missionStartStopLeavesToMonitor, getArraySize(missionStartStopLeavesToMonitor));
static const char *missionNpcAliasLeavesToMonitor [1] = {"NPC_ALIAS"};
IngameDbMngr.addBranchObserver( IngameDbMngr.getNodePtr(), "MISSIONS", MissionNpcAliasObserver, missionNpcAliasLeavesToMonitor, getArraySize(missionNpcAliasLeavesToMonitor));
// Skills
static const char *skillLeavesToMonitor [2] = {"SKILL", "BaseSKILL"};
IngameDbMngr.addBranchObserver( IngameDbMngr.getNodePtr(), "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.addBranchObserver( IngameDbMngr.getNodePtr(), "INVENTORY:BAG", MissionPrerequisitEventObserver, bagLeavesToMonitor, getArraySize(bagLeavesToMonitor));
// Worn Items
IngameDbMngr.addBranchObserver( "INVENTORY:HAND", &MissionPrerequisitEventObserver);
IngameDbMngr.addBranchObserver( "INVENTORY:EQUIP", &MissionPrerequisitEventObserver);
// Known Bricks
IngameDbMngr.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<TNPCIconCacheKey> 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; i<EntitiesMngr.entities().size(); ++i)
{
CEntityCL *entity = EntitiesMngr.entity(i);
if (!entity || !(entity->canHaveMissionIcon() && 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<TNPCIconCacheKey> 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<CCDBNodeBranch*>(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<CCDBNodeBranch*>(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<CCDBNodeLeaf*>(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<EntitiesMngr.entities().size(); ++i)
{
CEntityCL *entity = EntitiesMngr.entity(i);
if (!entity) continue;
CMissionGiverMap::iterator it = _MissionGivers.find(CNPCIconCache::entityToKey(entity));
if ((it!=_MissionGivers.end()) && ((*it).second.hasChanged() || force))
{
EntitiesMngr.refreshInsceneInterfaceOfFriendNPC(i);
(*it).second.setChanged(false);
}
}
}
bool CNPCIconCache::onReceiveMissionAvailabilityForThisChar(TNPCIconCacheKey npcIconCacheKey, NPC_ICON::TNPCMissionGiverState state)
{
CMissionGiverMap::iterator img = _MissionGivers.find(npcIconCacheKey);
BOMB_IF(img == _MissionGivers.end(), "Mission Giver " << npcIconCacheKey << "not found", return false);
//if (state != NPC_ICON::NotAMissionGiver)
//{
// static set<TNPCIconCacheKey> 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", "<alias>")
{
if (args.empty())
return false;
uint32 alias;
NLMISC::fromString(args[0], alias);
CNPCIconCache::getInstance().queryMissionGiverData(alias);
//giver.setDescTransient();
return true;
}
#endif
//#pragma optimize ("", on)