Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
241 lines
10 KiB
Python
241 lines
10 KiB
Python
"""Execute Cessna boarding policy; engine transitions/components are explicit doubles."""
|
|
from policy_harness import ROOT, body, run
|
|
|
|
SOURCE = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_PlaneFlight.c'
|
|
|
|
|
|
def cpp(method):
|
|
# Translate syntax only; the three production decision bodies stay intact.
|
|
method = method.replace('protected ', '')
|
|
method = method.replace(
|
|
'AIControlComponent.Cast(entry.Actor.FindComponent(AIControlComponent))',
|
|
'entry.Actor.Control',
|
|
).replace(
|
|
'CompartmentAccessComponent.Cast(entry.Actor.FindComponent(CompartmentAccessComponent))',
|
|
'entry.Actor.Access',
|
|
)
|
|
for before, after in (
|
|
('ERR_PlaneCrewSeat entry', 'Entry* entry'),
|
|
('AIControlComponent control', 'Control* control'),
|
|
('CompartmentAccessComponent access', 'Access* access'),
|
|
('CompartmentAccessComponent.', 'Access::'),
|
|
('SCR_ChimeraAIAgent.Cast(', 'CharacterAgent::Cast('),
|
|
('SCR_ChimeraAIAgent characterAgent', 'Agent* characterAgent'),
|
|
('SCR_FactionManager.', 'Factions::'),
|
|
('ERR_Actors.', 'Actors::'),
|
|
('ECloseDoorAfterActions.', 'DoorAction::'),
|
|
('string action', 'std::string action'),
|
|
):
|
|
method = method.replace(before, after)
|
|
for name in ('entry', 'Actor', 'Seat', 'Agent', 'access', 'control', 'm_Mode'):
|
|
method = method.replace(name + '.', name + '->')
|
|
return method
|
|
|
|
|
|
methods = '\n'.join(cpp(body(SOURCE, signature)) for signature in (
|
|
'protected bool OwnsCrewSeat(',
|
|
'protected bool CrewSeated(',
|
|
'protected bool PrepareCrewSeat(',
|
|
))
|
|
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
struct Entity;
|
|
struct Seat;
|
|
struct Access;
|
|
struct Agent {
|
|
int lod=2, activations=0, lodWrites=0;
|
|
bool active=false,playerPending=false,character=true;
|
|
std::vector<std::string> calls;
|
|
void SetPermanentLOD(int value) {lod=value;lodWrites++;calls.push_back("LOD");}
|
|
void ActivateAI() {active=true;activations++;calls.push_back("activate");}
|
|
bool IsPlayerPending_S(){return playerPending;}
|
|
};
|
|
struct CharacterAgent {static Agent* Cast(Agent* agent){return agent->character?agent:nullptr;}};
|
|
struct Control {Agent* agent=nullptr;Agent* GetControlAIAgent(){return agent;}};
|
|
struct Entity {
|
|
bool player=false,fit=true;
|
|
int faction=1;
|
|
Entity* group=nullptr;
|
|
struct Control* Control=nullptr;
|
|
struct Access* Access=nullptr;
|
|
};
|
|
struct Seat {
|
|
Entity* owner=nullptr;
|
|
Entity* occupant=nullptr;
|
|
Entity* reservation=nullptr;
|
|
bool occupied=false,accessible=true,locked=false,reserved=false;
|
|
Entity* GetOwner(){return owner;}
|
|
Entity* GetOccupant(){return occupant;}
|
|
bool IsOccupied(){return occupied||occupant;}
|
|
bool IsCompartmentAccessible(){return accessible;}
|
|
bool IsGetInLockedFor(Entity*){return locked;}
|
|
bool IsReserved(){return reserved;}
|
|
bool IsReservedBy(Entity* actor){return reservation==actor;}
|
|
};
|
|
struct DoorAction {enum {INVALID=-1};};
|
|
struct Access {
|
|
Entity* actor=nullptr;
|
|
Entity* vehicle=nullptr;
|
|
Seat* compartment=nullptr;
|
|
Seat* expectedSeat=nullptr;
|
|
bool entering=false,exiting=false,inside=false,accepted=true,synchronousSeat=false;
|
|
int requests=0;
|
|
bool IsGettingIn(){return entering;}
|
|
bool IsGettingOut(){return exiting;}
|
|
bool IsInCompartment(){return inside;}
|
|
Seat* GetCompartment(){return compartment;}
|
|
static Entity* GetVehicleIn(Entity* actor){return actor&&actor->Access?actor->Access->vehicle:nullptr;}
|
|
bool GetInVehicle(Entity* owner,Seat* seat,bool teleport,int door,int close,bool unknown) {
|
|
assert(seat==expectedSeat&&owner==expectedSeat->owner);
|
|
assert(teleport&&door==-1&&close==DoorAction::INVALID&&!unknown);
|
|
Agent* agent=actor->Control->agent;
|
|
assert(agent->active&&agent->lod==0);
|
|
assert(agent->calls.size()>=2&&agent->calls[agent->calls.size()-2]=="LOD"&&agent->calls.back()=="activate");
|
|
agent->calls.push_back("board");requests++;
|
|
if(synchronousSeat) {inside=true;compartment=seat;vehicle=owner;seat->occupant=actor;}
|
|
return accepted;
|
|
}
|
|
};
|
|
struct Actors {
|
|
static bool Player(Entity* actor){return actor->player;}
|
|
static Entity* Group(Entity* actor){return actor->group;}
|
|
static bool CanFight(Entity* actor){return actor&&actor->fit;}
|
|
};
|
|
struct Factions {static int SGetFaction(Entity* actor){return actor->faction;}};
|
|
struct Mode {int EnemyFaction(){return 1;}};
|
|
struct Entry {
|
|
Entity* Actor=nullptr;
|
|
struct Seat* Seat=nullptr;
|
|
struct Agent* Agent=nullptr;
|
|
int Attempts=0;
|
|
float NextAttempt=0;
|
|
};
|
|
struct Flight {
|
|
Mode* m_Mode=nullptr;
|
|
Entity* Crew=nullptr;
|
|
Entity* Aircraft=nullptr;
|
|
std::vector<std::string> actions;
|
|
void LogCrewSeat(Entry*,int,float,std::string action="",bool force=false) {
|
|
if(force)actions.push_back(action);
|
|
}
|
|
''' + methods + r'''
|
|
};
|
|
struct Setup {
|
|
Mode mode;
|
|
Entity aircraft,group,actor,other;
|
|
Agent agent,otherAgent;
|
|
Control control;
|
|
Access access;
|
|
Seat seat,otherSeat;
|
|
Entry entry;
|
|
Flight flight;
|
|
Setup() {
|
|
flight.m_Mode=&mode;flight.Aircraft=&aircraft;flight.Crew=&group;
|
|
actor.group=&group;actor.Control=&control;actor.Access=&access;control.agent=&agent;
|
|
access.actor=&actor;access.expectedSeat=&seat;seat.owner=&aircraft;
|
|
entry.Actor=&actor;entry.Seat=&seat;entry.Agent=&agent;
|
|
}
|
|
bool Tick(float now){return flight.PrepareCrewSeat(&entry,0,now);}
|
|
void Seated(){access.inside=true;access.compartment=&seat;access.vehicle=&aircraft;seat.occupant=&actor;}
|
|
};
|
|
int main() {
|
|
// Native acceptance can leave the actor on foot. All requests, including
|
|
// refused requests, consume the same finite budget with 5s between requests.
|
|
for(bool accepted:{false,true}) {
|
|
Setup s;s.access.accepted=accepted;
|
|
assert(!s.Tick(1000)&&s.entry.Attempts==1&&s.access.requests==1);
|
|
assert(s.entry.NextAttempt==6000&&s.agent.active&&s.agent.lod==0);
|
|
assert(!s.Tick(1001)&&!s.Tick(5999)&&s.access.requests==1);
|
|
assert(!s.Tick(6000)&&s.entry.Attempts==2&&s.entry.NextAttempt==11000);
|
|
assert(!s.Tick(10999)&&s.access.requests==2);
|
|
assert(!s.Tick(11000)&&s.entry.Attempts==3&&s.entry.NextAttempt==16000);
|
|
for(float now:{16000.f,45000.f,1000000.f})assert(!s.Tick(now));
|
|
assert(s.access.requests==3&&s.agent.activations==3&&s.agent.lodWrites==3);
|
|
assert(s.flight.actions==std::vector<std::string>(3,accepted?"boarding accepted":"boarding refused"));
|
|
// Delayed native completion is still accepted after the retry cap.
|
|
s.Seated();assert(s.Tick(1000001)&&s.access.requests==3);
|
|
}
|
|
// Even if a fake native request completes synchronously, that request is not
|
|
// the readiness proof. The next observation must confirm the exact seat.
|
|
{Setup s;s.access.synchronousSeat=true;assert(!s.Tick(1000));assert(s.Tick(1001)&&s.access.requests==1);}
|
|
{Setup s;s.Seated();assert(s.Tick(1000)&&s.access.requests==0&&s.agent.activations==0);}
|
|
|
|
// Each independent native reading must agree before readiness is reported;
|
|
// disagreement must not eject/reboard someone already transitioning/inside.
|
|
for(const auto& fault:std::vector<std::function<void(Setup&)>>{
|
|
[](Setup& s){s.seat.occupant=nullptr;},
|
|
[](Setup& s){s.seat.occupant=&s.other;},
|
|
[](Setup& s){s.access.inside=false;},
|
|
[](Setup& s){s.access.compartment=&s.otherSeat;},
|
|
[](Setup& s){s.access.vehicle=&s.other;},
|
|
[](Setup& s){s.access.vehicle=nullptr;},
|
|
[](Setup& s){s.access.entering=true;},
|
|
[](Setup& s){s.access.exiting=true;},
|
|
[](Setup& s){s.actor.Access=nullptr;},
|
|
}) {
|
|
Setup s;s.Seated();fault(s);
|
|
assert(!s.flight.CrewSeated(&s.entry)&&!s.Tick(10000));
|
|
assert(s.access.requests==0&&s.entry.Attempts==0&&s.agent.activations==0);
|
|
}
|
|
|
|
// Unfit, player-controlled, foreign, replaced or missing actors/components
|
|
// cannot receive another request, regardless of the elapsed retry interval.
|
|
for(const auto& fault:std::vector<std::function<void(Setup&)>>{
|
|
[](Setup& s){s.entry.Actor=nullptr;},
|
|
[](Setup& s){s.actor.player=true;},
|
|
[](Setup& s){s.actor.group=nullptr;},
|
|
[](Setup& s){s.actor.group=&s.other;},
|
|
[](Setup& s){s.actor.faction=2;},
|
|
[](Setup& s){s.actor.Control=nullptr;},
|
|
[](Setup& s){s.control.agent=&s.otherAgent;},
|
|
[](Setup& s){s.control.agent=nullptr;},
|
|
[](Setup& s){s.entry.Agent=nullptr;},
|
|
[](Setup& s){s.agent.playerPending=true;},
|
|
[](Setup& s){s.actor.fit=false;},
|
|
[](Setup& s){s.entry.Seat=nullptr;},
|
|
[](Setup& s){s.actor.Access=nullptr;},
|
|
[](Setup& s){s.access.entering=true;},
|
|
[](Setup& s){s.access.exiting=true;},
|
|
[](Setup& s){s.access.inside=true;s.access.compartment=&s.otherSeat;},
|
|
[](Setup& s){s.access.vehicle=&s.other;},
|
|
[](Setup& s){s.seat.occupied=true;},
|
|
[](Setup& s){s.seat.occupant=&s.other;},
|
|
[](Setup& s){s.seat.accessible=false;},
|
|
[](Setup& s){s.seat.locked=true;},
|
|
[](Setup& s){s.seat.reserved=true;s.seat.reservation=&s.other;},
|
|
}) {
|
|
Setup s;assert(!s.Tick(1000));fault(s);
|
|
assert(!s.Tick(10000)&&!s.Tick(20000));
|
|
assert(s.access.requests==1&&s.entry.Attempts==1&&s.entry.NextAttempt==6000);
|
|
assert(s.agent.activations==1&&s.agent.lodWrites==1&&s.otherAgent.activations==0);
|
|
}
|
|
// Capture/group/control/health changes also prevent already seated readiness.
|
|
for(const auto& fault:std::vector<std::function<void(Setup&)>>{
|
|
[](Setup& s){s.actor.player=true;},
|
|
[](Setup& s){s.actor.group=&s.other;},
|
|
[](Setup& s){s.actor.faction=2;},
|
|
[](Setup& s){s.control.agent=&s.otherAgent;},
|
|
[](Setup& s){s.agent.playerPending=true;},
|
|
[](Setup& s){s.actor.fit=false;},
|
|
}) {
|
|
Setup s;s.Seated();assert(s.Tick(1000));fault(s);assert(!s.Tick(10000)&&s.access.requests==0);
|
|
}
|
|
// Our own reservation is permitted. Native transitions can finish/fail on
|
|
// their own schedule; skipped ticks do not burn attempts or push the deadline.
|
|
{Setup s;s.seat.reserved=true;s.seat.reservation=&s.actor;assert(!s.Tick(1000)&&s.access.requests==1);}
|
|
{Setup s;assert(!s.Tick(1000));s.access.entering=true;assert(!s.Tick(6000)&&!s.Tick(10000));
|
|
assert(s.entry.Attempts==1&&s.entry.NextAttempt==6000);s.access.entering=false;
|
|
assert(!s.Tick(10250)&&s.entry.Attempts==2&&s.entry.NextAttempt==15250);}
|
|
// Pilot and observer have independent request budgets.
|
|
{Setup pilot,observer;for(float now:{1000.f,6000.f,11000.f})assert(!pilot.Tick(now));
|
|
assert(!pilot.Tick(16000)&&!observer.Tick(16000));assert(pilot.access.requests==3&&observer.access.requests==1);}
|
|
}
|
|
''')
|
|
|
|
print('PASS: production Cessna boarding requires exact observed occupancy, activates before requesting, caps retries at three with 5s spacing, and preserves transition, ownership, health and seat restrictions; engine behavior is faked')
|