Wire production aircraft callbacks and diagnose native prison action selection

This commit is contained in:
2026-09-19 20:03:38 +02:00
parent 53eaf3894d
commit f870443b9b
58 changed files with 40247 additions and 27 deletions
@@ -0,0 +1,18 @@
// Only a flight that already owns its qualified airframe can receive controls.
// No allocation, automatic dispatch, input-manager access or periodic work here.
modded class SCR_CharacterControllerComponent
{
override void OnPrepareControls(IEntity owner, ActionManager am, float dt, bool player)
{
ERR_AircraftFlight.DispatchActions(this, owner, am, dt, player);
super.OnPrepareControls(owner, am, dt, player);
}
}
modded class SCR_HelicopterControllerComponent
{
protected override event void OnPrepareControls()
{
super.OnPrepareControls();
ERR_AircraftFlight.DispatchHelicopter(this);
}
}
@@ -1,4 +1,5 @@
// Prepared control mechanism only. No global hooks, force allocation or HQ selection.
// Explicitly started control mechanism. The exact-owner production bridge adds
// no force allocation, flight creation or HQ selection.
enum ERR_AircraftFlightState
{
IDLE, HOOKS, STARTING, POSITIVE, NEGATIVE, CLIMB, LANDING, SHUTDOWN,
@@ -150,11 +151,36 @@ class ERR_AircraftFlight
{
if (!GetGame() || !GetGame().GetWorld() || !GetGame().GetPlayerManager()) return false;
if (!Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active() || !m_Aircraft || !m_Group || !m_Pilot) return false;
if (m_Mode.GetWorld() != GetGame().GetWorld() || m_Aircraft.GetWorld() != m_Mode.GetWorld() || m_Pilot.GetWorld() != m_Mode.GetWorld()) return false;
if (!m_Controller || m_Controller.GetOwner() != m_Aircraft || !m_Seat || m_Controller.GetPilotCompartmentSlot() != m_Seat || m_Seat.GetOccupant() != m_Pilot) return false;
if (CompartmentAccessComponent.GetVehicleIn(m_Pilot) != m_Aircraft || ERR_Actors.Group(m_Pilot) != m_Group || m_Group.GetFaction() != m_Mode.EnemyFaction()) return false;
if (SCR_FactionManager.SGetFaction(m_Aircraft) != m_Mode.EnemyFaction() || !NoPlayers()) return false;
return m_Agent && m_Agent.GetControlledEntity() == m_Pilot && m_Agent.GetActionManager() == m_Actions && m_Actions && m_Actions != GetGame().GetInputManager();
}
// Registry lookup never adopts an aircraft or starts a flight. Recheck the
// original lease/ownership before dispatch, including human capture. The
// character lookup is O(1), based on the callback actor's physical vehicle.
static void DispatchActions(CharacterControllerComponent controller, IEntity owner, ActionManager actions, float dt, bool player)
{
if (!Replication.IsServer() || !s_Owners || !controller || !owner || !actions) return;
Vehicle aircraft = Vehicle.Cast(CompartmentAccessComponent.GetVehicleIn(owner));
if (!aircraft) return;
ERR_AircraftFlight flight = s_Owners.Get(aircraft);
if (!flight || !flight.Lease()) return;
if (!flight.Owns()) { flight.Relinquish("native character callback lost original ownership"); return; }
if (player) return;
flight.PrepareActions(controller, owner, actions, dt);
}
static void DispatchHelicopter(HelicopterControllerComponent controller)
{
if (!Replication.IsServer() || !s_Owners || !controller) return;
Vehicle aircraft = Vehicle.Cast(controller.GetOwner());
if (!aircraft) return;
ERR_AircraftFlight flight = s_Owners.Get(aircraft);
if (!flight || !flight.Lease()) return;
if (!flight.Owns()) { flight.Relinquish("native helicopter callback lost original ownership"); return; }
flight.PrepareHelicopter(controller);
}
protected bool Healthy()
{
if (!ERR_Actors.Alive(m_Aircraft) || !ERR_Actors.CanFight(m_Pilot) || !m_Simulation || !m_Simulation.IsValid() || !m_Fuel || !m_Fuel.HasFuel()) return false;
@@ -170,7 +196,8 @@ class ERR_AircraftFlight
return ERR_AircraftFlightRules.Landed(m_Simulation.HasAnyGroundContact(), transform[3][1] - m_Home[1], m_Simulation.GetVelocity().Length(), m_Simulation.GetAngularVelocity().Length(), transform[1][1]);
}
// Single use. A future HQ owner supplies its immutable unique order token and
// retains this instance, calling Tick and the two scoped callbacks until terminal.
// retains this instance and calls Tick until terminal. The production bridge
// dispatches the two exact-owner native callbacks; callers must not duplicate them.
bool Start(ERR_GameMode mode, Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat, ERR_AircraftFlightEnvelope qualification, int token, float deadline)
{
if (m_StartAttempted || m_State != ERR_AircraftFlightState.IDLE || m_Token != 0 || !Replication.IsServer() || !GetGame() || !qualification || !aircraft || !pilot || !group || !seat) return false;
@@ -144,12 +144,14 @@ class ERR_NativeAircraftControlProbe
void PrepareActions(CharacterControllerComponent controller, IEntity owner, ActionManager am, float dt)
{
if (m_Stopped || !m_Flight || owner != m_Pilot) return;
m_Flight.PrepareActions(controller, owner, am, dt); m_PilotHooks++;
// Observation only: the production bridge owns the sole control dispatch.
m_PilotHooks++;
}
void PrepareHelicopter(HelicopterControllerComponent controller)
{
if (m_Stopped || !m_Flight || controller != m_Controller) return;
m_Flight.PrepareHelicopter(controller); m_HelicopterHooks++;
// Observation only: super-chain order cannot cause a second control write.
m_HelicopterHooks++;
}
protected bool FindPad(out vector result)
{
@@ -130,12 +130,14 @@ class ERR_NativeAircraftRouteProbe
void PrepareActions(CharacterControllerComponent controller, IEntity owner, ActionManager am, float dt)
{
if (m_Stopped || !m_Flight || owner != m_Pilot) return;
m_Flight.PrepareActions(controller, owner, am, dt); m_PilotHooks++;
// Observation only: the production bridge owns the sole control dispatch.
m_PilotHooks++;
}
void PrepareHelicopter(HelicopterControllerComponent controller)
{
if (m_Stopped || !m_Flight || controller != m_Controller) return;
m_Flight.PrepareHelicopter(controller); m_HelicopterHooks++;
// Observation only: super-chain order cannot cause a second control write.
m_HelicopterHooks++;
}
protected bool FindPad(out vector result)
{
@@ -136,6 +136,43 @@ class ERR_OrdinaryGateObserver
return true;
}
protected static string ChoiceIdentity(BaseUserAction action)
{
if (!action) return "none:-1";
// Native class metadata and local action ID only: never action names,
// denial text, player identity or CanBeShown/CanBePerformed callbacks.
return string.Format("%1:%2", action.Type().ToString(), action.GetActionID());
}
protected void ObserveChoices(int index, int now)
{
string choices;
if (!System.GetCLIParam("errOrdinaryGateChoices", choices) || choices != m_Nonce) return;
if (m_Stopped || !Context()) return;
BaseUserAction selected = m_Handler.GetSelectedAction();
bool contextMatch = m_Handler.GetCurrentContext() == m_Context;
array<BaseUserAction> actions = {};
array<bool> permitted = {};
m_Handler.GetFilteredActions(actions, permitted);
string ordered = "";
int shown = actions.Count();
if (shown > 8) shown = 8;
for (int i = 0; i < shown; i++)
{
int cached = -1;
if (i < permitted.Count())
{
cached = 0;
if (permitted[i]) cached = 1;
}
if (i > 0) ordered += ",";
ordered += string.Format("%1=%2:%3", i, ChoiceIdentity(actions[i]), cached);
}
if (!Context()) return;
string detail = string.Format("selected=%1 context=%2 actions=%3 permissions=%4 shown=%5 truncated=%6 ordered=[%7]", ChoiceIdentity(selected), contextMatch, actions.Count(), permitted.Count(), shown, actions.Count() > shown, ordered);
PrintFormat("ERR GATE CHOICE: run=%1 sample=%2 real=%3 %4", m_Nonce, index, now, detail);
}
protected void Observe(ERR_GameMode mode, BaseWorld world, PlayerController controller, IEntity actor, string nonce, int started, int index, int now)
{
if (m_Stopped) return;
@@ -174,6 +211,7 @@ class ERR_OrdinaryGateObserver
PrintFormat("ERR GATE OBSERVE: run=%1 sample=%2 real=%3 %4", m_Nonce, index, now, actionDetail);
string geometry = string.Format("GEOMETRY body=%1 context=%2 radius=%3 doorState=%4 doorControl=%5 prison=%6 gate=%7 gateForward=%8", origin, contextOrigin, radius, state, control, prison, gate, gateForward);
PrintFormat("ERR GATE OBSERVE: run=%1 sample=%2 real=%3 %4", m_Nonce, index, now, geometry);
ObserveChoices(index, now);
m_Index = index; m_Previous = now;
}
}