Files
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

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')