// Ryzom - MMORPG Framework <http://dev.ryzom.com/projects/ryzom/>
// 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 <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 . 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<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 \n Current 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 . size ( ) = = 0 )
return false ;
uint32 alias ;
NLMISC : : fromString ( args [ 0 ] , alias ) ;
CNPCIconCache : : getInstance ( ) . queryMissionGiverData ( alias ) ;
//giver.setDescTransient();
return true ;
}
# endif
//#pragma optimize ("", on)