Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
206 lines
14 KiB
Python
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')
|