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

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