Prepare independent successor playtest with spawn recovery and casualty feedback
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user