Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
351 lines
30 KiB
Diff
351 lines
30 KiB
Diff
--- /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<ref SCR_InventoryHitZonePointContainerUI> 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<ref SCR_InventoryHitZonePointContainerUI> 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<SCR_InventoryHitZonePointContainerUI> 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<ref SCR_InventoryHitZonePointContainerUI> 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<Point> seen = {};','Array<Point*> 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 <cassert>
|
|
+#include <string>
|
|
+#include <vector>
|
|
+#include <algorithm>
|
|
+#include <functional>
|
|
+using string=std::string;template<class T>struct Array:std::vector<T>{using std::vector<T>::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::function<void()>fire;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<Damage*>(p);}};
|
|
+struct Inventory{int opens=0;std::function<void()>opening;void OpenInventory(){opens++;if(opening)opening();}static Inventory*Cast(void*p){return static_cast<Inventory*>(p);}};
|
|
+struct Actor{Inventory*inventory;Damage*damage;bool fit=true;void*FindComponent(int n){return n==1?static_cast<void*>(inventory):static_cast<void*>(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;Array<Zone*>m_ERRHitZoneSources;
|
|
+POINT
|
|
+};
|
|
+struct Menu{Array<Point*>points;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<Point*>&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;Array<Point*>m_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),'<selector>','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')
|