Add typed field-flight prototype and preserve native qualification failures

This commit is contained in:
2026-09-19 21:01:20 +02:00
parent 83ea247e62
commit 61e165ef5b
91 changed files with 40570 additions and 85 deletions
+52 -8
View File
@@ -5,14 +5,14 @@ P=ROOT/'addons/ai/Scripts/Game/ERR/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'),('new ERR_AircraftRouteFlight()','new Flight()'),('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)
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'):
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 Find(', 'Entry* Find(').replace('ERR_AircraftServiceEntry entry)', 'Entry* entry)').replace('vector Destination(', 'Vec Destination(').replace('string Reason(', 'std::string Reason(')
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(', 'bool StartRoute(', '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(']
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>
@@ -30,8 +30,10 @@ struct Rules {static bool Deadline(float now,float deadline){return now>=0&&dead
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;}};
int starts=0,claims=0,ticks=0,cancels=0;
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){
@@ -46,6 +48,8 @@ struct Flight {
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);}};
@@ -61,7 +65,7 @@ struct Service {
};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=0;}
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);}
};
@@ -88,6 +92,26 @@ int main(){
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()
@@ -98,8 +122,28 @@ for required in ('entry.Seat.GetOccupant() != entry.Pilot','controller.GetPilotC
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
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 order history; native boundaries faked')
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/ai/Scripts/Game/ERR/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')