Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
176 lines
13 KiB
Diff
176 lines
13 KiB
Diff
--- a/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c
|
|
+++ b/addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c
|
|
@@ -36,6 +36,10 @@
|
|
protected bool m_ERRTestMountedAmmo;
|
|
protected ref ERR_NativeMountedAmmoProbe m_ERRMountedAmmo;
|
|
protected ref ERR_NativeMountedCourierProbe m_ERRMountedCourier;
|
|
+ protected bool m_ERRTestSupplyTravel;
|
|
+ protected DamageManagerComponent m_ERRSupplyTravelObserverDamage;
|
|
+ protected bool m_ERRSupplyTravelObserverDamageEnabled;
|
|
+ protected ref ERR_NativeSupplyTravelProbe m_ERRSupplyTravel;
|
|
protected bool m_ERRTestConvoy;
|
|
protected ref ERR_NativeConvoyProbe m_ERRConvoyProbe;
|
|
protected ref ERR_NativeHQMemoryProbe m_ERRHQMemoryProbe;
|
|
@@ -94,6 +98,7 @@
|
|
m_ERRTestAlarmHearing = outcome == "alarm-hearing";
|
|
m_ERRTestMountedAmmo = outcome == "mounted-ammo";
|
|
m_ERRTestConvoy = outcome == "convoy" || outcome == "supply-convoys";
|
|
+ m_ERRTestSupplyTravel = outcome == "supply-travel";
|
|
m_ERRTestMortar = outcome == "mortar-inventory";
|
|
m_ERRTestMapOwnership = outcome == "map-ownership";
|
|
m_ERRTestAircraftField = outcome == "aircraft-field";
|
|
@@ -129,6 +134,7 @@
|
|
if (m_ERRTestMortarSalvo) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
|
if (m_ERRTestMortarFire) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
|
if (m_ERRTestPatrolHQ) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 420000;
|
|
+ if (m_ERRTestSupplyTravel) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 420000;
|
|
OnStop().Insert(ERR_StopHQPatrol);
|
|
OnPhaseChanged().Insert(ERR_SmokePhase);
|
|
GetGame().GetCallqueue().CallLater(ERR_SmokeTick, 500, true);
|
|
@@ -243,6 +249,7 @@
|
|
if (m_ERRAircraftField) m_ERRAircraftField.Stop();
|
|
if (m_ERRHQResponseProof) m_ERRHQResponseProof.Stop();
|
|
if (m_ERRConvoyProbe) m_ERRConvoyProbe.Stop();
|
|
+ ERR_StopSupplyTravel();
|
|
if (m_ERRMountedAmmo) m_ERRMountedAmmo.Stop();
|
|
if (m_ERRMountedCourier) m_ERRMountedCourier.Stop();
|
|
if (m_ERRAlarmHearing) m_ERRAlarmHearing.Stop();
|
|
@@ -288,6 +295,15 @@
|
|
protected void ERR_SmokePhase(ERR_Phase previous, ERR_Phase current)
|
|
{
|
|
if (m_ERRSmokeDone || !ERR_MissionRules.Terminal(current)) return;
|
|
+ if (m_ERRTestSupplyTravel)
|
|
+ {
|
|
+ if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 3201, "production supply travel checked before explicit fixture reset")) return;
|
|
+ m_ERRSmokeDone = true;
|
|
+ GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
|
+ ERR_StopHQPatrol();
|
|
+ Print("ERR SMOKE COMPLETE: production supply convoy original six-vehicle travel; tactics and interception unqualified");
|
|
+ return;
|
|
+ }
|
|
if (m_ERRTestAircraftField)
|
|
{
|
|
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 241, "actual aircraft field fixture completed before explicit reset")) return;
|
|
@@ -504,12 +520,44 @@
|
|
if (!ERR_Check(damage.ERR_ForceFatalLossForTest(Instigator.CreateInstigator(m_ERRSmokePlayer), m_ERRSmokePlayer), "explicit terminal fixture fatal loss authorized")) return;
|
|
m_ERRSmokeWait = now + 4000;
|
|
}
|
|
+ protected void ERR_StopSupplyTravel()
|
|
+ {
|
|
+ ERR_NativeSupplyTravelProbe probe = m_ERRSupplyTravel;
|
|
+ DamageManagerComponent damage = m_ERRSupplyTravelObserverDamage;
|
|
+ bool enabled = m_ERRSupplyTravelObserverDamageEnabled;
|
|
+ m_ERRSupplyTravel = null; m_ERRSupplyTravelObserverDamage = null;
|
|
+ if (probe) probe.Stop();
|
|
+ if (damage) damage.EnableDamageHandling(enabled);
|
|
+ }
|
|
+ protected void ERR_BeginSupplyTravel(float now)
|
|
+ {
|
|
+ if (!ERR_Check(Players() && Players().CurrentActor(m_ERRSmokePlayer), "supply travel retains the current admitted observer")) return;
|
|
+ if (!ERR_Check(!m_ERRSupplyTravel && !m_ERRSupplyTravelObserverDamage, "supply travel setup occurs once")) return;
|
|
+ m_ERRSupplyTravelObserverDamage = DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent));
|
|
+ if (!ERR_Check(m_ERRSupplyTravelObserverDamage != null, "supply travel admitted observer damage component exists")) return;
|
|
+ m_ERRSupplyTravelObserverDamageEnabled = m_ERRSupplyTravelObserverDamage.IsDamageHandlingEnabled();
|
|
+ m_ERRSupplyTravelObserverDamage.EnableDamageHandling(false);
|
|
+ Print("ERR SUPPLY TRAVEL FIXTURE: admitted observer damage protected; enemy forces/AI budget/routes unchanged");
|
|
+ // Ordinary release starts production AI. No legacy convoy fixture setup,
|
|
+ // manual admission or friendly perception override.
|
|
+ if (!ERR_Check(UnlockPrison(m_ERRSmokePlayer), "supply travel fixture opens prison for ordinary automatic AI")) return;
|
|
+ m_ERRSupplyTravel = new ERR_NativeSupplyTravelProbe(this, now);
|
|
+ m_ERRSmokeStep = 3200;
|
|
+ }
|
|
+ protected void ERR_TickSupplyTravel(float now)
|
|
+ {
|
|
+ if (!ERR_Check(m_ERRSupplyTravel != null, "production supply travel probe exists")) return;
|
|
+ int result = m_ERRSupplyTravel.Tick(ERR_HQ.Cast(Command()), now);
|
|
+ if (result < 0) { ERR_Check(false, "production supply convoy travel; inspect original-identity diagnostics"); return; }
|
|
+ if (result == 1) { m_ERRSmokeStep = 3201; Transition(ERR_Phase.DEFEAT); }
|
|
+ }
|
|
protected void ERR_SmokeTick()
|
|
{
|
|
float now = GetWorld().GetWorldTime();
|
|
if (m_ERRSmokeDone) return;
|
|
if (now > m_ERRSmokeDeadline) { ERR_Check(false, "timeout at step " + m_ERRSmokeStep.ToString()); return; }
|
|
if (now < m_ERRSmokeWait) return;
|
|
+ if (m_ERRSmokeStep == 3200) { ERR_TickSupplyTravel(now); return; }
|
|
if (m_ERRSmokeStep == 240)
|
|
{
|
|
int result = m_ERRAircraftField.Tick(now);
|
|
@@ -598,6 +646,7 @@
|
|
if (!ERR_Check(CanStartSpawnPreload() && m_ERRSpawnPreloadRequested && !m_SpawnPreload, "prisoner admitted only after enabled native spawn preload finishes")) return;
|
|
m_ERRSmokePlayer = players[0];
|
|
if (!ERR_CheckRoster() || !ERR_NativeContractsProbe.Run(this, m_ERRSmokePlayer) || !ERR_CheckCompanionCommands()) return;
|
|
+ if (m_ERRTestSupplyTravel) { ERR_BeginSupplyTravel(now); return; }
|
|
if (m_ERRTestAircraftField)
|
|
{
|
|
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
|
--- /dev/null
|
|
+++ b/tests/test_native_supply_travel_hook.py
|
|
@@ -0,0 +1,66 @@
|
|
+"""Actual opt-in setup/tick/terminal/cleanup methods, native boundaries doubled."""
|
|
+from pathlib import Path
|
|
+import os,re
|
|
+from policy_harness import ROOT,body,run
|
|
+p=Path(os.environ.get('ERR_SMOKE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c'))
|
|
+s=p.read_text()
|
|
+methods='\n'.join(body(p,x) for x in ('void ERR_StopSupplyTravel(', 'void ERR_BeginSupplyTravel(', 'void ERR_TickSupplyTravel('))
|
|
+methods=methods.replace('protected ','').replace('null','nullptr')
|
|
+methods=methods.replace('DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent))','m_ERRSmokePlayer->damage').replace('ERR_HQ.Cast(Command())','Command()').replace('ERR_Phase.DEFEAT','2')
|
|
+for typ in ['ERR_NativeSupplyTravelProbe','DamageManagerComponent']:
|
|
+ methods=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',methods)
|
|
+for obj in ['probe','damage','m_ERRSupplyTravel','m_ERRSupplyTravelObserverDamage']:
|
|
+ methods=methods.replace(obj+'.',obj+'->')
|
|
+methods=methods.replace('Players().','Players()->')
|
|
+terminal=body(p,'if (m_ERRTestSupplyTravel)\n\t\t{')
|
|
+terminal=terminal.replace('ERR_Phase.DEFEAT','2').replace('GetGame().GetCallqueue().Remove(ERR_SmokeTick);','removed++;').replace('ERR_StopHQPatrol();','ERR_StopSupplyTravel();')
|
|
+# Actual startup expressions/deadline, not a duplicate selector model.
|
|
+a=s.index('m_ERRTestLostHelicopter = System.GetCLIParam(');b=s.index('OnStop().Insert(ERR_StopHQPatrol);',a)
|
|
+selector=s[a:b].replace('System.GetCLIParam("errSmokeOutcome", outcome) && ','').replace('GetWorld().GetWorldTime()','now')
|
|
+selector=re.sub(r'ERR_Native\w+Probe.Enabled\(\)','false',selector)
|
|
+flags=sorted(set(re.findall(r'\bm_ERRTest\w+',selector)))
|
|
+run(r'''
|
|
+#include <cassert>
|
|
+#include <string>
|
|
+using string=std::string;
|
|
+struct DamageManagerComponent{bool enabled=true;int writes=0;bool IsDamageHandlingEnabled(){return enabled;}void EnableDamageHandling(bool e){enabled=e;writes++;}};
|
|
+struct Actor{DamageManagerComponent*damage=nullptr;};struct ERR_HQ{};struct Fixture;
|
|
+struct ERR_NativeSupplyTravelProbe{static inline int created=0;int result=0,stops=0,ticks=0;float begun=0;ERR_HQ*seen=nullptr;ERR_NativeSupplyTravelProbe(Fixture*,float now){created++;begun=now;}void Stop(){stops++;}int Tick(ERR_HQ*h,float){seen=h;ticks++;return result;}};
|
|
+struct Roster{Actor*current=nullptr;bool CurrentActor(Actor*a){return a&&a==current;}};
|
|
+struct Fixture{Actor actor;DamageManagerComponent damage;Roster roster;ERR_HQ hq;Actor*m_ERRSmokePlayer=&actor;ERR_NativeSupplyTravelProbe*m_ERRSupplyTravel=nullptr;DamageManagerComponent*m_ERRSupplyTravelObserverDamage=nullptr;bool m_ERRSupplyTravelObserverDamageEnabled=false,m_ERRSmokeDone=false,m_ERRTestSupplyTravel=true,unlock=true;int m_ERRSmokeStep=0,failed=0,unlocks=0,transition=0,removed=0;string printed;
|
|
+ Fixture(){actor.damage=&damage;roster.current=&actor;}Roster*Players(){return &roster;}ERR_HQ*Command(){return &hq;}void Print(string s){printed=s;}bool UnlockPrison(Actor*a){assert(a==&actor&&!damage.enabled);unlocks++;return unlock;}void Transition(int p){transition=p;}bool ERR_Check(bool ok,string){if(!ok){failed++;m_ERRSmokeDone=true;ERR_StopSupplyTravel();}return ok;}
|
|
+METHODS
|
|
+ void Finish(int current){TERMINAL}
|
|
+};
|
|
+void selector(string outcome,float now){[[maybe_unused]] bool FLAGS;float m_ERRSmokeDeadline=0;SELECTOR
|
|
+ if(outcome=="supply-travel"){assert(m_ERRTestSupplyTravel&&!m_ERRTestConvoy&&!m_ERRTestPlayerDeath&&m_ERRSmokeDeadline==now+420000);}else assert(!m_ERRTestSupplyTravel);
|
|
+}
|
|
+int main(){
|
|
+ selector("supply-travel",0);selector("supply-travel",1234);selector("convoy",0);selector("supply-convoys",0);selector("normal-escape-route",0);
|
|
+ Fixture f;f.ERR_BeginSupplyTravel(100);auto p=f.m_ERRSupplyTravel;assert(p&&p->begun==100&&f.m_ERRSmokeStep==3200&&f.unlocks==1&&!f.damage.enabled);
|
|
+ f.ERR_TickSupplyTravel(500);assert(p->ticks==1&&p->seen==&f.hq&&f.transition==0);
|
|
+ p->result=1;f.ERR_TickSupplyTravel(1000);assert(f.m_ERRSmokeStep==3201&&f.transition==2);
|
|
+ f.Finish(2);assert(f.m_ERRSmokeDone&&f.removed==1&&p->stops==1&&f.damage.enabled&&!f.m_ERRSupplyTravel&&!f.m_ERRSupplyTravelObserverDamage);
|
|
+ assert(f.printed=="ERR SMOKE COMPLETE: production supply convoy original six-vehicle travel; tactics and interception unqualified");
|
|
+ int writes=f.damage.writes;f.ERR_StopSupplyTravel();assert(f.damage.writes==writes&&p->stops==1);
|
|
+ Fixture disabled;disabled.damage.enabled=false;disabled.ERR_BeginSupplyTravel(100);disabled.ERR_StopSupplyTravel();assert(!disabled.damage.enabled);
|
|
+ Fixture refused;refused.unlock=false;int before=ERR_NativeSupplyTravelProbe::created;refused.ERR_BeginSupplyTravel(100);assert(refused.failed==1&&refused.damage.enabled&&!refused.m_ERRSupplyTravel&&ERR_NativeSupplyTravelProbe::created==before);
|
|
+ Fixture replaced;replaced.ERR_BeginSupplyTravel(100);Actor next;DamageManagerComponent nextDamage;next.damage=&nextDamage;replaced.m_ERRSmokePlayer=&next;replaced.ERR_StopSupplyTravel();assert(replaced.damage.enabled&&nextDamage.writes==0);
|
|
+ Fixture lost;lost.roster.current=nullptr;lost.ERR_BeginSupplyTravel(100);assert(lost.failed==1&&lost.unlocks==0&&lost.damage.writes==0);
|
|
+ Fixture missing;missing.actor.damage=nullptr;missing.ERR_BeginSupplyTravel(100);assert(missing.failed==1&&missing.unlocks==0&&!missing.m_ERRSupplyTravel);
|
|
+ Fixture bad;bad.ERR_BeginSupplyTravel(100);auto bp=bad.m_ERRSupplyTravel;bp->result=-1;bad.ERR_TickSupplyTravel(500);assert(bad.failed==1&&bad.damage.enabled&&bp->stops==1&&bad.transition==0);
|
|
+ Fixture premature;premature.ERR_BeginSupplyTravel(100);premature.Finish(2);assert(premature.failed==1&&premature.damage.enabled&&premature.printed.find("COMPLETE")==string::npos);
|
|
+}
|
|
+'''.replace('METHODS',methods).replace('TERMINAL',terminal).replace('FLAGS',','.join(f+'=false' for f in flags)).replace('SELECTOR',selector))
|
|
+assert s.startswith('// Explicit, local Workbench diagnostic only.') and '#ifdef WORKBENCH' in s
|
|
+setup=body(p,'void ERR_BeginSupplyTravel(')
|
|
+for forbidden in ['StartConvoy','SetPerceivedFaction','SetOrigin','Teleport','Hunters()', 'Settings(', 'StartSupply', 'EnemyFaction']:
|
|
+ assert forbidden not in setup,forbidden
|
|
+assert 'ERR_StopSupplyTravel();' in body(p,'void ERR_StopHQPatrol(')
|
|
+assert 'ERR_StopHQPatrol();' in body(p,'bool ERR_Check(')
|
|
+assert 'OnStop().Insert(ERR_StopHQPatrol);' in s
|
|
+step=body(p,'void ERR_SmokeTick(')
|
|
+assert step.index('now > m_ERRSmokeDeadline') < step.index('m_ERRSmokeStep == 3200')
|
|
+assert step.index('ERR_CheckCompanionCommands()') < step.index('if (m_ERRTestSupplyTravel) { ERR_BeginSupplyTravel(now); return; }') < step.index('if (m_ERRTestConvoy)')
|
|
+assert 'm_ERRSmokeDeadline' not in methods
|
|
+print('PASS: actual distinct supply-travel hook, fixed420s selector, original observer protection/restoration, failure/terminal cleanup and no legacy convoy setup')
|