Files
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

150 lines
19 KiB
Python

"""Actual finite service lifecycle methods; controller/physical APIs are fakes."""
from policy_harness import ROOT, body, run
import re
P=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftService.c'
def cpp(s):
s=s.replace('protected ','').replace('return null;', 'return nullptr;')
for a,b in [('ERR_AircraftServiceEntry entry','Entry* entry'),('ERR_AircraftServiceEntry known','Entry* known'),('new ERR_AircraftServiceEntry()','new Entry()'),('ERR_AircraftRouteEnvelope qualification','Qualification* qualification'),('ERR_AircraftRouteFlight flight','RouteFlight* flight'),('ERR_AircraftFieldFlight flight','FieldFlight* flight'),('ERR_AircraftFieldEnvelope qualification','FieldQualification* qualification'),('new ERR_AircraftRouteFlight()','new RouteFlight()'),('new ERR_AircraftFieldFlight()','new FieldFlight()'),('ERR_GameMode mode','Mode* mode'),('ERR_GameMode.Get()','Mode::Get()'),('ERR_AircraftFlightRules.','Rules::'),('ERR_AircraftFlightState.','State::'),('Replication.IsServer()','server'),('string reason','std::string reason'),('vector destination = "0 0 0"','Vec destination')]:s=s.replace(a,b)
s=re.sub(r'foreach \(Entry\* (entry|known) : m_Entries\)',r'for (Entry* \1 : m_Entries.values)',s)
for name in ('entry','known','qualification','GetGame()','m_Mode','m_World','s_Retainers','entry.Flight','flight'):
s=s.replace(name+'.',name+'->')
s=s.replace('entry->Flight.','entry->Flight->').replace('entry->Reason.IsEmpty()','entry->Reason.empty()')
return s.replace('ERR_AircraftServiceEntry BeginAttempt(', 'Entry* BeginAttempt(').replace('ERR_AircraftServiceEntry Find(', 'Entry* Find(').replace('ERR_AircraftServiceEntry entry)', 'Entry* entry)').replace('vector Destination(', 'Vec Destination(').replace('string Reason(', 'std::string Reason(')
keys=['protected bool Context(', 'protected ERR_AircraftServiceEntry Find(', 'bool Register(', 'bool Ready(', 'protected ERR_AircraftServiceEntry BeginAttempt(', 'bool StartRoute(', 'bool StartField(', 'protected bool RecordStart(', 'protected void Finish(', 'bool Cancel(', 'void Tick(', 'void Stop(', 'protected void ReleaseRetainers(', 'bool Drained(', 'bool Stopping(', 'int RegisteredCount(', 'int AttemptedOrders(', 'bool Pending(', 'bool Retired(', 'int OrderToken(', 'float OrderDeadline(', 'int FlightState(', 'string Reason(', 'vector Destination(']
run(r'''
#include <cassert>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <cmath>
bool server=true;
namespace State {enum {IDLE,HOOKS,STARTING,POSITIVE,NEGATIVE,CLIMB,LANDING,SHUTDOWN,COMPLETE,CANCELLED,FAILED,RELINQUISHED,TRANSLATING};}
struct Vec {float x=0,y=0,z=0;};
struct World {float now=1000;float GetWorldTime(){return now;}}world;
struct Game {World* current=&world;World* GetWorld(){return current;}}game;Game* gamePointer=&game;Game* GetGame(){return gamePointer;}
struct Mode {bool active=true;World* world=&::world;static Mode* current;World* GetWorld(){return world;}bool Active(){return active;}static Mode* Get(){return current;}};Mode* Mode::current=nullptr;
struct Rules {static bool Deadline(float now,float deadline){return now>=0&&deadline>now&&deadline-now<=240000;}};
struct Entity {bool original=true,ready=true;int startMode=0,next=State::HOOKS;};
using Vehicle=Entity*;using SCR_AIGroup=Entity*;using IEntity=Entity*;using PilotCompartmentSlot=Entity*;
struct Qualification {bool claimed=false;Vec target={20,0,0};Vec Destination(){return target;}};
struct FieldQualification:Qualification {FieldQualification(){target={100,0,0};}};
int starts=0,claims=0,ticks=0,cancels=0,routeConstructions=0,fieldConstructions=0;
struct Flight {
virtual ~Flight()=default;int kind=0;
Entity* aircraft=nullptr;int token=0,state=State::IDLE;float deadline=0;bool cancelled=false;std::string reason;
bool Finished(){return state>=State::COMPLETE&&state<=State::RELINQUISHED;}
bool StartRoute(Mode*,Vehicle a,SCR_AIGroup,IEntity,PilotCompartmentSlot,Qualification* q,int t,float d){
starts++;aircraft=a;
if(a->startMode==1||q->claimed)return false;
q->claimed=true;claims++;token=t;deadline=d;state=State::HOOKS;
if(a->startMode==2){state=State::FAILED;return false;}
if(a->startMode==3)return false;
return true;
}
void Cancel(std::string r){if(Finished()||cancelled)return;cancelled=true;cancels++;reason=r;state=State::LANDING;}
void Tick(){ticks++;if(!aircraft->original||!Mode::Get()||!Mode::Get()->active){state=State::RELINQUISHED;return;}if(world.now>=deadline){state=State::FAILED;return;}state=aircraft->next;}
int State(){return state;}int OrderToken(){return token;}std::string Reason(){return reason;}
};
struct RouteFlight:Flight {RouteFlight(){kind=1;routeConstructions++;}};
struct FieldFlight:Flight {FieldFlight(){kind=2;fieldConstructions++;}bool StartField(Mode* m,Vehicle a,SCR_AIGroup g,IEntity p,PilotCompartmentSlot s,FieldQualification* q,int t,float d){return StartRoute(m,a,g,p,s,q,t,d);}};
struct Entry {Vehicle Aircraft=nullptr;SCR_AIGroup Group=nullptr;IEntity Pilot=nullptr;PilotCompartmentSlot Seat=nullptr;struct Flight* Flight=nullptr;int Token=0;float Deadline=0;Vec Destination;bool Pending=false,CancelRequested=false,Retired=false;std::string Reason;};
struct Entries {std::vector<Entry*> values;int Count(){return values.size();}void Insert(Entry* e){values.push_back(e);}};
struct Tokens {std::set<int> values;int Count(){return values.size();}bool Contains(int n){return values.count(n);}void Insert(int n){values.insert(n);}};
struct Service;
struct Registry {std::map<Entity*,Service*> values;Service* Get(Entity* a){auto i=values.find(a);return i==values.end()?nullptr:i->second;}void Set(Entity* a,Service* s){values[a]=s;}void Remove(Entity* a){values.erase(a);}}registry;
struct Service {
static const int MAX_AIRCRAFT=4,MAX_ORDERS=128;static Registry* s_Retainers;
Mode* m_Mode;World* m_World=&world;Entries m_Entries;Tokens m_UsedTokens;bool m_Stopping=false;
Service(Mode* m):m_Mode(m){}
bool Original(Entry* e){return Context()&&e&&e->Aircraft&&e->Aircraft->original;}
bool PhysicalReady(Entry* e){return Original(e)&&e->Aircraft->ready;}
''' + '\n'.join(cpp(body(P,k)) for k in keys) + r'''
};Registry* Service::s_Retainers=&registry;
struct Setup {
Mode mode;Entity a[5],g[5],p[5],seat[5];Service service;Qualification q;
Setup():service(&mode){registry.values.clear();server=true;gamePointer=&game;game.current=&world;world.now=1000;Mode::current=&mode;starts=claims=ticks=cancels=routeConstructions=fieldConstructions=0;}
bool Register(int i=0){return service.Register(&a[i],&g[i],&p[i],&seat[i]);}
bool Start(int token=1){return service.StartRoute(&a[0],&q,token,200000);}
};
int main(){
{Setup x;for(int i=0;i<4;i++)assert(x.Register(i));assert(!x.Register(4)&&x.service.RegisteredCount()==4&&starts==0&&claims==0);}
{Setup x;assert(x.Register());assert(!x.Register());Service other(&x.mode);assert(!other.Register(&x.a[0],&x.g[0],&x.p[0],&x.seat[0]));assert(!x.service.Register(&x.a[1],&x.g[1],&x.p[0],&x.seat[1]));assert(!x.service.Register(&x.a[1],&x.g[0],&x.p[1],&x.seat[1]));assert(!x.service.Register(&x.a[1],&x.g[1],&x.p[1],&x.seat[0]));}
{Setup x;x.a[0].ready=false;assert(!x.Register()&&registry.values.empty());x.a[0].ready=true;x.mode.active=false;assert(!x.Register());}
{Setup x;assert(x.Register());Mode other;Mode::current=&other;assert(!x.Start()&&starts==0);Mode::current=&x.mode;World otherWorld;game.current=&otherWorld;assert(!x.Start()&&starts==0);game.current=&world;server=false;assert(!x.Start()&&starts==0);}
{Setup x;assert(x.Register());assert(!x.service.StartRoute(&x.a[0],nullptr,1,200000));assert(!x.Start(0));assert(!x.service.StartRoute(&x.a[0],&x.q,1,world.now));assert(!x.service.StartRoute(&x.a[0],&x.q,1,world.now+240001));assert(!x.service.StartRoute(&x.a[0],&x.q,1,NAN));assert(starts==0&&x.service.AttemptedOrders()==0);}
{Setup x;assert(x.Register());assert(x.Start());assert(starts==1&&claims==1&&x.service.Pending(&x.a[0]));assert(!x.Start()&&!x.Start(2)&&starts==1&&claims==1);assert(x.service.OrderToken(&x.a[0])==1&&x.service.OrderDeadline(&x.a[0])==200000);x.q.target.x=999;assert(x.service.Destination(&x.a[0]).x==20);}
// Cancellation retains the same controller/token/deadline and continues Tick.
{Setup x;assert(x.Register()&&x.Start());Entry* entry=x.service.Find(&x.a[0]);Flight* flight=entry->Flight;x.a[0].next=State::TRANSLATING;x.service.Tick();assert(!x.service.Cancel(&x.a[0],2,"foreign"));assert(x.service.Cancel(&x.a[0],1,"report lost"));assert(!x.service.Cancel(&x.a[0],1,"duplicate"));assert(cancels==1&&entry->Flight==flight&&x.service.Pending(&x.a[0])&&!x.service.Ready(&x.a[0]));x.a[0].next=State::LANDING;x.service.Tick();assert(ticks==2&&entry->Flight==flight&&x.service.OrderDeadline(&x.a[0])==200000);x.a[0].next=State::CANCELLED;x.service.Tick();assert(x.service.Drained()&&!x.service.Retired(&x.a[0])&&x.service.Ready(&x.a[0]));assert(!x.Start());Qualification q;assert(x.service.StartRoute(&x.a[0],&q,2,210000)&&starts==2&&claims==2);}
// Stop is admission/cancellation, not fabricated completion or disposal.
{Setup x;assert(x.Register()&&x.Start());x.service.Stop();x.service.Stop();assert(x.service.Stopping()&&!x.service.Drained()&&cancels==1&&registry.Get(&x.a[0])==&x.service);assert(!x.Register(1)&&!x.Start(2));x.a[0].next=State::LANDING;x.service.Tick();assert(!x.service.Drained()&&registry.Get(&x.a[0])==&x.service);x.a[0].next=State::CANCELLED;x.service.Tick();assert(x.service.Drained()&&registry.values.empty()&&!x.service.Ready(&x.a[0]));}
// Completion must not overrule current seat/health/contact/fuel readiness.
{Setup x;assert(x.Register()&&x.Start());x.a[0].next=State::COMPLETE;x.a[0].ready=false;x.service.Tick();assert(x.service.Drained()&&!x.service.Ready(&x.a[0]));x.a[0].ready=true;assert(x.service.Ready(&x.a[0]));x.a[0].original=false;x.service.Tick();assert(x.service.Retired(&x.a[0])&&!x.service.Ready(&x.a[0]));x.a[0].original=true;assert(!x.service.Ready(&x.a[0]));}
// Native failure/capture and mode teardown never claim a usable returned asset.
for(int state:{State::FAILED,State::RELINQUISHED}){Setup x;assert(x.Register()&&x.Start());x.a[0].next=state;x.service.Tick();assert(x.service.Drained()&&x.service.Retired(&x.a[0])&&!x.service.Ready(&x.a[0]));}
{Setup x;assert(x.Register()&&x.Start());x.mode.active=false;x.service.Tick();assert(x.service.Stopping()&&x.service.Drained()&&x.service.Retired(&x.a[0])&&registry.values.empty());}
// Expiry is owned by the original controller deadline, never renewed by Tick.
{Setup x;assert(x.Register()&&x.Start());world.now=200000;x.service.Tick();assert(x.service.FlightState(&x.a[0])==State::FAILED&&x.service.Retired(&x.a[0])&&x.service.Drained()&&x.service.OrderDeadline(&x.a[0])==200000&&!x.service.Ready(&x.a[0]));}
// Unclaimed/claimed/ambiguous Start failures burn the attempted token; a
// nonterminal partial lease remains retained and canceled until actual result.
for(int fail:{1,2,3}){Setup x;assert(x.Register());x.a[0].startMode=fail;assert(!x.Start());assert(starts==1&&x.service.AttemptedOrders()==1&&x.service.Retired(&x.a[0]));assert(!x.Start(2)&&starts==1);if(fail==3){assert(x.service.Pending(&x.a[0])&&cancels==1);assert(!x.service.Cancel(&x.a[0],1,"duplicate"));x.a[0].next=State::CANCELLED;x.service.Tick();assert(x.service.Drained());}else assert(x.service.Drained());}
{Setup x;assert(x.Register());x.q.claimed=true;assert(!x.Start()&&starts==1&&claims==0&&x.service.AttemptedOrders()==1);}
{Setup x;assert(x.Register());for(int i=1;i<=Service::MAX_ORDERS;i++){Qualification q;assert(x.service.StartRoute(&x.a[0],&q,i,200000));x.a[0].next=State::COMPLETE;x.service.Tick();}assert(x.service.AttemptedOrders()==128);Qualification q;assert(!x.service.StartRoute(&x.a[0],&q,129,200000)&&starts==128);}
// Explicit field kind constructs one known controller, records actual target,
// and shares tokens/reservations with the normal route kind.
{Setup x;assert(x.Register()&&x.Register(1));FieldQualification field;assert(x.service.StartField(&x.a[0],&field,10,241000));Entry* e=x.service.Find(&x.a[0]);Flight* original=e->Flight;assert(original->kind==2&&fieldConstructions==1&&routeConstructions==0&&starts==1&&claims==1);assert(x.service.Destination(&x.a[0]).x==100);field.target.x=999;assert(x.service.Destination(&x.a[0]).x==100);
Qualification normal;FieldQualification another;assert(!x.service.StartRoute(&x.a[0],&normal,11,200000));assert(!x.service.StartRoute(&x.a[1],&normal,10,200000));assert(!x.service.StartField(&x.a[1],&another,10,241000));assert(starts==1&&claims==1);
assert(x.service.Cancel(&x.a[0],10,"field radio loss")&&!x.service.Cancel(&x.a[0],10,"duplicate"));assert(e->Flight==original&&x.service.Pending(&x.a[0])&&cancels==1&&x.service.OrderDeadline(&x.a[0])==241000);
x.a[0].next=State::LANDING;x.service.Tick();assert(e->Flight==original&&x.service.Pending(&x.a[0]));x.a[0].next=State::CANCELLED;x.service.Tick();assert(x.service.Drained()&&!x.service.Retired(&x.a[0]));
assert(!x.service.StartRoute(&x.a[0],&normal,10,200000));assert(x.service.StartRoute(&x.a[0],&normal,11,200000));assert(x.service.Find(&x.a[0])->Flight->kind==1&&routeConstructions==1&&claims==2);
}
// Tokens attempted by normal routes cannot replay as field routes even on
// another ready asset after physical completion.
{Setup x;assert(x.Register()&&x.Register(1)&&x.Start(21));x.a[0].next=State::COMPLETE;x.service.Tick();FieldQualification q;assert(!x.service.StartField(&x.a[1],&q,21,241000)&&fieldConstructions==0);assert(x.service.StartField(&x.a[1],&q,22,241000)&&claims==2);}
{Setup x;assert(x.Register());FieldQualification q;assert(!x.service.StartField(&x.a[0],nullptr,1,241000));assert(!x.service.StartField(&x.a[0],&q,0,241000));assert(!x.service.StartField(&x.a[0],&q,1,NAN));assert(starts==0&&claims==0&&x.service.AttemptedOrders()==0&&fieldConstructions==0);}
// Failed field admission/claim uses exactly the existing retirement and
// retained partial-start protocol. Changing kind cannot create a retry.
for(int fail:{1,2,3}){Setup x;assert(x.Register()&&x.Register(1));x.a[0].startMode=fail;FieldQualification q;assert(!x.service.StartField(&x.a[0],&q,31,241000));assert(x.service.Retired(&x.a[0])&&x.service.AttemptedOrders()==1&&fieldConstructions==1);Qualification route;assert(!x.service.StartRoute(&x.a[0],&route,32,200000)&&!x.service.StartRoute(&x.a[1],&route,31,200000));if(fail==3){Flight* retained=x.service.Find(&x.a[0])->Flight;assert(x.service.Pending(&x.a[0])&&cancels==1);x.a[0].next=State::LANDING;x.service.Tick();assert(x.service.Find(&x.a[0])->Flight==retained&&x.service.Pending(&x.a[0]));x.a[0].next=State::CANCELLED;x.service.Tick();}assert(x.service.Drained()&&x.service.Retired(&x.a[0]));}
{Setup x;assert(x.Register());FieldQualification q;q.claimed=true;assert(!x.service.StartField(&x.a[0],&q,41,241000)&&starts==1&&claims==0&&x.service.Retired(&x.a[0]));}
// Stop drains both kinds together; no early release or additional starts.
{Setup x;assert(x.Register()&&x.Register(1)&&x.Start(51));FieldQualification q;assert(x.service.StartField(&x.a[1],&q,52,241000));Flight* field=x.service.Find(&x.a[1])->Flight;x.service.Stop();x.service.Stop();assert(cancels==2&&x.service.Stopping()&&!x.service.Drained());x.a[0].next=State::CANCELLED;x.a[1].next=State::LANDING;x.service.Tick();assert(!x.service.Drained()&&registry.Get(&x.a[0])==&x.service&&x.service.Find(&x.a[1])->Flight==field);x.a[1].next=State::CANCELLED;x.service.Tick();assert(x.service.Drained()&&registry.values.empty()&&!x.service.Ready(&x.a[1]));}
// One bounded attempt budget covers both kinds without independent counters.
{Setup x;assert(x.Register());for(int i=1;i<=Service::MAX_ORDERS;i++){if(i%2){FieldQualification q;assert(x.service.StartField(&x.a[0],&q,i,241000));}else{Qualification q;assert(x.service.StartRoute(&x.a[0],&q,i,200000));}x.a[0].next=State::COMPLETE;x.service.Tick();}FieldQualification q;assert(!x.service.StartField(&x.a[0],&q,129,241000)&&starts==128&&claims==128&&routeConstructions==64&&fieldConstructions==64);}
}
''')
source=P.read_text()
# Native-boundary inventory: readiness invokes original occupancy/player/world
# checks plus actual fuel, rotor, engine and settled ground predicates.
original=body(P,'protected bool Original(');ready=body(P,'protected bool PhysicalReady(')
for required in ('entry.Seat.GetOccupant() != entry.Pilot','controller.GetPilotCompartmentSlot() != entry.Seat','CompartmentAccessComponent.GetVehicleIn(entry.Pilot) != entry.Aircraft','entry.Group.GetPlayerCount(true) > 0','GetPlayerControlledEntity(id)','entry.Aircraft.GetWorld() != m_World','entry.Pilot.GetWorld() != m_World'):
assert required in original
for required in ('!Original(entry)','!ERR_Actors.CanFight(entry.Pilot)','!fuel.HasFuel()','!damage.GetEngineFunctional()','simulation.RotorGetState(rotor) == RotorState.DESTROYED','!simulation.HasAnyGroundContact()','!physics.IsActive()','controller.IsEngineOn()'):
assert required in ready
assert source.count('new ERR_AircraftRouteFlight()')==1 and source.count('new ERR_AircraftFieldFlight()')==1
assert 'ref ERR_AircraftFlight Flight;' in source
assert source.count('m_UsedTokens.Insert(token)')==1
for signature in ('bool StartRoute(', 'bool StartField('):
method=body(P,signature)
assert method.index('if (!qualification) return false;')<method.index('BeginAttempt(')<method.index('entry.Flight = flight; entry.Pending = true;')<method.index('bool started = flight.')<method.index('RecordStart(entry, started)')
assert '.Claim(' not in source and 'QualifyRoute(' not in source
for prohibited in ('Spawn(', 'Delete(', 'DeleteSpawned(', 'SetOrigin(', 'SetWorldTransform(', 'SetVelocity(', 'SetActive(', 'SetFuel(', 'SetHealth(', 'PrepareActions(', 'PrepareHelicopter(', 'CallLater(', 'GetPlayerCount() == 0'):
assert prohibited not in source
print('PASS: actual finite registration, unique claims/tokens, retention/cancel/stop, readiness gating, capture/expiry retirement and bounded cross-kind order history and typed field/route construction; native boundaries faked')
# The existing route probe sees only its exact subtype; a field controller can
# never be relabeled as route evidence through the shared retained base pointer.
route_probe=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_NativeAircraftRouteProbe.c'
getter=body(route_probe,'ERR_AircraftRouteFlight ERR_NativeOriginalFlight(')
getter=getter.replace('ERR_AircraftRouteFlight.Cast(entry.Flight)','dynamic_cast<Route*>(entry->Flight)').replace('ERR_AircraftRouteFlight ','Route* ').replace('ERR_AircraftServiceEntry entry','Entry* entry').replace('Vehicle aircraft','int aircraft').replace('null','nullptr')
run(r'''#include <cassert>
struct Base {virtual ~Base()=default;};struct Route:Base{};struct Field:Base{};
struct Entry {Base* Flight=nullptr;};
struct Observer {Entry* entry=nullptr;Entry* Find(int){return entry;}
'''+getter+r'''
};
int main(){Observer o;assert(!o.ERR_NativeOriginalFlight(1));Entry e;o.entry=&e;assert(!o.ERR_NativeOriginalFlight(1));Route route;e.Flight=&route;assert(o.ERR_NativeOriginalFlight(1)==&route);Field field;e.Flight=&field;assert(!o.ERR_NativeOriginalFlight(1));}
''')
print('PASS: actual WORKBENCH route observer returns exact retained route subtype and refuses absent/field controllers')