Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
138 lines
8.4 KiB
Diff
138 lines
8.4 KiB
Diff
--- /dev/null
|
|
+++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_InventoryHitZoneCompat.c
|
|
@@ -0,0 +1,49 @@
|
|
+// Vanilla damage-state invokers supply one argument. Preserve the native UI
|
|
+// update while supplying its optional attachment-refresh argument explicitly.
|
|
+modded class SCR_InventoryHitZonePointContainerUI
|
|
+{
|
|
+ protected ERR_GameMode m_ERRHitZoneMode;
|
|
+ protected ref array<SCR_HitZone> m_ERRHitZoneSources = {};
|
|
+
|
|
+ override void InitializeHitZoneUI(BaseInventoryStorageComponent storage, SCR_InventoryMenuUI menuManager, int hitZoneId, IEntity player, bool updateAttachmentStorage = true)
|
|
+ {
|
|
+ ERR_ClearHitZoneCallbacks();
|
|
+ super.InitializeHitZoneUI(storage, menuManager, hitZoneId, player, updateAttachmentStorage);
|
|
+ m_ERRHitZoneMode = ERR_GameMode.Get();
|
|
+ if (!m_ERRHitZoneMode) return;
|
|
+ ERR_BindHitZoneCallback(m_pBloodHitZone);
|
|
+ foreach (HitZone hz : m_aGroupHitZones)
|
|
+ ERR_BindHitZoneCallback(SCR_CharacterHitZone.Cast(hz));
|
|
+ }
|
|
+ protected void ERR_BindHitZoneCallback(SCR_HitZone hz)
|
|
+ {
|
|
+ if (!hz || m_ERRHitZoneSources.Contains(hz)) return;
|
|
+ ScriptInvoker changed = hz.GetOnDamageStateChanged(false);
|
|
+ if (!changed) return;
|
|
+ changed.Remove(UpdateHitZoneState);
|
|
+ changed.Insert(ERR_HitZoneStateChanged);
|
|
+ m_ERRHitZoneSources.Insert(hz);
|
|
+ }
|
|
+ protected void ERR_HitZoneStateChanged(SCR_HitZone hz)
|
|
+ {
|
|
+ if (!m_ERRHitZoneMode || m_ERRHitZoneMode != ERR_GameMode.Get() || !m_ERRHitZoneSources.Contains(hz)) return;
|
|
+ UpdateHitZoneState(hz, true);
|
|
+ }
|
|
+ protected void ERR_ClearHitZoneCallbacks()
|
|
+ {
|
|
+ m_ERRHitZoneMode = null;
|
|
+ foreach (SCR_HitZone hz : m_ERRHitZoneSources)
|
|
+ {
|
|
+ if (!hz) continue;
|
|
+ ScriptInvoker changed = hz.GetOnDamageStateChanged(false);
|
|
+ if (changed) changed.Remove(ERR_HitZoneStateChanged);
|
|
+ }
|
|
+ m_ERRHitZoneSources.Clear();
|
|
+ }
|
|
+ override void HandlerDeattached(Widget w)
|
|
+ {
|
|
+ ERR_ClearHitZoneCallbacks();
|
|
+ super.HandlerDeattached(w);
|
|
+ }
|
|
+ void ~SCR_InventoryHitZonePointContainerUI() { ERR_ClearHitZoneCallbacks(); }
|
|
+}
|
|
--- /dev/null
|
|
+++ b/tests/test_inventory_hitzone_compat.py
|
|
@@ -0,0 +1,82 @@
|
|
+"""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')
|