Prepare independent successor playtest with spawn recovery and casualty feedback

This commit is contained in:
2026-09-19 22:12:52 +02:00
parent 61e165ef5b
commit 6cf989a060
266 changed files with 134064 additions and 154 deletions
@@ -22,12 +22,50 @@ class ERR_AircraftFieldEnvelope : ERR_AircraftFlightEnvelope
if (columns < 1 || rows < 1 || columns >= MAX_TERRAIN_NODES || rows >= MAX_TERRAIN_NODES) return false;
return (columns + 1) * (rows + 1) <= MAX_TERRAIN_NODES;
}
#ifdef WORKBENCH
// At most the first refusal of each exact survey call is logged. No query,
// predicate, retry or resource allocation is added to the qualification.
protected static bool SurveyDiagnostics()
{
string enabled, outcome;
return System.GetCLIParam("errSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "aircraft-field";
}
protected static void SurveyTraceFailure(string stage, float fraction, TraceBox trace)
{
if (!SurveyDiagnostics()) return;
IEntity hit = trace.TraceEnt;
string kind = "none", resource = "";
if (hit)
{
kind = hit.Type().ToString();
if (hit.GetPrefabData()) resource = hit.GetPrefabData().GetPrefabName();
}
if (hit)
{
vector lower, upper; hit.GetWorldBounds(lower, upper);
PrintFormat("ERR AIRCRAFT FIELD SURVEY HIT BOUNDS: lower=%1 upper=%2", lower, upper);
}
PrintFormat("ERR AIRCRAFT FIELD SURVEY REFUSED: stage=%1 fraction=%2 entity=%3 class=%4 prefab=%5 start=%6 end=%7", stage, fraction, hit, kind, resource, trace.Start, trace.End);
}
#endif
static bool Survey(vector start, vector end, array<IEntity> excluded = null)
{
if (!GetGame() || !GetGame().GetWorld() || !FinitePoint(start) || !FinitePoint(end)) return false;
if (!GetGame() || !GetGame().GetWorld() || !FinitePoint(start) || !FinitePoint(end))
{
#ifdef WORKBENCH
if (SurveyDiagnostics()) PrintFormat("ERR AIRCRAFT FIELD SURVEY REFUSED: stage=context-or-finite-points start=%1 end=%2", start, end);
#endif
return false;
}
float distance = vector.DistanceXZ(start, end);
int columns = TerrainColumns(distance), rows = 40;
if (!TerrainBudget(columns, rows) || !(start[1] >= 2) || !(Math.AbsFloat(end[1] - start[1]) <= 0.3)) return false;
if (!TerrainBudget(columns, rows) || !(start[1] >= 2) || !(Math.AbsFloat(end[1] - start[1]) <= 0.3))
{
#ifdef WORKBENCH
if (SurveyDiagnostics()) PrintFormat("ERR AIRCRAFT FIELD SURVEY REFUSED: stage=budget-or-endpoints start=%1 end=%2 distance=%3 columns=%4 rows=%5 startHeight=%6 endHeight=%7 delta=%8", start, end, distance, columns, rows, start[1], end[1], Math.AbsFloat(end[1] - start[1]));
#endif
return false;
}
vector along = end - start; along[1] = 0; along = along * (1 / distance);
vector across = Vector(-along[2], 0, along[0]);
float span = distance + 80;
@@ -38,7 +76,14 @@ class ERR_AircraftFieldEnvelope : ERR_AircraftFlightEnvelope
for (int z = 0; z <= rows; z++)
{
vector sample = start + along * (-40 + span * x / columns) + across * (-40 + 80 * z / rows);
if (!(Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(sample) - start[1]) <= 0.3)) return false;
float terrain = SCR_TerrainHelper.GetTerrainY(sample);
if (!(Math.AbsFloat(terrain - start[1]) <= 0.3))
{
#ifdef WORKBENCH
if (SurveyDiagnostics()) PrintFormat("ERR AIRCRAFT FIELD SURVEY REFUSED: stage=terrain x=%1 z=%2 columns=%3 rows=%4 sample=%5 terrainHeight=%6 startHeight=%7 delta=%8", x, z, columns, rows, sample, terrain, start[1], Math.AbsFloat(terrain - start[1]));
#endif
return false;
}
}
vector lower, upper;
for (int axis = 0; axis < 3; axis++)
@@ -55,11 +100,22 @@ class ERR_AircraftFieldEnvelope : ERR_AircraftFlightEnvelope
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;
float corridorFraction = GetGame().GetWorld().TraceMove(corridor, null);
if (!(corridorFraction >= 1))
{
#ifdef WORKBENCH
SurveyTraceFailure("corridor", corridorFraction, corridor);
#endif
return false;
}
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;
float lowFraction = GetGame().GetWorld().TraceMove(low, null);
#ifdef WORKBENCH
if (!(lowFraction >= 1)) SurveyTraceFailure("low-objects", lowFraction, low);
#endif
return lowFraction >= 1;
}
static ERR_AircraftFieldEnvelope QualifyField(ERR_GameMode mode, Vehicle aircraft, IEntity pilot, vector destination)
{
@@ -1,12 +1,11 @@
// Unqualified 100m field-route prototype. No allocation, HQ dispatch or service
// integration. The shared base registry/bridge remains the sole input owner.
// Unqualified 100m field-route prototype, admitted by the typed service only.
// No allocation or HQ dispatch; the shared registry/bridge owns native inputs.
class ERR_AircraftFieldFlight : ERR_AircraftFlight
{
protected ref ERR_AircraftFieldEnvelope m_Field;
protected int m_FieldPhase; // 0 startup, 1 cruise, 2 braking, 3 slow settling
protected vector m_FieldReference;
protected vector m_FieldIntegral;
protected float m_FieldBrakeSpeed;
protected vector m_FieldLandingPoint;
protected bool m_FieldStopFrozen;
protected bool m_FieldArrived;
@@ -55,6 +54,7 @@ class ERR_AircraftFieldFlight : ERR_AircraftFlight
{
if (m_FieldStopFrozen) return true;
if (!Owns() || !m_Field || !Healthy()) { Fail("field braking ownership or capability unavailable"); return false; }
if (m_Simulation.HasAnyGroundContact()) { Fail("field airborne braking refused ground contact"); return false; }
vector point = m_Field.Destination();
if (m_Cancelled)
{
@@ -70,9 +70,9 @@ class ERR_AircraftFieldFlight : ERR_AircraftFlight
m_Controller.SetAutohoverEnabled(true);
if (!m_Controller.GetAutohoverEnabled()) { Fail("native field braking autohover enable refused"); return false; }
m_FieldLandingPoint = point; m_FieldStopFrozen = true;
// Begin reducing the reference at entry, before the modeled reserve boundary.
// This one frozen ceiling cannot increase on another cancellation or tick.
m_FieldBrakeSpeed = m_FieldReference.Length() * 0.9;
// Remove cruise integral once; retain the bounded vector slew toward rest.
// Repeated cancellation must not reset this state or the frozen deadline.
m_FieldIntegral = "0 0 0";
m_FieldPhase = 2; m_State = ERR_AircraftFlightState.TRANSLATING;
m_FieldLowSince = -1; m_Stable = -1;
// Includes braking, two low-speed seconds and five seconds of final hold.
@@ -112,6 +112,7 @@ class ERR_AircraftFieldFlight : ERR_AircraftFlight
float now = Now();
if (now < 0 || now >= m_Deadline || now >= m_StageDeadline) { Fail("bounded field stage expired"); return false; }
if (!Healthy() || !m_Readback) { Fail("field capability or native input readback lost"); return false; }
if (m_FieldPhase == 2 && m_Simulation.HasAnyGroundContact()) { Fail("field ground contact during airborne braking"); return false; }
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
if (!FieldContains(transform[3], transform[1][1]) || !ERR_AircraftFieldRules.SpeedAllowed(m_Simulation.GetVelocity())) { Fail("qualified field position, attitude or speed exceeded"); return false; }
return true;
@@ -121,10 +122,12 @@ class ERR_AircraftFieldFlight : ERR_AircraftFlight
vector target = m_Field.Destination(); if (m_FieldStopFrozen) target = m_FieldLandingPoint;
if (m_FieldPhase == 3 || m_State == ERR_AircraftFlightState.LANDING) return ERR_AircraftTranslationRules.Demand(target, position, velocity);
vector direction = target - position; direction[1] = 0;
float remaining = direction.Length(); float targetSpeed = 2;
if (m_FieldPhase == 2) targetSpeed = ERR_AircraftFieldRules.ApproachSpeed(remaining, Math.Min(m_FieldReference.Length(), m_FieldBrakeSpeed));
vector desired;
if (remaining > 0.001) desired = direction * (targetSpeed / remaining);
float remaining = direction.Length();
vector desired = "0 0 0";
// Braking targets rest immediately, independently of distance or bearing.
// A lagging native airframe can still accelerate while the reference slews;
// only native measurements can qualify the declared actual-speed reserve.
if (m_FieldPhase != 2 && remaining > 0.001) desired = direction * (2 / remaining);
m_FieldReference = ERR_AircraftFieldRules.RampReference(m_FieldReference, desired, dt);
vector nextIntegral;
vector demand = ERR_AircraftFieldRules.VelocityDemand(m_FieldReference, velocity, m_FieldIntegral, dt, nextIntegral);
@@ -173,9 +176,10 @@ class ERR_AircraftFieldFlight : ERR_AircraftFlight
}
if (m_FieldPhase == 2)
{
if (m_Simulation.HasAnyGroundContact()) { Fail("field ground contact during airborne braking"); return; }
if (!(horizontal < 0.25)) m_FieldLowSince = -1;
else if (m_FieldLowSince < 0) m_FieldLowSince = now;
if (!ERR_AircraftFieldRules.LowSpeedHeld(horizontal, m_FieldLowSince, now)) return;
if (!ERR_AircraftFieldRules.LowSpeedHeld(horizontal, m_FieldLowSince, now) || !(m_FieldReference.Length() <= 0.01)) return;
m_FieldPhase = 3; m_FieldReference = "0 0 0"; m_FieldIntegral = "0 0 0"; m_Stable = -1;
// No new deadline: slow positioning/hold belongs to the same braking budget.
}
@@ -37,18 +37,6 @@ class ERR_AircraftFieldRules
desired = CapHorizontal(desired, 2);
return CapHorizontal(current + CapHorizontal(desired - current, 0.2 * dt), 2);
}
// Remaining target distance includes permitted takeoff/corridor drift; its
// 120m arithmetic bound grants no authority beyond Contains/Length.
// Invert the entire candidate stopping reserve (including1s latency and3m
// margin), not just its deceleration term. Caller also freezes a0.9 entry-speed
// cap to begin reducing the velocityReference immediately, then slews the vector.
// The assumed0.15m/s² physical deceleration still requires native measurement.
static float ApproachSpeed(float remaining, float current)
{
if (!(remaining >= 0 && remaining <= 120) || !(current >= 0 && current <= 2)) return 0;
float reservedSpeed = (Math.Sqrt(0.09 + 1.2 * Math.Max(0, remaining - 3)) - 0.3) * 0.5;
return Math.Min(current, Math.Max(0, reservedSpeed));
}
// Integral units are (m/s)*seconds; nextIntegral is caller-owned state.
// Horizontal demand is world-space; the exact-owner controller converts it
// to the native airframe basis. The caller resets state at phase transitions.
@@ -76,7 +64,9 @@ class ERR_AircraftFieldRules
static bool ShouldBrake(float remaining, float speed)
{
if (!(remaining >= 0 && remaining <= 120) || !(speed >= 0 && speed <= 2.5)) return true;
return remaining <= Math.Max(25, BrakingReserve(speed));
// The candidate reserve uses actual speed; phase2 immediately targets rest.
// This predicate does not assert measured native stopping performance.
return remaining <= BrakingReserve(speed);
}
// Admit only the full experimental lease, with <=1s dispatch allowance.
// Never recompute a deadline from these checks or renew the original lease.
@@ -18,7 +18,7 @@ modded class ERR_AircraftFieldFlight
bool alreadyFrozen = m_FieldStopFrozen;
bool result = super.BeginFieldBrake(now);
ERR_NativeAircraftFieldProbe probe = ERR_NativeAircraftFieldProbe.Current();
if (result && !alreadyFrozen && probe && m_Aircraft && m_Simulation) probe.BrakeBegan(this, now, m_Aircraft.GetOrigin(), m_Simulation.GetVelocity());
if (result && !alreadyFrozen && probe && m_Aircraft && m_Simulation) probe.BrakeBegan(this, now, m_Aircraft.GetOrigin(), m_Simulation.GetVelocity(), m_Simulation.HasAnyGroundContact());
return result;
}
protected override void CheckTranslation(float now, vector position, float height, float speed, float upright)
@@ -26,7 +26,7 @@ modded class ERR_AircraftFieldFlight
int oldPhase = m_FieldPhase;
super.CheckTranslation(now, position, height, speed, upright);
ERR_NativeAircraftFieldProbe probe = ERR_NativeAircraftFieldProbe.Current();
if (oldPhase == 2 && m_FieldPhase == 3 && probe && m_Aircraft && m_Simulation) probe.BrakeStopped(this, now, m_Aircraft.GetOrigin(), m_Simulation.GetVelocity());
if (oldPhase == 2 && m_FieldPhase == 3 && probe && m_Aircraft && m_Simulation) probe.BrakeStopped(this, now, m_Aircraft.GetOrigin(), m_Simulation.GetVelocity(), m_Simulation.HasAnyGroundContact());
}
}
@@ -65,9 +65,9 @@ class ERR_NativeAircraftFieldRules
{
return phase == 1 && !contact && alongSpeed >= 1 && horizontal >= 1 && horizontal <= 2.5;
}
static bool Braking(float reserve, float sampledTravel, float displacement, float lowSince, float now, float horizontal)
static bool Braking(float reserve, float sampledTravel, float displacement, float lowSince, float now, float horizontal, bool contactObserved)
{
return reserve >= 3 && reserve <= 26.334 && sampledTravel >= 0 && sampledTravel <= reserve && displacement >= 0 && displacement <= reserve && horizontal >= 0 && horizontal < 0.25 && lowSince >= 0 && now - lowSince >= 2000;
return !contactObserved && reserve >= 3 && reserve <= 26.334 && sampledTravel >= 0 && sampledTravel <= reserve && displacement >= 0 && displacement <= reserve && horizontal >= 0 && horizontal < 0.25 && lowSince >= 0 && now - lowSince >= 2000;
}
static bool Terminal(bool cancellation, int state, int stops, bool arrived, bool cruiseHeld, bool endpointHeld, bool braking, float displacement, float targetError)
{
@@ -123,6 +123,7 @@ class ERR_NativeAircraftFieldProbe
protected bool m_BrakeObserved;
protected bool m_BrakeStopped;
protected bool m_BrakePassed;
protected bool m_BrakeContactObserved;
protected string m_ObservationFailure;
protected vector m_BrakeOrigin;
protected vector m_BrakeLandingPoint;
@@ -140,6 +141,7 @@ class ERR_NativeAircraftFieldProbe
protected int m_HelicopterHooks;
protected bool m_LastEngine;
protected bool m_AllocationRequested;
protected bool m_TerrainAudited;
protected bool m_PilotRequested;
protected bool m_Stopped;
protected bool m_Complete;
@@ -196,14 +198,49 @@ class ERR_NativeAircraftFieldProbe
if (m_Stopped || !m_Flight || controller != m_Controller) return;
// Observation only: super-chain order cannot cause a second control write.
m_HelicopterHooks++;
ObserveBrakingContact(!m_Simulation || m_Simulation.HasAnyGroundContact());
}
static bool TerrainAuditEnabled()
{
string audit, cancellation;
return Enabled() && System.GetCLIParam("errSmokeFieldTerrainAudit", audit) && audit == "1" && !(System.GetCLIParam("errSmokeFieldServiceCancel", cancellation) && cancellation == "1");
}
protected void AuditTerrain()
{
if (m_TerrainAudited || !TerrainAuditEnabled() || m_Stopped || m_AllocationRequested || m_Aircraft || m_Group || m_Pilot || !GetGame() || !GetGame().GetWorld()) return;
m_TerrainAudited = true;
// Read-only fixed coarse grid for offline corridor authorship. No selected
// candidate, fallback, traces, entity allocation or qualification is produced.
Print("ERR AIRCRAFT FIELD TERRAIN AUDIT BEGIN: minX=4750 maxX=5150 minZ=11600 maxZ=12000 spacing=10 columns=41 rows=41 nodes=1681");
for (int x = 0; x <= 40; x++)
{
for (int z = 0; z <= 40; z++)
{
vector point = "0 0 0";
point[0] = 4750 + x * 10; point[2] = 11600 + z * 10;
float terrain = SCR_TerrainHelper.GetTerrainY(point);
int index = x * 41 + z;
if (!(Math.AbsFloat(terrain) <= 1000000))
{
PrintFormat("ERR AIRCRAFT FIELD TERRAIN AUDIT ABORT: index=%1 x=%2 z=%3 nonfiniteOrInvalidHeight=%4", index, point[0], point[2], terrain);
return;
}
PrintFormat("ERR AIRCRAFT FIELD TERRAIN AUDIT POINT: index=%1 x=%2 z=%3 height=%4", index, point[0], point[2], terrain);
}
}
Print("ERR AIRCRAFT FIELD TERRAIN AUDIT END: nodes=1681; original survey refusal unchanged; no candidate qualified");
}
protected bool FindPad(out vector result)
{
// This exact airport start was observed in the twenty-metre fixture. The
// longer corridor still requires its own complete survey; there is no search.
vector candidate = ERR_Actors.Ground("4850.953 0 11787.412");
vector destination = ERR_Actors.Ground(candidate + "100 0 0");
if (!ERR_AircraftFieldEnvelope.Survey(candidate, destination)) return false;
// Authored from terrain, warehouse and tree evidence: one south-shifted northbound candidate.
// Coarse data grants no clearance; the complete survey below is mandatory.
vector candidate = ERR_Actors.Ground("4890 0 11730");
vector destination = ERR_Actors.Ground(candidate + "0 0 100");
if (!ERR_AircraftFieldEnvelope.Survey(candidate, destination))
{
AuditTerrain();
return false;
}
PrintFormat("ERR AIRCRAFT FIELD FIXTURE CORRIDOR: singleStart=%1 destination=%2; fixed100m complete survey passed", candidate, destination);
result = candidate; m_Destination = destination; return true;
}
@@ -259,27 +296,35 @@ class ERR_NativeAircraftFieldProbe
PrintFormat("ERR AIRCRAFT FIELD MOTION: now=%1 state=%2 phase=%3 position=%4 velocity=%5 refVelocity=%6 integral=%7 observedLocalInput=%8 fuel=%9", now, m_Flight.State(), m_Flight.FieldPhase(), position, velocity, m_Flight.ReferenceVelocity(), m_Flight.IntegralDemand(), m_Flight.ERR_NativeFieldInputDemand(), m_Fuel.GetTotalFuel());
PrintFormat("ERR AIRCRAFT FIELD LEASE: token=%1 originalDeadline=%2 stageDeadline=%3 actions=%4 helicopter=%5 contact=%6 engine=%7 targetError=%8", m_Flight.OrderToken(), m_ControlDeadline, m_Flight.ERR_NativeFieldStageDeadline(), m_Flight.ERR_NativeFieldActionUpdates(), m_Flight.ERR_NativeFieldHelicopterUpdates(), m_Simulation.HasAnyGroundContact(), m_Controller.IsEngineOn(), vector.DistanceXZ(position, m_Flight.LandingPoint()));
}
void BrakeBegan(ERR_AircraftFieldFlight flight, float now, vector position, vector velocity)
void ObserveBrakingContact(bool contact)
{
if (m_Stopped || !m_BrakeObserved || m_BrakeStopped || !contact) return;
m_BrakeContactObserved = true;
m_ObservationFailure = "native ground contact cannot qualify airborne braking";
}
void BrakeBegan(ERR_AircraftFieldFlight flight, float now, vector position, vector velocity, bool contact)
{
if (m_Stopped || flight != m_Flight) return;
if (m_BrakeObserved) { m_ObservationFailure = "production brake event occurs exactly once"; return; }
m_BrakeObserved = true; m_BrakeAt = now;
ObserveBrakingContact(contact);
m_BrakeOrigin = position; m_BrakePrevious = position;
m_BrakeReserve = ERR_AircraftFieldRules.BrakingReserve(ERR_AircraftFieldRules.HorizontalSpeed(velocity));
m_BrakeStageDeadline = flight.ERR_NativeFieldStageDeadline(); m_BrakeLandingPoint = flight.LandingPoint();
PrintFormat("ERR AIRCRAFT FIELD BRAKE OBSERVED: now=%1 position=%2 velocity=%3 declaredHypothesisReserve=%4 stageDeadline=%5 originalDeadline=%6", now, position, velocity, m_BrakeReserve, m_BrakeStageDeadline, m_ControlDeadline);
}
void BrakeStopped(ERR_AircraftFieldFlight flight, float now, vector position, vector velocity)
void BrakeStopped(ERR_AircraftFieldFlight flight, float now, vector position, vector velocity, bool contact)
{
if (m_Stopped || flight != m_Flight) return;
if (!m_BrakeObserved || m_BrakeStopped) { m_ObservationFailure = "one actual low-speed braking transition after original brake event"; return; }
ObserveBrakingContact(contact);
m_BrakeStopped = true;
m_BrakeSampledTravel += vector.DistanceXZ(position, m_BrakePrevious);
m_BrakePrevious = position;
float displacement = vector.DistanceXZ(position, m_BrakeOrigin);
m_BrakePassed = ERR_NativeAircraftFieldRules.Braking(m_BrakeReserve, m_BrakeSampledTravel, displacement, m_BrakeLowSince, now, ERR_AircraftFieldRules.HorizontalSpeed(velocity));
PrintFormat("ERR AIRCRAFT FIELD STOP OBSERVED: now=%1 position=%2 sampledTravel=%3 displacement=%4 declaredReserve=%5 lowSince=%6 passed=%7", now, position, m_BrakeSampledTravel, displacement, m_BrakeReserve, m_BrakeLowSince, m_BrakePassed);
if (!m_BrakePassed) m_ObservationFailure = "observed braking exceeded declared reserve or lacked independent two-second low-speed observations";
m_BrakePassed = ERR_NativeAircraftFieldRules.Braking(m_BrakeReserve, m_BrakeSampledTravel, displacement, m_BrakeLowSince, now, ERR_AircraftFieldRules.HorizontalSpeed(velocity), m_BrakeContactObserved);
PrintFormat("ERR AIRCRAFT FIELD STOP OBSERVED: now=%1 position=%2 sampledTravel=%3 displacement=%4 declaredReserve=%5 lowSince=%6 passed=%7 contactObserved=%8", now, position, m_BrakeSampledTravel, displacement, m_BrakeReserve, m_BrakeLowSince, m_BrakePassed, m_BrakeContactObserved);
if (!m_BrakePassed && !m_BrakeContactObserved) m_ObservationFailure = "observed braking exceeded declared reserve or lacked independent two-second low-speed observations";
}
int Tick(float now)
{
@@ -356,6 +401,7 @@ class ERR_NativeAircraftFieldProbe
if (!StopRetainedField(now)) return -1;
if (m_BrakeObserved && !m_BrakeStopped)
{
ObserveBrakingContact(contact);
m_BrakeSampledTravel += vector.DistanceXZ(position, m_BrakePrevious); m_BrakePrevious = position;
if (!(horizontal < 0.25)) m_BrakeLowSince = -1;
else if (m_BrakeLowSince < 0) m_BrakeLowSince = now;
+3 -1
View File
@@ -31,7 +31,8 @@ ERR_ScenarioDefinition {
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 8 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 16 }
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
}
RevealOnEscape 1
}
@@ -82,6 +83,7 @@ ERR_ScenarioDefinition {
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 4 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 8 }
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 6 }
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
}
}
@@ -0,0 +1,51 @@
// Presentation only. The service still owns roster, settling and seat authority.
class ERR_CasualtyFeedback
{
static bool Visible(ERR_GameMode mode, IEntity caller, IEntity casualty)
{
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty || !ERR_CasualtyLoadingRules.Down(casualty)) return false;
Vehicle aircraft = mode.CasualtyLoadingAircraft();
Faction faction = mode.PlayerFaction();
return aircraft && faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin()));
}
static string PhysicalReason(ERR_GameMode mode, IEntity caller, IEntity casualty)
{
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION) return "Wait until helicopter extraction is available";
if (!caller || caller == casualty || !ERR_Actors.CanFight(caller)) return "A living, conscious rescuer is required";
if (CompartmentAccessComponent.GetVehicleIn(caller)) return "Leave the helicopter before loading a casualty";
if (!ERR_CasualtyLoadingRules.GroundSettled(casualty)) return "Place the living, unconscious casualty on stable ground";
Vehicle aircraft = mode.CasualtyLoadingAircraft();
if (!aircraft || !ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin()))) return "Stay beside the casualty and within twelve metres of the extraction helicopter";
if (!ERR_CasualtyLoadingRules.AircraftReady(aircraft)) return "The helicopter must be upright, grounded and stationary";
if (!ERR_CasualtyLoadingRules.Nearby(mode, caller, casualty)) return "Keep clear access to a fellow prisoner's body";
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(aircraft.FindComponent(BaseCompartmentManagerComponent));
if (!manager) return "No accessible transport seat is available";
array<BaseCompartmentSlot> seats = {}; manager.GetCompartments(seats);
foreach (BaseCompartmentSlot seat : seats)
if (ERR_CasualtyLoadingRules.TransportBench(seat, aircraft) && !seat.GetOccupant() && !seat.IsReserved() && seat.IsCompartmentAccessible() && !seat.IsGetInLockedFor(casualty)) return string.Empty;
return "No accessible transport seat is available";
}
}
modded class SCR_PlayerController
{
// Only the server addresses the still-current initiating rescuer's controller.
void ERR_CasualtyFeedback(ERR_GameMode mode, IEntity actor, string message)
{
if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.Active() || !actor || actor != GetControlledEntity() || !mode.Players() || !mode.Players().CurrentActor(actor)) return;
if (GetGame().GetPlayerManager().GetPlayerController(GetPlayerId()) != this) return;
RplId modeId = ERR_CasualtyLoadingRules.EntityId(mode);
RplId actorId = ERR_CasualtyLoadingRules.EntityId(actor);
if (modeId == RplId.Invalid() || actorId == RplId.Invalid() || message.IsEmpty()) return;
if (this == GetGame().GetPlayerController()) ERR_CasualtyFeedbackOwner(modeId, actorId, message);
else Rpc(ERR_CasualtyFeedbackOwner, modeId, actorId, message);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_CasualtyFeedbackOwner(RplId modeId, RplId actorId, string message)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!GetGame() || this != GetGame().GetPlayerController() || !mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || ERR_CasualtyLoadingRules.EntityId(GetControlledEntity()) != actorId || message.IsEmpty()) return;
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (!hints || !hints.ShowCustom(message, "Casualty loading", 10)) SCR_ChatComponent.RadioProtocolMessage(message);
}
}
@@ -131,6 +131,7 @@ class ERR_CasualtyLoadTicket
int CommittedAt;
bool Committed;
bool OwnerReported;
bool FeedbackSubmitted; // presentation only; never authorizes entry
}
class ERR_CasualtyLoadingService
{
@@ -204,6 +205,34 @@ class ERR_CasualtyLoadingService
if (!CurrentLife(caller) || !CurrentLife(casualty) || !Settled(casualty, System.GetTickCount()) || PendingActor(caller, casualty)) return false;
return AvailableSeat(casualty, m_Mode.ExtractionVehicle()) != null;
}
// Reasons never replace CanLoad/Begin authority or reserve a seat.
string RejectionReason(IEntity caller, IEntity casualty)
{
if (!Replication.IsServer() || m_Stopped) return "Casualty loading is unavailable";
string physical = ERR_CasualtyFeedback.PhysicalReason(m_Mode, caller, casualty);
if (!physical.IsEmpty()) return physical;
if (!CurrentLife(caller) || !CurrentLife(casualty)) return "Both prisoners must still be connected to this mission";
if (!Settled(casualty, System.GetTickCount())) return "Let the casualty rest on the ground for five seconds";
if (PendingActor(caller, casualty)) return "A casualty loading request is already in progress";
if (!AvailableSeat(casualty, m_Mode.ExtractionVehicle())) return "No accessible transport seat is available";
return string.Empty;
}
protected void NotifyCaller(IEntity caller, string message)
{
if (!Replication.IsServer() || !m_Mode || !m_Mode.Active() || m_Mode.Phase() != ERR_Phase.EXTRACTION || message.IsEmpty()) return;
ERR_PlayerLife life = CurrentLife(caller);
if (!life) return;
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(life.ConnectionId));
if (!controller || controller.GetControlledEntity() != caller) return;
controller.ERR_CasualtyFeedback(m_Mode, caller, message);
}
void Request(IEntity caller, IEntity casualty)
{
if (!Replication.IsServer()) return;
string reason = RejectionReason(caller, casualty);
if (!reason.IsEmpty()) { NotifyCaller(caller, reason); return; }
if (!Begin(caller, casualty)) NotifyCaller(caller, "Loading could not start. Check the casualty and available transport seats, then try again.");
}
bool Begin(IEntity caller, IEntity casualty)
{
if (!CanLoad(caller, casualty) || m_NextNonce >= 2147483646) return false;
@@ -221,6 +250,7 @@ class ERR_CasualtyLoadingService
m_Pending.Insert(ticket);
ticket.Seat.SetReserved(casualty);
if (!ticket.Seat.IsReservedBy(casualty)) { Resolve(ticket, false); return false; }
ticket.FeedbackSubmitted = true;
ScheduleMaintenance();
// Preparation never moves the actor. Its owner starts a local real-time lease.
controller.ERR_PrepareCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), ticket.Nonce);
@@ -264,12 +294,18 @@ class ERR_CasualtyLoadingService
{
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ERR_Actors.Alive(ticket.Casualty) && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection) && SameLife(ticket.Casualty, ticket.CasualtyAccount, ticket.CasualtyConnection) && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
}
protected void NotifyResolution(ERR_CasualtyLoadTicket ticket, bool success)
{
if (!success && ticket.FeedbackSubmitted && !m_Stopped && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection))
NotifyCaller(ticket.Caller, "Casualty boarding could not be confirmed. Check the helicopter before retrying; keep it stationary with an accessible transport seat.");
}
protected void Resolve(ERR_CasualtyLoadTicket ticket, bool success)
{
int index = m_Pending.Find(ticket); if (index < 0) return;
m_Pending.Remove(index);
if (ticket.Seat && ticket.Casualty && ticket.Seat.IsReservedBy(ticket.Casualty)) ticket.Seat.SetReserved(null);
if (m_Pending.IsEmpty()) CancelMaintenance();
NotifyResolution(ticket, success);
m_OnResolved.Invoke(ticket.Casualty, success);
}
void Tick(float worldNow)
+33 -1
View File
@@ -35,6 +35,7 @@ class ERR_GameMode : SCR_BaseGameMode
[RplProp()]
protected string m_RecoveryStatus;
protected bool m_StartedPlayers;
protected ref map<int, PlayerController> m_ExhaustedInitialAdmissions = new map<int, PlayerController>();
protected bool m_Stopped;
protected bool m_NativeEndHandled;
protected float m_NoSurvivorsSince = -1;
@@ -263,6 +264,36 @@ class ERR_GameMode : SCR_BaseGameMode
m_NoSurvivorsSince = -1;
}
// Exhaustion is admission state, never a spawned participant or spent life.
void InitialAdmissionStarted(int playerId)
{
if (!Replication.IsServer()) return;
m_ExhaustedInitialAdmissions.Remove(playerId);
if (!m_StartedPlayers) m_NoSurvivorsSince = -1;
}
void InitialAdmissionExhausted(int playerId, PlayerController controller)
{
if (!Replication.IsServer() || !Active() || m_StartedPlayers || !controller) return;
ERR_PlayerLife life = m_Roster.Get(playerId);
if (!life || !life.Connected || life.UsedLife || life.Actor) return;
if (GetGame().GetPlayerManager().GetPlayerController(playerId) != controller || controller.GetControlledEntity()) return;
m_ExhaustedInitialAdmissions.Set(playerId, controller);
}
protected bool InitialAdmissionsExhausted()
{
array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected);
if (connected.IsEmpty()) return false;
foreach (int id : connected)
{
PlayerController controller = GetGame().GetPlayerManager().GetPlayerController(id);
if (!controller || !m_ExhaustedInitialAdmissions.Contains(id) || m_ExhaustedInitialAdmissions.Get(id) != controller || controller.GetControlledEntity()) return false;
ERR_PlayerLife life = m_Roster.Get(id);
if (!life || !life.Connected || life.UsedLife || life.Actor) return false;
}
return true;
}
protected string SettingsPath() { return ERR_Settings.PATH; }
protected override void EOnInit(IEntity owner)
@@ -379,12 +410,13 @@ class ERR_GameMode : SCR_BaseGameMode
m_NoSurvivorsSince = -1;
return;
}
if (m_StartedPlayers && !m_Roster.HasSurvivor())
if ((m_StartedPlayers || InitialAdmissionsExhausted()) && !m_Roster.HasSurvivor())
{
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now;
if (now - m_NoSurvivorsSince >= 30000)
{
m_EndReason = "No surviving prisoners remain.";
if (!m_StartedPlayers) m_EndReason = "No prisoners could be admitted to this round.";
Transition(ERR_Phase.DEFEAT);
return;
}
@@ -3,21 +3,26 @@ class ERR_LoadCasualtyAction : ScriptedUserAction
{
override bool CanBeShownScript(IEntity user)
{
return ERR_CasualtyLoadingRules.Nearby(ERR_GameMode.Get(), user, GetOwner());
return ERR_CasualtyFeedback.Visible(ERR_GameMode.Get(), user, GetOwner());
}
override bool CanBePerformedScript(IEntity user)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!ERR_CasualtyLoadingRules.Nearby(mode, user, GetOwner())) return false;
// Clients do not own the authoritative roster. Server always checks again.
if (!Replication.IsServer()) return true;
return mode.CasualtyLoading() && mode.CasualtyLoading().CanLoad(user, GetOwner());
SetCannotPerformReason(string.Empty);
string reason = ERR_CasualtyFeedback.PhysicalReason(mode, user, GetOwner());
if (reason.IsEmpty() && Replication.IsServer())
{
if (!mode.CasualtyLoading()) reason = "Casualty loading is unavailable";
else reason = mode.CasualtyLoading().RejectionReason(user, GetOwner());
}
if (!reason.IsEmpty()) { SetCannotPerformReason(reason); return false; }
return true;
}
override bool HasLocalEffectOnlyScript() { return false; }
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
{
if (!Replication.IsServer() || pOwnerEntity != GetOwner()) return;
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && mode.CasualtyLoading()) mode.CasualtyLoading().Begin(pUserEntity, pOwnerEntity);
if (mode && mode.CasualtyLoading()) mode.CasualtyLoading().Request(pUserEntity, pOwnerEntity);
}
}
@@ -16,7 +16,10 @@ class ERR_NativeDefinitionProbe
cache.Id = "everon-prison-1";
if (!Check(!definition.Valid(), "duplicate site identity rejected")) return false;
cache.Id = "everon-supply-1";
ERR_SiteStock documents = cache.Stock[3];
ERR_SiteStock documents;
foreach (ERR_SiteStock stock : cache.Stock)
if (stock.Prefab == ERR_Resources.ObjectiveDocument()) { documents = stock; break; }
if (!Check(documents != null, "clue stock located independently of supply order")) return false;
documents.TargetId = "missing-radio";
if (!Check(!definition.Valid(), "dangling document target rejected")) return false;
documents.TargetId = "everon-prison-1";
+12 -2
View File
@@ -94,6 +94,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId)).ERR_SetObserver(ERR_ObserverReason.NONE);
return;
}
mode.InitialAdmissionStarted(playerId);
m_Attempts.Set(playerId, 0);
DoSpawn_S(playerId);
}
@@ -111,7 +112,12 @@ class ERR_SpawnLogic : SCR_SpawnLogic
// for the same server preparation result before consuming its retry budget.
if (!mode.Active()) { RetryLater(playerId); return; }
int attempts = m_Attempts.Get(playerId);
if (attempts >= 30) { Observe(playerId, ERR_ObserverReason.SPAWN_UNAVAILABLE); return; }
if (attempts >= 30)
{
Observe(playerId, ERR_ObserverReason.SPAWN_UNAVAILABLE);
mode.InitialAdmissionExhausted(playerId, controller);
return;
}
if (!mode.Players().CanEnter(playerId, mode.Phase()))
{
ERR_ObserverReason reason = ERR_ObserverReason.SPENT_LIFE;
@@ -333,7 +339,11 @@ class ERR_SpawnLogic : SCR_SpawnLogic
CancelAdmission(playerId, ERR_ObserverReason.NONE);
m_Identities.Remove(playerId);
ERR_GameMode mode = ERR_GameMode.Get();
if (mode) mode.Players().Disconnect(playerId);
if (mode)
{
mode.InitialAdmissionStarted(playerId);
mode.Players().Disconnect(playerId);
}
if (GetPlayerRespawnComponent_S(playerId)) super.OnPlayerDisconnected_S(playerId, cause, timeout);
}
bool Authorizes(int playerId, SCR_SpawnData data)