Files
EscapistsReintroduced/tests/test_inventory_hitzone_compat.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

83 lines
6.3 KiB
Python

"""Actual successor adapter methods; native UI/invoker boundaries are doubled.
The invoker models the SDK's one supplied argument and rejects a two-argument
subscription. It does not claim rendered medical UI or Enforce execution.
"""
import os
from pathlib import Path
from policy_harness import ROOT, body, run
source=Path(os.environ.get('ERR_HITZONE_COMPAT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_InventoryHitZoneCompat.c'))
signatures=['override void InitializeHitZoneUI(', 'protected void ERR_BindHitZoneCallback(', 'protected void ERR_HitZoneStateChanged(', 'protected void ERR_ClearHitZoneCallbacks(', 'override void HandlerDeattached(', 'void ~SCR_InventoryHitZonePointContainerUI(']
def cpp(s):
for a,b in [('override ',''),('protected ',''),('super.','Native::'),('ERR_GameMode.Get()','Mode::Get()'),('SCR_CharacterHitZone.Cast(hz)','Character::Cast(hz)'),('BaseInventoryStorageComponent storage','int storage'),('SCR_InventoryMenuUI menuManager','int menuManager'),('IEntity player','Player* player'),('Widget w','int w'),('SCR_HitZone hz','Zone* hz'),('HitZone hz','Zone* hz'),('ScriptInvoker changed','Invoker* changed'),('hz.','hz->'),('changed.','changed->'),('foreach (','for ('),('null','nullptr'),('void ~SCR_InventoryHitZonePointContainerUI()','void Destroy()')]:s=s.replace(a,b)
s=s.replace('changed->Remove(UpdateHitZoneState)','changed->Remove(this, 1)').replace('changed->Remove(ERR_HitZoneStateChanged)','changed->Remove(this, 2)')
s=s.replace('changed->Insert(ERR_HitZoneStateChanged)','changed->Insert(this, 2, [this](Zone* z){ ERR_HitZoneStateChanged(z); })')
return s
methods='\n'.join(cpp(body(source,sig)) for sig in signatures)
if os.environ.get('ERR_HITZONE_COMPAT_BASELINE')=='1':
methods='void Destroy(){}' # Installed native initializer/subscription without the new adapter.
run(r'''
#include <cassert>
#include <algorithm>
#include <functional>
#include <vector>
struct Zone;
struct Invoker {
struct Entry{void* owner;int kind;std::function<void(Zone*)> callback;};
std::vector<Entry> entries;int errors=0;
void Remove(void* o,int k){entries.erase(std::remove_if(entries.begin(),entries.end(),[&](Entry e){return e.owner==o&&e.kind==k;}),entries.end());}
void Insert(void* o,int k,std::function<void(Zone*)> cb={}){Remove(o,k);entries.push_back({o,k,cb});}
void Invoke(Zone*z){auto snapshot=entries;for(auto&e:snapshot){if(e.kind==1)errors++;else e.callback(z);}}
int Count(void*o){return std::count_if(entries.begin(),entries.end(),[&](Entry e){return e.owner==o;});}
};
struct Zone{Invoker inv;bool character=true,hasInvoker=true;Invoker*GetOnDamageStateChanged(bool create=true){assert(!create);return hasInvoker?&inv:nullptr;}};
struct Character{static Zone*Cast(Zone*z){return z&&z->character?z:nullptr;}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}void Insert(T t){this->push_back(t);}void Clear(){this->clear();}};
struct Mode{static inline Mode*current=nullptr;static Mode*Get(){return current;}};
struct Player{Zone*blood;Array<Zone*>zones;};
struct Native {
Zone*m_pBloodHitZone=nullptr;Array<Zone*>m_aGroupHitZones;
int init=0,detach=0,updates=0;bool lastRefresh=false;Zone*last=nullptr;Player*lastPlayer=nullptr;
void UpdateHitZoneState(Zone*z,bool refresh=true){updates++;lastRefresh=refresh;last=z;}
void InitializeHitZoneUI(int storage,int menuManager,int hitZoneId,Player*player,bool refresh=true){
assert(storage==11&&menuManager==22&&hitZoneId==3);init++;lastPlayer=player;
m_pBloodHitZone=player->blood;m_aGroupHitZones=player->zones;
if(m_pBloodHitZone){m_pBloodHitZone->inv.Insert(this,1);UpdateHitZoneState(m_pBloodHitZone,refresh);}
for(auto*z:m_aGroupHitZones)if(Character::Cast(z)){z->inv.Insert(this,1);UpdateHitZoneState(z,refresh);}
}
void HandlerDeattached(int w){assert(w==7);detach++;}
};
struct Adapter:Native {
Mode*m_ERRHitZoneMode=nullptr;Array<Zone*>m_ERRHitZoneSources;
METHODS
};
int main(){
Mode mode,next;Mode::current=&mode;Zone blood,limb,ignored;blood.character=false;ignored.character=false;
Player player{&blood,{&blood,&limb,&limb,&ignored,nullptr}};
Adapter a;a.InitializeHitZoneUI(11,22,3,&player,false);
assert(a.init==1&&a.lastPlayer==&player&&!a.lastRefresh); // Direct native initialization keeps false.
int before=a.updates;blood.inv.Invoke(&blood);limb.inv.Invoke(&limb);
assert(blood.inv.errors==0&&limb.inv.errors==0); // Baseline fails here after compiling.
assert(a.updates==before+2&&a.lastRefresh&&a.last==&limb);
assert(blood.inv.Count(&a)==1&&limb.inv.Count(&a)==1&&ignored.inv.Count(&a)==0);
a.InitializeHitZoneUI(11,22,3,&player,true);before=a.updates;blood.inv.Invoke(&blood);assert(a.updates==before+1&&blood.inv.Count(&a)==1);
// Each widget owns only its callbacks, even on the shared blood invoker.
Adapter other;other.InitializeHitZoneUI(11,22,3,&player,false);a.HandlerDeattached(7);before=other.updates;
blood.inv.Invoke(&blood);assert(a.detach==1&&blood.inv.Count(&a)==0&&other.updates==before+1);
// Current mode loss/replacement makes retained callbacks inert; cleanup still works.
before=other.updates;Mode::current=nullptr;blood.inv.Invoke(&blood);Mode::current=&next;limb.inv.Invoke(&limb);assert(other.updates==before);
auto late=limb.inv.entries.back().callback;other.Destroy();assert(blood.inv.Count(&other)==0&&limb.inv.Count(&other)==0);
Mode::current=&mode;late(&limb);assert(other.updates==before);
// Reinitialize in a non-successor mode: remove old wrappers, preserve native setup.
Mode::current=&mode;Adapter reuse;reuse.InitializeHitZoneUI(11,22,3,&player,false);
Zone b2,l2;b2.character=false;Player p2{&b2,{&l2}};Mode::current=nullptr;
reuse.InitializeHitZoneUI(11,22,3,&p2,false);assert(reuse.init==2&&!reuse.lastRefresh);
assert(blood.inv.Count(&reuse)==0&&limb.inv.Count(&reuse)==0);
b2.inv.Invoke(&b2);assert(b2.inv.errors==1); // Foreign native behavior untouched.
// Empty/invalid group entries are not adopted; repeated cleanup is harmless.
Mode::current=&mode;Adapter empty;Player missing{nullptr,{nullptr,&ignored}};empty.InitializeHitZoneUI(11,22,3,&missing,false);
empty.Destroy();empty.Destroy();assert(ignored.inv.Count(&empty)==0);
}
'''.replace('METHODS',methods))
print('PASS: actual one-argument adapter, explicit refresh, native initialization flag, duplicate/blood ownership, rebind/detach/destruction and foreign-mode transparency')