Files
Viano-Core/tests/clientless/test_adventure_supply_items.py
T

123 lines
10 KiB
Python

"""Execute production item selection/use and secondary-rank gates."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/AdventureSupplyMgr.cpp').read_text()
def function(signature):
a=source.index(signature);b=source.index('{',a);e=b+1;depth=1
while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1
return source[a:e]
header=(root/'src/server/game/World/AdventureSupplyMgr.h').read_text()
header='\n'.join(x for x in header.splitlines() if not x.startswith('#include')).replace('private:','public:')
code=r'''
#include "AdventureSupplyPolicy.h"
#include <algorithm>
#include <array>
#include <cassert>
#include <memory>
#include <ctime>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
using uint32=unsigned;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
constexpr int ITEM_CLASS_CONSUMABLE=0,ITEM_SUBCLASS_BANDAGE=7,ITEM_SUBCLASS_FOOD=5,ITEM_SUBCLASS_POTION=1,EQUIP_ERR_OK=0,UNIT_STATE_CASTING=1,ITEM_SPELLTRIGGER_ON_USE=0,TRIGGERED_NONE=0,TARGET_FLAG_UNIT=2,SPELL_EFFECT_LEARN_SPELL=36,SPELL_EFFECT_SKILL_STEP=44;
constexpr unsigned SKILL_FIRST_AID=129,SKILL_COOKING=185,SKILL_FISHING=356,SKILL_ARCHAEOLOGY=794;
struct Guid{unsigned id=26;unsigned GetCounter(){return id;}};
struct Effect{int Trigger=0,SpellID=0,Effect=0;unsigned TriggerSpell=0;int MiscValue=0,value=0;int CalcValue()const{return value;}};
struct ItemTemplate{
unsigned id,level,subclass,skill=0,rank=0;std::vector<Effect> Effects;
unsigned GetId()const{return id;}unsigned GetRequiredLevel()const{return level;}unsigned GetClass()const{return 0;}unsigned GetSubClass()const{return subclass;}bool IsPotion()const{return subclass==1;}
};
struct Item{ItemTemplate*proto;unsigned count=20;unsigned GetBagSlot(){return 19;}unsigned GetSlot(){return 0;}Guid GetGUID(){return {proto->id};}};
struct SpellInfo{unsigned Id=0,SpellLevel=0,BaseLevel=0;bool combat=true,autoCast=true;std::vector<Effect>Effects;bool CanBeUsedInCombat(){return combat;}};
struct Rank{unsigned skill,step,maximum;};
struct Player;
struct SpellMgr{
std::unordered_map<unsigned,SpellInfo>spells;std::unordered_map<unsigned,Rank>ranks;
SpellInfo*GetSpellInfo(unsigned id){auto i=spells.find(id);return i==spells.end()?nullptr:&i->second;}
Rank*GetSpellLearnSkill(unsigned id){auto i=ranks.find(id);return i==ranks.end()?nullptr:&i->second;}
static bool IsSpellValid(SpellInfo*,Player*,bool){return true;}
}spellMgr;auto sSpellMgr=&spellMgr;
struct ObjectMgr{std::unordered_map<unsigned,ItemTemplate>items;ItemTemplate*GetItemTemplate(unsigned id){auto i=items.find(id);return i==items.end()?nullptr:&i->second;}}objects;auto sObjectMgr=&objects;
struct RC{int MinLevel=1;}rc;
struct DBC{RC*GetSkillRaceClassInfo(unsigned,unsigned,unsigned){return &rc;}}sDBCManager;
struct Field{unsigned value=0;std::string GetString(){return {};};unsigned GetUInt32(){return value;}unsigned GetUInt8(){return value;}unsigned GetUInt16(){return value;}};
struct Row{std::vector<std::array<Field,3>> rows;size_t index=0;Field*Fetch(){return rows[index].data();}bool NextRow(){return ++index<rows.size();}};
struct Database{bool active=false;Row fixture;Row*Query(char const*){fixture.index=0;return active?&fixture:nullptr;}}WorldDatabase;
namespace GameTime{time_t GetGameTime(){return 100;}}
struct History{bool cooldown=false,gcd=false;bool HasGlobalCooldown(SpellInfo*){return gcd;}bool IsReady(SpellInfo*,unsigned){return !cooldown;}};
namespace WorldPackets{namespace Spells{struct SpellCastRequest{unsigned SpellID=0;struct{unsigned Flags=0;Guid Unit;}Target;};}}
struct SpellCastRequestItemData{unsigned bag,slot;Guid guid;SpellCastRequestItemData(unsigned b,unsigned s,Guid g):bag(b),slot(s),guid(g){}};
struct PendingSpellCastRequest{WorldPackets::Spells::SpellCastRequest request;SpellCastRequestItemData item;PendingSpellCastRequest(WorldPackets::Spells::SpellCastRequest&&r,SpellCastRequestItemData i):request(r),item(i){}};
struct Player{
unsigned level=60;bool combat=false,casting=false;History history;int requests=0;std::unordered_map<unsigned,unsigned>skills,caps,steps;std::unordered_set<unsigned>known;
std::unordered_map<unsigned,std::unique_ptr<Item>>inventory;
Guid GetGUID(){return {};}unsigned getLevel(){return level;}unsigned getRace(){return 1;}unsigned getClass(){return 1;}
bool IsSpellFitByClassAndRace(unsigned){return true;}bool HasSpell(unsigned id){return known.count(id);}
unsigned GetPureSkillValue(unsigned skill){return skills[skill];}unsigned GetPureMaxSkillValue(unsigned skill){return caps[skill];}unsigned GetSkillStep(unsigned skill){return steps[skill];}
void LearnSpell(unsigned id,bool){known.insert(id);auto rank=spellMgr.GetSpellLearnSkill(id);if(rank){caps[rank->skill]=rank->maximum;steps[rank->skill]=rank->step;}
else for(auto e:spellMgr.GetSpellInfo(id)->Effects)if(e.Effect==SPELL_EFFECT_SKILL_STEP){caps[e.MiscValue]=e.value*75;steps[e.MiscValue]=e.value;}}
void SetSkill(unsigned id,unsigned step,unsigned val,unsigned max){skills[id]=val;caps[id]=max;steps[id]=step;}
int CanUseItem(ItemTemplate const*p){return p->level<=level && (!p->skill || skills[p->skill]>=p->rank)?0:1;}
int CanUseItem(Item*i){return CanUseItem(i->proto);}
Item*GetItemByEntry(unsigned id){auto i=inventory.find(id);return i==inventory.end()?nullptr:i->second.get();}
unsigned GetItemCount(unsigned id){auto i=GetItemByEntry(id);return i?i->count:0;}
bool IsInCombat(){return combat;}bool IsNonMeleeSpellCast(bool){return casting;}bool HasUnitState(int){return casting;}
History*GetSpellHistory(){return &history;}bool CanRequestSpellCast(SpellInfo*){return true;}
void RequestSpellCast(std::unique_ptr<PendingSpellCastRequest>r,SpellInfo*){
assert(r->request.Target.Flags==TARGET_FLAG_UNIT && r->request.Target.Unit.id==26 && r->item.bag==19 && r->item.slot==0);
auto i=GetItemByEntry(r->item.guid.id);assert(i && i->count);++requests;
if(--i->count==0)inventory.erase(r->item.guid.id); // last item may disappear immediately
}
void Add(ItemTemplate*p,unsigned count=20){inventory[p->id]=std::make_unique<Item>(Item{p,count});}
};
struct Spell{Player*p;SpellInfo*info;Item*m_CastItem=nullptr;Spell(Player*q,SpellInfo*i,int):p(q),info(i){}bool CanAutoCast(Player*q){assert(q==p && m_CastItem);return info->autoCast;}};
'''+header+'\n'+function(' bool RoleMatches(')+'\n'+function('ItemTemplate const* AdventureSupplyMgr::Select(')+'\n'+function('AdventureSupplyMgr::UseResult AdventureSupplyMgr::TryUse(')+'\n'+function('void AdventureSupplyMgr::LearnSecondaries(')+r'''
int main(){
using U=AdventureSupplyPolicy::Use;AdventureSupplyMgr mgr;Player p;auto&r=mgr._records[26];
objects.items.emplace(21991,ItemTemplate{21991,0,7,129,325,{{0,100}}});
objects.items.emplace(14530,ItemTemplate{14530,0,7,129,225,{{0,101}}});
objects.items.emplace(22829,ItemTemplate{22829,55,1,0,0,{{0,102}}});
objects.items.emplace(57191,ItemTemplate{57191,80,1,0,0,{{0,103}}});
objects.items.emplace(27854,ItemTemplate{27854,55,5,0,0,{{0,104}}});
mgr._catalog={{"healing_potion",57191,80},{"bandage",21991,60},{"bandage",14530,45},{"healing_potion",22829,55},{"food",27854,55}};
for(unsigned id=100;id<=104;++id)spellMgr.spells[id].Id=id;
spellMgr.spells[104].combat=false;
assert(mgr.Select(&p,"bandage")->id==21991); // owning future tier never grants skill
p.Add(&objects.items[21991]);assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Unavailable && p.requests==0);
p.skills[129]=225;assert(mgr.Select(&p,"bandage")->id==14530);
p.skills[129]=375;assert(mgr.Select(&p,"bandage")->id==21991);
p.history.gcd=true;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Busy);p.history.gcd=false;
p.history.cooldown=true;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Unavailable);p.history.cooldown=false;
spellMgr.spells[100].autoCast=false;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Unavailable && p.requests==0);spellMgr.spells[100].autoCast=true;
p.inventory[21991]->count=1;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Started && p.requests==1 && p.GetItemCount(21991)==0);
p.Add(&objects.items[27854]);p.combat=true;assert(mgr.TryUse(&p,r,U::Food)==AdventureSupplyMgr::UseResult::Unavailable);
assert(mgr.Select(&p,"healing_potion")->id==22829);p.Add(&objects.items[22829]);assert(mgr.TryUse(&p,r,U::HealingPotion)==AdventureSupplyMgr::UseResult::Started && p.GetItemCount(22829)==19);
// Native trainer gates, native skill caps and explicit secondary-only ranks.
mgr._secondaryRanks.clear();p.skills.clear();p.caps.clear();p.steps.clear();p.known.clear();
for(unsigned skill:{129u,185u,356u,794u})for(unsigned step=1;step<=6;++step){unsigned id=skill*10+step;spellMgr.ranks[id]={skill,step,step<=4?step*75:step==5?375u:450u};spellMgr.spells[id].Id=id;mgr._secondaryRanks.push_back({skill,id,step,step==6?65u:step==5?50u:1u,step==1?0u:(step-1)*75-25});}
mgr.LearnSecondaries(&p);
for(unsigned skill:{129u,185u,356u,794u})assert(p.GetPureSkillValue(skill)==375 && p.GetPureMaxSkillValue(skill)==375 && !p.HasSpell(skill*10+6));
auto known=p.known;mgr.LearnSecondaries(&p);assert(p.known==known);
// Real profession rank representation: trainer wrapper -> learned effect44.
Player fresh;AdventureSupplyMgr discovered;
SpellInfo wrapper;wrapper.Id=3279;wrapper.Effects={{0,0,36,3273}};spellMgr.spells[3279]=wrapper;
SpellInfo apprentice;apprentice.Id=3273;apprentice.Effects={{0,0,44,0,129,1}};spellMgr.spells[3273]=apprentice;
SpellInfo primary;primary.Id=2575;primary.Effects={{0,0,44,0,186,1}};spellMgr.spells[2575]=primary;
WorldDatabase.active=true;WorldDatabase.fixture.rows={{{{3279},{0},{0}}},{{{2575},{0},{0}}}};
discovered.LearnSecondaries(&fresh);
assert(fresh.GetPureSkillValue(129)==75 && fresh.GetPureMaxSkillValue(129)==75);
assert(discovered._secondaryRanks.size()==1 && !fresh.skills.count(186));
assert(!p.skills.count(186) && !p.skills.count(164)); // never invent primary professions
}
'''
with tempfile.TemporaryDirectory() as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['g++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production native-item delegation: usable bandage tiers, native cooldown/lockout gates, real inventory consumption, self targeting and level60 secondary caps passed.')