--- /dev/null +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeInventoryHitZoneProbe.c @@ -0,0 +1,166 @@ +#ifdef WORKBENCH +// Read-only fixture access to native UI identities. No replacement widgets. +modded class SCR_InventoryMenuUI +{ + bool ERR_HitZoneProbeReady(IEntity actor) + { + return ERR_NativeInventoryHitZoneProbe.Enabled() && m_Player == actor && m_pStorageListUI + && m_pStorageListUI.GetStorage() && m_PlayerRenderAttributes && m_wAttachmentPointsContainer; + } + bool ERR_HitZoneProbePoints(out array points) + { + points.Clear(); + if (!ERR_NativeInventoryHitZoneProbe.Enabled() || m_aHitZonePoints.Count() != 7) return false; + foreach (SCR_InventoryHitZonePointContainerUI point : m_aHitZonePoints) points.Insert(point); + return true; + } +} + +modded class SCR_InventoryHitZonePointContainerUI +{ + protected SCR_HitZone m_ERRProbeBlood; + protected int m_ERRProbeUpdates; + protected bool m_ERRProbeFalseRefresh; + protected override void UpdateHitZoneState(SCR_HitZone hz, bool updateAttachmentStorage = true) + { + super.UpdateHitZoneState(hz, updateAttachmentStorage); + if (m_ERRProbeBlood && hz == m_ERRProbeBlood) + { + m_ERRProbeUpdates++; + if (!updateAttachmentStorage) m_ERRProbeFalseRefresh = true; + } + } + bool ERR_HitZoneProbeWatch(ERR_GameMode mode, SCR_HitZone blood) + { + if (!ERR_NativeInventoryHitZoneProbe.Enabled() || !mode || mode != m_ERRHitZoneMode + || !blood || !m_ERRHitZoneSources.Contains(blood)) return false; + m_ERRProbeBlood = blood; m_ERRProbeUpdates = 0; m_ERRProbeFalseRefresh = false; + return true; + } + bool ERR_HitZoneProbeOnce() { return m_ERRProbeUpdates == 1 && !m_ERRProbeFalseRefresh; } + bool ERR_HitZoneProbeDetached() { return !m_ERRHitZoneMode && m_ERRHitZoneSources.IsEmpty(); } + void ERR_HitZoneProbeStop() { m_ERRProbeBlood = null; } +} + +// Emits one real one-argument damage-state notification without changing health. +// It proves callback/native UI integration, not a natural injury or manual UI use. +class ERR_NativeInventoryHitZoneProbe +{ + protected ERR_GameMode m_Mode; + protected SCR_PlayerController m_Controller; + protected IEntity m_Actor; + protected SCR_InventoryStorageManagerComponent m_Inventory; + protected SCR_CharacterBloodHitZone m_Blood; + protected SCR_InventoryMenuUI m_Menu; + protected ref array m_Points = {}; + protected int m_Deadline; + protected int m_Stage; + protected int m_Result; + protected bool m_Stopped; + protected bool m_CloseRequested; + protected float m_Health; + static bool Enabled() + { + string enabled, outcome; + return Replication.IsServer() && System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" + && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "inventory-hitzone"; + } + void ERR_NativeInventoryHitZoneProbe(ERR_GameMode mode, IEntity actor) + { + m_Mode = mode; m_Actor = actor; m_Deadline = System.GetTickCount() + 30000; + m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController()); + } + protected bool Check(bool condition, string detail) + { + if (condition) { Print("ERR SMOKE PASS: inventory hitzone " + detail); return true; } + Print("ERR SMOKE FAIL: inventory hitzone " + detail, LogLevel.ERROR); + m_Result = -1; Stop(); return false; + } + protected bool Context() + { + return Enabled() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() + && m_Controller && m_Controller == GetGame().GetPlayerController() && m_Controller.GetControlledEntity() == m_Actor + && m_Mode.Players() && m_Mode.Players().CurrentActor(m_Actor) && ERR_Actors.CanFight(m_Actor); + } + protected bool OwnsMenu() + { + return m_Menu && GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.Inventory20Menu) == m_Menu; + } + protected bool UnchangedHealth() { return m_Blood && m_Blood.GetHealth() == m_Health; } + int Tick(float now) + { + if (m_Result != 0) return m_Result; + if (m_Stopped) return -1; + if (!Context()) { Check(false, "original conscious local round body changed"); return -1; } + if (System.GetTickCount() >= m_Deadline) { Check(false, "fixed 30-second UI/callback deadline"); return -1; } + if (m_Stage == 0) + { + if (!Check(!GetGame().GetMenuManager().GetTopMenu(), "no existing menu adopted")) return -1; + m_Inventory = SCR_InventoryStorageManagerComponent.Cast(m_Actor.FindComponent(SCR_InventoryStorageManagerComponent)); + SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_Actor.FindComponent(SCR_CharacterDamageManagerComponent)); + if (damage) m_Blood = damage.GetBloodHitZone(); + if (!Check(m_Inventory && m_Blood, "original native inventory and blood event source present")) return -1; + m_Health = m_Blood.GetHealth(); + m_Inventory.OpenInventory(); + m_Menu = SCR_InventoryMenuUI.Cast(GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.Inventory20Menu)); + if (!Check(OwnsMenu(), "ordinary native inventory opens without input automation")) return -1; + m_Stage = 1; return 0; + } + if (m_Stage == 1) + { + if (!Check(OwnsMenu() && UnchangedHealth(), "owned inventory and original blood health retained")) return -1; + if (!m_Menu.ERR_HitZoneProbeReady(m_Actor)) return 0; + m_Menu.InspectItem(null); // Native character inspection creates its normal seven containers over several frames. + m_Stage = 2; return 0; + } + if (m_Stage == 2) + { + if (!Check(OwnsMenu() && UnchangedHealth(), "native inspection retains original menu and health")) return -1; + if (!m_Menu.ERR_HitZoneProbePoints(m_Points)) return 0; + array seen = {}; + foreach (SCR_InventoryHitZonePointContainerUI point : m_Points) + { + if (!Check(point && !seen.Contains(point) && point.ERR_HitZoneProbeWatch(m_Mode, m_Blood), "unique native point retains original blood subscription")) return -1; + seen.Insert(point); + } + if (!Check(UnchangedHealth() && m_Blood.GetOnDamageStateChanged(false), "original blood event and health retained before notification")) return -1; + m_Blood.GetOnDamageStateChanged(false).Invoke(m_Blood); + foreach (SCR_InventoryHitZonePointContainerUI updated : m_Points) + if (!Check(updated.ERR_HitZoneProbeOnce(), "one-argument event reached native updater once with explicit true")) return -1; + if (!Check(UnchangedHealth() && OwnsMenu(), "notification changes no blood health or menu ownership")) return -1; + bool closeAccepted = GetGame().GetMenuManager().CloseMenu(m_Menu); + m_CloseRequested = closeAccepted; + if (!Check(closeAccepted, "exact owned inventory close request accepted")) return -1; + m_Stage = 3; return 0; + } + if (m_Stage == 3) + { + MenuBase current = GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.Inventory20Menu); + if (current == m_Menu && m_Menu) return 0; + if (!Check(!current, "native inventory closes without adopting a replacement")) return -1; + foreach (SCR_InventoryHitZonePointContainerUI detached : m_Points) + if (!Check(detached && detached.ERR_HitZoneProbeDetached(), "native detach cleared original point subscriptions")) return -1; + if (!Check(UnchangedHealth() && m_Blood.GetOnDamageStateChanged(false), "original blood event and health retained before notification")) return -1; + m_Blood.GetOnDamageStateChanged(false).Invoke(m_Blood); + foreach (SCR_InventoryHitZonePointContainerUI oldPoint : m_Points) + if (!Check(oldPoint.ERR_HitZoneProbeOnce(), "closed original point receives no late native update")) return -1; + if (!Check(UnchangedHealth(), "blood health unchanged through complete callback proof")) return -1; + m_Menu = null; m_Result = 1; Stop(); return 1; + } + Check(false, "unexpected fixture stage"); return -1; + } + void Stop() + { + if (m_Stopped) return; + m_Stopped = true; + foreach (SCR_InventoryHitZonePointContainerUI point : m_Points) if (point) point.ERR_HitZoneProbeStop(); + m_Points.Clear(); + if (GetGame() && GetGame().GetMenuManager() && !m_CloseRequested && OwnsMenu()) + { + m_CloseRequested = true; + GetGame().GetMenuManager().CloseMenu(m_Menu); + } + m_Menu = null; m_Blood = null; m_Inventory = null; + } +} +#endif --- a/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c @@ -20,6 +20,7 @@ protected ref ERR_NativeLocalAdmissionProbe m_ERRLocalAdmissionProbe; protected ref ERR_NativePoweredFlightProbe m_ERRPoweredFlightProbe; protected ref ERR_NativePrisonBreachProbe m_ERRPrisonBreachProbe; + protected ref ERR_NativeInventoryHitZoneProbe m_ERRInventoryHitZoneProbe; protected ref ERR_NativeItemStationTeardownProbe m_ERRItemStationTeardownProbe; protected ref ERR_NativeObjectiveLossProbe m_ERRObjectiveLossProbe; protected ref ERR_NativeMedicalProbe m_ERRBaseMedicalProbe; @@ -100,6 +101,7 @@ if (m_ERRLocalAdmissionProbe) m_ERRLocalAdmissionProbe.Stop(); if (m_ERRPoweredFlightProbe) m_ERRPoweredFlightProbe.Stop(); if (m_ERRPrisonBreachProbe) m_ERRPrisonBreachProbe.Stop(); + if (m_ERRInventoryHitZoneProbe) m_ERRInventoryHitZoneProbe.Stop(); if (m_ERRItemStationTeardownProbe) m_ERRItemStationTeardownProbe.Stop(); if (m_ERRCasualtyServiceProbe) m_ERRCasualtyServiceProbe.Stop(); if (m_ERRObjectiveLossProbe) m_ERRObjectiveLossProbe.Stop(); @@ -172,6 +174,13 @@ { if (!ERR_MissionRules.Terminal(current)) return; GetGame().GetCallqueue().Remove(ERR_BaseTick); + if (m_ERRExtractionCase == "inventory-hitzone") + { + if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 2102, "native inventory callback fixture finished before explicit reset")) return; + ERR_StopHealthProbes(); + Print("ERR SMOKE COMPLETE: real one-argument blood event reaches native inventory updater and detached points stay silent; synthetic notification, no injury/manual UI/multiplayer proof"); + return; + } if (m_ERRExtractionCase == "station-teardown") { if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 2002, "native station teardown fixture finished before explicit reset")) return; @@ -336,6 +345,13 @@ Print("ERR SMOKE COMPLETE: expected startup-failure"); return; } + if (m_ERRBaseStep == 2100) + { + int result = m_ERRInventoryHitZoneProbe.Tick(now); + if (result < 0) { ERR_BaseCheck(false, "native inventory hitzone callback fixture"); return; } + if (result == 1) { m_ERRBaseStep = 2102; Transition(ERR_Phase.DEFEAT); } + return; + } if (m_ERRBaseStep == 2000) { int result = m_ERRItemStationTeardownProbe.Tick(now); @@ -509,6 +525,12 @@ { if (!ERR_NativeDiscoveryMapProbe.Run()) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; } m_ERRDiscoveryMapChecked = true; + } + if (m_ERRExtractionCase == "inventory-hitzone") + { + m_ERRInventoryHitZoneProbe = new ERR_NativeInventoryHitZoneProbe(this, m_ERRBasePlayer); + m_ERRBaseStep = 2100; + return; } if (m_ERRExtractionCase == "station-teardown") { --- a/tools/headless_workbench.py +++ b/tools/headless_workbench.py @@ -14,7 +14,7 @@ p.add_argument('--external-addons', type=Path, help='Installed RHS/WCS/BMP source root; expose only audited closure in a separate Chat profile') p.add_argument('--compat', type=Path, required=True, help='Disposable cloned Proton compatdata directory outside Steam') p.add_argument('--profile', type=Path, required=True, help='Separate disposable successor Workbench profile') -p.add_argument('--smoke', choices=['crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory') +p.add_argument('--smoke', choices=['crash-start', 'crash-geometry', 'normal-escape-route', 'escape-route', 'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'hq-local-handoff', 'infantry-transfer', 'hq-body', 'convoy', 'supply-convoys', 'supply-travel', 'mounted-ammo', 'alarm-hearing', 'hq-memory', 'hq-awareness', 'aircraft-field', 'aircraft-route', 'native-end-active', 'native-end-preparing', 'aircraft-control', 'aircraft-cancel', 'aircraft-translate', 'map-ownership', 'mortar-refresh', 'mortar-salvo', 'mortar-courier', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'mortar-expiry', 'mortar-transfer', 'mortar-fire', 'patrol-hq', 'victory', 'lost-helicopter', 'player-death', 'radio-network', 'hq-response', 'mortar-inventory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'], default='victory') p.add_argument('--trace-random-radius', action='store_true', help='Workbench-only read-only zero-radius caller traces; one normal Chat round, capped at sixteen records') p.add_argument('--maintenance-refuel', action='store_true', help='Opt-in normal-route check: original held Southern fuel can transfers finite fuel through its native UAZ action; one arranged tank depletion, no stock creation') p.add_argument('--mounted-swap-eligibility', action='store_true', help='Read-only mounted-ammo variant: one exact finite cargo/mounted magazine CanSwap call; no transfer or radio proof') @@ -36,6 +36,7 @@ p.add_argument('--timeout', type=int, default=360) p.add_argument('--project', choices=['core', 'chat'], default='chat', help='Diagnostic fixture family (both load the combined mod)') a = p.parse_args() +if a.smoke == 'inventory-hitzone' and (a.project != 'core' or a.rounds != 1 or a.timeout < 240): p.error('inventory-hitzone requires --project core --rounds 1 and timeout at least 240 seconds') if a.smoke == 'crash-start' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 240): p.error('crash-start requires Chat, one round and timeout at least 240 seconds') if a.smoke == 'crash-geometry' and (a.project != 'chat' or a.rounds != 1 or a.timeout < 180): p.error('crash-geometry requires Chat, one round and timeout at least180 seconds') if a.trace_random_radius and (a.smoke != 'normal-escape-route' or a.project != 'chat' or a.rounds != 1 or a.timeout < 420): p.error('--trace-random-radius requires Chat normal-escape-route, one round and timeout at least 420 seconds') @@ -153,10 +154,10 @@ if a.recon_takeover_only: command += ['-errReconTakeoverOnly', '1'] if a.body_recovery_only: command += ['-errBodyRecoveryOnly', '1'] if a.project == 'core': - if a.smoke not in {'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'native-end-active', 'native-end-preparing', 'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture') + if a.smoke not in {'optional-site-skip', 'infantry-combat', 'medical-combat', 'hunter-faction', 'unconscious-camera', 'objective-map', 'town-garrisons', 'site-garrisons', 'radio-interaction', 'enemy-loadout', 'aggression', 'air-logistics', 'air-assault', 'air-recon', 'airfield-markers', 'opening-tools', 'mortar-station', 'generated-sites', 'native-end-active', 'native-end-preparing', 'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'victory', 'startup-failure', 'site-survey', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'}: p.error('Unsupported Core-only fixture') command[command.index('-plugin=ERR_ValidateWorldPlugin')] = '-plugin=ERR_ValidateBaseWorldPlugin' command[command.index('-errSmoke')] = '-errBaseSmoke' -if a.smoke in {'station-teardown', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core') +if a.smoke in {'station-teardown', 'inventory-hitzone', 'prison-breach', 'powered-flight', 'powered-translate', 'local-admission', 'cache-loss', 'extraction-capacity', 'early-helicopter-loss', 'extraction-startup-failure', 'extraction-boundary-failure', 'transport-loss', 'transport-survey', 'lifecycle', 'objective-ui', 'recovery', 'prisoner-kit', 'medical', 'casualty-load', 'casualty-direct', 'casualty-bench', 'casualty-remove', 'environment', 'environment-restart', 'patrols', 'radio-document-loss', 'radio-document-loss-partial', 'guard-cancel', 'casualty-service'} and a.project != 'core': p.error('This fixture requires --project core') if a.smoke in {'startup-failure', 'site-survey', 'transport-survey', 'extraction-startup-failure', 'extraction-boundary-failure'} and (a.project != 'core' or a.rounds != 1): p.error('This diagnostic requires --project core --rounds 1') env = os.environ.copy() env.update(STEAM_COMPAT_DATA_PATH=str(compat), STEAM_COMPAT_CLIENT_INSTALL_PATH=str(steam), WINEPREFIX=str(compat/'pfx')) --- /dev/null +++ b/tests/test_native_inventory_hitzone.py @@ -0,0 +1,87 @@ +"""Actual probe state machine/counters; menu, actor and invoker boundaries doubled.""" +import ast,os,re +from pathlib import Path +from types import SimpleNamespace +from policy_harness import ROOT,body,run +BASE=Path(os.environ.get('ERR_INVENTORY_PROBE_ROOT',ROOT)) +p=Path(os.environ.get('ERR_NATIVE_INVENTORY_SOURCE',BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeInventoryHitZoneProbe.c')) +s=p.read_text() +assert s.startswith('#ifdef WORKBENCH') and s.rstrip().endswith('#endif') +for forbidden in ['SetHealth(', 'FullHeal(', 'ForceUnconsciousness(', 'SetOrigin(', 'CreateWidgets(', 'InputManager', 'ERR_ClearHitZoneCallbacks(']: assert forbidden not in s +assert 'super.UpdateHitZoneState(hz, updateAttachmentStorage);' in s +assert 'm_Menu.InspectItem(null);' in s and 'm_Deadline = System.GetTickCount() + 30000' in s +assert 'out array points' in s + +def cpp(s): + for a,b in [('protected override ',''),('protected ',''),('override ',''),('super.UpdateHitZoneState','Native::UpdateHitZoneState'),('ERR_NativeInventoryHitZoneProbe','Probe'),('SCR_InventoryHitZonePointContainerUI','Point'),('SCR_CharacterDamageManagerComponent','Damage'),('SCR_InventoryStorageManagerComponent','Inventory'),('SCR_CharacterBloodHitZone','Zone'),('SCR_HitZone','Zone'),('ERR_GameMode','Mode'),('SCR_PlayerController','Controller'),('SCR_InventoryMenuUI','Menu'),('MenuBase','Menu'),('IEntity','Actor*'),('string ','string '),('null','nullptr'),('foreach (','for ('),('ref ',''),('array seen = {};','Array seen;')]:s=s.replace(a,b) + for name in ['mode','damage','point','updated','detached','oldPoint']: + s=re.sub(r'\b'+name+r'\.',name+'->',s) + for name in ['m_Mode','m_Controller','m_Actor','m_Inventory','m_Blood','m_Menu']:s=s.replace(name+'.',name+'->') + for cls in ['Probe','Mode','Controller','Damage','Inventory','Menu','System','Replication','ERR_Actors','ChimeraMenuPreset']:s=s.replace(cls+'.',cls+'::') + for cls,var in [('Mode','mode'),('Zone','hz'),('Zone','blood'),('Point','point'),('Point','updated'),('Point','detached'),('Point','oldPoint'),('Menu','current'),('Damage','damage')]:s=s.replace(cls+' '+var,cls+'* '+var) + s=s.replace('GetGame().','GetGame()->').replace('GetMenuManager().','GetMenuManager()->').replace('Players().','Players()->').replace('GetOnDamageStateChanged(false).','GetOnDamageStateChanged(false)->') + s=s.replace('FindComponent(Inventory)','FindComponent(1)').replace('FindComponent(Damage)','FindComponent(2)') + s=s.replace('Print("ERR SMOKE FAIL: inventory hitzone " + detail, LogLevel.ERROR);','Print("ERR SMOKE FAIL: inventory hitzone " + detail);') + return s +point='\n'.join(cpp(body(p,sig)) for sig in ['protected override void UpdateHitZoneState(','bool ERR_HitZoneProbeWatch(','bool ERR_HitZoneProbeOnce(','bool ERR_HitZoneProbeDetached(','void ERR_HitZoneProbeStop(']) +probe='\n'.join(cpp(body(p,sig)) for sig in ['void ERR_NativeInventoryHitZoneProbe(','protected bool Check(','protected bool Context(','protected bool OwnsMenu(','protected bool UnchangedHealth(','int Tick(','void Stop(']) +probe=probe.replace('void Probe(', 'Probe(') +run(r''' +#include +#include +#include +#include +#include +using string=std::string;templatestruct Array:std::vector{using std::vector::vector;void Clear(){this->clear();}void Insert(T x){this->push_back(x);}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}bool IsEmpty(){return this->empty();}}; +struct Actor;struct Mode;struct Point;struct Menu;struct Probe{static bool Enabled(){return enabled;}static inline bool enabled=true;}; +namespace System{int now=0;int GetTickCount(){return now;}} +namespace ChimeraMenuPreset{int Inventory20Menu=1;} +void Print(string){} +struct Event{std::functionfire;int calls=0;void Invoke(void*){calls++;if(fire)fire();}}; +struct Zone{float health=100;Event event;float GetHealth(){return health;}Event*GetOnDamageStateChanged(bool create){assert(!create);return &event;}}; +struct Damage{Zone*blood;Zone*GetBloodHitZone(){return blood;}static Damage*Cast(void*p){return static_cast(p);}}; +struct Inventory{int opens=0;std::functionopening;void OpenInventory(){opens++;if(opening)opening();}static Inventory*Cast(void*p){return static_cast(p);}}; +struct Actor{Inventory*inventory;Damage*damage;bool fit=true;void*FindComponent(int n){return n==1?static_cast(inventory):static_cast(damage);}}; +namespace ERR_Actors{bool CanFight(Actor*a){return a&&a->fit;}} +struct Controller{Actor*actor;Actor*GetControlledEntity(){return actor;}static Controller*Cast(Controller*p){return p;}}; +struct Roster{Actor*actor;bool CurrentActor(Actor*a){return a==actor;}}; +struct Mode{static inline Mode*current=nullptr;Roster roster;bool active=true;static Mode*Get(){return current;}bool Active(){return active;}Roster*Players(){return &roster;}}; +struct Native{int updates=0;void UpdateHitZoneState(Zone*,bool){updates++;}}; +struct Point:Native{Zone*m_ERRProbeBlood=nullptr;int m_ERRProbeUpdates=0;bool m_ERRProbeFalseRefresh=false;Mode*m_ERRHitZoneMode=nullptr;Arraym_ERRHitZoneSources; +POINT +}; +struct Menu{Arraypoints;Actor*actor=nullptr;bool ready=true;int inspect=0;bool ERR_HitZoneProbeReady(Actor*a){return ready&&actor==a;}void InspectItem(void*){inspect++;}bool ERR_HitZoneProbePoints(Array&out){out=points;return points.size()==7;}static Menu*Cast(Menu*m){return m;}}; +struct Manager{Menu*current=nullptr;bool foreignTop=false,accept=true;int closes=0;void*GetTopMenu(){return foreignTop||current?this:nullptr;}Menu*FindMenuByPreset(int){return current;}bool CloseMenu(Menu*m){assert(m==current);closes++;return accept;}}; +struct Game{Controller*controller;Manager manager;Controller*GetPlayerController(){return controller;}Manager*GetMenuManager(){return &manager;}};Game*game=nullptr;Game*GetGame(){return game;} +struct Actual:Probe{Mode*m_Mode;Controller*m_Controller;Actor*m_Actor;Inventory*m_Inventory=nullptr;Zone*m_Blood=nullptr;Menu*m_Menu=nullptr;Arraym_Points;int m_Deadline=0,m_Stage=0,m_Result=0;bool m_Stopped=false,m_CloseRequested=false;float m_Health=0; +PROBE +}; +struct Fixture{Zone blood;Damage damage{&blood};Inventory inventory;Actor actor{&inventory,&damage};Controller controller{&actor};Mode mode;Game world{&controller,{}};Menu menu;Point points[7];Fixture(){System::now=0;Probe::enabled=true;game=&world;Mode::current=&mode;mode.roster.actor=&actor;menu.actor=&actor;inventory.opening=[&](){world.manager.current=&menu;};for(auto&p:points){p.m_ERRHitZoneMode=&mode;p.m_ERRHitZoneSources.Insert(&blood);menu.points.Insert(&p);}blood.event.fire=[&](){for(auto&p:points)if(!p.m_ERRHitZoneSources.IsEmpty())p.UpdateHitZoneState(&blood,true);};} + void Close(){world.manager.current=nullptr;for(auto&p:points){p.m_ERRHitZoneMode=nullptr;p.m_ERRHitZoneSources.Clear();}} +}; +int main(){ + {Fixture f;Actual p(&f.mode,&f.actor);assert(p.Tick(0)==0&&f.inventory.opens==1);assert(p.Tick(0)==0&&f.menu.inspect==1);assert(p.Tick(0)==0&&f.blood.event.calls==1&&f.world.manager.closes==1);assert(p.Tick(0)==0&&f.world.manager.closes==1);f.Close();assert(p.Tick(0)==1&&f.blood.event.calls==2&&f.blood.health==100);assert(p.Tick(0)==1);for(auto&x:f.points)assert(x.updates==1);} + {Fixture f;Actual p(&f.mode,&f.actor);f.world.manager.foreignTop=true;assert(p.Tick(0)==-1&&f.inventory.opens==0&&f.world.manager.closes==0);} + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);f.menu.ready=false;assert(p.Tick(0)==0&&f.menu.inspect==0);System::now=30000;assert(p.Tick(0)==-1&&f.world.manager.closes==1);} + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);p.Tick(0);f.menu.points.pop_back();assert(p.Tick(0)==0&&f.blood.event.calls==0);p.Stop();assert(f.world.manager.closes==1);} + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);p.Tick(0);f.menu.points[6]=f.menu.points[0];assert(p.Tick(0)==-1&&f.blood.event.calls==0);} + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);p.Tick(0);f.blood.event.fire=[&](){for(auto&x:f.points)x.UpdateHitZoneState(&f.blood,false);};assert(p.Tick(0)==-1);} + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);p.Tick(0);p.Tick(0);f.world.manager.current=nullptr;assert(p.Tick(0)==-1);}// missing real detach + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);p.Tick(0);p.Tick(0);f.Close();f.blood.event.fire=[&](){f.points[0].UpdateHitZoneState(&f.blood,true);};assert(p.Tick(0)==-1);}// late retained callback + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);p.Tick(0);f.world.manager.accept=false;assert(p.Tick(0)==-1&&f.world.manager.closes==2);} + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);f.blood.health=99;assert(p.Tick(0)==-1);} + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);Menu foreign;f.world.manager.current=&foreign;assert(p.Tick(0)==-1&&f.world.manager.closes==0);} + {Fixture f;Actual p(&f.mode,&f.actor);p.Tick(0);Controller replacement{&f.actor};f.world.controller=&replacement;assert(p.Tick(0)==-1);} + {Fixture f;Actual p(&f.mode,&f.actor);Probe::enabled=false;assert(p.Tick(0)==-1&&f.inventory.opens==0);} +} +'''.replace('POINT',point).replace('PROBE',probe.replace('Probe(', 'Actual('))) +base=(BASE/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c').read_text() +assert 'm_ERRInventoryHitZoneProbe.Tick(now)' in base and 'm_ERRInventoryHitZoneProbe.Stop()' in base +assert 'current == ERR_Phase.DEFEAT && m_ERRBaseStep == 2102' in base +assert 'if (result == 1) { m_ERRBaseStep = 2102; Transition(ERR_Phase.DEFEAT); }' in base +runner=(BASE/'tools/headless_workbench.py').read_text();tree=ast.parse(runner) +node=next(n for n in tree.body if isinstance(n,ast.If) and "a.smoke == 'inventory-hitzone'" in ast.unparse(n.test)) +for project,rounds,timeout,bad in [('core',1,240,False),('chat',1,240,True),('core',2,240,True),('core',1,239,True)]: + assert eval(compile(ast.Expression(node.test),'','eval'),{'a':SimpleNamespace(smoke='inventory-hitzone',project=project,rounds=rounds,timeout=timeout)})==bad +assert runner.count("'inventory-hitzone'")>=3 +print('PASS: actual native-probe lifecycle/counters with doubled boundaries; fixed deadline, exact seven sources, true forwarding, async close, no late updates and selector wiring')