Improve base guidance and qualify bounded aircraft route and hidden test lifecycle
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// Experimental direct leg only; the original point envelope is unchanged.
|
||||
class ERR_AircraftRouteRules
|
||||
{
|
||||
static bool Length(float distance) { return distance >= 18 && distance <= 20.05; }
|
||||
static vector Nearest(vector start, vector end, vector point)
|
||||
{
|
||||
vector leg = end - start; leg[1] = 0;
|
||||
vector offset = point - start; offset[1] = 0;
|
||||
float squared = vector.Dot(leg, leg);
|
||||
if (!(squared > 0)) return start;
|
||||
return start + leg * Math.Clamp(vector.Dot(offset, leg) / squared, 0, 1);
|
||||
}
|
||||
static bool Contains(vector start, vector end, vector point, float upright)
|
||||
{
|
||||
if (!Length(vector.DistanceXZ(start, end))) return false;
|
||||
float height = point[1] - start[1];
|
||||
return height >= -3 && height <= 18 && upright >= 0.85 && vector.DistanceXZ(point, Nearest(start, end, point)) < 8;
|
||||
}
|
||||
static bool Held(bool contact, float height, float speed, float vertical, float upright, float horizontal, float error)
|
||||
{
|
||||
return !contact && ERR_AircraftFlightRules.Hover(height, speed, vertical, upright) && horizontal >= 0 && horizontal < 0.25 && error >= 0 && error <= 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
// Inherits the exact single-claim identity/clock guard, not caller-supplied validity.
|
||||
class ERR_AircraftRouteEnvelope : ERR_AircraftFlightEnvelope
|
||||
{
|
||||
protected vector m_Destination;
|
||||
vector Destination() { return m_Destination; }
|
||||
bool Contains(vector position, float upright) { return ERR_AircraftRouteRules.Contains(m_Home, m_Destination, position, upright); }
|
||||
// This deliberately qualifies a larger enclosing rectangle, not just endpoints.
|
||||
// Whole-model/rotor radius+8m deviation+1m margin already fits the 40m half-width.
|
||||
static bool Survey(vector start, vector end, array<IEntity> excluded = null)
|
||||
{
|
||||
if (!GetGame() || !GetGame().GetWorld() || !ERR_AircraftRouteRules.Length(vector.DistanceXZ(start, end))) return false;
|
||||
vector lower, upper;
|
||||
for (int axis = 0; axis < 3; axis++)
|
||||
{
|
||||
lower[axis] = Math.Min(start[axis], end[axis]);
|
||||
upper[axis] = Math.Max(start[axis], end[axis]);
|
||||
}
|
||||
// Near-level bare-ground route. Grid covers both pads and every possible
|
||||
// corridor landing centre plus the native gear footprint, at <=2m spacing.
|
||||
float spanX = upper[0] - lower[0] + 44, spanZ = upper[2] - lower[2] + 44;
|
||||
int columns = Math.Ceil(spanX / 2), rows = Math.Ceil(spanZ / 2);
|
||||
if (columns > 34 || rows > 34) return false;
|
||||
for (int x = 0; x <= columns; x++)
|
||||
for (int z = 0; z <= rows; z++)
|
||||
{
|
||||
vector sample = Vector(lower[0] - 22 + spanX * x / columns, 0, lower[2] - 22 + spanZ * z / rows);
|
||||
if (!(Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(sample) - start[1]) <= 0.3)) return false;
|
||||
}
|
||||
vector centre = (lower + upper) * 0.5; centre[1] = start[1];
|
||||
vector half = (upper - lower) * 0.5; half[0] = half[0] + 40; half[2] = half[2] + 40;
|
||||
TraceBox corridor = new TraceBox();
|
||||
corridor.Start = centre + "0 3 0"; corridor.End = centre + "0 60 0";
|
||||
corridor.Mins = Vector(-half[0], -1, -half[2]); corridor.Maxs = Vector(half[0], 1, half[2]);
|
||||
corridor.Flags = TraceFlags.ENTS | TraceFlags.WORLD; corridor.ExcludeArray = excluded;
|
||||
if (!(GetGame().GetWorld().TraceMove(corridor, null) >= 1)) return false;
|
||||
// Separate low object sweep makes intermediate emergency landing ground as
|
||||
// conservative as both endpoints; WORLD is above because the ground is wanted.
|
||||
TraceBox low = new TraceBox(); low.Start = centre + "0 0.5 0"; low.End = centre + "0 3 0";
|
||||
low.Mins = Vector(-half[0], -0.2, -half[2]); low.Maxs = Vector(half[0], 0.2, half[2]);
|
||||
low.Flags = TraceFlags.ENTS; low.ExcludeArray = excluded;
|
||||
return GetGame().GetWorld().TraceMove(low, null) >= 1;
|
||||
}
|
||||
static ERR_AircraftRouteEnvelope QualifyRoute(ERR_GameMode mode, Vehicle aircraft, IEntity pilot, vector destination)
|
||||
{
|
||||
// Existing native qualification supplies health, seat, complete rotor/model
|
||||
// footprint, near-ground and exact original-airframe checks; no bypass.
|
||||
ERR_AircraftFlightEnvelope point = ERR_AircraftFlightEnvelope.Qualify(mode, aircraft, pilot);
|
||||
if (!point) return null;
|
||||
vector start = ERR_Actors.Ground(point.Home());
|
||||
vector end = ERR_Actors.Ground(destination);
|
||||
if (!ERR_AircraftRouteRules.Length(vector.DistanceXZ(start, end)) || !(end[1] >= 2) || !(Math.AbsFloat(end[1] - start[1]) <= 0.3)) return null;
|
||||
array<IEntity> excluded = {};
|
||||
if (!Hierarchy(aircraft, excluded) || !Hierarchy(pilot, excluded)) return null;
|
||||
if (!ClearColumn(aircraft, pilot, end) || !Survey(start, end, excluded)) return null;
|
||||
ERR_AircraftRouteEnvelope record = new ERR_AircraftRouteEnvelope();
|
||||
record.m_Mode = mode; record.m_Aircraft = aircraft; record.m_Pilot = pilot;
|
||||
record.m_Home = point.Home(); record.m_Radius = point.Radius();
|
||||
record.m_Destination = end; record.m_Destination[1] = record.m_Home[1];
|
||||
record.m_QualifiedAt = GetGame().GetWorld().GetWorldTime();
|
||||
return record;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
// Experimental immutable 18..20m direct leg. No HQ selection or global dispatch.
|
||||
// Inheritance deliberately shares the proven controller's exclusive airframe
|
||||
// registry, exact native ownership/health, signed-input startup and release paths.
|
||||
class ERR_AircraftRouteFlight : ERR_AircraftFlight
|
||||
{
|
||||
protected ref ERR_AircraftRouteEnvelope m_Route;
|
||||
protected vector m_LandingPoint;
|
||||
protected bool m_RouteArrived;
|
||||
bool Arrived() { return m_RouteArrived; }
|
||||
vector Destination() { vector destination = "0 0 0"; if (m_Route) destination = m_Route.Destination(); return destination; }
|
||||
vector LandingPoint() { return m_LandingPoint; }
|
||||
bool StartRoute(ERR_GameMode mode, Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat, ERR_AircraftRouteEnvelope route, int token, float deadline)
|
||||
{
|
||||
if (m_StartAttempted || m_Route || !route || !ERR_AircraftRouteRules.Length(vector.DistanceXZ(route.Home(), route.Destination()))) return false;
|
||||
m_Route = route; m_TranslationRequested = true; m_TranslationTarget = route.Destination();
|
||||
vector home = route.Home(); m_TranslationTarget[1] = home[1] + 8;
|
||||
m_LandingPoint = route.Home();
|
||||
return super.Start(mode, aircraft, group, pilot, seat, route, token, deadline);
|
||||
}
|
||||
// A derived controller cannot be started through the point-only public APIs.
|
||||
override bool Start(ERR_GameMode mode, Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat, ERR_AircraftFlightEnvelope qualification, int token, float deadline) { return false; }
|
||||
override bool StartTranslation(ERR_GameMode mode, Vehicle aircraft, SCR_AIGroup group, IEntity pilot, PilotCompartmentSlot seat, ERR_AircraftFlightEnvelope qualification, int token, float deadline, vector target) { return false; }
|
||||
protected bool RouteContains(vector position, float upright)
|
||||
{
|
||||
if (!m_Route || !m_Route.Contains(position, upright)) return false;
|
||||
// Until the command actually enters translation, retain the original pad cap.
|
||||
if (m_State < ERR_AircraftFlightState.LANDING && !m_TranslationStarted)
|
||||
return vector.DistanceXZ(position, m_Home) < 8;
|
||||
return true;
|
||||
}
|
||||
protected override bool Grounded()
|
||||
{
|
||||
if (!super.Grounded()) return false;
|
||||
if (m_State == ERR_AircraftFlightState.LANDING || m_State == ERR_AircraftFlightState.SHUTDOWN)
|
||||
return vector.DistanceXZ(m_Aircraft.GetOrigin(), m_LandingPoint) <= 0.75;
|
||||
return true;
|
||||
}
|
||||
protected override bool Landing(float now)
|
||||
{
|
||||
if (!Owns() || !m_Route || !m_Route.Contains(m_Aircraft.GetOrigin(), 1)) { Fail("route landing ownership or corridor unavailable"); return false; }
|
||||
// Normal destination is immutable. Cancellation freezes the actual landing
|
||||
// centre once inside the fully qualified corridor, never rebases the route.
|
||||
if (m_Cancelled || !m_RouteArrived) m_LandingPoint = m_Aircraft.GetOrigin();
|
||||
else m_LandingPoint = m_Route.Destination();
|
||||
m_LandingPoint[1] = m_Home[1];
|
||||
return super.Landing(now);
|
||||
}
|
||||
protected override bool BeginTranslation(float now, vector position)
|
||||
{
|
||||
if (!m_Route || !m_TranslationRequested || m_TranslationStarted || m_State != ERR_AircraftFlightState.CLIMB) { Cancel("route stage ownership refused"); return false; }
|
||||
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
|
||||
if (!signals || signals.FindSignal("MainRotorCyclicForward") < 0 || signals.FindSignal("MainRotorCyclicAside") < 0 || !m_Controller.GetAutohoverEnabled()) { Cancel("native route cyclic signals unavailable"); return false; }
|
||||
m_TranslationOrigin = position; m_TranslationDistance = vector.DistanceXZ(position, m_Route.Destination());
|
||||
m_TranslationStarted = true; m_State = ERR_AircraftFlightState.TRANSLATING;
|
||||
m_Stable = -1; m_StageDeadline = Math.Min(m_Deadline, now + 160000);
|
||||
PrintFormat("ERR AIRCRAFT ROUTE BEGIN: token=%1 home=%2 destination=%3 actualOrigin=%4 distance=%5 deadline=%6", m_Token, m_Home, m_Route.Destination(), position, m_TranslationDistance, m_StageDeadline);
|
||||
return true;
|
||||
}
|
||||
protected override void CheckTranslation(float now, vector position, float height, float speed, float upright)
|
||||
{
|
||||
if (!m_Route || !m_TranslationStarted || !m_Controller.GetAutohoverEnabled()) { Cancel("route ownership or autohover lost"); return; }
|
||||
vector velocity = m_Simulation.GetVelocity(); vector horizontal = velocity; horizontal[1] = 0;
|
||||
bool held = m_TranslationUpdates >= 3 && ERR_AircraftRouteRules.Held(m_Simulation.HasAnyGroundContact(), height, speed, velocity[1], upright, horizontal.Length(), vector.DistanceXZ(position, m_Route.Destination()));
|
||||
if (!held) { m_Stable = -1; return; }
|
||||
if (m_Stable < 0) m_Stable = now;
|
||||
if (now - m_Stable < 5000) return;
|
||||
m_RouteArrived = true; m_TranslationProven = true;
|
||||
PrintFormat("ERR AIRCRAFT ROUTE HELD: token=%1 targetError=%2 displacement=%3 updates=%4", m_Token, vector.DistanceXZ(position, m_Route.Destination()), vector.DistanceXZ(position, m_TranslationOrigin), m_TranslationUpdates);
|
||||
Landing(now);
|
||||
}
|
||||
protected override bool ControlLease()
|
||||
{
|
||||
if (!Lease()) return false;
|
||||
if (!Owns()) { Relinquish("ownership or world unavailable at control callback"); return false; }
|
||||
float now = Now();
|
||||
if (now < 0 || now >= m_Deadline || now >= m_StageDeadline) { Fail("control callback reached bounded flight deadline"); return false; }
|
||||
if (!Healthy() || !m_Readback) { Fail("flight capability or input readback lost"); return false; }
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
if (!RouteContains(transform[3], transform[1][1])) { Fail("control callback exceeded qualified flight envelope"); return false; }
|
||||
return true;
|
||||
}
|
||||
override void PrepareActions(CharacterControllerComponent controller, IEntity owner, ActionManager actions, float dt)
|
||||
{
|
||||
if (!Lease() || controller != m_Character || owner != m_Pilot || actions != m_Actions) return;
|
||||
if (!ControlLease()) return;
|
||||
if (!m_Readback || dt <= 0 || dt > 0.2) return;
|
||||
if (!actions.IsContextActive("HelicopterContext")) actions.ActivateContext("HelicopterContext", 1);
|
||||
if (!actions.IsContextActive("HelicopterContext")) { m_Readback = false; return; }
|
||||
float collective, torque, forward, aside;
|
||||
if (m_State == ERR_AircraftFlightState.POSITIVE) collective = 0.05;
|
||||
else if (m_State == ERR_AircraftFlightState.NEGATIVE) collective = -0.05;
|
||||
else if (m_State == ERR_AircraftFlightState.SHUTDOWN) collective = -1;
|
||||
else if (m_State == ERR_AircraftFlightState.CLIMB || m_State == ERR_AircraftFlightState.LANDING || m_State == ERR_AircraftFlightState.TRANSLATING)
|
||||
{
|
||||
if (!m_Controller.GetAutohoverEnabled()) { Fail("native autohover lost while flying"); return; }
|
||||
float height = m_Aircraft.GetOrigin()[1] - m_Home[1];
|
||||
float error = ERR_AircraftFlightRules.Vertical(height, m_State == ERR_AircraftFlightState.LANDING) - m_Simulation.GetVelocity()[1];
|
||||
m_Collective = Math.Clamp(m_Collective + error * dt * 0.12, -0.95, 0.8);
|
||||
collective = Math.Clamp(m_Collective + error * 0.12, -1, 0.9);
|
||||
float headingError = m_Heading - m_Aircraft.GetYawPitchRoll()[0];
|
||||
while (headingError > 180) headingError -= 360;
|
||||
while (headingError < -180) headingError += 360;
|
||||
torque = Math.Clamp(headingError * 0.01 - m_Simulation.GetAngularVelocityLocal()[1] * 0.3, -0.3, 0.3);
|
||||
if (m_State == ERR_AircraftFlightState.TRANSLATING || m_State == ERR_AircraftFlightState.LANDING)
|
||||
{
|
||||
if (!m_Route) { Fail("immutable route unavailable"); return; }
|
||||
vector target = m_Route.Destination(); if (m_State == ERR_AircraftFlightState.LANDING) target = m_LandingPoint;
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
vector demand = ERR_AircraftTranslationRules.Demand(target, transform[3], m_Simulation.GetVelocity());
|
||||
forward = Math.Clamp(vector.Dot(demand, transform[2]), -0.03, 0.03);
|
||||
aside = Math.Clamp(vector.Dot(demand, transform[0]), -0.03, 0.03);
|
||||
if (m_State == ERR_AircraftFlightState.TRANSLATING) m_TranslationUpdates++;
|
||||
}
|
||||
if (m_State == ERR_AircraftFlightState.LANDING && Grounded()) { collective = -1; forward = 0; aside = 0; }
|
||||
}
|
||||
bool a = Pair("HelicopterCollectiveIncrease", "HelicopterCollectiveDecrease", collective);
|
||||
bool b = Pair("HelicopterCyclicForward", "HelicopterCyclicBack", forward);
|
||||
bool c = Pair("HelicopterCyclicRight", "HelicopterCyclicLeft", aside);
|
||||
bool d = Pair("HelicopterAntiTorqueRight", "HelicopterAntiTorqueLeft", torque);
|
||||
m_Readback = a && b && c && d; m_ActionUpdates++; m_LastControl = Now();
|
||||
}
|
||||
override void Tick()
|
||||
{
|
||||
if (!Lease()) return;
|
||||
float now = Now();
|
||||
if (!Owns()) { Relinquish("ownership or mission changed"); return; }
|
||||
if (!Healthy()) { Fail("flight capability lost"); return; }
|
||||
if (!m_Readback) { Fail("native action readback failed"); return; }
|
||||
if (now >= m_Deadline || now >= m_StageDeadline || now < 0) { Fail("bounded flight stage expired"); return; }
|
||||
if (m_LastTick >= 0 && (now < m_LastTick || now - m_LastTick > 1500)) m_Stable = -1;
|
||||
m_LastTick = now;
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
float height = transform[3][1] - m_Home[1], speed = m_Simulation.GetVelocity().Length();
|
||||
if (!RouteContains(transform[3], transform[1][1])) { Fail("qualified flight envelope exceeded"); return; }
|
||||
if (m_State == ERR_AircraftFlightState.HOOKS)
|
||||
{
|
||||
if (m_ActionUpdates < 3 || m_HelicopterUpdates < 3) return;
|
||||
if (m_Controller.GetCollectiveMode() != CollectiveMode.Direct || m_Input.GetVehicleCollective() != 0 || m_Controller.IsEngineOn()) { Fail("native initial control state refused"); return; }
|
||||
m_Controller.StartEngine(); m_State = ERR_AircraftFlightState.STARTING; m_StageDeadline = Math.Min(m_Deadline, now + 65000); return;
|
||||
}
|
||||
if (m_LastControl < 0 || now - m_LastControl > 1000) { Fail("pilot callback stopped"); return; }
|
||||
if (m_State == ERR_AircraftFlightState.STARTING)
|
||||
{
|
||||
float rpm = m_Simulation.RotorGetRPMTarget(0);
|
||||
if (!m_Controller.IsEngineOn() || rpm <= 0 || m_Simulation.RotorGetRPM(0) < rpm * 0.9 || m_Fuel.GetTotalFuel() >= m_InitialFuel) return;
|
||||
m_State = ERR_AircraftFlightState.POSITIVE; m_StageDeadline = Math.Min(m_Deadline, now + 3000); m_GateUpdates = m_ActionUpdates; m_Stable = -1; return;
|
||||
}
|
||||
if (m_State != ERR_AircraftFlightState.SHUTDOWN && !m_Controller.IsEngineOn()) { Fail("running engine lost"); return; }
|
||||
if (m_State == ERR_AircraftFlightState.POSITIVE || m_State == ERR_AircraftFlightState.NEGATIVE)
|
||||
{
|
||||
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
|
||||
int index = -1; if (signals) index = signals.FindSignal("MainRotorCollective");
|
||||
if (index < 0 || m_Controller.GetCollectiveMode() != CollectiveMode.Direct || m_Controller.GetAutohoverEnabled()) { Fail("native collective proof unavailable"); return; }
|
||||
float expected = 0.05; if (m_State == ERR_AircraftFlightState.NEGATIVE) expected = -0.05;
|
||||
bool matched = m_ActionUpdates >= m_GateUpdates + 3 && Math.AbsFloat(signals.GetSignalValue(index) - expected) <= 0.01 && Math.AbsFloat(m_Input.GetVehicleCollective() - expected) <= 0.01;
|
||||
if (!matched) { m_Stable = -1; return; }
|
||||
if (m_Stable < 0) m_Stable = now;
|
||||
if (now - m_Stable < 500) return;
|
||||
m_Stable = -1;
|
||||
if (m_State == ERR_AircraftFlightState.POSITIVE) { m_State = ERR_AircraftFlightState.NEGATIVE; m_StageDeadline = Math.Min(m_Deadline, now + 3000); m_GateUpdates = m_ActionUpdates; return; }
|
||||
m_Controller.SetAutohoverEnabled(true);
|
||||
if (!m_Controller.GetAutohoverEnabled()) { Fail("native autohover enable refused"); return; }
|
||||
m_State = ERR_AircraftFlightState.CLIMB; m_StageDeadline = Math.Min(m_Deadline, now + 65000); return;
|
||||
}
|
||||
if (m_State == ERR_AircraftFlightState.TRANSLATING) { CheckTranslation(now, transform[3], height, speed, transform[1][1]); return; }
|
||||
if (m_State == ERR_AircraftFlightState.CLIMB || m_State == ERR_AircraftFlightState.LANDING)
|
||||
{
|
||||
if (!m_Controller.GetAutohoverEnabled()) { Fail("native autohover lost"); return; }
|
||||
bool stable = Grounded(); float duration = 2000;
|
||||
if (m_State == ERR_AircraftFlightState.CLIMB) { stable = !m_Simulation.HasAnyGroundContact() && ERR_AircraftFlightRules.Hover(height, speed, m_Simulation.GetVelocity()[1], transform[1][1]); duration = 5000; }
|
||||
if (!stable) m_Stable = -1;
|
||||
if (stable && m_Stable < 0) m_Stable = now;
|
||||
if (m_Stable < 0 || now - m_Stable < duration) return;
|
||||
if (m_State == ERR_AircraftFlightState.CLIMB)
|
||||
{
|
||||
if (m_TranslationRequested) BeginTranslation(now, transform[3]);
|
||||
else Landing(now);
|
||||
return;
|
||||
}
|
||||
Shutdown(now); return;
|
||||
}
|
||||
if (m_State == ERR_AircraftFlightState.SHUTDOWN && !m_Controller.IsEngineOn())
|
||||
{
|
||||
if (!Grounded()) { Fail("engine-off physical ground check failed"); return; }
|
||||
if (m_Cancelled) m_State = ERR_AircraftFlightState.CANCELLED;
|
||||
else m_State = ERR_AircraftFlightState.COMPLETE;
|
||||
Release(true); RecordState();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
#ifdef WORKBENCH
|
||||
// Separate opt-in experimental route fixture; original aircraft outcomes unchanged.
|
||||
// Full terrain/clearance refusal is retained; no operational dispatch is installed.
|
||||
modded class ERR_AircraftRouteFlight
|
||||
{
|
||||
float ERR_NativeRouteStageDeadline() { return m_StageDeadline; }
|
||||
}
|
||||
modded class SCR_CharacterControllerComponent
|
||||
{
|
||||
override void OnPrepareControls(IEntity owner, ActionManager am, float dt, bool player)
|
||||
{
|
||||
ERR_NativeAircraftRouteProbe probe = ERR_NativeAircraftRouteProbe.Current();
|
||||
if (probe && !player) probe.PrepareActions(this, owner, am, dt);
|
||||
super.OnPrepareControls(owner, am, dt, player);
|
||||
}
|
||||
}
|
||||
modded class SCR_HelicopterControllerComponent
|
||||
{
|
||||
protected override event void OnPrepareControls()
|
||||
{
|
||||
super.OnPrepareControls();
|
||||
ERR_NativeAircraftRouteProbe probe = ERR_NativeAircraftRouteProbe.Current();
|
||||
if (probe) probe.PrepareHelicopter(this);
|
||||
}
|
||||
}
|
||||
class ERR_NativeAircraftRouteRules
|
||||
{
|
||||
static bool Terminal(bool cancellation, int state, int cancellationCount)
|
||||
{
|
||||
if (cancellation) return state == ERR_AircraftFlightState.CANCELLED && cancellationCount == 1;
|
||||
return state == ERR_AircraftFlightState.COMPLETE && cancellationCount == 0;
|
||||
}
|
||||
static bool TranslateProof(bool proven, bool held, float requestedDistance, float displacement, float targetError)
|
||||
{
|
||||
return proven && held && requestedDistance >= 10 && requestedDistance <= 28.05 && displacement >= requestedDistance - 0.75 && targetError >= 0 && targetError <= 0.75;
|
||||
}
|
||||
static bool PhysicalFinish(bool originalOccupancy, bool landed, bool engineOff, float fuelUsed, int wakes, int starts, int controllerStarts, int pilotHooks, int helicopterHooks)
|
||||
{
|
||||
if (!originalOccupancy || !landed || !engineOff) return false;
|
||||
return fuelUsed > 0.05 && wakes == 1 && starts == 1 && controllerStarts == 1 && pilotHooks > 100 && helicopterHooks > 100;
|
||||
}
|
||||
static bool CanDispose(bool context, bool noPlayers, bool sameFaction, bool groupUnchanged, bool pilotUnchanged)
|
||||
{
|
||||
return context && noPlayers && sameFaction && groupUnchanged && pilotUnchanged;
|
||||
}
|
||||
}
|
||||
class ERR_NativeAircraftRouteProbe
|
||||
{
|
||||
protected static ERR_NativeAircraftRouteProbe s_Current;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected Vehicle m_Aircraft;
|
||||
protected SCR_AIGroup m_Group;
|
||||
protected IEntity m_Pilot;
|
||||
protected PilotCompartmentSlot m_Seat;
|
||||
protected HelicopterControllerComponent m_Controller;
|
||||
protected VehicleHelicopterSimulation m_Simulation;
|
||||
protected FuelManagerComponent m_Fuel;
|
||||
protected ref ERR_AircraftRouteEnvelope m_Envelope;
|
||||
protected ref ERR_AircraftRouteFlight m_Flight;
|
||||
protected float m_Deadline;
|
||||
protected float m_StageDeadline;
|
||||
protected float m_Stable = -1;
|
||||
protected float m_LastTick = -1;
|
||||
protected float m_NextLog;
|
||||
protected float m_InitialFuel;
|
||||
protected float m_ControlDeadline;
|
||||
protected int m_CancelCount;
|
||||
protected bool m_CancelFixture;
|
||||
protected bool m_TranslationFixture;
|
||||
protected bool m_TranslationObserved;
|
||||
protected bool m_TranslationHeld;
|
||||
protected vector m_TranslationOrigin;
|
||||
protected vector m_TranslationTarget;
|
||||
protected float m_TranslationStable = -1;
|
||||
protected vector m_Pad;
|
||||
protected vector m_Destination;
|
||||
protected int m_Step;
|
||||
protected int m_Wakes;
|
||||
protected int m_ControllerStarts;
|
||||
protected int m_EngineStarts;
|
||||
protected int m_PilotHooks;
|
||||
protected int m_HelicopterHooks;
|
||||
protected bool m_LastEngine;
|
||||
protected bool m_AllocationRequested;
|
||||
protected bool m_PilotRequested;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Complete;
|
||||
static const int ORDER_TOKEN = 771001;
|
||||
static const ResourceName AIRCRAFT = "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et";
|
||||
static const ResourceName GROUP = "{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et";
|
||||
static const ResourceName PILOT = "{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et";
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "aircraft-route";
|
||||
}
|
||||
static ERR_NativeAircraftRouteProbe Current() { return s_Current; }
|
||||
void ERR_NativeAircraftRouteProbe(ERR_GameMode mode)
|
||||
{
|
||||
m_Mode = mode;
|
||||
if (!GetGame() || !GetGame().GetWorld() || !Enabled() || s_Current) { m_Stopped = true; return; }
|
||||
string outcome; System.GetCLIParam("errSmokeOutcome", outcome); m_CancelFixture = false; m_TranslationFixture = true;
|
||||
m_Deadline = GetGame().GetWorld().GetWorldTime() + 300000;
|
||||
s_Current = this;
|
||||
}
|
||||
protected bool Check(bool passed, string detail)
|
||||
{
|
||||
string outcome = "aircraft-route";
|
||||
if (passed) { Print("ERR SMOKE PASS: " + outcome + " " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: " + outcome + " " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected bool NoPlayers()
|
||||
{
|
||||
if (!GetGame() || !GetGame().GetPlayerManager()) return false;
|
||||
if (m_Group && m_Group.GetPlayerCount(true) > 0) return false;
|
||||
array<int> players = {}; GetGame().GetPlayerManager().GetPlayers(players);
|
||||
foreach (int id : players)
|
||||
{
|
||||
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (actor && (actor == m_Pilot || actor == m_Aircraft || (m_Aircraft && CompartmentAccessComponent.GetVehicleIn(actor) == m_Aircraft))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool OriginalOccupancy()
|
||||
{
|
||||
if (!m_Pilot || !m_Group || !m_Aircraft || !m_Controller || !m_Seat || !NoPlayers()) return false;
|
||||
if (m_Group.GetAgentsCount() != 1 || ERR_Actors.Group(m_Pilot) != m_Group || !ERR_Actors.CanFight(m_Pilot)) return false;
|
||||
return m_Seat.GetOccupant() == m_Pilot && m_Controller.GetPilotCompartmentSlot() == m_Seat && CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft;
|
||||
}
|
||||
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++;
|
||||
}
|
||||
void PrepareHelicopter(HelicopterControllerComponent controller)
|
||||
{
|
||||
if (m_Stopped || !m_Flight || controller != m_Controller) return;
|
||||
m_Flight.PrepareHelicopter(controller); m_HelicopterHooks++;
|
||||
}
|
||||
protected bool FindPad(out vector result)
|
||||
{
|
||||
vector anchor = m_Mode.ExtractionVehicle().GetOrigin();
|
||||
float bestSlope = float.MAX; vector bestCandidate, refinementAnchor;
|
||||
int localCandidates = ERR_PoweredFlightRules.PAD_CANDIDATES + ERR_PoweredFlightRules.PAD_REFINEMENTS;
|
||||
for (int index = 0; index < localCandidates + ERR_PoweredFlightRules.AIRPORT_CANDIDATES; index++)
|
||||
{
|
||||
if (index == ERR_PoweredFlightRules.PAD_CANDIDATES)
|
||||
{
|
||||
refinementAnchor = anchor; if (bestSlope != float.MAX) refinementAnchor = bestCandidate;
|
||||
}
|
||||
vector candidate;
|
||||
if (index < ERR_PoweredFlightRules.PAD_CANDIDATES) candidate = ERR_Actors.Ground(anchor + ERR_PoweredFlightRules.PadOffset(index));
|
||||
else if (index < localCandidates) candidate = ERR_Actors.Ground(refinementAnchor + ERR_PoweredFlightRules.RefineOffset(index - ERR_PoweredFlightRules.PAD_CANDIDATES));
|
||||
else candidate = ERR_Actors.Ground("4900.953 0 11787.412" + ERR_PoweredFlightRules.AirportOffset(index - localCandidates));
|
||||
if (candidate[1] < 2) continue;
|
||||
float delta;
|
||||
for (int edge = 0; edge < 4; edge++)
|
||||
{
|
||||
float side = edge * Math.PI / 2;
|
||||
vector point = candidate + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
|
||||
delta = Math.Max(delta, Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(point) - candidate[1]));
|
||||
}
|
||||
if (delta < bestSlope) { bestSlope = delta; bestCandidate = candidate; }
|
||||
if (delta > 0.8) continue;
|
||||
if (!SCR_WorldTools.TraceCilinderUtil(candidate + "0 2.5 0", 40, 4, TraceFlags.ENTS)) continue;
|
||||
TraceBox clearance = new TraceBox(); clearance.Start = candidate + "0 3 0"; clearance.End = candidate + "0 60 0";
|
||||
clearance.Mins = "-40 -1 -40"; clearance.Maxs = "40 1 40"; clearance.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
if (!(GetGame().GetWorld().TraceMove(clearance, null) >= 1)) continue;
|
||||
PrintFormat("ERR AIRCRAFT ROUTE FIXTURE PAD: candidate=%1 tested=%2 terrainDelta=%3; no fallback", candidate, index + 1, delta);
|
||||
vector destination = ERR_Actors.Ground(candidate + "20 0 0");
|
||||
if (!ERR_AircraftRouteEnvelope.Survey(candidate, destination)) continue;
|
||||
PrintFormat("ERR AIRCRAFT ROUTE FIXTURE CORRIDOR: acceptedStart=%1 destination=%2 tested=%3; complete survey passed", candidate, destination, index + 1);
|
||||
m_Destination = destination; result = candidate; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected bool ReadyComponents()
|
||||
{
|
||||
m_Controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
|
||||
if (!m_Controller) return false;
|
||||
m_Simulation = m_Controller.GetSimulation();
|
||||
m_Fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
|
||||
if (!m_Simulation || !m_Simulation.IsValid() || !m_Fuel || !m_Fuel.HasFuel()) return false;
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Aircraft.FindComponent(BaseCompartmentManagerComponent));
|
||||
if (!manager) return false;
|
||||
array<BaseCompartmentSlot> slots = {}; manager.GetCompartments(slots);
|
||||
if (slots.Count() > 64) return false;
|
||||
foreach (BaseCompartmentSlot slot : slots)
|
||||
{
|
||||
PilotCompartmentSlot pilotSlot = PilotCompartmentSlot.Cast(slot);
|
||||
if (!pilotSlot || pilotSlot.GetOwner() != m_Aircraft || pilotSlot.GetVehicle() != m_Aircraft || pilotSlot.GetCompartmentName() != "PilotCompartment") continue;
|
||||
if (pilotSlot.GetType() != ECompartmentType.PILOT) continue;
|
||||
if (pilotSlot.GetOccupant() || pilotSlot.IsReserved() || pilotSlot.IsGetInLocked() || !pilotSlot.IsCompartmentAccessible()) continue;
|
||||
m_Seat = pilotSlot; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected void Diagnostic(float now)
|
||||
{
|
||||
if (!m_Aircraft || !m_Controller || !m_Simulation || now < m_NextLog) return;
|
||||
m_NextLog = now + 2000;
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
vector home = m_Pad; if (m_Envelope) home = m_Envelope.Home();
|
||||
int state = -1; string reason; if (m_Flight) { state = m_Flight.State(); reason = m_Flight.Reason(); }
|
||||
PrintFormat("ERR AIRCRAFT ROUTE FIXTURE: step=%1 state=%2 height=%3 drift=%4 speed=%5 contact=%6 engine=%7 fuelUsed=%8 reason=%9", m_Step, state, transform[3][1] - home[1], vector.DistanceXZ(transform[3], home), m_Simulation.GetVelocity().Length(), m_Simulation.HasAnyGroundContact(), m_Controller.IsEngineOn(), m_InitialFuel - m_Fuel.GetTotalFuel(), reason);
|
||||
if (!m_Flight || !m_Envelope) return;
|
||||
vector destination = m_Envelope.Destination();
|
||||
vector leg = destination - home; leg[1] = 0;
|
||||
float length = leg.Length(); if (!(length > 0)) return;
|
||||
vector offset = transform[3] - home; offset[1] = 0;
|
||||
float along = vector.Dot(offset, leg * (1 / length));
|
||||
float cross = vector.DistanceXZ(transform[3], ERR_AircraftRouteRules.Nearest(home, destination, transform[3]));
|
||||
vector target = destination; if (state == ERR_AircraftFlightState.LANDING || state == ERR_AircraftFlightState.SHUTDOWN) target = m_Flight.LandingPoint();
|
||||
vector velocity = m_Simulation.GetVelocity(); vector demand;
|
||||
if (state == ERR_AircraftFlightState.TRANSLATING || state == ERR_AircraftFlightState.LANDING)
|
||||
demand = ERR_AircraftTranslationRules.Demand(target, transform[3], velocity);
|
||||
float forward = Math.Clamp(vector.Dot(demand, transform[2]), -0.03, 0.03);
|
||||
float aside = Math.Clamp(vector.Dot(demand, transform[0]), -0.03, 0.03);
|
||||
PrintFormat("ERR AIRCRAFT ROUTE MOTION: token=%1 position=%2 velocity=%3 along=%4 cross=%5 targetError=%6 computedDemand=%7 projectedForward=%8 projectedAside=%9", m_Flight.OrderToken(), transform[3], velocity, along, cross, vector.DistanceXZ(transform[3], target), demand, forward, aside);
|
||||
PrintFormat("ERR AIRCRAFT ROUTE BASIS: forward=%1 right=%2 up=%3 target=%4 stageRemaining=%5 overallRemaining=%6 now=%7", transform[2], transform[0], transform[1], target, m_Flight.ERR_NativeRouteStageDeadline() - now, m_Flight.ERR_NativeControlDeadline() - now, now);
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Complete) return 1;
|
||||
if (m_Stopped) return -1;
|
||||
if (!Enabled() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active() || now >= m_Deadline) { Check(false, "active exact mission and bounded300-second fixture"); return -1; }
|
||||
if (!NoPlayers()) { Check(false, "fixture remains unclaimed by any human"); return -1; }
|
||||
if (m_LastTick >= 0 && (now < m_LastTick || now - m_LastTick > 1500)) { m_Stable = -1; m_TranslationStable = -1; }
|
||||
m_LastTick = now;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!m_Mode.ExtractionVehicle()) return 0;
|
||||
if (!Check(!m_AllocationRequested && FindPad(m_Pad), "independent qualified candidate found before one finite allocation")) return -1;
|
||||
m_AllocationRequested = true;
|
||||
m_Aircraft = Vehicle.Cast(ERR_Actors.Spawn(AIRCRAFT, m_Pad));
|
||||
m_Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(GROUP, m_Pad + "20 0 0"));
|
||||
if (!Check(m_Aircraft && m_Group && m_Group.InitFactionKey("USSR") && m_Group.GetFaction() == m_Mode.EnemyFaction(), "one finite native Mi8 and memberless enemy group created")) return -1;
|
||||
m_Group.SetPermanentLOD(0); m_Group.ActivateAI();
|
||||
m_Step = 1; m_StageDeadline = now + 30000; return 0;
|
||||
}
|
||||
if (!m_Aircraft || !m_Group || !ERR_Actors.Alive(m_Aircraft)) { Check(false, "original airframe and group survive without replacement"); return -1; }
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "real components and original pilot initialize within30seconds"); return -1; }
|
||||
if (!ReadyComponents()) return 0;
|
||||
if (!Check(!m_PilotRequested && m_Group.GetAgentsCount() == 0, "one original native pilot request only")) return -1;
|
||||
m_PilotRequested = true; AIGroup originalGroup = m_Group;
|
||||
m_Pilot = m_Seat.SpawnCharacterInCompartment(PILOT, originalGroup, GROUP);
|
||||
AIControlComponent control; if (m_Pilot) control = AIControlComponent.Cast(m_Pilot.FindComponent(AIControlComponent));
|
||||
AIAgent agent; if (control) agent = control.GetControlAIAgent();
|
||||
if (!Check(m_Pilot && originalGroup == m_Group && agent, "pilot seated by native mechanism in original group")) return -1;
|
||||
agent.SetPermanentLOD(0); agent.ActivateAI(); m_Step = 2; return 0;
|
||||
}
|
||||
Diagnostic(now);
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "actual initial seating and native ground settling within30seconds"); return -1; }
|
||||
if (!OriginalOccupancy()) { m_Stable = -1; return 0; }
|
||||
Physics physics = m_Aircraft.GetPhysics();
|
||||
if (!physics || !physics.IsDynamic()) return 0;
|
||||
if (m_Wakes == 0) { m_Wakes = 1; physics.SetActive(ActiveState.ACTIVE); m_Stable = -1; return 0; }
|
||||
if (!m_Simulation.HasAnyGroundContact() || !ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft)) { m_Stable = -1; return 0; }
|
||||
if (m_Stable < 0) m_Stable = now;
|
||||
if (now - m_Stable < 1000) return 0;
|
||||
if (!Check(!m_Controller.IsEngineOn(), "post-spawn qualification precedes native engine start")) return -1;
|
||||
m_Envelope = ERR_AircraftRouteEnvelope.QualifyRoute(m_Mode, m_Aircraft, m_Pilot, m_Destination);
|
||||
if (!Check(m_Envelope != null, "actual full corridor, both pads and complete native rotor/model footprint issue exact route qualification")) return -1;
|
||||
m_InitialFuel = m_Fuel.GetTotalFuel();
|
||||
m_Flight = new ERR_AircraftRouteFlight(); m_ControllerStarts++;
|
||||
m_ControlDeadline = Math.Min(m_Deadline, now + 240000);
|
||||
bool started;
|
||||
started = m_Flight.StartRoute(m_Mode, m_Aircraft, m_Group, m_Pilot, m_Seat, m_Envelope, ORDER_TOKEN, m_ControlDeadline);
|
||||
if (!Check(started, "actual controller accepts one qualified original pilot/aircraft order")) return -1;
|
||||
m_Step = 3; return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (!OriginalOccupancy()) { Check(false, "original pilot remains conscious and in its exact seat/group"); return -1; }
|
||||
bool engine = m_Controller.IsEngineOn(); if (engine && !m_LastEngine) m_EngineStarts++; m_LastEngine = engine;
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
vector home = m_Envelope.Home(); float height = transform[3][1] - home[1];
|
||||
bool contact = m_Simulation.HasAnyGroundContact();
|
||||
bool landed = ERR_AircraftFlightRules.Landed(contact, height, m_Simulation.GetVelocity().Length(), m_Simulation.GetAngularVelocity().Length(), transform[1][1]);
|
||||
if (m_TranslationFixture && m_Flight.State() == ERR_AircraftFlightState.TRANSLATING)
|
||||
{
|
||||
if (!m_TranslationObserved)
|
||||
{
|
||||
m_TranslationObserved = true; m_TranslationOrigin = m_Flight.TranslationOrigin(); m_TranslationTarget = m_Flight.TranslationTarget();
|
||||
if (!Check(vector.DistanceXZ(m_TranslationTarget, m_Destination) < 0.001 && ERR_AircraftRouteRules.Length(vector.DistanceXZ(home, m_TranslationTarget)), "pre-takeoff route endpoint remains the original qualified destination")) return -1;
|
||||
}
|
||||
if (m_Flight.TranslationTarget() != m_TranslationTarget || m_Flight.TranslationOrigin() != m_TranslationOrigin || m_Flight.ERR_NativeControlDeadline() != m_ControlDeadline) { Check(false, "translation never retargets or renews its original deadline"); return -1; }
|
||||
vector horizontal = m_Simulation.GetVelocity(); horizontal[1] = 0;
|
||||
bool held = ERR_AircraftRouteRules.Held(contact, height, m_Simulation.GetVelocity().Length(), m_Simulation.GetVelocity()[1], transform[1][1], horizontal.Length(), vector.DistanceXZ(transform[3], m_TranslationTarget));
|
||||
if (!held) m_TranslationStable = -1;
|
||||
else if (m_TranslationStable < 0) m_TranslationStable = now;
|
||||
if (m_TranslationStable >= 0 && now - m_TranslationStable >= 5000) m_TranslationHeld = true;
|
||||
}
|
||||
m_Flight.Tick();
|
||||
if (!m_Flight.Finished()) return 0;
|
||||
PrintFormat("ERR AIRCRAFT ROUTE FIXTURE RESULT: state=%1 token=%2 engineStarts=%3 controllerStarts=%4 wakes=%5 pilotHooks=%6 helicopterHooks=%7 reason=%8", m_Flight.State(), m_Flight.OrderToken(), m_EngineStarts, m_ControllerStarts, m_Wakes, m_PilotHooks, m_HelicopterHooks, m_Flight.Reason());
|
||||
bool complete = ERR_NativeAircraftRouteRules.Terminal(m_CancelFixture, m_Flight.State(), m_CancelCount);
|
||||
complete = complete && ERR_NativeAircraftRouteRules.PhysicalFinish(OriginalOccupancy(), landed, !m_Controller.IsEngineOn(), m_InitialFuel - m_Fuel.GetTotalFuel(), m_Wakes, m_EngineStarts, m_ControllerStarts, m_PilotHooks, m_HelicopterHooks);
|
||||
if (m_TranslationFixture)
|
||||
{
|
||||
float requestedDistance = vector.DistanceXZ(m_TranslationOrigin, m_TranslationTarget);
|
||||
float displacement = vector.DistanceXZ(transform[3], m_TranslationOrigin);
|
||||
float targetError = vector.DistanceXZ(transform[3], m_TranslationTarget);
|
||||
bool translated = ERR_NativeAircraftRouteRules.TranslateProof(m_Flight.TranslationProven(), m_TranslationObserved && m_TranslationHeld, requestedDistance, displacement, targetError);
|
||||
PrintFormat("ERR AIRCRAFT ROUTE RESULT: proven=%1 held=%2 requested=%3 displacement=%4 targetError=%5 updates=%6", m_Flight.TranslationProven(), m_TranslationHeld, requestedDistance, displacement, targetError, m_Flight.TranslationUpdates());
|
||||
complete = complete && translated && m_Flight.Arrived() && vector.DistanceXZ(transform[3], m_Destination) <= 0.75;
|
||||
}
|
||||
if (!Check(complete && m_Flight.OrderToken() == ORDER_TOKEN, "actual control order completes on original physical grounded aircraft with engine off, finite fuel use and exact pilot")) return -1;
|
||||
m_Complete = true; Stop(); return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (s_Current == this) s_Current = null;
|
||||
if (m_Flight && !m_Flight.Finished() && m_CancelCount == 0) m_Flight.Cancel("native fixture stopped");
|
||||
// End-of-test disposal is not successful flight or a production cancellation.
|
||||
// These assets are fixture-owned only, never registered with mission cleanup.
|
||||
// Release the controller before deleting disposable unclaimed physical assets.
|
||||
m_Flight = null;
|
||||
bool context = GetGame() && GetGame().GetWorld() && GetGame().GetPlayerManager() && m_Mode && m_Mode == ERR_GameMode.Get();
|
||||
bool faction = context && (!m_Group || (!m_PilotRequested && m_Group.GetAgentsCount() == 0 && !m_Group.GetFaction()) || m_Group.GetFaction() == m_Mode.EnemyFaction()) && (!m_Aircraft || SCR_FactionManager.SGetFaction(m_Aircraft) == m_Mode.EnemyFaction());
|
||||
bool groupUnchanged = context && (!m_Group || m_Group.GetAgentsCount() == 0 || (m_Group.GetAgentsCount() == 1 && m_Pilot && ERR_Actors.Group(m_Pilot) == m_Group));
|
||||
bool pilotUnchanged = context && (!m_Pilot || (m_Group && ERR_Actors.Group(m_Pilot) == m_Group && !ERR_Actors.Player(m_Pilot)));
|
||||
if (ERR_NativeAircraftRouteRules.CanDispose(context, NoPlayers(), faction, groupUnchanged, pilotUnchanged))
|
||||
{
|
||||
ERR_Actors.DeleteSpawned(m_Group); ERR_Actors.DeleteSpawned(m_Pilot); ERR_Actors.DeleteSpawned(m_Aircraft);
|
||||
}
|
||||
else Print("ERR AIRCRAFT ROUTE FIXTURE CLEANUP: ownership changed; captured entities preserved");
|
||||
m_Envelope = null;
|
||||
}
|
||||
void ~ERR_NativeAircraftRouteProbe() { Stop(); }
|
||||
}
|
||||
#endif
|
||||
@@ -19,6 +19,8 @@ modded class ERR_GameMode
|
||||
protected bool m_ERRTestMortar;
|
||||
protected bool m_ERRTestMortarFire;
|
||||
protected bool m_ERRTestMapOwnership;
|
||||
protected bool m_ERRTestAircraftRoute;
|
||||
protected ref ERR_NativeAircraftRouteProbe m_ERRAircraftRoute;
|
||||
protected bool m_ERRTestAircraftControl;
|
||||
protected bool m_ERRTestAircraftCancel;
|
||||
protected bool m_ERRTestAircraftTranslate;
|
||||
@@ -47,6 +49,7 @@ modded class ERR_GameMode
|
||||
m_ERRTestHQResponse = outcome == "hq-response";
|
||||
m_ERRTestMortar = outcome == "mortar-inventory";
|
||||
m_ERRTestMapOwnership = outcome == "map-ownership";
|
||||
m_ERRTestAircraftRoute = outcome == "aircraft-route";
|
||||
m_ERRTestAircraftCancel = outcome == "aircraft-cancel";
|
||||
m_ERRTestAircraftTranslate = outcome == "aircraft-translate";
|
||||
m_ERRTestAircraftControl = outcome == "aircraft-control" || m_ERRTestAircraftCancel || m_ERRTestAircraftTranslate;
|
||||
@@ -57,7 +60,7 @@ modded class ERR_GameMode
|
||||
m_ERRTestPatrolHQ = outcome == "patrol-hq";
|
||||
m_ERRTestPlayerDeath = outcome == "player-death" || m_ERRTestRadioNetwork || m_ERRTestHQResponse || m_ERRTestMortar;
|
||||
m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 150000;
|
||||
if (m_ERRTestAircraftControl) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
if (m_ERRTestAircraftControl || m_ERRTestAircraftRoute) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
if (m_ERRTestMortarCourier) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 450000;
|
||||
if (m_ERRTestMortarTransfer) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
if (m_ERRTestMortarSalvo) m_ERRSmokeDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
@@ -82,6 +85,7 @@ modded class ERR_GameMode
|
||||
{
|
||||
if (m_ERRMapOwnership) m_ERRMapOwnership.Stop();
|
||||
if (m_ERRAircraftControl) m_ERRAircraftControl.Stop();
|
||||
if (m_ERRAircraftRoute) m_ERRAircraftRoute.Stop();
|
||||
if (m_ERRHQResponseProof) m_ERRHQResponseProof.Stop();
|
||||
if (m_ERRMortarSalvo) m_ERRMortarSalvo.Stop();
|
||||
if (m_ERRMortarCourier) m_ERRMortarCourier.Stop();
|
||||
@@ -122,6 +126,15 @@ modded class ERR_GameMode
|
||||
protected void ERR_SmokePhase(ERR_Phase previous, ERR_Phase current)
|
||||
{
|
||||
if (m_ERRSmokeDone || !ERR_MissionRules.Terminal(current)) return;
|
||||
if (m_ERRTestAircraftRoute)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 231, "actual aircraft route completed before explicit fixture reset")) return;
|
||||
m_ERRSmokeDone = true;
|
||||
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
||||
ERR_StopHQPatrol();
|
||||
Print("ERR SMOKE COMPLETE: actual immutable short aircraft route, destination hold and contact landing; experimental mechanism, not HQ transport");
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestAircraftControl)
|
||||
{
|
||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 221, "actual aircraft controller completed before explicit fixture reset")) return;
|
||||
@@ -242,6 +255,13 @@ modded class ERR_GameMode
|
||||
if (m_ERRSmokeDone) return;
|
||||
if (now > m_ERRSmokeDeadline) { ERR_Check(false, "timeout at step " + m_ERRSmokeStep.ToString()); return; }
|
||||
if (now < m_ERRSmokeWait) return;
|
||||
if (m_ERRSmokeStep == 230)
|
||||
{
|
||||
int result = m_ERRAircraftRoute.Tick(now);
|
||||
if (result < 0) { ERR_Check(false, "native actual aircraft route fixture"); return; }
|
||||
if (result == 1) { m_ERRSmokeStep = 231; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRSmokeStep == 220)
|
||||
{
|
||||
int result = m_ERRAircraftControl.Tick(now);
|
||||
@@ -309,6 +329,13 @@ modded class ERR_GameMode
|
||||
if (players.Count() != 1 || Phase() != ERR_Phase.PRISON) return;
|
||||
m_ERRSmokePlayer = players[0];
|
||||
if (!ERR_CheckRoster() || !ERR_NativeContractsProbe.Run(this, m_ERRSmokePlayer) || !ERR_CheckCompanionCommands()) return;
|
||||
if (m_ERRTestAircraftRoute)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRAircraftRoute = new ERR_NativeAircraftRouteProbe(this);
|
||||
m_ERRSmokeStep = 230;
|
||||
return;
|
||||
}
|
||||
if (m_ERRTestAircraftControl)
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
|
||||
@@ -318,6 +318,10 @@ modded class ERR_GameMode
|
||||
if (m_ERRStartupFailure)
|
||||
{
|
||||
if (PreparationError().IsEmpty()) return;
|
||||
int presentation = ERR_NativePreparationStatusProbe.Tick(this, now);
|
||||
if (presentation < 0) { ERR_BaseCheck(false, "actual persistent startup-failure loading presentation within30seconds"); return; }
|
||||
if (presentation == 0) return;
|
||||
if (!ERR_BaseCheck(true, "actual failed-start loading widgets persist beyond17seconds with spinner hidden; diagnostic screenshot requested")) return;
|
||||
if (!ERR_BaseCheck(Phase() == ERR_Phase.PREPARING && !Active() && m_Stopped, "invalid faction setup stops services without starting the mission")) return;
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#ifdef WORKBENCH
|
||||
// Only the explicit startup-failure fixture calls this read-only observation.
|
||||
// No placeholder/widget creation, presentation calls or preparation-state writes.
|
||||
class ERR_NativePreparationStatusProbe
|
||||
{
|
||||
protected static ERR_GameMode s_Mode;
|
||||
protected static float s_Deadline;
|
||||
protected static float s_Since = -1;
|
||||
protected static float s_Last = -1;
|
||||
protected static float s_CaptureAt = -1;
|
||||
static int Tick(ERR_GameMode mode, float now)
|
||||
{
|
||||
if (s_Mode != mode)
|
||||
{
|
||||
s_Mode = mode; s_Deadline = now + 30000;
|
||||
s_Since = -1; s_Last = -1; s_CaptureAt = -1;
|
||||
}
|
||||
if (!mode || mode != ERR_GameMode.Get() || mode.Phase() != ERR_Phase.PREPARING || mode.PreparationError().IsEmpty() || now < 0 || now >= s_Deadline) return -1;
|
||||
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
|
||||
bool visible = respawn && respawn.ERR_ProbePreparationVisible(mode, true);
|
||||
if (s_Last >= 0 && (now < s_Last || now - s_Last > 1500)) s_Since = -1;
|
||||
s_Last = now;
|
||||
if (!visible) { s_Since = -1; return 0; }
|
||||
if (s_Since < 0) s_Since = now;
|
||||
// Exceed the old15-second hint lifetime; inspect the real widget every tick.
|
||||
if (now - s_Since < 17000) return 0;
|
||||
if (s_CaptureAt < 0)
|
||||
{
|
||||
System.MakeScreenshot("$logs:err-preparation-failure");
|
||||
s_CaptureAt = now;
|
||||
Print("ERR PREPARATION UI: actual failed-start title/body and hidden native spinner observed continuously for17seconds; screenshot requested");
|
||||
return 0;
|
||||
}
|
||||
if (now - s_CaptureAt < 500) return 0;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -44,9 +44,17 @@ class ERR_ObjectiveText
|
||||
{
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
mode.Locations(locations);
|
||||
bool roughRadio;
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
if (location.Kind() == ERR_LocationKind.RADIO && location.Knowledge() == 2)
|
||||
{
|
||||
if (!location || location.Kind() != ERR_LocationKind.RADIO) continue;
|
||||
if (location.Knowledge() == 2)
|
||||
return "The radio station is identified on your map. Reach its radio and complete the 'Contact extraction HQ' action to start the transmission.";
|
||||
if (location.Knowledge() == 1) roughRadio = true;
|
||||
}
|
||||
// Scan all sites before acknowledging rough intel: an identified radio wins.
|
||||
if (roughRadio)
|
||||
return "Intel recovered: a 500 m search square is now marked on the map. Use a carried field map to view it. Take a different intelligence folder into your inventory to pinpoint the radio; a folder already recovered cannot provide another clue. Try the other marked supply cache if needed. A close approach with a clear view can also identify the station.";
|
||||
return "Choose either marked supply cache. Take its intelligence folders into your own inventory to reveal the station on your map. If only a search area appears, take another folder. Each cache has enough folders; use the other if one is lost. If every folder is lost, a close approach with a clear view can identify the station itself. Nearby medical and guard sites can also be discovered by approaching with a clear view.";
|
||||
}
|
||||
if (phase == ERR_Phase.RADIO_DEFENCE)
|
||||
|
||||
@@ -229,7 +229,7 @@ modded class SCR_PlayerController
|
||||
{
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (!hints) return; // retry on the existing monitor; do not lose the first hint
|
||||
hints.ShowCustom(ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable), "Spectating", 12);
|
||||
if (!hints.ShowCustom(ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable), "Spectating", 12)) return;
|
||||
m_ERRObserverReady = true;
|
||||
m_ERRShownObserverReason = m_ERRObserverReason;
|
||||
m_ERRShownObserverAvailable = survivorAvailable;
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
#ifndef WORKBENCH
|
||||
// Explicit ordinary-game observation only. No input, actor, menu or camera writes.
|
||||
class ERR_OrdinaryInputObserverRules
|
||||
{
|
||||
static bool Nonce(string nonce)
|
||||
{
|
||||
if (nonce.Length() < 1 || nonce.Length() > 32) return false;
|
||||
string allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
for (int i = 0; i < nonce.Length(); i++) if (!allowed.Contains(nonce[i])) return false;
|
||||
return true;
|
||||
}
|
||||
static bool Within(int start, int previous, int now, int samples)
|
||||
{
|
||||
return now >= start && now >= previous && now - start < 45000 && samples < 450;
|
||||
}
|
||||
static bool Stationary(float forward, vector input, vector velocity, float displacement)
|
||||
{
|
||||
return Math.AbsFloat(forward) <= 0.01 && input.Length() <= 0.01 && velocity.Length() <= 0.05 && displacement >= 0 && displacement <= 0.05;
|
||||
}
|
||||
}
|
||||
class ERR_OrdinaryInputObserver : Managed
|
||||
{
|
||||
protected static ref ERR_OrdinaryInputObserver s_Request;
|
||||
protected static bool s_Attempted;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected BaseWorld m_World;
|
||||
protected PlayerController m_Controller;
|
||||
protected IEntity m_Actor;
|
||||
protected CharacterControllerComponent m_Character;
|
||||
protected ref ERR_PlayerLife m_Life;
|
||||
protected UUID m_Account;
|
||||
protected int m_PlayerId;
|
||||
protected string m_Nonce;
|
||||
protected bool m_Screenshots;
|
||||
protected bool m_Finished;
|
||||
protected bool m_Ready;
|
||||
protected bool m_Opened;
|
||||
protected bool m_Closed;
|
||||
protected SCR_InventoryMenuUI m_Menu;
|
||||
protected int m_Start;
|
||||
protected int m_Previous;
|
||||
protected int m_Samples;
|
||||
protected int m_BaselineSince = -1;
|
||||
protected vector m_BaselineOrigin;
|
||||
protected float m_LastWorldTime;
|
||||
|
||||
protected static bool Enabled(out string nonce)
|
||||
{
|
||||
string project;
|
||||
if (!Replication.IsServer() || System.IsConsoleApp()) return false;
|
||||
if (!System.GetCLIParam("errOrdinaryScenario", project) || (project != "core" && project != "chat")) return false;
|
||||
return System.GetCLIParam("errOrdinaryInputObserve", nonce) && ERR_OrdinaryInputObserverRules.Nonce(nonce);
|
||||
}
|
||||
static void Queue(ERR_GameMode mode, PlayerController controller, IEntity actor)
|
||||
{
|
||||
string nonce;
|
||||
if (s_Attempted || !Enabled(nonce) || !GetGame() || !mode || !controller || !actor || !mode.Players()) return;
|
||||
ERR_PlayerLife life = mode.Players().CurrentActor(actor);
|
||||
if (!life) return;
|
||||
ERR_OrdinaryInputObserver request = new ERR_OrdinaryInputObserver();
|
||||
request.m_Mode = mode; request.m_World = GetGame().GetWorld();
|
||||
request.m_Controller = controller; request.m_Actor = actor;
|
||||
request.m_Character = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
|
||||
request.m_Life = life; request.m_Account = life.AccountId;
|
||||
request.m_PlayerId = controller.GetPlayerId(); request.m_Nonce = nonce;
|
||||
if (!request.Valid()) return;
|
||||
string screenshots;
|
||||
request.m_Screenshots = System.GetCLIParam("errOrdinaryInputScreenshots", screenshots) && screenshots == "1";
|
||||
request.m_Start = System.GetTickCount(); request.m_Previous = request.m_Start;
|
||||
request.m_LastWorldTime = request.m_World.GetWorldTime();
|
||||
s_Attempted = true; s_Request = request;
|
||||
mode.OnStop().Insert(request.Cancel);
|
||||
request.Record("QUEUED");
|
||||
GetGame().GetCallqueue().CallLater(request.Sample, 100, false);
|
||||
}
|
||||
protected bool Valid()
|
||||
{
|
||||
string nonce;
|
||||
if (!Enabled(nonce) || nonce != m_Nonce || !GetGame() || !m_Mode || m_Mode != ERR_GameMode.Get()) return false;
|
||||
if (!m_World || GetGame().GetWorld() != m_World || m_Mode.GetWorld() != m_World || !m_Mode.Active() || m_Mode.Phase() != ERR_Phase.PRISON) return false;
|
||||
if (!m_Controller || m_Controller != GetGame().GetPlayerController() || m_Controller.GetPlayerId() != m_PlayerId || m_PlayerId <= 0) return false;
|
||||
PlayerManager manager = GetGame().GetPlayerManager();
|
||||
if (!manager || manager.GetPlayerController(m_PlayerId) != m_Controller || manager.GetPlayerControlledEntity(m_PlayerId) != m_Actor || m_Controller.GetControlledEntity() != m_Actor) return false;
|
||||
if (!ERR_Actors.CanFight(m_Actor) || m_Actor.GetWorld() != m_World || !m_Mode.Players()) return false;
|
||||
if (!m_Character || m_Actor.FindComponent(CharacterControllerComponent) != m_Character || m_Character.IsUnconscious()) return false;
|
||||
ERR_PlayerLife current = m_Mode.Players().CurrentActor(m_Actor);
|
||||
return current && current == m_Life && current.AccountId == m_Account;
|
||||
}
|
||||
protected void Record(string detail)
|
||||
{
|
||||
PrintFormat("ERR INPUT OBSERVE: run=%1 sample=%2 real=%3 world=%4 %5", m_Nonce, m_Samples, System.GetTickCount(), m_LastWorldTime, detail);
|
||||
}
|
||||
protected void Capture(string suffix)
|
||||
{
|
||||
if (!m_Screenshots || !Valid()) return;
|
||||
System.MakeScreenshot("$logs:err-input-" + m_Nonce + "-" + suffix);
|
||||
Record("CAPTURE_REQUEST " + suffix);
|
||||
}
|
||||
// Snapshot sequencing is separate from native reads so its policy is executable.
|
||||
protected void Observe(int now, vector origin, float forward, vector input, vector velocity, bool inventoryOpen)
|
||||
{
|
||||
if (!m_Ready)
|
||||
{
|
||||
float displacement = vector.Distance(origin, m_BaselineOrigin);
|
||||
if (m_BaselineSince < 0) displacement = 0;
|
||||
if (inventoryOpen || now - m_Previous > 500 || !ERR_OrdinaryInputObserverRules.Stationary(forward, input, velocity, displacement))
|
||||
{
|
||||
m_BaselineSince = -1;
|
||||
return;
|
||||
}
|
||||
if (m_BaselineSince < 0) { m_BaselineSince = now; m_BaselineOrigin = origin; }
|
||||
if (now - m_BaselineSince < 2000) return;
|
||||
m_Ready = true; Record("READY"); Capture("baseline");
|
||||
}
|
||||
if (inventoryOpen && !m_Opened)
|
||||
{
|
||||
m_Opened = true; Record("INVENTORY_OPEN"); Capture("open");
|
||||
}
|
||||
else if (!inventoryOpen && m_Opened && !m_Closed)
|
||||
{
|
||||
m_Closed = true; Record("INVENTORY_CLOSED"); Capture("closed");
|
||||
}
|
||||
}
|
||||
protected void Sample()
|
||||
{
|
||||
if (m_Finished) return;
|
||||
if (!Valid()) { End("CANCELLED original-admission-lost"); return; }
|
||||
int now = System.GetTickCount();
|
||||
float worldTime = m_World.GetWorldTime();
|
||||
if (worldTime < m_LastWorldTime) { End("CANCELLED world-clock-reversed"); return; }
|
||||
m_LastWorldTime = worldTime;
|
||||
if (!ERR_OrdinaryInputObserverRules.Within(m_Start, m_Previous, now, m_Samples)) { End("FINISHED observation-bound"); return; }
|
||||
if (!GetGame().GetInputManager() || !GetGame().GetMenuManager()) { End("CANCELLED native-input-or-menu-unavailable"); return; }
|
||||
SCR_InventoryMenuUI menu = SCR_InventoryMenuUI.GetInventoryMenu();
|
||||
bool open;
|
||||
if (menu)
|
||||
{
|
||||
Widget root = menu.GetRootWidget();
|
||||
if (menu.GetCharacterController() != m_Character || !root || !root.IsVisible() || GetGame().GetMenuManager().GetTopMenu() != menu)
|
||||
{ End("CANCELLED inventory-context-mismatch"); return; }
|
||||
if (m_Opened && (m_Closed || menu != m_Menu)) { End("CANCELLED inventory-instance-replaced"); return; }
|
||||
open = true;
|
||||
}
|
||||
vector origin = m_Actor.GetOrigin();
|
||||
vector input = m_Character.GetMovementInput();
|
||||
vector velocity = m_Character.GetVelocity();
|
||||
float forward = GetGame().GetInputManager().GetActionValue("CharacterForward");
|
||||
m_Samples++;
|
||||
PrintFormat("ERR INPUT OBSERVE: run=%1 sample=%2 real=%3 world=%4 SAMPLE origin=%5 forward=%6 input=%7 velocity=%8 inventory=%9", m_Nonce, m_Samples, now, m_LastWorldTime, origin, forward, input, velocity, open);
|
||||
Observe(now, origin, forward, input, velocity, open);
|
||||
if (m_Opened && !m_Closed && menu) m_Menu = menu;
|
||||
m_Previous = now;
|
||||
if (m_Samples >= 450) { End("FINISHED sample-bound"); return; }
|
||||
GetGame().GetCallqueue().CallLater(Sample, 100, false);
|
||||
}
|
||||
protected void End(string reason)
|
||||
{
|
||||
if (m_Finished) return;
|
||||
m_Finished = true;
|
||||
Record(reason);
|
||||
if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(Sample);
|
||||
if (m_Mode) m_Mode.OnStop().Remove(Cancel);
|
||||
m_Mode = null; m_World = null; m_Controller = null; m_Actor = null;
|
||||
m_Character = null; m_Life = null; m_Menu = null;
|
||||
}
|
||||
void Cancel() { End("CANCELLED mission-or-world-stopped"); }
|
||||
static void ClearWorld()
|
||||
{
|
||||
if (s_Request) s_Request.Cancel();
|
||||
s_Request = null;
|
||||
// Process-wide single attempt: a restart cannot silently create new evidence.
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -248,6 +248,7 @@ modded class ArmaReforgerScripted
|
||||
override protected void OnBeforeWorldCleanup()
|
||||
{
|
||||
ERR_OrdinaryScreenshotRequest.ClearWorld();
|
||||
ERR_OrdinaryInputObserver.ClearWorld();
|
||||
if (m_ERRDiagnosticLaunchRequest) m_ERRDiagnosticLaunchRequest.Cancel();
|
||||
m_ERRDiagnosticLaunchRequest = null;
|
||||
super.OnBeforeWorldCleanup();
|
||||
@@ -267,6 +268,7 @@ modded class ERR_GameMode
|
||||
if (!Players().CurrentActor(actor) || Phase() != ERR_Phase.PRISON) return;
|
||||
PrintFormat("ERR ORDINARY: local participant verified project=%1 phase=prison", project);
|
||||
ERR_OrdinaryScreenshotRequest.Queue(this, local, actor);
|
||||
ERR_OrdinaryInputObserver.Queue(this, local, actor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,12 +25,13 @@ modded class SCR_PlayerController
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode) return;
|
||||
if (mode.Phase() != ERR_Phase.PREPARING && !GetControlledEntity()) return;
|
||||
if (mode.Phase() != ERR_Phase.PREPARING && (ERR_ObserverState() != ERR_ObserverReason.NONE || !ERR_Actors.Alive(GetControlledEntity()))) return;
|
||||
string message = ERR_ObjectiveText.Description(mode, false);
|
||||
if (message.IsEmpty()) return;
|
||||
if (m_ERRGuidePhase == mode.Phase() && m_ERRGuideMessage == message) return;
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (!hints) return;
|
||||
if (!message.IsEmpty()) hints.ShowCustom(message, ERR_ObjectiveText.Title(mode.Phase()), 15);
|
||||
if (!hints.ShowCustom(message, ERR_ObjectiveText.Title(mode.Phase()), 15)) return;
|
||||
m_ERRGuidePhase = mode.Phase();
|
||||
m_ERRGuideMessage = message;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Local presentation attached to the native respawn loading placeholder.
|
||||
// No timer, camera, admission, or mission-state changes.
|
||||
modded class SCR_RespawnSystemComponent
|
||||
{
|
||||
protected Widget m_ERRPreparationParent;
|
||||
protected Widget m_ERRPreparationRoot;
|
||||
protected TextWidget m_ERRPreparationTitle;
|
||||
protected TextWidget m_ERRPreparationDescription;
|
||||
protected Widget m_ERRPreparationSpinner;
|
||||
protected bool m_ERRPreparationSpinnerHidden;
|
||||
protected bool m_ERRPreparationSpinnerWasVisible;
|
||||
protected string m_ERRPreparationLastTitle;
|
||||
protected string m_ERRPreparationLastDescription;
|
||||
|
||||
protected void ERR_ClearPreparationStatus()
|
||||
{
|
||||
if (m_ERRPreparationSpinnerHidden && m_ERRPreparationSpinner)
|
||||
m_ERRPreparationSpinner.SetVisible(m_ERRPreparationSpinnerWasVisible);
|
||||
m_ERRPreparationSpinnerHidden = false;
|
||||
m_ERRPreparationSpinner = null;
|
||||
if (m_ERRPreparationRoot) m_ERRPreparationRoot.RemoveFromHierarchy();
|
||||
m_ERRPreparationRoot = null;
|
||||
m_ERRPreparationParent = null;
|
||||
m_ERRPreparationTitle = null;
|
||||
m_ERRPreparationDescription = null;
|
||||
m_ERRPreparationLastTitle = "";
|
||||
m_ERRPreparationLastDescription = "";
|
||||
}
|
||||
|
||||
override void UpdateLoadingPlaceholder(float dt)
|
||||
{
|
||||
super.UpdateLoadingPlaceholder(dt);
|
||||
// Vanilla OnInit(IEntity owner) stores its owning mode in m_pGameMode.
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (System.IsConsoleApp() || !mode || m_pGameMode != mode || mode.Phase() != ERR_Phase.PREPARING || !m_wLoadingPlaceholder)
|
||||
{
|
||||
ERR_ClearPreparationStatus();
|
||||
return;
|
||||
}
|
||||
if (m_ERRPreparationParent != m_wLoadingPlaceholder || !m_ERRPreparationRoot || m_ERRPreparationRoot.GetParent() != m_wLoadingPlaceholder)
|
||||
{
|
||||
ERR_ClearPreparationStatus();
|
||||
if (!GetGame() || !GetGame().GetWorkspace()) return;
|
||||
m_ERRPreparationRoot = GetGame().GetWorkspace().CreateWidgets("{BCCAE287751344D9}UI/layouts/ERR/PreparationStatus.layout", m_wLoadingPlaceholder);
|
||||
if (!m_ERRPreparationRoot) return;
|
||||
m_ERRPreparationParent = m_wLoadingPlaceholder;
|
||||
m_ERRPreparationTitle = TextWidget.Cast(m_ERRPreparationRoot.FindAnyWidget("ERR_PreparationTitle"));
|
||||
m_ERRPreparationDescription = TextWidget.Cast(m_ERRPreparationRoot.FindAnyWidget("ERR_PreparationDescription"));
|
||||
if (!m_ERRPreparationTitle || !m_ERRPreparationDescription) { ERR_ClearPreparationStatus(); return; }
|
||||
m_ERRPreparationSpinner = m_wLoadingPlaceholder.FindAnyWidget("Spinner");
|
||||
}
|
||||
bool failed = !mode.PreparationError().IsEmpty();
|
||||
string title = "Preparing mission";
|
||||
if (failed) title = "Mission could not start";
|
||||
string description = ERR_ObjectiveText.Description(mode, false);
|
||||
if (title != m_ERRPreparationLastTitle)
|
||||
{
|
||||
m_ERRPreparationTitle.SetText(title);
|
||||
m_ERRPreparationLastTitle = title;
|
||||
}
|
||||
if (description != m_ERRPreparationLastDescription)
|
||||
{
|
||||
m_ERRPreparationDescription.SetText(description);
|
||||
m_ERRPreparationLastDescription = description;
|
||||
}
|
||||
if (failed && m_ERRPreparationSpinner && !m_ERRPreparationSpinnerHidden)
|
||||
{
|
||||
m_ERRPreparationSpinnerWasVisible = m_ERRPreparationSpinner.IsVisible();
|
||||
m_ERRPreparationSpinner.SetVisible(false);
|
||||
m_ERRPreparationSpinnerHidden = true;
|
||||
}
|
||||
else if (!failed && m_ERRPreparationSpinnerHidden)
|
||||
{
|
||||
if (m_ERRPreparationSpinner) m_ERRPreparationSpinner.SetVisible(m_ERRPreparationSpinnerWasVisible);
|
||||
m_ERRPreparationSpinnerHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
override void DestroyLoadingPlaceholder()
|
||||
{
|
||||
ERR_ClearPreparationStatus();
|
||||
super.DestroyLoadingPlaceholder();
|
||||
}
|
||||
|
||||
#ifdef WORKBENCH
|
||||
protected bool ERR_PreparationWidgetVisible(Widget widget)
|
||||
{
|
||||
if (!widget || !widget.IsVisible()) return false;
|
||||
float width, height;
|
||||
widget.GetScreenSize(width, height);
|
||||
return width > 0 && height > 0;
|
||||
}
|
||||
// Inspect the actual production-created widgets, without creating or changing them.
|
||||
bool ERR_ProbePreparationVisible(ERR_GameMode mode, bool failed)
|
||||
{
|
||||
if (!mode || mode != ERR_GameMode.Get() || m_pGameMode != mode || !m_wLoadingPlaceholder || !m_wLoadingPlaceholder.IsVisible()) return false;
|
||||
if (!m_ERRPreparationRoot || m_ERRPreparationParent != m_wLoadingPlaceholder || m_ERRPreparationRoot.GetParent() != m_wLoadingPlaceholder || !m_ERRPreparationRoot.IsVisible()) return false;
|
||||
if (!ERR_PreparationWidgetVisible(m_ERRPreparationTitle) || !ERR_PreparationWidgetVisible(m_ERRPreparationDescription) || !ERR_PreparationWidgetVisible(m_ERRPreparationRoot)) return false;
|
||||
string title = "Preparing mission"; if (failed) title = "Mission could not start";
|
||||
if (m_ERRPreparationTitle.GetText() != title || m_ERRPreparationDescription.GetText() != ERR_ObjectiveText.Description(mode, false)) return false;
|
||||
if (failed) return m_ERRPreparationSpinner && m_ERRPreparationSpinnerHidden && !m_ERRPreparationSpinner.IsVisible();
|
||||
return !m_ERRPreparationSpinnerHidden;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
FrameWidgetClass {
|
||||
Name "ERR_PreparationStatus"
|
||||
Slot FrameWidgetSlot {
|
||||
Anchor 0.5 0.5 0.5 0.5
|
||||
OffsetLeft 0
|
||||
OffsetTop 0
|
||||
SizeX 900
|
||||
OffsetRight -900
|
||||
SizeY 240
|
||||
OffsetBottom -240
|
||||
Alignment 0.5 0.5
|
||||
}
|
||||
"Z Order" 10
|
||||
{
|
||||
TextWidgetClass {
|
||||
Name "ERR_PreparationTitle"
|
||||
Slot FrameWidgetSlot {
|
||||
Anchor 0 0 1 0
|
||||
OffsetLeft 0
|
||||
OffsetTop 0
|
||||
SizeY 58
|
||||
OffsetBottom -58
|
||||
}
|
||||
Color 1 1 1 1
|
||||
Text "Preparing mission"
|
||||
"Font Size" 32
|
||||
"Min Font Size" 32
|
||||
}
|
||||
TextWidgetClass {
|
||||
Name "ERR_PreparationDescription"
|
||||
Slot FrameWidgetSlot {
|
||||
Anchor 0 0 1 1
|
||||
OffsetLeft 0
|
||||
OffsetTop 70
|
||||
OffsetRight 0
|
||||
OffsetBottom 0
|
||||
}
|
||||
Color 1 1 1 1
|
||||
Text "Preparing mission sites and equipment. You will spawn when the mission is ready."
|
||||
"Font Size" 22
|
||||
"Min Font Size" 22
|
||||
Wrap 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
MetaFileClass {
|
||||
Name "{BCCAE287751344D9}UI/layouts/ERR/PreparationStatus.layout"
|
||||
Configurations {
|
||||
LayoutResourceClass PC {
|
||||
}
|
||||
LayoutResourceClass HEADLESS : PC {
|
||||
}
|
||||
LayoutResourceClass XBOX_ONE : PC {
|
||||
}
|
||||
LayoutResourceClass XBOX_SERIES : PC {
|
||||
}
|
||||
LayoutResourceClass PS4 : PC {
|
||||
}
|
||||
LayoutResourceClass PS5 : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user