Files
EscapistsReintroduced/tests/test_mortar_aim_renewal.py
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

84 lines
6.5 KiB
Python

"""Execute actual aim-equivalence and renewal control flow with synthetic API data.
No fake native ballistic result is represented as a measured firing result.
"""
import re
from policy_harness import ROOT, body, run
r=ROOT/'addons/core/Scripts/Game/ERR/10_AI'
solution=r/'ERR_MortarAimSolution.c'; firing=r/'ERR_MortarFiring.c'
eq=body(solution,'static bool Equivalent(').replace('vector.Distance','vector::Distance').replace('Math.AbsFloat','std::fabs')
matches=body(solution,'bool Matches(').replace('ERR_MortarBattery battery','ERR_MortarBattery* battery').replace('ERR_HQReport report','ERR_HQReport* report').replace('battery.','battery->')
fresh=body(r/'ERR_MortarPrediction.c','static bool Fresh(').replace('ERR_HQReport report','ERR_HQReport* report').replace('report.','report->')
renew=body(firing,'bool ERR_RevalidateAim(')
for typ in ['ERR_MortarBattery','ERR_MortarSystem','ERR_HQReport','ERR_MortarAimSolution']:
renew=re.sub(r'\b'+typ+r' (battery|system|report|commanded|updated)\b',typ+r'* \1',renew)
renew=renew.replace('IEntity gun;','IEntity gun=nullptr;').replace('ResourceName ammo;','std::string ammo;')
for name in ['battery','system','report','commanded','updated']:renew=renew.replace(name+'.',name+'->')
for name in ['ERR_MortarBattery','ERR_MortarAimRules','ERR_MortarPrediction']:renew=renew.replace(name+'.',name+'::')
# Native trace formatting is deliberately inert. Authorization branches remain
# the production method body, and every solver invocation is counted below.
renew=re.sub(r', string\.Format\([^\n]*\)\);',');',renew)
run('''#include <cassert>
#include <cmath>
#include <limits>
#include <string>
struct vector {float x=0,y=0,z=0; static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}};
using IEntity=void*;
struct ERR_HQReport {IEntity Target=this;bool Distress=false;float ObservedAt=0;std::string TravelMode="foot";};
struct ERR_MortarPrediction {static constexpr float MAX_REPORT_AGE_MS=60000;
'''+fresh+'''};
struct ERR_MortarAimRules {
'''+eq+'''};
struct Node;struct ERR_MortarAimSolution;
struct ERR_MortarBattery {static inline std::string SHELL="nativeHE";IEntity Gun=this;ERR_MortarAimSolution* AimSolution=nullptr;Node* AimSolver=nullptr;int OrderId=1,OrderShots=0;float AimedAt=1000,AimValidatedAt=1000; void Trace(const char*,const char*){}};
struct ERR_MortarAimSolution {IEntity Gun=nullptr;ERR_HQReport* Report=nullptr;int OrderId=1,Shot=0,Charge=0;vector GunOrigin{},Impact{500,0,0},AimTarget{500,500,0};float FlightTime=30;
'''+matches+'''};
struct ERR_MortarSystem {int rejects=0;bool Withhold(ERR_MortarBattery*,const char*){rejects++;return false;}const char* ContactFailure(ERR_MortarBattery*){return "stale";}};
struct Node {
static constexpr int PORT_ARTILLERY_ENTITY=1,PORT_AMMO_PREFAB=2;
IEntity portGun=nullptr;std::string portAmmo=ERR_MortarBattery::SHELL;ERR_MortarAimSolution* calculated=nullptr;int solves=0;
void GetVariableIn(int,IEntity& out){out=portGun;}void GetVariableIn(int,std::string& out){out=portAmmo;}
ERR_MortarAimSolution* ERR_Solve(ERR_MortarBattery*,ERR_MortarSystem* system,ERR_HQReport*,float){solves++;if(!calculated)system->rejects++;return calculated;}
'''+renew+'''};
struct Fixture {
ERR_MortarBattery battery;ERR_HQReport report;ERR_MortarAimSolution original,next;ERR_MortarSystem system;Node node;
Fixture(){original.Gun=battery.Gun;original.Report=&report;next=original;battery.AimSolution=&original;battery.AimSolver=&node;node.portGun=battery.Gun;node.calculated=&next;}
bool attempt(float now=25000){return node.ERR_RevalidateAim(&battery,&system,&report,now);}
};
int main(){
Fixture stable; assert(stable.attempt()); assert(stable.node.solves==1&&stable.battery.AimedAt==1000&&stable.battery.AimValidatedAt==25000);
assert(stable.report.ObservedAt==0&&stable.battery.AimSolution==&stable.original);
assert(stable.attempt(45000)&&stable.node.solves==2); // fresh recomputation every renewal
assert(!stable.attempt(60001)&&stable.node.solves==2&&stable.battery.AimedAt==1000&&stable.battery.AimValidatedAt==45000); // observation never renewed
Fixture limit;assert(limit.attempt(60000));
Fixture future;future.report.ObservedAt=26000;assert(!future.attempt()&&future.node.solves==0);
Fixture gone;gone.battery.AimSolver=nullptr;assert(!gone.attempt()&&gone.node.solves==0);
Fixture wrongGun;wrongGun.node.portGun=&wrongGun.report;assert(!wrongGun.attempt()&&wrongGun.node.solves==0);
Fixture wrongAmmo;wrongAmmo.node.portAmmo="other";assert(!wrongAmmo.attempt()&&wrongAmmo.node.solves==0);
Fixture newReport;ERR_HQReport different;newReport.original.Report=&different;assert(!newReport.attempt()&&newReport.node.solves==0);
Fixture nextOrder;nextOrder.battery.OrderId++;assert(!nextOrder.attempt()&&nextOrder.node.solves==0);
Fixture nextShot;nextShot.battery.OrderShots++;assert(!nextShot.attempt()&&nextShot.node.solves==0);
Fixture solverFail;solverFail.node.calculated=nullptr;assert(!solverFail.attempt()&&solverFail.battery.AimedAt==1000);
Fixture charge;charge.next.Charge++;assert(!charge.attempt()&&charge.battery.AimedAt==1000);
Fixture gunMove;gunMove.next.GunOrigin.y=.051f;assert(!gunMove.attempt());
Fixture impactMove;impactMove.next.Impact.y=.101f;assert(!impactMove.attempt());
Fixture aimMove;aimMove.next.AimTarget.y+=.101f;assert(!aimMove.attempt());
Fixture flight;flight.next.FlightTime+=.051f;assert(!flight.attempt());
Fixture invalid;invalid.next.FlightTime=std::numeric_limits<float>::quiet_NaN();assert(!invalid.attempt());
Fixture slowDrift;slowDrift.next.Impact.x+=.06f;assert(slowDrift.attempt());
slowDrift.next.Impact.x+=.06f;assert(!slowDrift.attempt(35000)); // compare original, not last accepted approximation
assert(slowDrift.battery.AimSolution==&slowDrift.original&&slowDrift.original.Impact.x==500);
}
''')
# The tested renewal is used at both existing pre-shot and physical-transfer
# gates; it may neither emit a replacement barrel command nor change a report.
source=firing.read_text();system=(r/'ERR_MortarSystem.c').read_text()
assert source.count('system.AllowShot(battery)')==2
assert 'SetVariableOut' not in body(firing,'bool ERR_RevalidateAim(')
assert 'ObservedAt =' not in source
assert 'now - battery.AimValidatedAt > 10000' in system
assert 'battery.AimSolver.ERR_RevalidateAim(battery, this, report, now)' in system
assert source.count('BallisticTable.GetAimHeightOfProjectileAltitudeFromSource(')==1
assert 'GetAmmoInitialSpeedCoefficients(ammo, coefficients)' in body(firing,'protected ERR_MortarAimSolution ERR_Solve(')
print('PASS: actual aim renewal requires fresh same-report solve and original-command equivalence; native firing untested')