98 lines
6.6 KiB
Python
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.')
|