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

98 lines
6.6 KiB
Python

"""Execute the production recovery state machine with native-item adapters."""
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 <cassert>
#include <ctime>
#include <string>
#include <vector>
#include <unordered_map>
using uint32=unsigned;
#define TC_GAME_API
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_WARN(...) ((void)0)
constexpr int POWER_MANA=0,POWER_RAGE=1,CURRENT_CHANNELED_SPELL=0,CURRENT_GENERIC_SPELL=1,UNIT_STAND_STATE_STAND=0,UNIT_STAND_STATE_SIT=1;
struct Guid{unsigned GetCounter(){return 26;}};
struct Spell{struct Info{unsigned Id=1;}info;Info* GetSpellInfo(){return &info;}};
struct Player{
unsigned hp=100,maxhp=100,mana=100,maxmana=100,type=POWER_RAGE;bool alive=true,combat=false,casting=false,aura=false,bandageAllowed=true,busy=false,foodAllowed=true;
int posture=0,stops=0;Spell spell;std::unordered_map<unsigned,unsigned> items;
Guid GetGUID(){return {};}bool IsInCombat(){return combat;}bool IsAlive(){return alive;}bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsFlying(){return false;}bool IsCharmed(){return false;}bool IsFalling(){return false;}
unsigned GetHealth(){return hp;}unsigned GetMaxHealth(){return maxhp;}unsigned GetPower(int){return mana;}unsigned GetMaxPower(int){return maxmana;}unsigned GetPowerType(){return type;}
unsigned GetItemCount(unsigned entry){return items[entry];}void StopMoving(){++stops;}void SetStandState(int s){posture=s;}
Spell*GetCurrentSpell(int slot){return casting && slot==0?&spell:nullptr;}
bool HasAura(unsigned id,Guid){return aura && spell.info.Id==id;}
void InterruptNonMeleeSpells(bool,unsigned id){if(spell.info.Id==id)casting=false;}
void RemoveAurasDueToSpell(unsigned id,Guid){if(spell.info.Id==id)aura=false;}
};
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
unsigned today=20261027;unsigned Day(time_t){return today;}
struct Behavior{int interrupts=0;void InterruptForCombat(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
'''+header+r'''
std::vector<AdventureSupplyPolicy::Use> uses;int stocks=0;unsigned savedDay=0;
void AdventureSupplyMgr::Online(Player*p){auto&r=_records[26];r.Loaded=true;r.LastDay=savedDay;}
bool AdventureSupplyMgr::Stock(Player*,Record&r,unsigned date){++stocks;r.LastDay=date;r.AfterCombat=false;savedDay=date;return true;}
AdventureSupplyMgr::UseResult AdventureSupplyMgr::TryUse(Player*p,Record&r,Use use){
uses.push_back(use);if(p->busy)return UseResult::Busy;
if((use==Use::Bandage && !p->bandageAllowed)||(use==Use::Food && !p->foodAllowed))return UseResult::Unavailable;
if(!p->combat){r.Active=use;r.Spell=unsigned(use);p->spell.info.Id=r.Spell;p->aura=true;r.Requested=GameTime::now;}
return UseResult::Started;
}
'''+function('void AdventureSupplyMgr::Cancel(')+'\n'+function('bool AdventureSupplyMgr::Update(')+r'''
int main(){
using U=AdventureSupplyPolicy::Use;using namespace AdventureSupplyPolicy;
assert(!MissingTenPercent(91,100));assert(MissingTenPercent(90,100));assert(!MissingTenPercent(1,0));
assert(!MissingTenPercent(4725,5249));assert(MissingTenPercent(4724,5249));
assert(TopUp(19,20)==1 && TopUp(20,20)==0 && TopUp(21,20)==0);
AdventureSupplyMgr mgr;Player p;auto tick=[&]{GameTime::now+=3;return mgr.Update(&p,500,false);};
assert(!tick() && stocks==1);assert(!tick() && stocks==1);
p.hp=91;assert(!tick() && uses.empty());
// At the exact threshold, bandage has priority over food.
p.hp=90;assert(tick() && uses.back()==U::Bandage);
p.aura=false;p.hp=95;assert(!tick() && uses.size()==1); // no food below10% missing
// New recovery: bandage finishes but health deficit still10% or more.
p.hp=40;assert(tick() && uses.back()==U::Bandage);
p.aura=false;p.hp=75;assert(tick() && uses.back()==U::Food);
auto count=uses.size();p.hp=95;assert(tick() && uses.size()==count); // keep current meal to full
p.hp=100;assert(!tick() && !p.aura && p.posture==UNIT_STAND_STATE_STAND);
// Missing First Aid / native bandage lockout: immediate food fallback.
p.hp=80;p.bandageAllowed=false;uses.clear();assert(tick());assert((uses==std::vector<U>{U::Bandage,U::Food}));
p.hp=100;assert(!tick());
// A busy global cooldown must not count as a failed bandage attempt.
p.bandageAllowed=true;p.hp=80;p.busy=true;uses.clear();assert(tick() && uses.back()==U::Bandage);
assert(!mgr._records[26].BandageTried);p.busy=false;assert(tick() && uses.back()==U::Bandage);
// Incoming combat interrupts only owned recovery; combat never eats/bandages.
p.combat=true;uses.clear();assert(!tick() && uses.back()==U::HealingPotion && !p.aura);
p.hp=100;p.type=POWER_MANA;p.mana=89;uses.clear();assert(!tick() && uses.back()==U::ManaPotion);
// Exactly one top-up after the actual combat interval, not per enemy/tick.
auto previous=stocks;p.combat=false;assert(tick() && stocks==previous+1 && uses.back()==U::Drink);
assert(tick() && stocks==previous+1);p.mana=100;assert(!tick());
// Warriors never drink; mana-only recovery starts at10% missing as well.
p.type=POWER_RAGE;p.mana=0;uses.clear();assert(!tick() && uses.empty());
p.type=POWER_MANA;p.mana=91;assert(!tick());p.mana=90;assert(tick() && uses.back()==U::Drink);
p.mana=100;assert(!tick());
// Persisted day blocks logout/login farming; next day allows a single top-up.
mgr._records.clear();uses.clear();previous=stocks;assert(!tick() && stocks==previous);
++today;assert(!tick() && stocks==previous+1);assert(!tick() && stocks==previous+1);
// Dead or combat-reaction-controlled bots never start a meal.
p.hp=40;p.alive=false;uses.clear();assert(!tick() && uses.empty());p.alive=true;
GameTime::now+=3;assert(!mgr.Update(&p,500,true) && uses.empty());
}
'''
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 supplies: exact10% boundary, bandage-before-food, native lockout fallback, recovery completion, combat potions, mana-only drinking and daily/post-combat top-ups passed.')