// 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 "game_share/visual_slot_manager.h"
#include "server_share/r2_variables.h"
#include "ai_bot_npc.h"
#include "ai_grp_npc.h"
#include "ai_mgr_npc.h"
#include "ai_player.h"
#include "states.h"
#include "ai_profile_npc.h"
#include "ai_control_npc.h"
#include "ais_user_models.h"
#include "dyn_grp_inline.h"
using namespace MULTI_LINE_FORMATER;
using namespace std;
using namespace RYAI_MAP_CRUNCH;
// Stuff used for management of log messages
static bool VerboseLog = false;
#define LOG if (!VerboseLog) { } else nlinfo
//////////////////////////////////////////////////////////////////////////////
// CSpawnBotNpc //
//////////////////////////////////////////////////////////////////////////////
CSpawnBotNpc::CSpawnBotNpc(TDataSetRow const& row, CBot& owner, NLMISC::CEntityId const& id, float radius, uint32 level, RYAI_MAP_CRUNCH::TAStarFlag denyFlags)
: CSpawnBot(row, owner, id, radius, level, denyFlags)
{
_OldHpPercentage = -1.f;
_NbCurrentDynChats = 0;
_FacingTheta = 0;
_FacingTick = 0;
}
bool CSpawnBotNpc::isBotAttackable() const
{
return getPersistent().grp().getBotAttackable();
}
void CSpawnBotNpc::sendInfoToEGS() const
{
if (!EGSHasMirrorReady)
return;
CSpawnBot::sendInfoToEGS();
TGenNpcDescMsgImp msg;
msg.setEntityIndex(dataSetRow());
getPersistent().fillDescriptionMsg(msg);
msg.setChat(_CurrentChatProfile);
msg.send("EGS");
}
CSpawnGroupNpc& CSpawnBotNpc::spawnGrp() const
{
return static_cast(CSpawnBot::spawnGrp());
}
void CSpawnBotNpc::processEvent(CCombatInterface::CEvent const& event)
{
// no self aggro.
if (event._targetRow==event._originatorRow)
return;
if ((event._nature==ACTNATURE::FIGHT || event._nature==ACTNATURE::OFFENSIVE_MAGIC) && !getPersistent().ignoreOffensiveActions())
{
float aggro = event._weight;
if (aggro > -0.20f)
{
aggro = -0.20f;
}
if (event._nature==ACTNATURE::OFFENSIVE_MAGIC)
{
aggro=(1.f+aggro)*0.5f-1.f; // maximize aggro for magic
//insure if aggressor is player, player have it's target seted for BOSS assist
CAIEntityPhysical *ep=CAIS::instance().getEntityPhysical(event._originatorRow);
CBotPlayer *player=dynamic_cast(ep);
if(player)
{
CAIEntityPhysical *target=player->getVisualTarget();
if (target)
player->setTarget(target);
}
}
addAggroFor(event._originatorRow, aggro, true);
spawnGrp().addAggroFor(event._originatorRow, aggro, true);
}
}
void CSpawnBotNpc::update(uint32 ticks)
{
++AISStat::BotTotalUpdCtr;
++AISStat::BotNpcUpdCtr;
{
H_AUTO(AIHpTrig);
// Fix for HP triggers
// :FIXME: Clean that triggering stuff, make it generic
CGroupNpc& persGrp = spawnGrp().getPersistent();
if (persGrp.haveHpTriggers())
{
float newHpPercentage = getPhysical().hpPercentage();
if (_OldHpPercentage>=0.f && newHpPercentage!=_OldHpPercentage)
{
persGrp.hpTriggerCb(_OldHpPercentage, newHpPercentage);
}
_OldHpPercentage = newHpPercentage;
}
else
{
_OldHpPercentage = getPhysical().hpPercentage();
}
}
// Bot chat and dyn chat override AI profile unless fighting
// The directing adjustment is done client-slide (each player sees the NPC facing him)
if (!getActiveChats().empty())
return;
if (getNbActiveDynChats() > 0)
return;
if (isStuned())
return;
// position before profile update
CAIPos const startPos = CAIPos(pos());
// nullify _PlayerController if it is not valid anymore
if (_PlayerController != NULL && !_PlayerController->isValid())
{
CSpawnBot* sp = _PlayerController->getSpawnBot();
if (sp)
{
sp->getPersistent().notifyStopNpcControl();
}
_PlayerController = NULL;
}
if (_PlayerController == NULL)
{
if (!getAISpawnProfile().isNull())
{
H_AUTO(BotNpcUpdateProfile);
updateProfile(ticks); // then we can update the current bot profile.
}
}
else
{
H_AUTO(BotNpcUpdateControl);
// if there is a valid player controller, it overrides the current profile
_PlayerController->updateControl(ticks);
}
{
H_AUTO(BotNpcUpdateAgro);
// every time we need to update bot aggros.
this->CBotAggroOwner::update(ticks);
}
// If sit and move then stand up
if (IsRingShard)
{
if (getMode() == MBEHAV::SIT
&& (x().asInt() != startPos.x().asInt() || y().asInt() != startPos.y().asInt()) )
{
setMode(MBEHAV::NORMAL);
}
if (_FacingTick != 0)
{
uint32 tick = CTimeInterface::gameCycle();
if ( (tick - _FacingTick) > 40)
{
setTheta(_FacingTheta);
_FacingTick = 0;
}
}
}
}
void CSpawnBotNpc::setFacing(CAngle theta)
{
if (_FacingTick == 0)
{
_FacingTheta = pos().theta();
}
setTheta(theta);
_FacingTick = CTimeInterface::gameCycle();
}
void CSpawnBotNpc::setUserModelId(const std::string &id)
{
_UserModelId = id;
}
void CSpawnBotNpc::setCustomLootTableId(const std::string &id)
{
_CustomLootTableId = id;
}
void CSpawnBotNpc::setPrimAlias(uint32 primAlias)
{
_PrimAlias = primAlias;
}
void CSpawnBotNpc::updateChat(CAIState const* state)
{
if (!state)
return;
CBotNpc const& botNpc = getPersistent();
FOREACHC(itChat, CCont, state->chats())
{
if (!itChat->testCompatibility(botNpc))
continue;
// update chat information if any
CNpcChatProfileImp const* const chatProfile = botNpc.getChat();
if (!chatProfile)
{
_CurrentChatProfile = CNpcChatProfileImp::combineChatProfile(*chatProfile, itChat->getChat());
// the chat profile has been combined, send it to EGS
sendInfoToEGS();
}
break;
}
}
std::vector CSpawnBotNpc::getMultiLineInfoString() const
{
std::vector container;
std::vector strings;
pushTitle(container, "CSpawnBotNpc");
strings.clear();
strings = CSpawnBot::getMultiLineInfoString();
FOREACHC(itString, std::vector, strings)
pushEntry(container, *itString);
pushEntry(container, "profile: " + CProfilePtr::getOneLineInfoString());
pushEntry(container, "state: " + spawnGrp().getPersistent().buidStateInstanceDebugString());
std::string userModelId = getPersistent().getUserModelId();
if (!(userModelId.empty()))
{
pushEntry(container, "UserModelId: " + userModelId);
}
if (_CurrentChatProfile.getMissions().empty())
pushEntry(container, "no mission");
else
{
vector const& missions = _CurrentChatProfile.getMissions();
pushEntry(container, "missions:");
for (size_t i=0; ifindMissionName(missions[i]);
pushEntry(container, NLMISC::toString(" %u (%s)", missions[i], name.c_str()));
}
}
pushFooter(container);
return container;
}
void CSpawnBotNpc::beginBotChat(CBotPlayer* plr)
{
#ifdef NL_DEBUG
for (size_t i=0; i<_ActiveChats.size(); ++i)
{
if (_ActiveChats[i]==plr)
nlwarning("Chat pair added more than once!!!");
}
#endif
// add an entry to the bot chat vector
_ActiveChats.push_back(plr);
}
void CSpawnBotNpc::endBotChat(CBotPlayer* plr)
{
// run through the bot chat vector looking for a player to erase
for (size_t i=0;i<_ActiveChats.size();++i)
{
if (_ActiveChats[i]!=plr)
continue;
// we've found a match for the player so remove the entry from the _aciveChats vector and return
_ActiveChats[i] = _ActiveChats[_ActiveChats.size()-1];
_ActiveChats.pop_back();
#ifdef NL_DEBUG
for (size_t j=i;j<_ActiveChats.size();++j)
{
if (_ActiveChats[i]==plr)
{
nlwarning("Chat pair removed but another of the same still exists!!!");
}
}
#endif
return;
}
// we should never end up here!
nlwarning("Chat pair removed but not previously added!!!");
}
void CSpawnBotNpc::propagateAggro() const
{
if (getAggroPropagationRadius()<0.f)
return;
CGroup* pGroup = getPersistent().getOwner();
CSpawnGroup* spGroup = pGroup->getSpawnObj();
CDynGrpBase* myDgb = pGroup->getGrpDynBase();
nlassert(myDgb);
CFamilyBehavior* myfb = myDgb->getFamilyBehavior();
CAIVision vision;
// look for bots around
vision.updateBotsAndPlayers(pGroup->getAIInstance(), CAIVector(pos()), 0, (uint32)getAggroPropagationRadius());
typedef map TCandidatesCont;
TCandidatesCont candidates;
if (myfb!=NULL)
{
FOREACH(it, CAIVision, vision)
{
CBot& otherPBot = static_cast(*it);
CSpawnBot* otherSpBot = otherPBot.getSpawnObj();
CGroup* otherPGroup = otherPBot.getOwner();
CSpawnGroup* otherSpGroup = otherPGroup->getSpawnObj();
// If bot is in our group skip it
if (otherSpGroup==spGroup)
continue;
// If bot is not in a dynamic system skip it
CDynGrpBase* otherDGB = otherPGroup->getGrpDynBase();
if (!otherDGB)
continue;
// If bot is not in our family skip it
CFamilyBehavior* otherFB = otherDGB->getFamilyBehavior();
if (otherFB!=myfb)
continue;
// ok, this group should help us !
// TODO : take only groups with the 'activity_figth' property
// filter out if the group is already in combat mode
// If group is not spawned skip it (???) (:TODO: See why this test)
if (!otherPGroup->isSpawned())
continue;
CProfilePtr& otherFightProfile = otherSpGroup->fightProfile();
if (!otherFightProfile.getAIProfile() || otherFightProfile.getAIProfileType()!=AITYPES::FIGHT_NORMAL || !(static_cast(otherFightProfile.getAIProfile())->stillHaveEnnemy()))
{
float dist = (float)CAIVector(otherSpBot->pos()).quickDistTo(pos());
if (distsecond / getAggroPropagationRadius());
nldebug(" prop aggro from %p to %p at %f meters, prop factor = %f",
this,
it->first,
it->second,
propFactor);
// add aggro list of this group to the other group pondered by distance
it->first->mergeAggroList(*this, propFactor);
}
}
float CSpawnBotNpc::getReturnDistCheck() const
{
if (getPersistent().isSheetValid() && getPersistent().getSheet()->AggroReturnDistCheck()>=0.f)
return getPersistent().getSheet()->AggroReturnDistCheck();
else
return AggroReturnDistCheckNpc;
}
void CSpawnBotNpc::setPlayerController(CBotPlayer* player)
{
if (player != NULL)
{
_PlayerController = new CPlayerControlNpc(player, this);
nlassert(_PlayerController->isValid());
}
else
{
_PlayerController = NULL;
}
}
void CSpawnBotNpc::setCurrentChatProfile(CNpcChatProfileImp* chatProfile)
{
if (chatProfile)
_CurrentChatProfile = *chatProfile;
else
_CurrentChatProfile.clear(); // clear the chat profile
}
CBotNpc& CSpawnBotNpc::getPersistent() const
{
return static_cast(CSpawnBot::getPersistent());
}
//////////////////////////////////////////////////////////////////////////////
// CBotNpc //
//////////////////////////////////////////////////////////////////////////////
CBotNpc::CBotNpc(CGroup* owner, CAIAliasDescriptionNode* alias)
: CBot(owner, alias)
, _Sheet(NULL)
{
_Sheet = CBotNpcSheetPtr(new CBotNpcSheet(NULL));
_Sheet->setSheet(CBot::getSheet());
init();
}
CBotNpc::CBotNpc(CGroup* owner, uint32 alias, std::string const& name)
: CBot(owner, alias, name)
, _Sheet(NULL)
{
_Sheet = CBotNpcSheetPtr(new CBotNpcSheet(NULL));
_Sheet->setSheet(CBot::getSheet());
init();
}
CBotNpc::~CBotNpc()
{
if (isSpawned())
{
despawnBot();
}
}
void CBotNpc::calcSpawnPos(RYAI_MAP_CRUNCH::CWorldMap const& worldMap)
{
CAIStatePositional const* const state = static_cast(grp().getStartState());
RYAI_MAP_CRUNCH::CWorldPosition wp;
uint32 maxTries = 100;
breakable
{
if (!state)
break;
if (state->shape().hasPatat() && state->shape().getRandomPosCount())
{
do
{
RYAI_MAP_CRUNCH::CWorldPosition wp;
state->shape().getRandomPos(wp);
_StartPos.setXY(wp);
maxTries--;
} while(!worldMap.setWorldPosition(_VerticalPos, wp, _StartPos) && maxTries);
_StartPos.setTheta(0); // to initialise among vertices deltas (no?).
break;
}
if (!state->shape().hasPoints())
break;
std::vector const& posList = state->shape().getGeometry();
do
{
const uint32 a=CAIS::rand16((uint32)posList.size());
const uint32 b=(a+1)%posList.size();
const double weight=CAIS::frand();
_StartPos.setXY(posList[a].toAIVector()+(posList[b].toAIVector()-posList[a].toAIVector())*weight);
--maxTries;
} while (!worldMap.setWorldPosition(_VerticalPos, wp, _StartPos) && maxTries);
_StartPos.setTheta(0); // to initialise among vertices deltas (no?).
}
if (!wp.isValid() && !isStuck())
nlwarning("Cannot generate a valid position for Npc %s", getFullName().c_str());
}
CGroupNpc& CBotNpc::grp() const
{
return *static_cast(getOwner());
}
void CBotNpc::setUserModelId(const std::string &userModelId)
{
if (!userModelId.empty())
{
if (!CAIUserModelManager::getInstance()->isUserModel(_PrimAlias, userModelId))
{
nlwarning("Error while parsing equipment params: cannot find user model id '%s' associated to primAlias '%u'", userModelId.c_str(), _PrimAlias);
_UserModelId = "";
return;
}
_UserModelId = userModelId;
}
}
void CBotNpc::setCustomLootTableId(const std::string &customLootTableId)
{
if (!customLootTableId.empty())
{
if (!CAIUserModelManager::getInstance()->isCustomLootTable(_PrimAlias, customLootTableId))
{
nlwarning("Error while parsing equipment params: invalid customLootTableId specified: '%s'", customLootTableId.c_str());
_CustomLootTableId = "";
return;
}
_CustomLootTableId = customLootTableId;
}
}
std::string CBotNpc::getUserModelId()
{
return _UserModelId;
}
std::string CBotNpc::getCustomLootTableId()
{
return _CustomLootTableId;
}
void CBotNpc::setPrimAlias(uint32 alias)
{
_PrimAlias = alias;
}
uint32 CBotNpc::getPrimAlias() const
{
return _PrimAlias;
}
void CBotNpc::fillDescriptionMsg(RYMSG::TGenNpcDescMsg& msg) const
{
msg.setPlayerAttackable(grp().getPlayerAttackable());
msg.setBotAttackable(grp().getBotAttackable());
msg.setAlias(getAlias());
msg.setGrpAlias(grp().getAlias());
msg.setSheet(getSheet()->SheetId());
msg.setRightHandItem(getSheet()->RightItem());
msg.setRightHandItemQuality(1);
msg.setLeftHandItem(getSheet()->LeftItem());
msg.setLeftHandItemQuality(1);
msg.setDontFollow(isStuck());
msg.setBuildingBot(isBuildingBot());
for (size_t i=0; i<_LootList.size(); ++i)
{
NLMISC::CSheetId const& sheetRef = _LootList[i];
if (sheetRef!=NLMISC::CSheetId::Unknown)
msg.getLootList().push_back(sheetRef);
}
CGroupNpc::TFactionAttackableSet const& factionAttackableAbove = grp().getFactionAttackableAbove();
FOREACHC(itFaction, CGroupNpc::TFactionAttackableSet, factionAttackableAbove)
msg.getOptionalProperties().push_back("FactionAttackableAbove:" + itFaction->first + ":" + NLMISC::toString(itFaction->second));
CGroupNpc::TFactionAttackableSet const& factionAttackableBelow = grp().getFactionAttackableBelow();
FOREACHC(itFaction, CGroupNpc::TFactionAttackableSet, factionAttackableBelow)
msg.getOptionalProperties().push_back("FactionAttackableBelow:" + itFaction->first + ":" + NLMISC::toString(itFaction->second));
msg.setMaxHitRangeForPC(_MaxHitRangeForPC);
// msg.setIsMissionStepIconDisplayable(_MissionIconFlags.IsMissionStepIconDisplayable);
// msg.setIsMissionGiverIconDisplayable(_MissionIconFlags.IsMissionGiverIconDisplayable);
msg.setUserModelId(_UserModelId);
msg.setCustomLootTableId(_CustomLootTableId);
msg.setPrimAlias(_PrimAlias);
}
// :KLUDGE: This methods is a part of the trick for bot respawn
// :TODO: Clean that mess
bool CBotNpc::finalizeSpawnNpc()
{
// For squads, setup specific mirror properties
COutpost* ownerOutpost = dynamic_cast(getOwner()->getOwner()->getOwner());
if (ownerOutpost)
{
// Propagate the alliance id and outpost alias to the instanciated bot
// Currently, all squads are assumed to be defender of the outpost (not attacker)
getSpawn()->setOutpostAlias(ownerOutpost->getAlias());
getSpawn()->setOutpostSide(_OutpostSide);
}
CMirrors::initSheetServer(getSpawn()->dataSetRow(), getSheet()->SheetId());
getSpawn()->setCurrentChatProfile(_ChatProfile);
getSpawn()->sendInfoToEGS();
if (_useVisualProperties) // use VisualPropertyA, B, C
{
sendVisualProperties();
}
else // use alternate VPA
{
sendVPA ();
}
getSpawn()->spawnGrp().botHaveSpawn(this);
return true;
}
void CBotNpc::initAdditionalMirrorValues()
{
// Write the Mission Alias to mirror now - it must be done before CMirrors::declareEntity()
// to ensure the FS will receive the Sheet and it at the same time, hence it can't be
// done in finalizeSpawnNpc(), which is called after that
EGSPD::CPeople::TPeople race = getSheet()->Race();
if (race < EGSPD::CPeople::Creature ||
race == EGSPD::CPeople::Kami ||
race == EGSPD::CPeople::Unknown) // only for humanoid NPCs and bot objects (beware, even some creatures have the entity type RYZOMID::npc)
{
//if ((_ChatProfile != NULL) && (!_ChatProfile->getMissions().empty())) // only if the bot has missions to give: not
CMirrors::initNPCAlias(getSpawn()->dataSetRow(), getAlias());
}
}
bool CBotNpc::spawn()
{
if (!isSheetValid())
{
nlwarning("spawn() Aborted for bot '%s' due to bad sheet", getFullName().c_str());
return false;
}
if (!CBot::spawn())
return false;
// :KLUDGE: Last part calls a tricky method also called by sheetChanged
// :TODO: Clean that mess
return finalizeSpawnNpc();
}
void CBotNpc::sendVPA() // alternate VPA
{
SAltLookProp visProp;
// sets weapons information.
{
CVisualSlotManager* visualSlotManager = CVisualSlotManager::getInstance();
NLMISC::CSheetId rightSheet = getSheet()->RightItem();
NLMISC::CSheetId leftSheet = getSheet()->LeftItem();
visProp.Element.WeaponRightHand = visualSlotManager->rightItem2Index(rightSheet);
visProp.Element.WeaponLeftHand = visualSlotManager->leftItem2Index(leftSheet);
}
// setting up the visual property A mirror record
visProp.Element.ColorTop = getSheet()->ColorBody();
visProp.Element.ColorBot = getSheet()->ColorLegs();
visProp.Element.ColorHair = getSheet()->ColorHead();
visProp.Element.ColorGlove = getSheet()->ColorHands();
visProp.Element.ColorBoot = getSheet()->ColorFeets();
visProp.Element.ColorArm = getSheet()->ColorArms();
visProp.Element.Hat = _Hat;
visProp.Element.Seed = getAlias()!=0?getAlias():(uint32)(size_t)(void*)this;
LOG("BOT: %s L: %d R: %u H: %u CHEAD: %u CARMS: %u CHANDS: %u CBODY: %u CLEGS: %u CFEETS: %u SEED: %u",
getName().c_str(),
visProp.Element.WeaponLeftHand,
visProp.Element.WeaponRightHand,
visProp.Element.Hat,
visProp.Element.ColorTop,
visProp.Element.ColorBot,
visProp.Element.ColorHair,
visProp.Element.ColorGlove,
visProp.Element.ColorBoot,
visProp.Element.ColorArm,
visProp.Element.Seed);
CMirrors::setVPA(getSpawn()->dataSetRow(), visProp);
}
void CBotNpc::sendVisualProperties() // VisualPropertyA, B, C
{
CMirrors::setVisualPropertyA( getSpawn()->dataSetRow(), _VisualPropertyA );
CMirrors::setVisualPropertyB( getSpawn()->dataSetRow(), _VisualPropertyB );
CMirrors::setVisualPropertyC( getSpawn()->dataSetRow(), _VisualPropertyC );
}
bool CBotNpc::reSpawn(bool sendMessage)
{
if (!spawn())
return false;
getSpawn()->updateChat(grp().getCAIState());
return true;
}
void CBotNpc::despawnBot()
{
if (isSpawned())
{
getSpawn()->spawnGrp().botHaveDespawn(this);
CBot::despawnBot();
}
}
void CBotNpc::equipmentInit()
{
_Sheet->reset();
_Hat = false;
_useVisualProperties = false; // default is to use alternate VPA
_VisualPropertyA = 0;
_VisualPropertyB = 0;
_VisualPropertyC = 0;
_FaunaBotUseBotName = false;
}
void CBotNpc::equipmentAdd(std::string const& input)
{
// if string is empty just return without making a fuss
if (input.empty())
return;
// split string into keyword and tail
std::string keyword, tail;
if (!AI_SHARE::stringToKeywordAndTail(input,keyword,tail))
{
nlwarning("Bot '%s'%s: failed to parse equipment text: '%s'",
getAliasFullName().c_str(),
getAliasString().c_str(),
input.c_str());
return;
}
// do something depending on keyword
if (NLMISC::nlstricmp(keyword,"ri")==0)
{
if (tail.empty())
{
nlwarning("No sheet name supplied for equipment slot 'ri': '%s'%s",getAliasFullName().c_str(),getAliasString().c_str());
_Sheet->setRightItem(NLMISC::CSheetId::Unknown);
}
else if (NLMISC::nlstricmp(tail,"none")==0)
{
_Sheet->setRightItem(NLMISC::CSheetId::Unknown);
}
else
{
_Sheet->setRightItem(NLMISC::CSheetId(tail));
}
}
else if (NLMISC::nlstricmp(keyword,"li")==0)
{
if (tail.empty())
{
nlwarning("No sheet name supplied for equipment slot 'li': '%s'%s",getAliasFullName().c_str(),getAliasString().c_str());
_Sheet->setLeftItem(NLMISC::CSheetId::Unknown);
}
else if (NLMISC::nlstricmp(tail,"none")==0)
{
_Sheet->setLeftItem(NLMISC::CSheetId::Unknown);
}
else
{
_Sheet->setLeftItem(NLMISC::CSheetId(tail));
}
}
else if (NLMISC::nlstricmp(keyword,"HAT")==0 || NLMISC::nlstricmp(keyword,"IH")==0)
{
_Hat = true;
}
else if (
NLMISC::nlstricmp(keyword,"UPPER" )==0 || NLMISC::nlstricmp(keyword,"CU")==0 ||
NLMISC::nlstricmp(keyword,"LOWER" )==0 || NLMISC::nlstricmp(keyword,"CL")==0 ||
NLMISC::nlstricmp(keyword,"HAIR" )==0 || NLMISC::nlstricmp(keyword,"CH")==0 ||
NLMISC::nlstricmp(keyword,"CHEAD" )==0 ||
NLMISC::nlstricmp(keyword,"CARMS" )==0 ||
NLMISC::nlstricmp(keyword,"CHANDS")==0 ||
NLMISC::nlstricmp(keyword,"CBODY" )==0 ||
NLMISC::nlstricmp(keyword,"CLEGS" )==0 ||
NLMISC::nlstricmp(keyword,"CFEETS")==0
)
{
setColours(input);
}
else if(
NLMISC::nlstricmp(keyword,"VPA")==0 || NLMISC::nlstricmp(keyword,"VPB")==0 ||
NLMISC::nlstricmp(keyword,"VPC")==0
)
{
setVisualProperties(input);
}
else if ( NLMISC::nlstricmp(keyword,"CLIENT_SHEET")==0 )
{
setClientSheet(tail + ".creature");
}
else if (NLMISC::nlstricmp(keyword,"loot")==0)
{
if (tail.empty())
{
nlwarning("No sheet name supplied for loot list entry: '%s'%s",getAliasFullName().c_str(),getAliasString().c_str());
}
else
{
_LootList.push_back(NLMISC::CSheetId(tail));
}
}
else if (NLMISC::nlstricmp(keyword,"FAUNA_BOT_USE_BOTNAME")==0)
{
_FaunaBotUseBotName = true;
}
else if (NLMISC::nlstricmp(keyword, "USER_MODEL") == 0)
{
uint32 primAlias = getAlias() >> LigoConfig.getDynamicAliasSize();
nldebug("Parsing userModelId '%s' with primAlias: '%u'", tail.c_str(), primAlias);
setPrimAlias(primAlias);
setUserModelId(tail);
}
else if (NLMISC::nlstricmp(keyword, "CUSTOM_LOOT_TABLE") == 0)
{
uint32 primAlias = getAlias() >> LigoConfig.getDynamicAliasSize();
nldebug("Parsing customLootTableId '%s' with primAlias: '%u'", tail.c_str(), primAlias);
setPrimAlias(primAlias);
setCustomLootTableId(tail);
}
else
{
nlwarning("Bot '%s'%s:failed to parse equipment argument: '%s'",
getAliasFullName().c_str(),
getAliasString().c_str(),
input.c_str());
}
}
/*
Colors are something like that:
3D INTERFACE MP
0: ROUGE ROUGE RED
1: BEIGE ORANGE BEIGE
2: VERT CITRON VERT CITRON GREEN
3: VERT VERT TURQUOISE
4: BLEU BLEU BLUE
5: ROUGE fonce ROUGE (tout court) CRIMSON
6: BLANC JAUNE WHITE
7: NOIR BLEU tres fonce BLACK
3D column is (probably) used for equipment
*/
void CBotNpc::setColour(uint8 colour)
{
_Sheet->setColorHead(colour);
_Sheet->setColorArms(colour);
_Sheet->setColorHands(colour);
_Sheet->setColorBody(colour);
_Sheet->setColorLegs(colour);
_Sheet->setColorFeets(colour);
}
void CBotNpc::setColours(std::string input)
{
// stuff for manageing colour names
int const numColours = 8;
static std::vector colourNames[numColours];
static bool init = false;
// if string is empty just return without making a fuss
if (input.empty())
return;
if (!init)
{
// lookup 'ColourNames' in config file (should be a multi-line field)
NLMISC::CConfigFile::CVar* varPtr = NLNET::IService::getInstance()->ConfigFile.getVarPtr(std::string("ColourNames"));
if (varPtr!=NULL)
{
// for each line in config file var try to add an alternative name for one of the colour slots
for (uint i=0; isize(); ++i)
{
// split line into name and idx (line example: 'red: 5')
std::string name, idxStr;
if (AI_SHARE::stringToKeywordAndTail(varPtr->asString(i),name,idxStr))
{
// split succeeded so verify that idxStr contains anumber in range 0..7 and add
// the name to the list of valid names for the given slot
uint32 idx;
NLMISC::fromString(idxStr, idx);
if (NLMISC::toString(idx)==idxStr && idxfullName().c_str());
return;
}
// do something depending on keyword
if (NLMISC::nlstricmp(keyword,"UPPER" )==0 || NLMISC::nlstricmp(keyword,"CU")==0 ||
NLMISC::nlstricmp(keyword,"LOWER" )==0 || NLMISC::nlstricmp(keyword,"CL")==0 ||
NLMISC::nlstricmp(keyword,"HAIR" )==0 || NLMISC::nlstricmp(keyword,"CH")==0 ||
NLMISC::nlstricmp(keyword,"CHEAD" )==0 ||
NLMISC::nlstricmp(keyword,"CARMS" )==0 ||
NLMISC::nlstricmp(keyword,"CHANDS")==0 ||
NLMISC::nlstricmp(keyword,"CBODY" )==0 ||
NLMISC::nlstricmp(keyword,"CLEGS" )==0 ||
NLMISC::nlstricmp(keyword,"CFEETS")==0
)
{
std::vector results;
std::string colour;
while (!tail.empty())
{
// extract the next word from the tail
AI_SHARE::stringToWordAndTail(tail,colour,tail);
// if the colour string is a number then treat it directly
uint32 idx;
NLMISC::fromString(colour, idx);
if (NLMISC::toString(idx)==colour)
{
// we've got a number so make sue it's in valid range and add to results vector
if (idxsetColorArms(val);
_Sheet->setColorHands(val);
_Sheet->setColorBody(val);
}
else if ( NLMISC::nlstricmp(keyword,"LOWER")==0 || NLMISC::nlstricmp(keyword,"CL")==0 )
{
// lower body colour
generator.srand(seed+977*0x10000);
uint8 val = (uint8)results[generator.rand((uint16)results.size()-1)];
_Sheet->setColorLegs(val);
_Sheet->setColorFeets(val);
}
else if ( NLMISC::nlstricmp(keyword,"HAIR")==0 || NLMISC::nlstricmp(keyword,"CH")==0 )
{
// hair colour, mapped to head
generator.srand(seed+976*0x10000);
uint8 val = (uint8)results[generator.rand((uint16)results.size()-1)];
_Sheet->setColorHead(val);
}
else if ( NLMISC::nlstricmp(keyword,"CHEAD")==0)
{
// head color
generator.srand(seed+979*0x10000);
uint8 val = (uint8)results[generator.rand((uint16)results.size()-1)];
_Sheet->setColorHead(val);
}
else if ( NLMISC::nlstricmp(keyword,"CARMS")==0)
{
// arms color
generator.srand(seed+981*0x10000);
uint8 val = (uint8)results[generator.rand((uint16)results.size()-1)];
_Sheet->setColorArms(val);
}
else if ( NLMISC::nlstricmp(keyword,"CHANDS")==0)
{
// arms color
generator.srand(seed+983*0x10000);
uint8 val = (uint8)results[generator.rand((uint16)results.size()-1)];
_Sheet->setColorHands(val);
}
else if ( NLMISC::nlstricmp(keyword,"CBODY")==0)
{
// arms color
generator.srand(seed+985*0x10000);
uint8 val = (uint8)results[generator.rand((uint16)results.size()-1)];
_Sheet->setColorBody(val);
}
else if ( NLMISC::nlstricmp(keyword,"CLEGS")==0)
{
// arms color
generator.srand(seed+987*0x10000);
uint8 val = (uint8)results[generator.rand((uint16)results.size()-1)];
_Sheet->setColorLegs(val);
}
else if ( NLMISC::nlstricmp(keyword,"CFEETS")==0)
{
// arms color
generator.srand(seed+989*0x10000);
uint8 val = (uint8)results[generator.rand((uint16)results.size()-1)];
_Sheet->setColorFeets(val);
}
}
else
{
nlwarning("Bot '%s'%s: failed to parse colours argument: '%s'",
getAliasFullName().c_str(),
getAliasString().c_str(),
input.c_str());
}
}
void CBotNpc::setVisualProperties(std::string input) // AJM
{
// set the VisualPropertyA, VisualPropertyB, VisualPropertyC bitfield values
// using a hexadecimal entry format
// if string is empty just return without making a fuss
if(input.empty())
return;
// split 'input' string into keyword and tail
std::string keyword, tail;
if(!AI_SHARE::stringToKeywordAndTail(input,keyword,tail))
{
nlwarning("Failed to parse visual property text: '%s' for bot: '%s'",input.c_str(),getAliasNode()->fullName().c_str());
return;
}
// load val from tail
// accept 64bit hex value
uint64 val;
sscanf( tail.c_str(), "%"NL_I64"x", &val );
// can't set into mirror row until bot is spawned, so save away
if( NLMISC::nlstricmp( keyword,"VPA")==0 ) // VisualPropertyA
{
_VisualPropertyA = val;
_useVisualProperties = true;
}
else if( NLMISC::nlstricmp( keyword,"VPB")==0 ) // VisualPropertyB
{
_VisualPropertyB = val;
_useVisualProperties = true;
}
else if( NLMISC::nlstricmp( keyword,"VPC")==0 ) // VisualPropertyC
{
_VisualPropertyC = val;
_useVisualProperties = true;
}
else
{
nlwarning("Bot '%s'%s: failed to parse visual property argument: '%s'",
getAliasFullName().c_str(),
getAliasString().c_str(),
input.c_str());
}
}
void CBotNpc::setStartPos(double x, double y, float theta, AITYPES::TVerticalPos verticalPos)
{
_StartPos = CAIPos(x, y, 0, theta);
_VerticalPos = verticalPos;
}
void CBotNpc::init()
{
_ChatProfile = NULL;
_MaxHitRangeForPC = -1.0f;
// _MissionIconFlags.IsMissionStepIconDisplayable = true;
// _MissionIconFlags.IsMissionGiverIconDisplayable = true;
equipmentInit();
}
CSpawnBotNpc* CBotNpc::getSpawn()
{
return static_cast(getSpawnObj());
}
CSpawnBotNpc const* CBotNpc::getSpawn() const
{
return static_cast(getSpawnObj());
}
void CBotNpc::getSpawnPos(CAIVector& triedPos, RYAI_MAP_CRUNCH::CWorldPosition& pos, RYAI_MAP_CRUNCH::CWorldMap const& worldMap, CAngle& spawnTheta)
{
if (_StartPos.isNull())
calcSpawnPos(worldMap);
if (isStuck() || IsRingShard)
worldMap.setWorldPosition(_VerticalPos, pos, _StartPos);
else
CWorldContainer::calcNearestWPosFromPosAnRadius(_VerticalPos, worldMap, pos, _StartPos, 10, 200, CWorldContainer::CPosValidatorDefault());
#ifdef NL_DEBUG
if (!pos.isValid() && !isStuck())
{
nlwarning("Npc Spawn Pos Error %s", pos.toString().c_str());
}
#endif
spawnTheta = _StartPos.theta();
triedPos = _StartPos;
}
CSpawnBot* CBotNpc::getSpawnBot(TDataSetRow const& row, NLMISC::CEntityId const& id, float radius)
{
return new CSpawnBotNpc(row, *this, id, radius, getSheet()->Level(), getGroup().getAStarFlag());
}
void CBotNpc::newChat()
{
_ChatProfile = new CNpcChatProfileImp();
}
CAIS::CCounter& CBotNpc::getSpawnCounter()
{
return CAIS::instance()._NpcBotCounter;
}
void CBotNpc::setSheet(AISHEETS::ICreatureCPtr const& sheet)
{
_Sheet->setSheet(sheet);
sheetChanged();
}
// :KLUDGE: This method is very tricky. It's a copy'n'paste of the method in CBot (BAD!) with more content
// :TODO: Clean that mess
void CBotNpc::sheetChanged()
{
if (getSpawnObj())
{
// Get bot state
RYAI_MAP_CRUNCH::CWorldPosition botWPos = getSpawnObj()->wpos();
CAngle spawnTheta = getSpawnObj()->theta();
float botMeterSize = getSheet()->Scale()*getSheet()->Radius();
// :TODO: Save profile info
// If stuck bot position may be outside collision and must be recomputed
if (isStuck() || IsRingShard)
getSpawnPos(lastTriedPos, botWPos, CWorldContainer::getWorldMap(), spawnTheta);
// Delete old bot
CMirrors::removeEntity(getSpawnObj()->getEntityId());
setSpawn(NULL); // automatic smart pointer deletion
notifyBotDespawn();
// Finalize spawn object creation
if (!finalizeSpawn(botWPos, spawnTheta, botMeterSize))
return;
// :KLUDGE: Both finalizeSpawn and finalizeSpawnNpc are called,
// sheetChanged has a strange herited meaning and may confuse future
// coders
// :TODO: Clean that mess and find a more elegant C++ solution to the
// problem
finalizeSpawnNpc();
}
}
//////////////////////////////////////////////////////////////////////////////
// Commmands //
//////////////////////////////////////////////////////////////////////////////
// Control over verbose nature of logging
NLMISC_COMMAND(verboseNPCBotProfiles, "Turn on or off or check the state of verbose bot profile change logging","")
{
if (args.size()>1)
return false;
if (args.size()==1)
NLMISC::fromString(args[0], VerboseLog);
nlinfo("VerboseLogging is %s",VerboseLog?"ON":"OFF");
return true;
}
// virtual function so do not need to be inlined
bool CBotNpc::getFaunaBotUseBotName() const
{
return _FaunaBotUseBotName;
}