Add profile-driven AdventureBot character generation
This commit is contained in:
1 parent
e333f2f9fc
commit
04aa87fcfc
33 files changed
+3465
-2
No files matched your search
@@ -75,10 +75,11 @@ namespace Trainer
|
||||
Type GetTrainerType() const { return _type; }
|
||||
uint32 GetTrainerRequirement() const { return _requirement; }
|
||||
bool IsTrainerValidForPlayer(Player const* player) const;
|
||||
// Read-only eligibility, shared by native teaching and character provisioning.
|
||||
bool CanTeachSpell(Player const* player, Spell const* trainerSpell) const;
|
||||
|
||||
private:
|
||||
Spell const* GetSpell(uint32 spellId) const;
|
||||
bool CanTeachSpell(Player const* player, Spell const* trainerSpell) const;
|
||||
SpellState GetSpellState(Player const* player, Spell const* trainerSpell) const;
|
||||
void SendTeachFailure(Creature const* npc, Player const* player, uint32 spellId, FailReason reason) const;
|
||||
void SendTeachSucceeded(Creature const* npc, Player const* player, uint32 spellId) const;
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
#include "AdventureCombatPreparationPolicy.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureLocationMgr.h"
|
||||
#include "AdventureProfessionPolicy.h"
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "CharacterCache.h"
|
||||
#include "Chat.h"
|
||||
#include "ClientlessCharacterFactory.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "DBCStores.h"
|
||||
#include "GameTime.h"
|
||||
#include "Item.h"
|
||||
#include "ItemTemplate.h"
|
||||
#include "Log.h"
|
||||
#include "MapManager.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "Stats.h"
|
||||
#include "Trainer.h"
|
||||
#include "World.h"
|
||||
#include "WorldSession.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <filesystem>
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
using AdventureGeneratorPolicy::Archetype;
|
||||
bool Schema()
|
||||
{return bool(CharacterDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_generated_bot'"));}
|
||||
uint32 LearnedSkill(uint32 id,uint32& step)
|
||||
{
|
||||
if(auto rank=sSpellMgr->GetSpellLearnSkill(id))
|
||||
if(rank->step){step=rank->step;return rank->skill;}
|
||||
if(auto info=sSpellMgr->GetSpellInfo(id))for(auto const& effect:info->Effects)
|
||||
if(effect.Effect==SPELL_EFFECT_SKILL_STEP && effect.CalcValue()>0)
|
||||
{step=effect.CalcValue();return effect.MiscValue;}
|
||||
return 0;
|
||||
}
|
||||
bool Learnable(Player* p,uint32 id)
|
||||
{
|
||||
auto info=sSpellMgr->GetSpellInfo(id);
|
||||
return info && info->SpellLevel<=p->getLevel() && info->BaseLevel<=p->getLevel() &&
|
||||
p->IsSpellFitByClassAndRace(id) && SpellMgr::IsSpellValid(info,p,false);
|
||||
}
|
||||
std::vector<uint32> Taught(uint32 id)
|
||||
{
|
||||
std::vector<uint32> result;
|
||||
if(auto info=sSpellMgr->GetSpellInfo(id))
|
||||
{
|
||||
for(auto const& effect:info->Effects)
|
||||
if(effect.Effect==SPELL_EFFECT_LEARN_SPELL && effect.TriggerSpell)result.push_back(effect.TriggerSpell);
|
||||
if(result.empty())result.push_back(id);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void ClassTraining(Player* p)
|
||||
{
|
||||
if(auto rows=WorldDatabase.Query("SELECT Id FROM trainer WHERE Type=0 ORDER BY Id"))do
|
||||
{
|
||||
auto trainer=sObjectMgr->GetTrainer(rows->Fetch()[0].GetUInt32());
|
||||
if(!trainer || !trainer->IsTrainerValidForPlayer(p))continue;
|
||||
// Repeat for prerequisite chains without granting arbitrary class-
|
||||
// family NPC spells or abilities from a different specialization.
|
||||
for(unsigned pass=0;pass<8;++pass)for(auto const& spell:trainer->GetSpells())
|
||||
{
|
||||
if(!trainer->CanTeachSpell(p,&spell))continue;
|
||||
for(auto id:Taught(spell.SpellId))
|
||||
if(!p->HasSpell(id) && Learnable(p,id))p->LearnSpell(id,false);
|
||||
}
|
||||
}while(rows->NextRow());
|
||||
for(auto ability:sSkillLineAbilityStore)
|
||||
{
|
||||
auto skill=sSkillLineStore.LookupEntry(ability->SkillLine);
|
||||
auto rc=sDBCManager.GetSkillRaceClassInfo(ability->SkillLine,p->getRace(),p->getClass());
|
||||
if(!skill || !rc || rc->MinLevel>p->getLevel() ||
|
||||
(skill->CategoryID!=SKILL_CATEGORY_ARMOR && skill->CategoryID!=SKILL_CATEGORY_WEAPON))continue;
|
||||
if(!p->HasSpell(ability->Spell) && Learnable(p,ability->Spell))p->LearnSpell(ability->Spell,false);
|
||||
}
|
||||
p->UpdateSkillsForLevel();
|
||||
for(auto skill:sSkillLineStore)
|
||||
if(skill->CategoryID==SKILL_CATEGORY_WEAPON && p->HasSkill(skill->ID))
|
||||
if(auto maximum=p->GetPureMaxSkillValue(skill->ID))
|
||||
p->SetSkill(skill->ID,p->GetSkillStep(skill->ID),maximum,maximum);
|
||||
}
|
||||
bool Talents(Player* p,Archetype const& a)
|
||||
{
|
||||
if(p->getLevel()<10)return true;
|
||||
p->InitTalentForLevel();
|
||||
if(!p->LearnPrimaryTalentSpecialization(a.Spec))return false;
|
||||
auto tabs=sDBCManager.GetTalentTabPages(p->getClass());if(!tabs)return false;
|
||||
if(a.Class==CLASS_WARRIOR && a.Spec==0)
|
||||
AdventureCombatPreparationPolicy::SpendArms60([&](uint32 id,unsigned rank){return p->LearnTalent(id,rank);},[&](){return p->GetFreeTalentPoints();});
|
||||
// Native LearnTalent enforces tier/dependency/rank and 31-point gates.
|
||||
// The named solo spec is preferred; secondary trees only use leftovers.
|
||||
for(unsigned tree=0;tree<MAX_TALENT_TABS && p->GetFreeTalentPoints();++tree)
|
||||
{
|
||||
uint32 tab=tabs[(a.Spec+tree)%MAX_TALENT_TABS];std::vector<TalentEntry const*> talents;
|
||||
for(auto entry:sTalentStore)if(entry->TabID==tab)talents.push_back(entry);
|
||||
std::sort(talents.begin(),talents.end(),[](TalentEntry const* x,TalentEntry const* y){return std::tie(x->TierID,x->ColumnIndex,x->ID)<std::tie(y->TierID,y->ColumnIndex,y->ID);});
|
||||
for(unsigned pass=0;pass<50 && p->GetFreeTalentPoints();++pass)
|
||||
{
|
||||
bool progress=false;
|
||||
for(auto talent:talents)for(unsigned rank=0;rank<MAX_TALENT_RANK && p->GetFreeTalentPoints();++rank)
|
||||
if(talent->SpellRank[rank] && p->LearnTalent(talent->ID,rank))progress=true;
|
||||
if(!progress)break;
|
||||
}
|
||||
}
|
||||
return !p->GetFreeTalentPoints();
|
||||
}
|
||||
bool Profession(Player* p,unsigned wanted)
|
||||
{
|
||||
struct Rank {uint32 Id,Step,Level,Required;};std::vector<Rank> ranks;
|
||||
if(auto rows=WorldDatabase.Query("SELECT DISTINCT SpellId,ReqLevel,ReqSkillRank FROM trainer_spell"))do
|
||||
{
|
||||
auto f=rows->Fetch();
|
||||
for(auto id:Taught(f[0].GetUInt32()))
|
||||
{
|
||||
uint32 step=0;if(LearnedSkill(id,step)!=wanted || !step)continue;
|
||||
auto info=sSpellMgr->GetSpellInfo(id);if(!info)continue;
|
||||
ranks.push_back({id,step,std::max<uint32>({f[1].GetUInt8(),info->SpellLevel,info->BaseLevel}),f[2].GetUInt32()});
|
||||
}
|
||||
}while(rows->NextRow());
|
||||
std::sort(ranks.begin(),ranks.end(),[](Rank const& a,Rank const& b){return std::tie(a.Step,a.Level,a.Id)<std::tie(b.Step,b.Level,b.Id);});
|
||||
for(auto const& rank:ranks)
|
||||
{
|
||||
if(rank.Level>p->getLevel() || rank.Required>p->GetPureSkillValue(wanted) || !Learnable(p,rank.Id))continue;
|
||||
if(!p->HasSpell(rank.Id))p->LearnSpell(rank.Id,false);
|
||||
if(!p->IsInWorld() && !ClientlessCharacterFactory::PrepareSkillRank(p,wanted,rank.Step))continue;
|
||||
auto maximum=p->GetPureMaxSkillValue(wanted);
|
||||
if(maximum)p->SetSkill(wanted,p->GetSkillStep(wanted),maximum,maximum);
|
||||
}
|
||||
return p->HasSkill(wanted) && p->GetPureSkillValue(wanted)>0;
|
||||
}
|
||||
bool UsefulSpell(uint32 id,Archetype const& a,unsigned depth=0)
|
||||
{
|
||||
if(depth>3)return true;
|
||||
auto spell=sSpellMgr->GetSpellInfo(id);if(!spell)return true;
|
||||
bool caster=a.Attribute==ITEM_MOD_INTELLECT;
|
||||
for(auto const& effect:spell->Effects)
|
||||
{
|
||||
bool manaAura=effect.ApplyAuraName==SPELL_AURA_PERIODIC_ENERGIZE || effect.ApplyAuraName==SPELL_AURA_MOD_POWER_REGEN ||
|
||||
effect.ApplyAuraName==SPELL_AURA_MOD_POWER_REGEN_PERCENT || effect.ApplyAuraName==SPELL_AURA_MOD_INCREASE_ENERGY || effect.ApplyAuraName==SPELL_AURA_MOD_INCREASE_ENERGY_PERCENT;
|
||||
if(!caster && effect.MiscValue==POWER_MANA && (manaAura || effect.Effect==SPELL_EFFECT_ENERGIZE || effect.Effect==SPELL_EFFECT_ENERGIZE_PCT))return false;
|
||||
if(!caster && (effect.ApplyAuraName==SPELL_AURA_MOD_SPELL_HIT_CHANCE || effect.ApplyAuraName==SPELL_AURA_MOD_SPELL_CRIT_CHANCE ||
|
||||
effect.ApplyAuraName==SPELL_AURA_MOD_SPELL_CRIT_CHANCE_SCHOOL || effect.ApplyAuraName==SPELL_AURA_MOD_HEALING_DONE ||
|
||||
(effect.ApplyAuraName==SPELL_AURA_MOD_DAMAGE_DONE && !(effect.MiscValue&1))))return false;
|
||||
if(caster && (effect.ApplyAuraName==SPELL_AURA_MOD_ATTACK_POWER || effect.ApplyAuraName==SPELL_AURA_MOD_RANGED_ATTACK_POWER ||
|
||||
(effect.ApplyAuraName==SPELL_AURA_MOD_DAMAGE_DONE && !(effect.MiscValue&126))))return false;
|
||||
if(effect.ApplyAuraName==SPELL_AURA_MOD_STAT &&
|
||||
((!caster && effect.MiscValue==int(StatType::Intellect)) || (caster && (effect.MiscValue==int(StatType::Strength) || effect.MiscValue==int(StatType::Agility)))))return false;
|
||||
if(effect.TriggerSpell && !UsefulSpell(effect.TriggerSpell,a,depth+1))return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool Slot(ItemTemplate const* item,unsigned slot,Archetype const& a)
|
||||
{
|
||||
unsigned type=item->GetInventoryType();
|
||||
switch(slot)
|
||||
{
|
||||
case EQUIPMENT_SLOT_HEAD:return type==INVTYPE_HEAD;
|
||||
case EQUIPMENT_SLOT_NECK:return type==INVTYPE_NECK;
|
||||
case EQUIPMENT_SLOT_SHOULDERS:return type==INVTYPE_SHOULDERS;
|
||||
case EQUIPMENT_SLOT_CHEST:return type==INVTYPE_CHEST || type==INVTYPE_ROBE;
|
||||
case EQUIPMENT_SLOT_WAIST:return type==INVTYPE_WAIST;
|
||||
case EQUIPMENT_SLOT_LEGS:return type==INVTYPE_LEGS;
|
||||
case EQUIPMENT_SLOT_FEET:return type==INVTYPE_FEET;
|
||||
case EQUIPMENT_SLOT_WRISTS:return type==INVTYPE_WRISTS;
|
||||
case EQUIPMENT_SLOT_HANDS:return type==INVTYPE_HANDS;
|
||||
case EQUIPMENT_SLOT_FINGER1:case EQUIPMENT_SLOT_FINGER2:return type==INVTYPE_FINGER;
|
||||
case EQUIPMENT_SLOT_TRINKET1:case EQUIPMENT_SLOT_TRINKET2:return type==INVTYPE_TRINKET;
|
||||
case EQUIPMENT_SLOT_BACK:return type==INVTYPE_CLOAK;
|
||||
case EQUIPMENT_SLOT_MAINHAND:
|
||||
if(a.Class==CLASS_ROGUE)return type==INVTYPE_WEAPON || type==INVTYPE_WEAPONMAINHAND;
|
||||
if(a.Attribute==ITEM_MOD_INTELLECT)return type==INVTYPE_2HWEAPON && item->GetSubClass()==ITEM_SUBCLASS_WEAPON_STAFF;
|
||||
return type==INVTYPE_2HWEAPON;
|
||||
case EQUIPMENT_SLOT_OFFHAND:return a.Class==CLASS_ROGUE && (type==INVTYPE_WEAPON || type==INVTYPE_WEAPONOFFHAND);
|
||||
case EQUIPMENT_SLOT_RANGED:
|
||||
return a.Class==CLASS_HUNTER?item->IsRangedWeapon():a.Armor==ITEM_SUBCLASS_ARMOR_CLOTH && item->GetClass()==ITEM_CLASS_WEAPON && item->GetSubClass()==ITEM_SUBCLASS_WEAPON_WAND;
|
||||
default:return false;
|
||||
}
|
||||
}
|
||||
bool Equip(Player* p,Archetype const& a,std::mt19937& random,std::string& error)
|
||||
{
|
||||
for(unsigned slot=EQUIPMENT_SLOT_START;slot<EQUIPMENT_SLOT_END;++slot)
|
||||
{
|
||||
if(slot==EQUIPMENT_SLOT_BODY || slot==EQUIPMENT_SLOT_TABARD ||
|
||||
(slot==EQUIPMENT_SLOT_OFFHAND && a.Class!=CLASS_ROGUE) ||
|
||||
(slot==EQUIPMENT_SLOT_RANGED && a.Class!=CLASS_HUNTER && a.Armor!=ITEM_SUBCLASS_ARMOR_CLOTH))continue;
|
||||
struct Candidate {uint32 Id;float Score;};std::vector<Candidate> candidates;
|
||||
uint32 salt=random();
|
||||
for(auto const& pair:*sObjectMgr->GetItemTemplateStore())
|
||||
{
|
||||
auto const* item=&pair.second;
|
||||
if(!Slot(item,slot,a) || p->GetItemCount(pair.first)>0 ||
|
||||
!AdventureGeneratorPolicy::EligibleItem(p->getLevel(),std::max(0,item->GetRequiredLevel()),item->GetQuality(),item->GetBaseItemLevel(),
|
||||
item->GetDuration() || (item->GetFlags()&ITEM_FLAG_CONJURED),item->GetRequiredReputationFaction() || item->GetArea() || (item->GetMap()!=0 && item->GetMap()!=uint32(-1)) || item->GetHolidayID()!=HOLIDAY_NONE || item->GetRandomProperty() || item->GetRandomSuffix()) ||
|
||||
p->CanUseItem(item)!=EQUIP_ERR_OK)continue;
|
||||
if(item->GetClass()==ITEM_CLASS_ARMOR && slot!=EQUIPMENT_SLOT_NECK && slot!=EQUIPMENT_SLOT_BACK &&
|
||||
slot!=EQUIPMENT_SLOT_FINGER1 && slot!=EQUIPMENT_SLOT_FINGER2 && slot!=EQUIPMENT_SLOT_TRINKET1 && slot!=EQUIPMENT_SLOT_TRINKET2 &&
|
||||
item->GetSubClass()!=AdventureGeneratorPolicy::Armor(a,p->getLevel()))continue;
|
||||
bool effects=true;
|
||||
for(auto const& effect:item->Effects)if(effect.SpellID && !UsefulSpell(effect.SpellID,a))effects=false;
|
||||
if(!effects)continue;
|
||||
float stat=0,wrong=0;
|
||||
for(unsigned index=0;index<MAX_ITEM_PROTO_STATS;++index)
|
||||
{
|
||||
auto type=item->GetItemStatType(index);float value=item->GetStatValue(index,p);
|
||||
if(type==int(a.Attribute))stat+=value;
|
||||
else if(type==ITEM_MOD_STRENGTH || type==ITEM_MOD_AGILITY || type==ITEM_MOD_INTELLECT ||
|
||||
(a.Attribute==ITEM_MOD_INTELLECT && (type==ITEM_MOD_ATTACK_POWER || type==ITEM_MOD_RANGED_ATTACK_POWER)) ||
|
||||
(a.Attribute!=ITEM_MOD_INTELLECT && (type==ITEM_MOD_SPIRIT || type==ITEM_MOD_MANA || type==ITEM_MOD_MANA_REGENERATION || type==ITEM_MOD_SPELL_POWER || type==ITEM_MOD_SPELL_PENETRATION)))wrong+=value;
|
||||
}
|
||||
if(wrong>0 && stat==0)continue;
|
||||
candidates.push_back({pair.first,item->GetBaseItemLevel()+stat*2-wrong+float((pair.first*2654435761u+salt)%100)/100});
|
||||
}
|
||||
std::sort(candidates.begin(),candidates.end(),[](Candidate const& x,Candidate const& y){return x.Score==y.Score?x.Id<y.Id:x.Score>y.Score;});
|
||||
if(!candidates.empty())
|
||||
{
|
||||
auto end=candidates.begin();
|
||||
while(end!=candidates.end() && end-candidates.begin()<8 && end->Score>=candidates.front().Score*0.9f)++end;
|
||||
std::shuffle(candidates.begin(),end,random);
|
||||
}
|
||||
bool equipped=false;
|
||||
for(auto const& candidate:candidates)
|
||||
{
|
||||
auto dispose=[&](Item* item){if(item){RemoveItemFromUpdateQueueOf(item,p);delete item;}};
|
||||
std::unique_ptr<Item,decltype(dispose)> item(Item::CreateItem(candidate.Id,1,p),dispose);uint16 destination=0;
|
||||
if(!item || p->CanEquipItem(slot,destination,item.get(),true)!=EQUIP_ERR_OK)continue;
|
||||
if(auto previous=p->GetItemByPos(INVENTORY_SLOT_BAG_0,slot))
|
||||
{
|
||||
ItemPosCountVec storage;
|
||||
if(p->CanStoreItem(NULL_BAG,NULL_SLOT,storage,previous,false)!=EQUIP_ERR_OK){error="No space to retain initial equipment.";return false;}
|
||||
p->RemoveItem(INVENTORY_SLOT_BAG_0,slot,true);p->StoreItem(storage,previous,true);
|
||||
}
|
||||
if(!p->EquipItem(destination,item.get(),true)){error="Native equipment placement failed.";return false;}
|
||||
item.release();equipped=true;break;
|
||||
}
|
||||
bool required=slot==EQUIPMENT_SLOT_MAINHAND || (a.Class==CLASS_HUNTER && slot==EQUIPMENT_SLOT_RANGED) ||
|
||||
slot==EQUIPMENT_SLOT_CHEST || slot==EQUIPMENT_SLOT_LEGS || slot==EQUIPMENT_SLOT_FEET ||
|
||||
(p->getLevel()>=30 && slot!=EQUIPMENT_SLOT_RANGED && slot!=EQUIPMENT_SLOT_OFFHAND);
|
||||
if(!equipped && required){error="No legal profile-matching equipment for slot "+std::to_string(slot);return false;}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
AdventureGeneratorMgr* AdventureGeneratorMgr::instance(){static AdventureGeneratorMgr mgr;return &mgr;}
|
||||
bool AdventureGeneratorMgr::Profile(uint32 guid,std::string& lua)
|
||||
{
|
||||
if(!Schema())return false;
|
||||
auto row=CharacterDatabase.PQuery("SELECT profile_lua FROM vcore_adventure_generated_bot WHERE character_guid=%u AND state='ready'",guid);
|
||||
if(!row)return false;
|
||||
lua=row->Fetch()[0].GetString();
|
||||
return !lua.empty();
|
||||
}
|
||||
bool AdventureGeneratorMgr::Prepare(Player* p,Archetype const& a,std::string& error)
|
||||
{
|
||||
ClassTraining(p);
|
||||
if(!Talents(p,a)){error="Native talent plan could not be completed.";return false;}
|
||||
for(auto skill:a.Skills)if(!Profession(p,skill)){error="No native profession rank could be learned for skill "+std::to_string(skill);return false;}
|
||||
p->SetFreePrimaryProfessions(0); // the generated profile owns exactly two primaries
|
||||
if(!sAdventureSupplyMgr->PrepareCharacter(p)){error="Could not prepare secondary professions and bags.";return false;}
|
||||
return true;
|
||||
}
|
||||
bool AdventureGeneratorMgr::Create(ChatHandler* handler,uint32 account,uint32 seed,uint8 level,unsigned faction,std::string const& key,bool persistent)
|
||||
{
|
||||
std::mt19937 random(seed);
|
||||
struct Choice {Archetype const* Type;uint8 Race;};std::vector<Choice> choices;
|
||||
for(auto const& a:AdventureGeneratorPolicy::Archetypes())
|
||||
{
|
||||
if(key!="any" && key!=a.Key)continue;
|
||||
if(a.Class==CLASS_DEATH_KNIGHT && level<55)continue;
|
||||
for(uint8 race:{1,2,3,4,5,6,7,8,9,10,11,22})
|
||||
if((!faction || AdventureGeneratorPolicy::Faction(race)==faction) && sObjectMgr->GetPlayerInfo(race,a.Class))choices.push_back({&a,race});
|
||||
}
|
||||
if(choices.empty()){handler->SendSysMessage("No legal race/class archetype fits this request.");return false;}
|
||||
auto choice=choices[random()%choices.size()];auto const& a=*choice.Type;
|
||||
uint8 gender=random()%2;std::string name;
|
||||
for(unsigned attempt=0;attempt<1024;++attempt)
|
||||
{
|
||||
auto candidate=AdventureGeneratorPolicy::Name(choice.Race,gender,random);
|
||||
if(ObjectMgr::CheckPlayerName(candidate,LOCALE_enUS,true)!=CHAR_NAME_SUCCESS || sObjectMgr->IsReservedName(candidate) ||
|
||||
sCharacterCache->GetCharacterCacheByName(candidate))continue;
|
||||
std::string lower=candidate;
|
||||
std::transform(lower.begin(),lower.end(),lower.begin(),[](unsigned char ch){return char(std::tolower(ch));});
|
||||
auto path=std::filesystem::path(sClientlessLuaEngine->GetScriptRoot())/"adventure"/"profiles"/(lower+".lua");
|
||||
if(std::filesystem::exists(path))continue; // reserve hand-written identities too
|
||||
std::string escaped=candidate;CharacterDatabase.EscapeString(escaped);
|
||||
if(!CharacterDatabase.PQuery("SELECT 1 FROM characters WHERE name='%s' LIMIT 1",escaped.c_str())){name=candidate;break;}
|
||||
}
|
||||
if(name.empty()){handler->SendSysMessage("Roleplay name pool exhausted; no numeric suffix was substituted.");return false;}
|
||||
// Mature goblin/worgen travelers have left their phased introduction.
|
||||
uint8 homeRace=choice.Race==9?2:choice.Race==22?1:choice.Race;
|
||||
auto info=sObjectMgr->GetPlayerInfo(homeRace,CLASS_WARRIOR);
|
||||
if(!info){handler->SendSysMessage("Native racial home data unavailable.");return false;}
|
||||
if(!sAdventureLocationMgr->EnsureLoaded()){handler->SendSysMessage("Adventure location catalog unavailable.");return false;}
|
||||
AdventureLocation const* home=nullptr;float best=1e30f;
|
||||
for(auto const& location:sAdventureLocationMgr->Locations())
|
||||
{
|
||||
if(location.IsArea || location.Point.MapId!=info->mapId ||
|
||||
(location.Faction && location.Faction!=AdventureGeneratorPolicy::Faction(choice.Race)) ||
|
||||
(location.Point.Category!="tavern" && location.Point.Category!="settlement") ||
|
||||
sAdventureLocationMgr->Weight(location,"social")<=0)continue;
|
||||
float dx=location.Point.X-info->positionX,dy=location.Point.Y-info->positionY;
|
||||
float distance=dx*dx+dy*dy;
|
||||
if(distance<best){best=distance;home=&location;}
|
||||
}
|
||||
if(!home){handler->SendSysMessage("No faction-compatible home near the native racial starting area.");return false;}
|
||||
std::string profile=AdventureGeneratorPolicy::Profile(name,a,choice.Race,home->Point.Id,home->Region,random);
|
||||
std::string escapedProfile=profile;CharacterDatabase.EscapeString(escapedProfile);
|
||||
ClientlessCharacterFactory::Definition definition{name,choice.Race,uint8(a.Class),gender,level,seed};
|
||||
auto result=ClientlessCharacterFactory::Create(account,definition,
|
||||
[&](Player* player,std::string& error)
|
||||
{
|
||||
if(player->GetMapId()!=home->Point.MapId)
|
||||
{
|
||||
player->ResetMap();player->SetMap(sMapMgr->CreateMap(home->Point.MapId,player));
|
||||
}
|
||||
player->Relocate(home->Point.X,home->Point.Y,home->Point.Z,home->Point.O);
|
||||
if(!Prepare(player,a,error) || !Equip(player,a,random,error))return false;
|
||||
// Save its bind row in the SAME
|
||||
// transaction instead of calling SetHomebind's immediate UPDATE.
|
||||
return true;
|
||||
},
|
||||
[&](CharacterDatabaseTransaction const& tx,Player* player)
|
||||
{
|
||||
tx->PAppend("INSERT INTO vcore_adventure_generated_bot (character_guid,account_id,archetype,seed,persistent,state,profile_lua,created_at) VALUES (%u,%u,'%s',%u,%u,'ready','%s',%llu)",
|
||||
player->GetGUID().GetCounter(),account,a.Key,seed,persistent?1:0,escapedProfile.c_str(),static_cast<unsigned long long>(GameTime::GetGameTime()));
|
||||
tx->PAppend("INSERT INTO character_homebind (guid,mapId,zoneId,posX,posY,posZ) VALUES (%u,%u,%u,%f,%f,%f)",
|
||||
player->GetGUID().GetCounter(),home->Point.MapId,home->ZoneId,home->Point.X,home->Point.Y,home->Point.Z);
|
||||
});
|
||||
if(!result.Guid){handler->PSendSysMessage("Generation failed name='%s': %s",name.c_str(),result.Error.c_str());return false;}
|
||||
handler->PSendSysMessage("Generated %s guid=%u race=%u class=%u level=%u archetype=%s persistent=%u seed=%u home=%s (offline).",
|
||||
name.c_str(),result.Guid,choice.Race,a.Class,level,a.Key,persistent?1:0,seed,home->Point.Id.c_str());
|
||||
TC_LOG_INFO("server.adventurebot","BOT_GENERATED guid=%u name='%s' race=%u class=%u level=%u archetype='%s' seed=%u persistent=%u",result.Guid,name.c_str(),choice.Race,a.Class,level,a.Key,seed,persistent?1:0);
|
||||
return true;
|
||||
}
|
||||
bool AdventureGeneratorMgr::Command(ChatHandler* handler,char const* args)
|
||||
{
|
||||
if(!Schema()){handler->SendSysMessage("Apply the AdventureBot generator character migration first.");return false;}
|
||||
std::istringstream input(args?args:"");std::string op;input>>op;
|
||||
if(op=="create")
|
||||
{
|
||||
uint32 account=0,count=0,level=60,seed=uint32(GameTime::GetGameTime());std::string side="any",key="any",mode="persistent",extra;
|
||||
if(!(input>>account>>count)){handler->SendSysMessage("Usage: clientless generate create <bot-account> <count 1..10> [level 10..85] [any|alliance|horde] [archetype|any] [persistent|temporary] [seed]");return false;}
|
||||
input>>std::ws;
|
||||
if(!input.eof())
|
||||
{
|
||||
if(!(input>>level>>side>>key>>mode>>seed) || input>>extra){handler->SendSysMessage("Optional arguments must be supplied together: level faction archetype persistence seed.");return false;}
|
||||
}
|
||||
if(count<1 || count>10 || level<10 || level>85 || (side!="any" && side!="alliance" && side!="horde") || (mode!="persistent" && mode!="temporary"))return false;
|
||||
unsigned created=0;
|
||||
for(uint32 i=0;i<count;++i)if(Create(handler,account,seed+i,uint8(level),side=="alliance"?1:side=="horde"?2:0,key,mode=="persistent"))++created;else break;
|
||||
handler->PSendSysMessage("Generation batch complete: %u/%u characters, each atomically saved; no existing characters modified.",created,count);
|
||||
return created==count;
|
||||
}
|
||||
if(op=="list")
|
||||
{
|
||||
if(auto rows=CharacterDatabase.Query("SELECT g.character_guid,c.name,g.archetype,g.persistent,g.state,c.online,g.seed FROM vcore_adventure_generated_bot g JOIN characters c ON c.guid=g.character_guid ORDER BY g.character_guid LIMIT 100"))do
|
||||
{
|
||||
auto f=rows->Fetch();handler->PSendSysMessage("%u %s archetype=%s persistent=%u state=%s online=%u seed=%u",f[0].GetUInt32(),f[1].GetString().c_str(),f[2].GetString().c_str(),f[3].GetUInt8(),f[4].GetString().c_str(),f[5].GetUInt8(),f[6].GetUInt32());
|
||||
}while(rows->NextRow());
|
||||
return true;
|
||||
}
|
||||
uint32 guid=0;input>>guid;
|
||||
auto row=CharacterDatabase.PQuery("SELECT g.account_id,g.state,g.persistent,c.online,g.archetype,g.seed FROM vcore_adventure_generated_bot g JOIN characters c ON c.guid=g.character_guid WHERE g.character_guid=%u",guid);
|
||||
if(!row){handler->SendSysMessage("Not a generator-owned character; no action taken.");return false;}
|
||||
auto f=row->Fetch();
|
||||
if(op=="profile")
|
||||
{
|
||||
std::string lua;if(!Profile(guid,lua))return false;handler->SendSysMessage(lua.c_str());return true;
|
||||
}
|
||||
if(op=="equip" && f[1].GetString()=="ready")
|
||||
{
|
||||
auto session=sWorld->FindClientlessSession(guid);auto player=session?session->GetPlayer():nullptr;
|
||||
if(!player || !player->IsInWorld() || !player->IsAlive() || player->IsInCombat() || player->IsBeingTeleported() ||
|
||||
player->GetTransport() || player->IsInFlight() || player->HasUnitState(UNIT_STATE_CASTING))
|
||||
{handler->SendSysMessage("Equipment refresh requires an idle online generator-owned bot outside combat/transit/casting.");return false;}
|
||||
for(auto const& archetype:AdventureGeneratorPolicy::Archetypes())
|
||||
if(f[4].GetString()==archetype.Key && player->getClass()==archetype.Class)
|
||||
{
|
||||
sAdventureBehaviorMgr->InterruptForInteraction(player);
|
||||
std::mt19937 random(f[5].GetUInt32());std::string error;
|
||||
if(!Equip(player,archetype,random,error)){handler->PSendSysMessage("Equipment refresh stopped: %s",error.c_str());return false;}
|
||||
player->UpdateMaxHealth();player->SetFullHealth();player->SetFullPower(POWER_MANA);
|
||||
auto tx=CharacterDatabase.BeginTransaction();player->SaveToDB(tx);
|
||||
auto saved=CharacterDatabase.AsyncCommitTransaction(tx);if(!saved.m_future.get())return false;
|
||||
handler->SendSysMessage("Profile equipment refreshed through native checks; previous items retained in bags.");
|
||||
TC_LOG_INFO("server.adventurebot","BOT_EQUIPMENT_REFRESH guid=%u archetype='%s'",guid,archetype.Key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if(op=="login" && f[1].GetString()=="ready")return sClientlessPlayerMgr->StartPlayer(f[0].GetUInt32(),guid,CreateClientlessAdventureLuaController());
|
||||
if(op=="logout")return sClientlessPlayerMgr->StopPlayer(guid);
|
||||
if(op=="retire")
|
||||
{
|
||||
if(f[2].GetUInt8() || f[3].GetUInt8() || sWorld->GetClientlessPlayerState(guid)!=ClientlessPlayerState::Offline)
|
||||
{handler->SendSysMessage("Retirement requires an offline temporary generator-owned character. Persistent characters are protected.");return false;}
|
||||
auto tx=CharacterDatabase.BeginTransaction();tx->PAppend("UPDATE vcore_adventure_generated_bot SET state='retired',retired_at=%llu WHERE character_guid=%u AND persistent=0 AND state='ready'",static_cast<unsigned long long>(GameTime::GetGameTime()),guid);
|
||||
auto result=CharacterDatabase.AsyncCommitTransaction(tx);
|
||||
if(!result.m_future.get())return false;
|
||||
handler->SendSysMessage("Temporary bot retired. Native character, items and profile retained; no deletion or automatic replacement.");return true;
|
||||
}
|
||||
handler->SendSysMessage("Usage: clientless generate create|list|profile <guid>|login <guid>|logout <guid>|equip <guid>|retire <guid>");return false;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#ifndef VCORE_ADVENTURE_GENERATOR_MGR_H
|
||||
#define VCORE_ADVENTURE_GENERATOR_MGR_H
|
||||
#include "Common.h"
|
||||
#include "AdventureGeneratorPolicy.h"
|
||||
#include <string>
|
||||
class Player;
|
||||
class ChatHandler;
|
||||
class TC_GAME_API AdventureGeneratorMgr
|
||||
{
|
||||
public:
|
||||
static AdventureGeneratorMgr* instance();
|
||||
bool Command(ChatHandler* handler,char const* args);
|
||||
// Lua profile stays in the character DB; hand-written named profiles retain
|
||||
// precedence. A generated identity is never inferred from account/name.
|
||||
static bool Profile(uint32 guid,std::string& lua);
|
||||
private:
|
||||
bool Create(ChatHandler* handler,uint32 account,uint32 seed,uint8 level,unsigned faction,
|
||||
std::string const& archetype,bool persistent);
|
||||
bool Prepare(Player* player,AdventureGeneratorPolicy::Archetype const& archetype,std::string& error);
|
||||
};
|
||||
#define sAdventureGeneratorMgr AdventureGeneratorMgr::instance()
|
||||
#endif
|
||||
@@ -0,0 +1,105 @@
|
||||
#ifndef VCORE_ADVENTURE_GENERATOR_POLICY_H
|
||||
#define VCORE_ADVENTURE_GENERATOR_POLICY_H
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include "AdventureProfessionPolicy.h"
|
||||
namespace AdventureGeneratorPolicy
|
||||
{
|
||||
struct Archetype
|
||||
{
|
||||
char const* Key;unsigned Class,Spec,Attribute,Armor;
|
||||
std::array<unsigned,2> Skills;
|
||||
char const* Vocation;
|
||||
};
|
||||
inline std::array<Archetype,10> const& Archetypes()
|
||||
{
|
||||
static std::array<Archetype,10> const values={{{"warrior",1,0,4,4,{186,164},"Wandernder Waffenschmied"},
|
||||
{"paladin",2,2,4,4,{186,755},"Pilger und Edelsteinschleifer"},
|
||||
{"hunter",3,1,3,3,{393,165},"Faehrtensucher und Lederhandwerker"},
|
||||
{"rogue",4,1,3,2,{186,202},"Kundschafter und Tueftler"},
|
||||
{"priest",5,2,5,1,{182,773},"Reisender Chronist"},
|
||||
{"death_knight",6,2,4,4,{186,164},"Schweigsamer Wanderschmied"},
|
||||
{"shaman",7,0,5,3,{182,171},"Pfadfinder und Kraeuterkundiger"},
|
||||
{"mage",8,1,5,1,{197,333},"Reisender Gelehrter"},
|
||||
{"warlock",9,0,5,1,{197,333},"Sucher vergessener Schriften"},
|
||||
{"druid",11,0,5,2,{182,171},"Hueter der wilden Pfade"}}};
|
||||
return values;
|
||||
}
|
||||
inline unsigned Faction(unsigned race)
|
||||
{return race==1 || race==3 || race==4 || race==7 || race==11 || race==22?1:2;}
|
||||
inline char const* Origin(unsigned race)
|
||||
{
|
||||
switch(race)
|
||||
{
|
||||
case 1:return "Elwynn";case 2:return "Durotar";case 3:return "Dun Morogh";
|
||||
case 4:return "Teldrassil";case 5:return "Tirisfal";case 6:return "Mulgore";
|
||||
case 7:return "Dun Morogh";case 8:return "den Echoinseln";case 9:return "Kezan";
|
||||
case 10:return "dem Immersangwald";case 11:return "der Azurmythosinsel";case 22:return "Gilneas";
|
||||
default:return "fernen Landen";
|
||||
}
|
||||
}
|
||||
inline std::string Name(unsigned race,unsigned gender,std::mt19937& random)
|
||||
{
|
||||
// Small cultural grammars, not fragments of arbitrary player/NPC names.
|
||||
// ASCII names keep native client naming rules and SQL/Lua paths simple.
|
||||
char const* first=nullptr;char const* last=nullptr;
|
||||
switch(race)
|
||||
{
|
||||
case 2:first=gender?"Gra Sha Mog Drak Ruk Gar Kor Ur":"Gor Thok Drak Rog Mok Gar Kor Ur";last=gender?"sha ra ga dra ka na gra tha":"gar thar mok nak gor rak dan zug";break;
|
||||
case 3:first=gender?"Hild Bryn Dag Ragn Tor Hel Marn Brin":"Brol Durn Thar Karg Brom Dorn Thol Grun";last=gender?"ra dis na hild ri dra li da":"gar grim din rik gan rum dan rin";break;
|
||||
case 4:first=gender?"Lun Shae Ely Nara Thala Syl Mira Aela":"Ther Dala Shan Fael Reth Lor Ael Thal";last=gender?"ria na riel dra ra niel sia nor":"ion dris nor rion dor thas riel ron";break;
|
||||
case 6:first=gender?"Mahu Hara Tahu Nara Asha Oru Runa Yana":"Hamu Tahu Ranu Maku Oru Koru Toru Yaru";last=gender?"na ra la mi sha ni ma wa":"ran mak tar rok nan tam hon kan";break;
|
||||
case 7:first=gender?"Tilli Bixi Nimi Pelli Fizzi Wini Lilli Miri":"Bim Nib Pib Gim Fizz Tink Wib Nim";last=gender?"na ra li wi mi ni la si":"wick bin win nix mik rik wip lin";break;
|
||||
case 8:first=gender?"Zan Sha Vola Jara Tali Zula Yala Mala":"Zan Vol Jar Zul Rok Tal Sen Jin";last=gender?"ra jin sha li na ri la ni":"jin rok zan zul tal kan rin mar";break;
|
||||
case 9:first=gender?"Kiz Vex Nix Rix Gix Zix Bix Tix":"Gaz Rix Niz Vex Gix Zik Bix Kaz";last=gender?"zi ra ni za li ki na si":"nik wick zik rat zle nix bin zek";break;
|
||||
case 10:first=gender?"Ela Lora Syla Vela Nara Fala Mira Sera":"Vael Aeth Lor Ther Bel Sel Dar Fael";last=gender?"ria riel na ra lin thia sia niel":"rin thas rion dor nor dris ran riel";break;
|
||||
case 11:first=gender?"Yra Sora Nara Vela Isha Tali Asha Ela":"Vara Mora Aru Tura Iru Dara Kora Baru";last=gender?"na ra li sha la ni sa ri":"dor nun ran vos mar das nol ven";break;
|
||||
default:first=gender?"Ela Mira Mara Lena Nora Sera Talia Rena":"Ald Gar Ed Bran Cor Dar Ren Hal";last=gender?"na ra lin ria la nda sia nora":"ren ric dan win lan den ron ven";break;
|
||||
}
|
||||
auto pick=[&](char const* words)
|
||||
{
|
||||
std::string list(words);unsigned choice=random()%8;std::size_t start=0;
|
||||
for(unsigned i=0;i<choice;++i)start=list.find(' ',start)+1;
|
||||
auto end=list.find(' ',start);return list.substr(start,end-start);
|
||||
};
|
||||
return pick(first)+pick(last);
|
||||
}
|
||||
inline std::string LuaString(std::string const& input)
|
||||
{
|
||||
std::string out="\"";
|
||||
for(char ch:input){if(ch=='\\' || ch=='\"')out+='\\';if(ch=='\n')out+="\\n";else if(ch=='\r')out+="\\r";else out+=ch;}
|
||||
return out+'\"';
|
||||
}
|
||||
inline unsigned Armor(Archetype const& a,unsigned level)
|
||||
{return level<40 && a.Armor>=3?a.Armor-1:a.Armor;}
|
||||
inline bool EligibleItem(unsigned level,unsigned required,unsigned quality,unsigned itemLevel,bool temporary,bool restricted)
|
||||
{
|
||||
return required<=level && quality>=1 && quality<=3 && !temporary && !restricted &&
|
||||
(required+15>=level || level<20) && itemLevel<=level*5+20;
|
||||
}
|
||||
inline std::string Profile(std::string const& name,Archetype const& a,unsigned race,
|
||||
std::string const& location,std::string const& region,std::mt19937& random)
|
||||
{
|
||||
auto quote=LuaString;
|
||||
std::ostringstream lua;lua<<std::fixed<<std::setprecision(3);
|
||||
auto personality=[&](){return 0.25f+float(random()%651)/1000;};
|
||||
lua<<"return { identity = { name = "<<quote(name)<<", home_region = "<<quote(region)
|
||||
<<", vocation = "<<quote(a.Vocation)<<", background = "
|
||||
<<quote(std::string("Herkunft: ")+AdventureGeneratorPolicy::Origin(race)+". Sucht auf Reisen Arbeit, neue Wege und Begegnungen.")
|
||||
<<" }, behavior = { enabled = true, daily_schedule = false, initial_intent = 'social', initial_location = "<<quote(location)
|
||||
<<", initial_duration_seconds = 3600, locale = 'deDE' }, professions = { primary = { "
|
||||
<<quote(AdventureProfessionPolicy::PrimaryForSkill(a.Skills[0]))<<", "<<quote(AdventureProfessionPolicy::PrimaryForSkill(a.Skills[1]))
|
||||
<<" } }, personality = { sociability = "<<personality()<<", adventurousness = "<<personality()
|
||||
<<", wanderlust = "<<personality()<<", group_loyalty = "<<personality()<<", tavern_affinity = "<<personality()
|
||||
<<", wilderness_affinity = "<<personality()<<", city_affinity = "<<personality()<<", danger_tolerance = "<<personality()
|
||||
<<" }, region_affinity = { ["<<quote(region)<<"] = 1.0 }, activity_affinity = { social = 0.8, adventure = 0.7, profession = 0.8, leisure = 0.7, economy = 0.5, rest = 0.7, instance = 0.3 }, "
|
||||
<<"movement = { can_ground_mount = false, can_free_fly = false } }";
|
||||
return lua.str();
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "ClientlessCharacterFactory.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "DatabaseEnv.h"
|
||||
@@ -86,6 +87,7 @@ void AdventureSupplyMgr::LearnSecondaries(Player* p)
|
||||
auto info=sSpellMgr->GetSpellInfo(rank.Spell);
|
||||
if(!rc || uint32(rc->MinLevel)>p->getLevel() || !info || !SpellMgr::IsSpellValid(info,p,false))continue;
|
||||
if(!p->HasSpell(rank.Spell))p->LearnSpell(rank.Spell,false);
|
||||
if(!p->IsInWorld() && !ClientlessCharacterFactory::PrepareSkillRank(p,rank.Skill,rank.Step))continue;
|
||||
auto maximum=p->GetPureMaxSkillValue(rank.Skill);
|
||||
if(maximum && p->GetPureSkillValue(rank.Skill)<maximum)
|
||||
{
|
||||
@@ -306,3 +308,8 @@ void AdventureSupplyMgr::Report(ChatHandler* handler,Player* p)
|
||||
for(uint8 slot=INVENTORY_SLOT_BAG_START;slot<INVENTORY_SLOT_BAG_END;++slot)
|
||||
if(auto bag=p->GetBagByPos(slot))handler->PSendSysMessage("bagSlot=%u item=%u capacity=%u",slot,bag->GetEntry(),bag->GetBagSize());
|
||||
}
|
||||
|
||||
bool AdventureSupplyMgr::PrepareCharacter(Player* player)
|
||||
{
|
||||
LoadCatalog();LearnSecondaries(player);return Bags(player);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ class TC_GAME_API AdventureSupplyMgr
|
||||
public:
|
||||
static AdventureSupplyMgr* instance();
|
||||
void Online(Player* player);
|
||||
bool PrepareCharacter(Player* player);
|
||||
void Offline(Player* player);
|
||||
// True only while an out-of-combat recovery owns movement/intent execution.
|
||||
bool Update(Player* player,uint32 diff,bool combatReaction);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#include "ClientlessCharacterFactory.h"
|
||||
#include "AccountMgr.h"
|
||||
#include "CharacterCache.h"
|
||||
#include "DBCStores.h"
|
||||
#include "GameTime.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "Realm.h"
|
||||
#include "ScriptMgr.h"
|
||||
#include "World.h"
|
||||
#include "WorldSession.h"
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct CreateInfo final : CharacterCreateInfo
|
||||
{
|
||||
explicit CreateInfo(ClientlessCharacterFactory::Definition const& d)
|
||||
{
|
||||
Name=d.Name;Race=d.Race;Class=d.Class;Gender=d.Gender;
|
||||
std::mt19937 random(d.Seed);
|
||||
// Native appearance checks are authoritative, including hero-class
|
||||
// and race-specific restrictions. No unvalidated display overrides.
|
||||
for(unsigned attempt=0;attempt<20000;++attempt)
|
||||
{
|
||||
Skin=random()%16;Face=random()%10;HairStyle=random()%20;HairColor=random()%16;FacialHair=random()%9;
|
||||
if(Player::ValidateAppearance(Race,Class,Gender,HairStyle,HairColor,Face,FacialHair,Skin,true))return;
|
||||
}
|
||||
Skin=Face=HairStyle=HairColor=FacialHair=0; // native Create still validates this fallback
|
||||
}
|
||||
};
|
||||
}
|
||||
ClientlessCharacterFactory::Result ClientlessCharacterFactory::Create(uint32 account,Definition const& d,Prepare prepare,Append append)
|
||||
{
|
||||
auto fail=[](char const* error){return Result{0,error};};
|
||||
if(!account || sWorld->FindSession(account))return fail("Account is missing or has an active network session.");
|
||||
if(AccountMgr::GetSecurity(account,realm.Id.Realm)!=SEC_PLAYER)return fail("Use a normal dedicated bot account, never a privileged account.");
|
||||
if(!sObjectMgr->GetPlayerInfo(d.Race,d.Class) || d.Gender>GENDER_FEMALE ||
|
||||
d.Level<1 || d.Level>sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL) || (d.Class==CLASS_DEATH_KNIGHT && d.Level<55))
|
||||
return fail("Invalid race/class/gender/level combination.");
|
||||
std::string name=d.Name;
|
||||
if(!normalizePlayerName(name) || name!=d.Name || ObjectMgr::CheckPlayerName(name,LOCALE_enUS,true)!=CHAR_NAME_SUCCESS ||
|
||||
sObjectMgr->IsReservedName(name) || sCharacterCache->GetCharacterCacheByName(name))return fail("Name is invalid, reserved or already used.");
|
||||
CharacterDatabase.EscapeString(name);
|
||||
if(CharacterDatabase.PQuery("SELECT 1 FROM characters WHERE name='%s' LIMIT 1",name.c_str()))return fail("Name already exists in the character database.");
|
||||
auto acc=LoginDatabase.PQuery("SELECT username,expansion,locale FROM account WHERE id=%u",account);
|
||||
if(!acc)return fail("Account does not exist.");
|
||||
auto f=acc->Fetch();
|
||||
auto race=sChrRacesStore.LookupEntry(d.Race);auto cls=sChrClassesStore.LookupEntry(d.Class);
|
||||
if(!race || !cls || race->Race_related>f[1].GetUInt8() || cls->Required_expansion>f[1].GetUInt8())return fail("Account expansion does not permit this race/class.");
|
||||
auto count=CharacterDatabase.PQuery("SELECT COUNT(*) FROM characters WHERE account=%u AND deleteDate IS NULL",account);
|
||||
if(!count || count->Fetch()[0].GetUInt32()>=sWorld->getIntConfig(CONFIG_CHARACTERS_PER_REALM))return fail("Native per-realm character limit reached.");
|
||||
WorldSession session(account,f[0].GetString(),nullptr,SEC_PLAYER,f[1].GetUInt8(),0,
|
||||
LocaleConstant(std::min<uint8>(f[2].GetUInt8(),TOTAL_LOCALES-1)),0,false,WorldSession::WorldSessionMode::Clientless);
|
||||
auto cleanup=[](Player* player){player->CleanupsBeforeDelete();delete player;};
|
||||
std::unique_ptr<Player,decltype(cleanup)> player(new Player(&session),cleanup);
|
||||
player->GetMotionMaster()->Initialize();CreateInfo info(d);
|
||||
if(!player->Create(sObjectMgr->GetGenerator<HighGuid::Player>().Generate(),&info))return fail("Native character creation failed.");
|
||||
player->setCinematic(1);
|
||||
if(player->getLevel()!=d.Level)
|
||||
{
|
||||
// This character is born at its requested level, rather than earning
|
||||
// levels. Avoid GiveLevel's independent reward-mail transactions/hooks.
|
||||
player->SetLevel(d.Level);player->InitStatsForLevel(true);
|
||||
player->InitTalentForLevel();player->InitTaxiNodesForLevel();player->InitGlyphsForLevel();
|
||||
}
|
||||
player->SetCanModifyStats(true);
|
||||
player->SetMoney(0);player->LearnDefaultSkills();player->UpdateSkillsForLevel();
|
||||
std::string error;
|
||||
if(prepare && !prepare(player.get(),error))return {0,error};
|
||||
player->UpdateMaxHealth();player->UpdateMaxPower(POWER_MANA);
|
||||
player->SetFullHealth();player->SetFullPower(POWER_MANA);
|
||||
auto tx=CharacterDatabase.BeginTransaction();player->SaveToDB(tx,true);
|
||||
if(append)append(tx,player.get());
|
||||
auto commit=CharacterDatabase.AsyncCommitTransaction(tx);
|
||||
if(!commit.m_future.get())return fail("Atomic character save failed; no ready character was published.");
|
||||
uint32 guid=player->GetGUID().GetCounter();
|
||||
sCharacterCache->AddCharacterCacheEntry(player->GetGUID(),account,d.Name,d.Gender,d.Race,d.Class,d.Level);
|
||||
sScriptMgr->OnPlayerCreate(player.get());
|
||||
auto realmCount=LoginDatabase.GetPreparedStatement(LOGIN_REP_REALM_CHARACTERS);
|
||||
realmCount->setUInt32(0,count->Fetch()[0].GetUInt32()+1);realmCount->setUInt32(1,account);realmCount->setUInt32(2,realm.Id.Realm);
|
||||
LoginDatabase.Execute(realmCount);
|
||||
return {guid,{}};
|
||||
}
|
||||
|
||||
bool ClientlessCharacterFactory::PrepareSkillRank(Player* player,uint32 skill,uint32 step)
|
||||
{
|
||||
if(!player || player->IsInWorld() || !step || step>MAX_SKILL_STEP)return false;
|
||||
auto rc=sDBCManager.GetSkillRaceClassInfo(skill,player->getRace(),player->getClass());
|
||||
if(!rc || rc->MinLevel>player->getLevel())return false;
|
||||
auto tier=sSkillTiersStore.LookupEntry(rc->SkillTierID);
|
||||
if(!tier || !tier->Value[step-1])return false;
|
||||
if(player->GetSkillStep(skill)>step)return true;
|
||||
auto maximum=tier->Value[step-1];
|
||||
player->SetSkill(skill,step,maximum,maximum);
|
||||
return player->GetPureSkillValue(skill)==maximum && player->GetPureMaxSkillValue(skill)==maximum;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef VCORE_CLIENTLESS_CHARACTER_FACTORY_H
|
||||
#define VCORE_CLIENTLESS_CHARACTER_FACTORY_H
|
||||
#include "Common.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
class Player;
|
||||
// Native character creation for clientless consumers. No AdventureBot policy,
|
||||
// source-character cloning, authentication credentials or deletion behavior.
|
||||
class TC_GAME_API ClientlessCharacterFactory
|
||||
{
|
||||
public:
|
||||
struct Definition
|
||||
{
|
||||
std::string Name;
|
||||
uint8 Race=0,Class=0,Gender=0,Level=60;
|
||||
uint32 Seed=0;
|
||||
};
|
||||
struct Result { uint32 Guid=0;std::string Error; };
|
||||
using Prepare=std::function<bool(Player*,std::string&)>;
|
||||
using Append=std::function<void(CharacterDatabaseTransaction const&,Player*)>;
|
||||
// Administrative preparation before world insertion; callers still gate
|
||||
// trainer/rank/level eligibility. Uses the native SkillRaceClass/SkillTiers cap.
|
||||
static bool PrepareSkillRank(Player* player,uint32 skill,uint32 step);
|
||||
static Result Create(uint32 account,Definition const& definition,Prepare prepare,Append append);
|
||||
};
|
||||
#endif
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "AdventureSocialActions.h"
|
||||
#include "Config.h"
|
||||
@@ -335,8 +336,12 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
|
||||
fs::path generic = root / "adventure" / "profiles" / "generic.lua";
|
||||
fs::path chosen = fs::exists(specific) ? specific : generic;
|
||||
|
||||
std::string generated;
|
||||
bool databaseProfile=!fs::exists(specific) && AdventureGeneratorMgr::Profile(player->GetGUID().GetCounter(),generated);
|
||||
if(databaseProfile)chosen=fs::path("generated:"+std::to_string(player->GetGUID().GetCounter()));
|
||||
int base = lua_gettop(_state);
|
||||
if (luaL_loadfile(_state, chosen.string().c_str()) != LUA_OK)
|
||||
int loaded=databaseProfile?luaL_loadbuffer(_state,generated.data(),generated.size(),chosen.string().c_str()):luaL_loadfile(_state,chosen.string().c_str());
|
||||
if (loaded != LUA_OK)
|
||||
{
|
||||
TC_LOG_ERROR("server.adventurebot",
|
||||
"[AdventureBot][Profile] guid=%u name='%s' load failed path='%s': %s",
|
||||
@@ -384,6 +389,8 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
|
||||
{
|
||||
profile.CharacterName = ReadStringField(_state, -1, "name", profile.CharacterName);
|
||||
profile.HomeRegion = ReadStringField(_state, -1, "home_region");
|
||||
profile.Background = ReadStringField(_state, -1, "background");
|
||||
profile.Vocation = ReadStringField(_state, -1, "vocation");
|
||||
}
|
||||
lua_pop(_state, 1);
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ struct TC_GAME_API ClientlessAdventureProfile
|
||||
std::unordered_set<std::string> PrimaryProfessions;
|
||||
std::string CharacterName;
|
||||
std::string HomeRegion;
|
||||
std::string Background, Vocation;
|
||||
std::string SourcePath;
|
||||
|
||||
bool CanGroundMount = false;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureInteractionMgr.h"
|
||||
#include "AdventureGeneratorMgr.h"
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessRouteRecorderMgr.h"
|
||||
@@ -63,6 +64,8 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
bool GenerateBot(ChatHandler* handler,char const* args)
|
||||
{ return sAdventureGeneratorMgr->Command(handler,args); }
|
||||
bool InteractionStatus(ChatHandler* handler,char const* args)
|
||||
{
|
||||
std::istringstream input(args?args:"");uint32 guid=0;input>>guid;
|
||||
@@ -2518,6 +2521,7 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
||||
{
|
||||
{ "combat", rbac::RBAC_PERM_COMMAND_GM, true, &ClientlessCombatPreparation::Command, "" },
|
||||
{ "interaction", rbac::RBAC_PERM_COMMAND_GM, true, &InteractionStatus, "" },
|
||||
{ "generate", rbac::RBAC_PERM_COMMAND_GM, true, &GenerateBot, "" },
|
||||
{ "luareload", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLuaReload, "" },
|
||||
{ "transport", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessTransport, "" },
|
||||
{ "login", rbac::RBAC_PERM_COMMAND_GM, true, &HandleClientlessLogin, "" },
|
||||
|
||||
Reference in new issue
Block a user