Files
EscapistsReintroduced/tests/test_mortar_transactions.py
T
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

206 lines
14 KiB
Python

"""Actual Enforce policies and transaction observation body under narrow API fakes."""
from policy_harness import ROOT, body, run
from pathlib import Path
import os
r=ROOT/'addons/core/Scripts/Game/ERR/10_AI'
readiness=r/'ERR_MortarReadiness.c'; transfer=Path(os.environ.get('ERR_MORTAR_SUPPLY_TRANSFER_SOURCE',r/'ERR_MortarSupplyTransfer.c')); battery=r/'ERR_MortarBattery.c'
policy='\n'.join([body(readiness,'static ERR_MortarSupplyState Population('),body(readiness,'static bool AllocationValid('),body(readiness,'static int CycleGate('),body(transfer,'static bool Authority('),body(transfer,'static bool Proven(')])
policy=policy.replace('ERR_MortarSupplyState.','ERR_MortarSupplyState::')
run('''#include <cassert>
enum class ERR_MortarSupplyState {FAILED=-1,PENDING,READY};
'''+policy+'''
int main(){
using S=ERR_MortarSupplyState;
assert(Population(0,0,0,false,false)==S::PENDING);
assert(Population(3,3,0,true,false)==S::PENDING);
assert(Population(4,3,1,false,false)==S::PENDING);
assert(Population(4,3,1,true,false)==S::READY);
assert(Population(4,3,1,true,true)==S::FAILED);
assert(Population(0,0,0,false,true)==S::FAILED);
assert(Population(5,3,2,true,false)==S::FAILED);
assert(Population(4,4,0,true,false)==S::FAILED);
assert(AllocationValid(0)&&AllocationValid(9));
assert(!AllocationValid(-1)&&!AllocationValid(10));
assert(Authority(true,false,1,1));
assert(!Authority(false,false,1,1));
assert(!Authority(true,true,1,1));
assert(!Authority(true,false,1,2));
for(int bits=0;bits<16;bits++) {
bool callback=bits&1, source=bits&2, destination=bits&4, exact=bits&8;
assert(Proven(callback,source,destination,exact)==(bits==13));
}
int counter=0;
assert(CycleGate(true,0,counter,8,1)==0); // no physical projectile, no count
for(int i=0;i<1000;i++) if(CycleGate(true,1,counter,8,0)==1) counter++;
assert(counter==0); // pending supply never invokes native super
assert(CycleGate(true,1,counter,8,-1)==-1);
if(CycleGate(true,1,counter,8,1)==1) counter++;
assert(counter==1 && CycleGate(true,1,counter,8,1)==0); // same event not counted twice
assert(CycleGate(false,2,counter,8,1)==-1);
assert(CycleGate(true,8,7,8,-1)==1); // final confirmed round needs no ninth shell
}
''')
# Retain actual control flow; translate only native member/type syntax and the
# component lookup into a directly supplied fake item component.
poll=body(transfer,'ERR_MortarSupplyState Poll(')
poll=poll.replace('ERR_MortarSupplyState.','ERR_MortarSupplyState::').replace('Replication.IsServer()','Replication::IsServer()').replace('InventoryItemComponent item;','InventoryItemComponent* item=nullptr;').replace('if (m_Shell) item = InventoryItemComponent.Cast(m_Shell.FindComponent(InventoryItemComponent));','if (m_Shell) item = m_Shell;').replace('item.GetParentSlot().GetStorage()','item->GetParentSlot()->GetStorage()').replace('item.GetParentSlot()','item->GetParentSlot()')
ack=body(transfer,'void Acknowledge(')
run('''#include <cassert>
#include <initializer_list>
enum class ERR_MortarSupplyState {FAILED=-1,PENDING,READY};
struct Replication { static bool IsServer(){return true;} };
struct Slot { void* storage; void* GetStorage(){return storage;} };
struct InventoryItemComponent { Slot* slot; Slot* GetParentSlot(){return slot;} };
'''+body(transfer,'static bool Proven(')+'''
struct Transfer {
bool valid=true,source=true,destination=false,m_Accepted=true,m_Invalidated=false,m_Resolved=false,m_Ready=false;
int m_Source=1,m_Destination=2,m_Acknowledgement=0;
float m_Deadline=8000;
void* m_Storage=this;
Slot slot{this}; InventoryItemComponent item{&slot}; InventoryItemComponent* m_Shell=&item;
bool Valid(float){return valid&&!m_Invalidated;}
bool Contains(int which){return m_Shell && (which==1 ? source : destination);}
'''+ack+body(transfer,'bool Unresolved(')+poll+'''
};
struct Battery {Transfer* m_Transfer=nullptr;
'''+body(battery,'bool SupplyPending(').replace('m_Transfer.Unresolved()', 'm_Transfer->Unresolved()')+'''
};
int main(){
using S=ERR_MortarSupplyState;
Transfer t; assert(t.Poll(0)==S::PENDING && !t.m_Resolved);
t.Acknowledge(true); t.Acknowledge(false); assert(t.m_Acknowledgement==1);
assert(t.Poll(1)==S::PENDING && !t.m_Resolved); // callback alone cannot supply
t.destination=true; assert(t.Poll(2)==S::PENDING); // source still claims ownership
t.source=false; t.slot.storage=nullptr; assert(t.Poll(3)==S::PENDING); // wrong storage
t.slot.storage=t.m_Storage; assert(t.Poll(4)==S::READY && t.m_Resolved);
Transfer stopped; stopped.m_Invalidated=true; stopped.source=false; stopped.destination=true; stopped.Acknowledge(true);
assert(stopped.Poll(5)==S::FAILED && !stopped.m_Ready && stopped.m_Resolved);
Transfer timeout; assert(timeout.Poll(8000)==S::FAILED && !timeout.m_Resolved && timeout.m_Invalidated);
timeout.source=false;timeout.destination=true;timeout.Acknowledge(true);
assert(timeout.Poll(9000)==S::FAILED && timeout.m_Resolved && !timeout.m_Ready); // late success cannot authorize
Transfer late;late.source=false;late.destination=true;late.Acknowledge(true);
assert(late.Poll(8000)==S::FAILED); // no deadline-boundary success bypass
Transfer failed;failed.Acknowledge(false);failed.Acknowledge(true);
assert(failed.Poll(1)==S::FAILED && failed.m_Acknowledgement==-1 && failed.m_Resolved);
// Exact deletion is permanent loss, not an operation that can still arrive.
for(int ack=0;ack<3;ack++)for(bool invalidated:{false,true}){
Transfer gone;Battery battery;battery.m_Transfer=&gone;gone.m_Invalidated=invalidated;assert(gone.Unresolved()&&battery.SupplyPending());gone.m_Shell=nullptr;
if(ack==1)gone.Acknowledge(true);
if(ack==2)gone.Acknowledge(false);
assert(gone.Poll(1)==S::FAILED&&gone.m_Resolved&&gone.m_Invalidated&&!gone.m_Ready&&!gone.Unresolved());
assert(!battery.SupplyPending());
gone.Acknowledge(true);assert(gone.Poll(90000)==S::FAILED&&!gone.Unresolved()&&!gone.m_Ready);
}
// A surviving detached shell, missing inventory or lost authority remains
// uncertain; only exact identity deletion permits unconditional settlement.
Transfer detached;detached.source=false;detached.destination=false;detached.Acknowledge(true);
assert(detached.Poll(1)==S::PENDING&&detached.Unresolved());assert(detached.Poll(8000)==S::FAILED&&detached.Unresolved());
Transfer unauthorized;unauthorized.valid=false;
assert(unauthorized.Poll(0)==S::FAILED && !unauthorized.m_Resolved);
}
''')
# Execute the production classification helper and its actual delivered-report
# lookup. The freshness adapter supplies only the report/time boundary; no
# inventory retry, reservation release or native firing is modeled here.
system_path=r/'ERR_MortarSystem.c'
classification='\n'.join(body(system_path,signature) for signature in ['ERR_HQReport Latest(', 'string ContactFailure(', 'string SupplyFailure('])
classification=classification.replace('ERR_HQReport Latest(', 'Report* Latest(').replace('ERR_MortarBattery battery','Battery* battery').replace('battery.Report','battery->Report').replace('ERR_MortarPrediction.Fresh','Prediction::Fresh').replace('return null;', 'return nullptr;').replace('string ', 'std::string ')
run('''#include <cassert>
#include <string>
struct Report {float observed=1000;bool valid=true;};
struct Battery {struct Report* Report=nullptr;};
struct Prediction {static bool Fresh(Report* report,float now){return report&&report->valid&&now>=report->observed&&now-report->observed<=60000;}};
struct System {
'''+classification+'''};
int main(){
System system;Battery battery;
assert(system.SupplyFailure(&battery,20000)=="no delivered target report");
Report report;battery.Report=&report;
assert(system.SupplyFailure(&battery,61000)=="next native load lacks confirmed physical gunner ammunition");
assert(system.SupplyFailure(&battery,61001)=="delivered target report expired or invalid");
assert(system.SupplyFailure(&battery,999)=="delivered target report expired or invalid");
report.valid=false;assert(system.SupplyFailure(&battery,20000)=="delivered target report expired or invalid");
report.valid=true;report.observed=20000;
assert(system.SupplyFailure(&battery,30000)=="next native load lacks confirmed physical gunner ammunition");
assert(battery.Report==&report&&report.observed==20000); // classification cannot retimestamp or replace a report
}
''')
# Source contract checks constrain integration that cannot run without the engine.
b=battery.read_text();sys=(r/'ERR_MortarSystem.c').read_text();f=(r/'ERR_MortarFiring.c').read_text();rd=readiness.read_text();tr=transfer.read_text()
update=body(battery,'void Update(float now)')
assert update.index('m_Readiness.Poll(now)') < update.index('if (!Operational())')
assert 'm_Allocated = true;' in rd and rd.count('TrySpawnPrefabToStorage(ERR_MortarBattery.SHELL')==1
assert rd.index('m_Allocated = true;') < rd.index('TrySpawnPrefabToStorage(ERR_MortarBattery.SHELL')
assert 'm_aUnitPrefabSlots = new array<ResourceName>()' in rd and 'PurgeSpawnRequestsForGroup(group)' in rd
assert 'DeleteSpawned' not in body(readiness,'void Stop(')
assert 'battery.Stop();' in body(r/'ERR_MortarSystem.c','void Stop(')
assert tr.count('TryMoveItemToStorage(')==1 and 'GetItem()' not in tr.replace('callback.GetItem()','')
assert 'm_AttemptedShells.Insert(item)' in b and 'm_AttemptedShells.Contains(item)' in b
assert 'm_Transfer.CapturedOrder() == OrderId' in b
assert 'battery.PendingSupplyReport' in sys and 'supply == ERR_MortarSupplyState.PENDING) return;' in sys
shoot=f[f.index('modded class SCR_AIStaticArtilleryBehavior_ShootingLogic'):]
assert shoot.index('battery.SupplyState(') < shoot.index('supply == ERR_MortarSupplyState.PENDING') < shoot.index('supply == ERR_MortarSupplyState.FAILED') < shoot.index('system.SupplyFailure(battery, now)') < shoot.index('super.EOnTaskSimulate(')
assert 'system.Withhold(battery, system.SupplyFailure(battery, now))' in shoot
supply_state=body(battery,'ERR_MortarSupplyState SupplyState(')
assert supply_state.index('m_Transfer.Poll(now)') < supply_state.index('if (!SupplyAuthorized(now))')
assert shoot.count('super.EOnTaskSimulate(')==1 and 'battery.SupplyGunner()' not in shoot
print('PASS: actual startup/generation/ownership/timeout/callback policies and native-counter wait; integration preserves one-shot allocation and pending-crew cancellation')
# Execute the production post-shot node, including the superclass completion
# callback. A fast repeater must not start a ninth aim while HQ is still waiting
# to observe native completion; pending ammunition and duplicate ticks stay safe.
from types import SimpleNamespace
section=(r/'ERR_MortarFiring.c').read_text().split('modded class SCR_AIStaticArtilleryBehavior_ShootingLogic',1)[1]
cycle=body(SimpleNamespace(read_text=lambda:section),'override ENodeResult EOnTaskSimulate(')
cycle=cycle.replace('override ', '').replace('AIAgent owner','void* owner')
cycle=cycle.replace('ERR_MortarBattery battery;', 'ERR_MortarBattery* battery=nullptr;').replace('SCR_AIStaticArtilleryBehavior behavior;', 'SCR_AIStaticArtilleryBehavior* behavior=nullptr;')
cycle=cycle.replace('SCR_AIStaticArtilleryBehavior.Cast(m_Utility.GetExecutedAction())','m_Utility->GetExecutedAction()')
cycle=cycle.replace('ERR_MortarSystem system = ERR_MortarSystem.Get();','ERR_MortarSystem* system = &systemInstance;')
for name in ['battery','behavior','system']:cycle=cycle.replace(name+'.',name+'->')
for name in ['ERR_MortarSupplyRules','ERR_MortarSalvo','ERR_MortarSupplyState','ENodeResult']:cycle=cycle.replace(name+'.',name+'::')
cycle=cycle.replace('GetGame().GetWorld().GetWorldTime()', '1000.f').replace('super.EOnTaskSimulate(owner, dt)', 'Base::EOnTaskSimulate(owner, dt)')
gate=body(readiness,'static int CycleGate(')
run('''#include <cassert>
enum class ENodeResult {FAIL,RUNNING,SUCCESS};
enum ERR_MortarSupplyState {FAILED=-1,PENDING,READY};
struct ERR_MortarSalvo {static constexpr int ROUNDS=8;};
struct ERR_MortarSupplyRules {'''+gate+'''};
struct ERR_MortarBattery {
bool CommandOpen=true;int OrderShots=0,checks=0;ERR_MortarSupplyState supply=ERR_MortarSupplyState::READY;
ERR_MortarSupplyState SupplyState(float){checks++;return supply;}
};
struct SCR_AIStaticArtilleryBehavior {
ERR_MortarBattery* battery=nullptr;int count=0,completions=0;
int GetShotCount(){return count;}ERR_MortarBattery* ERR_GetMortarBattery(){return battery;}
};
struct Utility {SCR_AIStaticArtilleryBehavior* behavior;auto GetExecutedAction(){return behavior;}};
struct ERR_MortarSystem {int refusals=0;void Withhold(ERR_MortarBattery*,int){refusals++;}int SupplyFailure(ERR_MortarBattery*,float){return 0;}} systemInstance;
struct Base {
Utility* m_Utility=nullptr;int calls=0;bool aiming=false;static constexpr int PORT_AIMING_NEEDED=1;
ENodeResult EOnTaskSimulate(void*,float){calls++;auto b=m_Utility->behavior;if(++b->count==8)b->completions++;return ENodeResult::SUCCESS;}
void SetVariableOut(int,bool value){aiming=value;}
};
struct Node:Base {'''+cycle+'''};
int main(){
ERR_MortarBattery battery;SCR_AIStaticArtilleryBehavior behavior;behavior.battery=&battery;Utility utility{&behavior};Node n;n.m_Utility=&utility;
assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::RUNNING&&n.calls==0);
battery.OrderShots=1;battery.supply=ERR_MortarSupplyState::PENDING;
assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::RUNNING&&n.calls==0);
battery.supply=ERR_MortarSupplyState::READY;
for(int shot=1;shot<=8;shot++){
battery.OrderShots=shot;n.aiming=false;
if(shot==8)battery.supply=ERR_MortarSupplyState::FAILED; // final shot needs no ninth shell
auto result=n.EOnTaskSimulate(nullptr,0);
assert(n.calls==shot&&behavior.count==shot);
assert(result==(shot<8?ENodeResult::SUCCESS:ENodeResult::RUNNING));
assert(n.aiming==(shot<8));
}
assert(behavior.completions==1&&systemInstance.refusals==0);
for(int tick=0;tick<100;tick++)assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::RUNNING);
assert(n.calls==8&&behavior.completions==1); // no duplicate count or ninth aim
battery.CommandOpen=false;assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::FAIL);
behavior.battery=nullptr;assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::SUCCESS); // unrelated native artillery unchanged
}
''')
print('PASS: rapid mortar cycle counts eight physical rounds once and waits for native completion without a ninth aim or shell')