Build independent base gameplay and source-bound native verification

This commit is contained in:
2026-09-19 08:09:14 +02:00
parent 16f397e581
commit af5146a317
580 changed files with 65780 additions and 107 deletions
@@ -2,6 +2,12 @@
// authorization, derived on the server from its authenticated player ID.
class ERR_CommandArguments
{
static string CoreSetting(string section, string option)
{
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
return "";
}
static bool Integer(string text, out int value)
{
if (text.IsEmpty() || text.Length() > 6) return false;
@@ -15,8 +21,7 @@ class ERR_AdminService
{
protected string Setting(string section, string option)
{
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
return "";
return ERR_CommandArguments.CoreSetting(section, option);
}
protected bool Extension(ERR_GameMode mode, string arguments, array<string> replies) { return false; }
protected bool TimeCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
@@ -42,9 +47,30 @@ class ERR_AdminService
else replies.Insert("Invalid time or settings could not be saved. Use mission time set HH:MM (00:00-23:59) or mission speed set N (1-24). Previous setting retained.");
return true;
}
protected string SettingHelp(string key)
{
if (key == "intelRevealChancePercent")
return "Use /esc intel reveal set N (0-100). This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
return "";
}
protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies)
{
if (parts.Count() == 3 && parts[2] == "show") { replies.Insert(key + "=" + settings.Get(key).ToString()); return; }
string help = SettingHelp(key);
if (parts.Count() == 3 && parts[2] == "help" && !help.IsEmpty()) { replies.Insert(help); return; }
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Integer(parts[3], value) && settings.Set(key, value))
replies.Insert(key + "=" + value.ToString() + " saved");
else
{
string failure = "Invalid value or settings could not be saved; previous setting retained.";
if (!help.IsEmpty()) failure += " " + help;
replies.Insert(failure);
}
}
protected string Help()
{
return "/esc status | settings | mission radio show|set N | mission time show|set HH:MM | mission speed show|set N | spectate next|previous";
return "/esc status | settings | mission abandon | mission radio show|set N | mission date show|set YYYY-MM-DD|authored | mission weather show|set authored|clear|overcast|rain | mission time show|set HH:MM | mission speed show|set N | intel reveal show|set N|help | spectate next|previous";
}
void Execute(ERR_GameMode mode, bool authorized, string arguments, array<string> replies)
{
@@ -54,6 +80,12 @@ class ERR_AdminService
if (arguments.Length() > 160) { replies.Insert("Command is too long."); return; }
arguments = arguments.Trim(); arguments.ToLower();
if (arguments == "status") { replies.Insert(mode.Status()); Extension(mode, arguments, replies); return; }
if (arguments == "mission abandon")
{
if (mode.AbandonRound()) replies.Insert("Round abandoned. The normal defeat and restart sequence has begun.");
else replies.Insert("Only an active round can be abandoned.");
return;
}
if (!mode.Settings()) { replies.Insert("Mission settings are not ready."); return; }
if (arguments == "settings")
{
@@ -63,17 +95,14 @@ class ERR_AdminService
if (Extension(mode, arguments, replies)) return;
array<string> parts = {};
arguments.Split(" ", parts, true);
if (ERR_EnvironmentCommands.Execute(mode.Settings(), mode.Environment(), parts, replies)) return;
if (TimeCommand(mode, parts, replies)) return;
if (parts.Count() == 3 || parts.Count() == 4)
{
string key = Setting(parts[0], parts[1]);
if (!key.IsEmpty())
{
if (parts.Count() == 3 && parts[2] == "show") { replies.Insert(key + "=" + mode.Settings().Get(key).ToString()); return; }
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Integer(parts[3], value) && mode.Settings().Set(key, value))
replies.Insert(key + "=" + value.ToString() + " saved");
else replies.Insert("Invalid value or settings could not be saved; previous setting retained.");
SettingCommand(mode.Settings(), key, parts, replies);
return;
}
}
@@ -0,0 +1,82 @@
// Conservative fallback for a sleeping, engine-off Mi8 whose native simulation
// has not yet populated contact flags. Reads physical support; never wakes it.
class ERR_CasualtyGroundSupport
{
static bool SupportHit(float fraction, float normalY, bool selfHit, bool dynamicHit)
{
return fraction >= 0 && fraction < 1 && normalY >= 0.85 && !selfHit && !dynamicHit;
}
static bool BelongsTo(IEntity entity, IEntity root)
{
while (entity)
{
if (entity == root) return true;
entity = entity.GetParent();
}
return false;
}
static bool WheelCenter(Vehicle aircraft, string bone, out vector center, bool diagnostics = false)
{
if (!aircraft) return false;
if (aircraft.GetBoneIndex(bone) == -1)
{
if (diagnostics) Print("ERR GEAR PIVOT MISSING: " + bone);
return false;
}
vector identity[4]; Math3D.MatrixIdentity4(identity);
PointInfo pivot = new PointInfo();
pivot.Set(aircraft, bone, identity);
vector world[4]; pivot.GetWorldTransform(world);
center = world[3];
if (diagnostics) PrintFormat("ERR GEAR PIVOT: bone=%1 node=%2 center=%3", bone, pivot.GetNodeId(), center);
return pivot.GetNodeId() != -1;
}
static bool WheelSupported(Vehicle aircraft, vector center, float radius, bool diagnostics = false)
{
vector bottom = center - Vector(0, radius, 0);
TraceParam trace = new TraceParam();
trace.Start = bottom + "0 0.10 0";
trace.End = bottom - "0 0.12 0";
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.LayerMask = TRACE_LAYER_CAMERA;
trace.Exclude = aircraft;
float fraction = aircraft.GetWorld().TraceMove(trace, null);
bool selfHit = trace.TraceEnt && BelongsTo(trace.TraceEnt, aircraft);
bool dynamicHit;
if (trace.TraceEnt)
{
Physics surface = trace.TraceEnt.GetPhysics();
// Unknown/entity-only support and moving actors/vehicles are not parking.
dynamicHit = !surface || surface.IsDynamic();
}
bool supported = SupportHit(fraction, trace.TraceNorm[1], selfHit, dynamicHit);
if (diagnostics)
{
vector contact = vector.Lerp(trace.Start, trace.End, fraction);
float terrainY = aircraft.GetWorld().GetSurfaceY(bottom[0], bottom[2]);
PrintFormat("ERR GEAR TERRAIN: bottom=%1 terrainY=%2 tireTerrainGap=%3", bottom, terrainY, bottom[1] - terrainY);
PrintFormat("ERR GEAR SUPPORT: center=%1 radius=%2 bottom=%3 fraction=%4 normal=%5 self=%6 dynamic=%7 supported=%8 gap=%9", center, radius, bottom, fraction, trace.TraceNorm, selfHit, dynamicHit, supported, bottom[1] - contact[1]);
}
return supported;
}
static bool Parked(Vehicle aircraft, bool diagnostics = false)
{
if (!aircraft || !aircraft.GetPrefabData() || !aircraft.GetPrefabData().GetPrefabName().Contains("/Mi8MT/")) return false;
Physics physics = aircraft.GetPhysics();
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
if (!physics || !controller || !controller.GetSimulation()) return false;
if (diagnostics) PrintFormat("ERR PARKED SUPPORT: physicsAwake=%1 engineOn=%2 nativeContact=%3", physics.IsActive(), controller.GetSimulation().EngineIsOn(), controller.GetSimulation().HasAnyGroundContact());
if (physics.IsActive() || controller.GetSimulation().EngineIsOn()) return false;
vector front, left, right;
if (!WheelCenter(aircraft, "v_wheel_F", front, diagnostics) || !WheelCenter(aircraft, "v_wheel_bL", left, diagnostics) || !WheelCenter(aircraft, "v_wheel_bR", right, diagnostics)) return false;
// Fail closed if native pivot resolution collapses to an owner origin or
// otherwise cannot represent the authored three-point landing footprint.
if (vector.DistanceXZ(front, left) < 1 || vector.DistanceXZ(front, right) < 1 || vector.DistanceXZ(left, right) < 1) return false;
// Radii come from installed Mi8_base.et's authored landing-gear configuration.
// Every wheel needs a short, upward-facing static collision directly below.
bool a = WheelSupported(aircraft, front, 0.30, diagnostics);
bool b = WheelSupported(aircraft, left, 0.45, diagnostics);
bool c = WheelSupported(aircraft, right, 0.45, diagnostics);
return a && b && c;
}
}
@@ -0,0 +1,48 @@
// Round-owned server service; only the aircraft reference is replicated for action visibility.
modded class ERR_GameMode
{
protected ref ERR_CasualtyLoadingService m_ERRCasualtyLoading;
[RplProp()]
protected RplId m_ERRCasualtyAircraftId = RplId.Invalid();
ERR_CasualtyLoadingService CasualtyLoading() { return m_ERRCasualtyLoading; }
Vehicle CasualtyLoadingAircraft()
{
if (Replication.IsServer()) return ExtractionVehicle();
RplComponent component = RplComponent.Cast(Replication.FindItem(m_ERRCasualtyAircraftId));
if (!component) return null;
return Vehicle.Cast(component.GetEntity());
}
protected void ERR_UpdateCasualtyAircraft(float now)
{
Vehicle aircraft = ExtractionVehicle();
if (!aircraft)
{
if (m_ERRCasualtyAircraftId != RplId.Invalid()) { m_ERRCasualtyAircraftId = RplId.Invalid(); Replication.BumpMe(); }
return;
}
RplComponent component = RplComponent.Cast(aircraft.FindComponent(RplComponent));
if (!component || m_ERRCasualtyAircraftId == component.Id()) return;
m_ERRCasualtyAircraftId = component.Id();
Replication.BumpMe();
}
protected void ERR_StopCasualtyLoading()
{
if (m_ERRCasualtyLoading)
{
Players().OnChanged().Remove(m_ERRCasualtyLoading.RosterChanged);
OnTick().Remove(m_ERRCasualtyLoading.Tick);
m_ERRCasualtyLoading.Stop();
}
OnTick().Remove(ERR_UpdateCasualtyAircraft);
}
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
if (!Replication.IsServer() || SCR_Global.IsEditMode()) return;
m_ERRCasualtyLoading = new ERR_CasualtyLoadingService(this);
OnTick().Insert(ERR_UpdateCasualtyAircraft);
OnTick().Insert(m_ERRCasualtyLoading.Tick);
Players().OnChanged().Insert(m_ERRCasualtyLoading.RosterChanged);
OnStop().Insert(ERR_StopCasualtyLoading);
}
}
@@ -0,0 +1,75 @@
// Only successor service methods initiate this controller-owned handshake.
// No caller-supplied entity/seat RPC can ask the server to load another player.
modded class SCR_PlayerController
{
protected RplId m_ERRPreparedLoadMode = RplId.Invalid();
protected int m_ERRPreparedLoadNonce;
protected int m_ERRPreparedLoadClock;
protected int m_ERRLastLoadNonce;
void ERR_PrepareCasualtyLoad(RplId modeId, int nonce)
{
if (!Replication.IsServer()) return;
if (this == GetGame().GetPlayerController()) ERR_PrepareCasualtyLoadOwner(modeId, nonce);
else Rpc(ERR_PrepareCasualtyLoadOwner, modeId, nonce);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_PrepareCasualtyLoadOwner(RplId modeId, int nonce)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (this != GetGame().GetPlayerController() || !mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || !ERR_CasualtyLoadingRules.GroundSettled(GetControlledEntity())) return;
if (m_ERRPreparedLoadMode == modeId && nonce <= m_ERRLastLoadNonce) return;
if (nonce <= 0) return;
m_ERRPreparedLoadMode = modeId;
m_ERRPreparedLoadNonce = nonce;
m_ERRLastLoadNonce = nonce;
m_ERRPreparedLoadClock = System.GetTickCount();
if (Replication.IsServer()) ERR_CasualtyLoadReady(modeId, nonce);
else Rpc(ERR_CasualtyLoadReady, modeId, nonce);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_CasualtyLoadReady(RplId modeId, int nonce)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || !mode.CasualtyLoading()) return;
mode.CasualtyLoading().OwnerReady(this, nonce);
}
void ERR_CommitCasualtyLoad(RplId modeId, int nonce, RplId callerId, RplId actorId, RplId vehicleId, RplId ownerId, int slotId)
{
if (!Replication.IsServer()) return;
if (this == GetGame().GetPlayerController()) ERR_CommitCasualtyLoadOwner(modeId, nonce, callerId, actorId, vehicleId, ownerId, slotId);
else Rpc(ERR_CommitCasualtyLoadOwner, modeId, nonce, callerId, actorId, vehicleId, ownerId, slotId);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_CommitCasualtyLoadOwner(RplId modeId, int nonce, RplId callerId, RplId actorId, RplId vehicleId, RplId ownerId, int slotId)
{
bool lease = this == GetGame().GetPlayerController() && m_ERRPreparedLoadMode == modeId && m_ERRPreparedLoadNonce == nonce && ERR_CasualtyLoadingRules.Within(m_ERRPreparedLoadClock, System.GetTickCount(), 3000);
if (m_ERRPreparedLoadMode == modeId && m_ERRPreparedLoadNonce == nonce) m_ERRPreparedLoadNonce = 0;
// Consume the local lease before the native call so duplicates cannot act.
bool accepted;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity actor = GetControlledEntity();
IEntity caller = ERR_CasualtyLoadingRules.Entity(callerId);
Vehicle vehicle = Vehicle.Cast(ERR_CasualtyLoadingRules.Entity(vehicleId));
IEntity owner = ERR_CasualtyLoadingRules.Entity(ownerId);
if (lease && mode && ERR_CasualtyLoadingRules.EntityId(mode) == modeId && ERR_CasualtyLoadingRules.EntityId(actor) == actorId && vehicle == mode.CasualtyLoadingAircraft() && ERR_CasualtyLoadingRules.Nearby(mode, caller, actor) && owner)
{
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(owner.FindComponent(BaseCompartmentManagerComponent));
SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(actor.FindComponent(SCR_CompartmentAccessComponent));
BaseCompartmentSlot seat;
if (manager) seat = manager.FindCompartment(slotId);
if (access && ERR_CasualtyLoadingRules.TransportBench(seat, vehicle) && seat.GetOwner() == owner && !seat.GetOccupant() && seat.IsCompartmentAccessible() && !seat.IsGetInLockedFor(actor) && (!seat.IsReserved() || seat.IsReservedBy(actor)))
accepted = access.GetInVehicle(owner, seat, true, -1, ECloseDoorAfterActions.INVALID, false);
}
// Missing replicated entities or rejected native entry fail once; never retry.
if (Replication.IsServer()) ERR_CasualtyLoadResult(modeId, nonce, accepted);
else Rpc(ERR_CasualtyLoadResult, modeId, nonce, accepted);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_CasualtyLoadResult(RplId modeId, int nonce, bool accepted)
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!Replication.IsServer() || !mode || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || !mode.CasualtyLoading()) return;
mode.CasualtyLoading().OwnerResult(this, nonce, accepted);
}
}
@@ -0,0 +1,305 @@
// First release supports only the registered Mi8 extraction aircraft's physical
// child transport benches. Nothing changes health, ownership, or seat classes.
class ERR_CasualtyLoadingRules
{
static int RoundAction(int phase)
{
if (phase == ERR_Phase.PREPARING) return 0;
if (ERR_MissionRules.Terminal(phase)) return 2;
return 1;
}
static bool ObserveTicket(bool committed, bool physical, bool authorized)
{
return physical && (committed || authorized);
}
static bool Within(int started, int now, int limit) { return now >= started && now - started < limit; }
static bool SettledFor(int started, int observed, int now)
{
return started >= 0 && now >= started && now - started >= 5000 && Within(observed, now, 2000);
}
static bool Ranges(float callerToCasualty, float callerToAircraft, float casualtyToAircraft)
{
return callerToCasualty >= 0 && callerToCasualty <= 3 && callerToAircraft >= 0 && callerToAircraft <= 12 && casualtyToAircraft >= 0 && casualtyToAircraft <= 12;
}
static bool AircraftMotion(bool groundContact, float up, float speed, float angularSpeed)
{
return groundContact && up >= 0.85 && speed >= 0 && speed <= 0.5 && angularSpeed >= 0 && angularSpeed <= 0.1;
}
static RplId EntityId(IEntity entity)
{
if (!entity) return RplId.Invalid();
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (!component) return RplId.Invalid();
return component.Id();
}
static IEntity Entity(RplId id)
{
if (id == RplId.Invalid()) return null;
RplComponent component = RplComponent.Cast(Replication.FindItem(id));
if (!component) return null;
return component.GetEntity();
}
static bool Down(IEntity actor)
{
if (!ERR_Actors.Alive(actor)) return false;
CharacterControllerComponent controller = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
return controller && controller.GetLifeState() == ECharacterLifeState.INCAPACITATED;
}
static bool GroundSettled(IEntity actor)
{
if (!Down(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) return false;
CharacterAnimationComponent animation = CharacterAnimationComponent.Cast(actor.FindComponent(CharacterAnimationComponent));
SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(actor.FindComponent(SCR_CompartmentAccessComponent));
Physics physics = actor.GetPhysics();
return animation && !animation.PhysicsIsFalling() && physics && physics.GetVelocity().Length() <= 0.5 && access && !access.IsGettingIn() && !access.IsGettingOut();
}
static bool AircraftReady(Vehicle aircraft)
{
if (!ERR_Actors.Alive(aircraft)) return false;
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
Physics physics = aircraft.GetPhysics();
if (!controller || !controller.GetSimulation() || !physics) return false;
vector transform[4]; aircraft.GetWorldTransform(transform);
if (!AircraftMotion(true, transform[1][1], physics.GetVelocity().Length(), physics.GetAngularVelocity().Length())) return false;
if (controller.GetSimulation().HasAnyGroundContact()) return true;
return ERR_CasualtyGroundSupport.Parked(aircraft);
}
static bool LineOfSight(IEntity caller, IEntity casualty)
{
ChimeraCharacter rescuer = ChimeraCharacter.Cast(caller);
ChimeraCharacter patient = ChimeraCharacter.Cast(casualty);
if (!rescuer || !patient || !GetGame()) return false;
TraceParam trace = new TraceParam();
trace.Start = rescuer.AimingPosition();
trace.End = patient.AimingPosition();
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.LayerMask = TRACE_LAYER_CAMERA;
trace.Exclude = caller;
if (GetGame().GetWorld().TraceMove(trace, null) >= 1) return true;
// Hitting the intended body's clothing/attachment is still sight of that body.
IEntity hit = trace.TraceEnt;
while (hit)
{
if (hit == casualty) return true;
hit = hit.GetParent();
}
return false;
}
static bool Nearby(ERR_GameMode mode, IEntity caller, IEntity casualty)
{
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty) return false;
CharacterControllerComponent rescuer = CharacterControllerComponent.Cast(caller.FindComponent(CharacterControllerComponent));
if (!ERR_Actors.CanFight(caller) || !rescuer || rescuer.GetLifeState() != ECharacterLifeState.ALIVE || !GroundSettled(casualty) || CompartmentAccessComponent.GetVehicleIn(caller)) return false;
Vehicle aircraft = mode.CasualtyLoadingAircraft();
if (!aircraft || !Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin())) || !AircraftReady(aircraft)) return false;
Faction faction = mode.PlayerFaction();
return faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && LineOfSight(caller, casualty);
}
static bool TransportBench(BaseCompartmentSlot seat, Vehicle aircraft)
{
if (!seat || !aircraft || seat.GetType() != ECompartmentType.CARGO || SCR_PatientCompartmentSlot.Cast(seat)) return false;
IEntity owner = seat.GetOwner();
if (!owner || owner == aircraft || !owner.GetPrefabData() || !owner.GetPrefabData().GetPrefabName().Contains("VehPart_MI8_seats")) return false;
while (owner)
{
if (owner == aircraft) return true;
owner = owner.GetParent();
}
return false;
}
}
class ERR_CasualtySettling
{
IEntity Actor;
UUID Account;
int Connection;
int Since;
int Observed;
}
class ERR_CasualtyLoadTicket
{
IEntity Caller;
IEntity Casualty;
Vehicle Aircraft;
BaseCompartmentSlot Seat;
UUID CallerAccount;
UUID CasualtyAccount;
int CallerConnection;
int CasualtyConnection;
int Nonce;
int Started;
int CommittedAt;
bool Committed;
bool OwnerReported;
}
class ERR_CasualtyLoadingService
{
protected ERR_GameMode m_Mode;
protected bool m_Stopped;
protected int m_NextNonce;
protected ref array<ref ERR_CasualtySettling> m_Settling = {};
protected ref array<ref ERR_CasualtyLoadTicket> m_Pending = {};
protected ref ScriptInvoker m_OnResolved = new ScriptInvoker();
void ERR_CasualtyLoadingService(ERR_GameMode mode) { m_Mode = mode; }
ScriptInvoker OnResolved() { return m_OnResolved; } // (IEntity casualty, bool physicallyLoaded)
int PendingCount() { return m_Pending.Count(); }
protected ERR_PlayerLife CurrentLife(IEntity actor)
{
if (!m_Mode || !m_Mode.Players()) return null;
return m_Mode.Players().CurrentActor(actor);
}
protected bool SameLife(IEntity actor, UUID account, int connection)
{
ERR_PlayerLife life = CurrentLife(actor);
return life && life.AccountId == account && life.ConnectionId == connection;
}
protected bool Settled(IEntity actor, int now)
{
foreach (ERR_CasualtySettling sample : m_Settling)
if (sample.Actor == actor && SameLife(actor, sample.Account, sample.Connection)) return ERR_CasualtyLoadingRules.GroundSettled(actor) && ERR_CasualtyLoadingRules.SettledFor(sample.Since, sample.Observed, now);
return false;
}
protected void Observe(int now)
{
for (int index = m_Settling.Count() - 1; index >= 0; index--)
{
ERR_CasualtySettling sample = m_Settling[index];
if (!SameLife(sample.Actor, sample.Account, sample.Connection) || !ERR_CasualtyLoadingRules.GroundSettled(sample.Actor) || !ERR_CasualtyLoadingRules.Within(sample.Observed, now, 2000)) m_Settling.Remove(index);
else sample.Observed = now;
}
array<ref ERR_PlayerLife> lives = {}; m_Mode.Players().Participants(lives);
foreach (ERR_PlayerLife life : lives)
{
if (!CurrentLife(life.Actor) || !ERR_CasualtyLoadingRules.GroundSettled(life.Actor)) continue;
bool found;
foreach (ERR_CasualtySettling tracked : m_Settling) if (tracked.Actor == life.Actor) { found = true; break; }
if (found) continue;
ERR_CasualtySettling added = new ERR_CasualtySettling();
added.Actor = life.Actor; added.Account = life.AccountId; added.Connection = life.ConnectionId; added.Since = now; added.Observed = now;
m_Settling.Insert(added);
}
}
protected bool PendingActor(IEntity caller, IEntity casualty)
{
foreach (ERR_CasualtyLoadTicket pending : m_Pending)
if (pending.Caller == caller || pending.Casualty == casualty) return true;
return false;
}
protected BaseCompartmentSlot AvailableSeat(IEntity casualty, Vehicle aircraft)
{
array<BaseCompartmentSlot> seats = {}; m_Mode.Extraction().Seats(seats);
foreach (BaseCompartmentSlot seat : seats)
{
if (!ERR_CasualtyLoadingRules.TransportBench(seat, aircraft) || seat.GetOccupant() || seat.IsReserved() || !seat.IsCompartmentAccessible() || seat.IsGetInLockedFor(casualty)) continue;
bool pendingSeat;
foreach (ERR_CasualtyLoadTicket pending : m_Pending) if (pending.Seat == seat) { pendingSeat = true; break; }
if (!pendingSeat) return seat;
}
return null;
}
bool CanLoad(IEntity caller, IEntity casualty)
{
if (!Replication.IsServer() || m_Stopped || !ERR_CasualtyLoadingRules.Nearby(m_Mode, caller, casualty)) return false;
if (!CurrentLife(caller) || !CurrentLife(casualty) || !Settled(casualty, System.GetTickCount()) || PendingActor(caller, casualty)) return false;
return AvailableSeat(casualty, m_Mode.ExtractionVehicle()) != null;
}
bool Begin(IEntity caller, IEntity casualty)
{
if (!CanLoad(caller, casualty) || m_NextNonce >= 2147483646) return false;
ERR_PlayerLife callerLife = CurrentLife(caller);
ERR_PlayerLife casualtyLife = CurrentLife(casualty);
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(casualtyLife.ConnectionId));
if (!controller || ERR_CasualtyLoadingRules.EntityId(m_Mode) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(caller) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(casualty) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(m_Mode.ExtractionVehicle()) == RplId.Invalid()) return false;
ERR_CasualtyLoadTicket ticket = new ERR_CasualtyLoadTicket();
ticket.Caller = caller; ticket.Casualty = casualty; ticket.Aircraft = m_Mode.ExtractionVehicle();
ticket.Seat = AvailableSeat(casualty, ticket.Aircraft);
if (!ticket.Seat || ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()) == RplId.Invalid()) return false;
ticket.CallerAccount = callerLife.AccountId; ticket.CasualtyAccount = casualtyLife.AccountId;
ticket.CallerConnection = callerLife.ConnectionId; ticket.CasualtyConnection = casualtyLife.ConnectionId;
ticket.Started = System.GetTickCount(); ticket.Nonce = ++m_NextNonce;
m_Pending.Insert(ticket);
ticket.Seat.SetReserved(casualty);
if (!ticket.Seat.IsReservedBy(casualty)) { Resolve(ticket, false); return false; }
// Preparation never moves the actor. Its owner starts a local real-time lease.
controller.ERR_PrepareCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), ticket.Nonce);
return true;
}
protected ERR_CasualtyLoadTicket Ticket(SCR_PlayerController controller, int nonce)
{
if (!controller || !GetGame() || !GetGame().GetPlayerManager() || GetGame().GetPlayerManager().GetPlayerController(controller.GetPlayerId()) != controller) return null;
foreach (ERR_CasualtyLoadTicket ticket : m_Pending)
if (ticket.Nonce == nonce && ticket.CasualtyConnection == controller.GetPlayerId() && ticket.Casualty == controller.GetControlledEntity()) return ticket;
return null;
}
void OwnerReady(SCR_PlayerController controller, int nonce)
{
if (!Replication.IsServer() || m_Stopped) return;
ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce);
if (!ticket || ticket.Committed) return;
int now = System.GetTickCount();
if (!ValidTicket(ticket) || !ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !Settled(ticket.Casualty, now) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty) || ticket.Seat.GetOccupant() || !ticket.Seat.IsCompartmentAccessible() || ticket.Seat.IsGetInLockedFor(ticket.Casualty) || !ticket.Seat.IsReservedBy(ticket.Casualty)) { Resolve(ticket, false); return; }
ticket.Committed = true; ticket.CommittedAt = now;
// Final authority/physical validation is the commit boundary. No retries.
controller.ERR_CommitCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), nonce, ERR_CasualtyLoadingRules.EntityId(ticket.Caller), ERR_CasualtyLoadingRules.EntityId(ticket.Casualty), ERR_CasualtyLoadingRules.EntityId(ticket.Aircraft), ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()), ticket.Seat.GetCompartmentSlotID());
}
void OwnerResult(SCR_PlayerController controller, int nonce, bool accepted)
{
if (!Replication.IsServer() || m_Stopped) return;
ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce);
if (!ticket || !ticket.Committed || ticket.OwnerReported) return;
ticket.OwnerReported = true;
if (!accepted) Print("Rereintroduced [casualty]: owner declined loading; observing physical outcome until expiry.");
// Neither positive nor negative client acknowledgements release a reserved
// seat while an authorized owner command could still be in flight.
}
protected bool PhysicalTicket(ERR_CasualtyLoadTicket ticket)
{
// Death cannot revoke an in-flight commit. Keep the vacant seat reserved
// while the captured body exists; its owner may still see the prior state.
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ticket.Casualty != null && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
}
protected bool ValidTicket(ERR_CasualtyLoadTicket ticket)
{
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 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);
m_OnResolved.Invoke(ticket.Casualty, success);
}
void Tick(float worldNow)
{
if (!Replication.IsServer() || m_Stopped || !m_Mode) return;
int roundAction = ERR_CasualtyLoadingRules.RoundAction(m_Mode.Phase());
if (roundAction == 0) return;
if (roundAction == 2) { Stop(); return; }
int now = System.GetTickCount();
Observe(now);
array<ref ERR_CasualtyLoadTicket> snapshot = {}; foreach (ERR_CasualtyLoadTicket item : m_Pending) snapshot.Insert(item);
foreach (ERR_CasualtyLoadTicket ticket : snapshot)
{
if (m_Stopped) return;
// Commit authorizes one move of the captured body; a later caller/target
// disconnect cannot revoke that command or release its seat prematurely.
if (!ERR_CasualtyLoadingRules.ObserveTicket(ticket.Committed, PhysicalTicket(ticket), ValidTicket(ticket))) { Resolve(ticket, false); continue; }
bool aboard = CompartmentAccessComponent.GetVehicleIn(ticket.Casualty) == ticket.Aircraft && ticket.Seat.GetOccupant() == ticket.Casualty;
if (aboard && ticket.Committed) { Resolve(ticket, ERR_Actors.Alive(ticket.Casualty)); continue; }
if (ticket.Committed)
{
// Owner's prepare-to-commit lease is only 3s; this 8s observation budget
// leaves room for replication. Timeouts do not issue compensating teleports.
if (!ERR_CasualtyLoadingRules.Within(ticket.CommittedAt, now, 8000)) Resolve(ticket, false);
}
else if (!ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty)) Resolve(ticket, false);
}
}
void RosterChanged(int connection) { if (GetGame()) Tick(GetGame().GetWorld().GetWorldTime()); }
void Stop()
{
if (!Replication.IsServer() || m_Stopped) return;
m_Stopped = true; m_Settling.Clear();
while (!m_Pending.IsEmpty()) Resolve(m_Pending[m_Pending.Count() - 1], false);
}
}
@@ -31,7 +31,7 @@ modded class ERR_LocationComponent
{
if (!Label().IsEmpty()) label = Label();
else if (Kind() == ERR_LocationKind.RADIO) label = "Radio station";
else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Extraction helicopter";
else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Initial helicopter pickup site";
else if (Kind() == ERR_LocationKind.DEPOT) label = "Supply cache";
else if (Kind() == ERR_LocationKind.PRISON) label = "Prison";
else label = Id();
@@ -10,12 +10,15 @@ class ERR_DiscoveryService
folders.Clear();
SCR_PrefabNamePredicate predicate = new SCR_PrefabNamePredicate();
predicate.prefabName = ERR_Resources.IntelFolder();
inventory.FindItems(folders, predicate, EStoragePurpose.PURPOSE_ANY);
array<IEntity> intel = {};
inventory.FindItems(intel, predicate, EStoragePurpose.PURPOSE_ANY);
foreach (IEntity folder : intel)
if (folder && !folders.Contains(folder)) folders.Insert(folder);
array<IEntity> objectives = {};
predicate.prefabName = ERR_Resources.ObjectiveDocument();
inventory.FindItems(objectives, predicate, EStoragePurpose.PURPOSE_ANY);
foreach (IEntity objective : objectives)
if (!folders.Contains(objective)) folders.Insert(objective);
if (objective && !folders.Contains(objective)) folders.Insert(objective);
}
void Tick(float now)
@@ -0,0 +1,164 @@
// The server applies optional date/weather choices once at round preparation.
// Zero values preserve the world's authored environment and existing defaults.
class ERR_EnvironmentService
{
protected TimeAndWeatherManagerEntity m_Manager;
protected bool m_Started;
protected bool m_Stopped;
protected int m_DateCode;
protected int m_Profile;
static string Pad2(int value)
{
if (value < 10) return "0" + value.ToString();
return value.ToString();
}
static string Date(int code)
{
if (code == 0) return "authored";
if (!ERR_Settings.Valid("startDateCode", code)) return "unavailable";
int year = 1900 + code / 10000;
int month = (code % 10000) / 100;
int day = code % 100;
return year.ToString() + "-" + Pad2(month) + "-" + Pad2(day);
}
static bool ParseDate(string text, out int code)
{
if (text == "authored") { code = 0; return true; }
if (text.Length() != 10 || text.Substring(4, 1) != "-" || text.Substring(7, 1) != "-") return false;
int year, month, day;
if (!ERR_CommandArguments.Integer(text.Substring(0, 4), year) || !ERR_CommandArguments.Integer(text.Substring(5, 2), month) || !ERR_CommandArguments.Integer(text.Substring(8, 2), day)) return false;
if (year < 1900 || year > 2050 || month < 1 || month > 12 || day < 1 || day > 31) return false;
int parsed = (year - 1900) * 10000 + month * 100 + day;
if (!ERR_Settings.Valid("startDateCode", parsed)) return false;
code = parsed;
return true;
}
static string ProfileName(int profile)
{
if (profile == 0) return "authored";
if (profile == 1) return "clear";
if (profile == 2) return "overcast";
if (profile == 3) return "rain";
return "unavailable";
}
static string WeatherStateName(int profile)
{
if (profile == 1) return "Clear";
if (profile == 2) return "Overcast";
if (profile == 3) return "Rainy";
return "";
}
static bool ParseProfile(string text, out int profile)
{
if (text == "authored") { profile = 0; return true; }
if (text == "clear") { profile = 1; return true; }
if (text == "overcast") { profile = 2; return true; }
if (text == "rain" || text == "rainy") { profile = 3; return true; }
return false;
}
static bool HasWeatherState(TimeAndWeatherManagerEntity manager, string name)
{
if (!manager || name.IsEmpty()) return false;
array<ref WeatherState> states = {};
manager.GetWeatherStatesList(states);
foreach (WeatherState state : states)
if (state && state.GetStateName() == name) return true;
return false;
}
bool Start(ERR_GameMode mode)
{
if (!Replication.IsServer() || !mode || !mode.Settings()) return false;
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
if (!world) return false;
return StartOn(world.GetTimeAndWeatherManager(), mode.Settings().Get("startDateCode"), mode.Settings().Get("weatherProfile"));
}
protected bool StartOn(TimeAndWeatherManagerEntity manager, int dateCode, int profile)
{
if (!Replication.IsServer() || m_Stopped) return false;
if (m_Started) return true;
if (!manager || !ERR_Settings.Valid("startDateCode", dateCode) || !ERR_Settings.Valid("weatherProfile", profile)) return false;
int year = 1900 + dateCode / 10000;
int month = (dateCode % 10000) / 100;
int day = dateCode % 100;
if (dateCode != 0 && !manager.CheckValidDate(year, month, day)) return false;
string weather = WeatherStateName(profile);
BaseWeatherStateTransitionManager transitions = manager.GetTransitionManager();
if (profile != 0 && (!transitions || !manager.GetCurrentWeatherState() || !HasWeatherState(manager, weather))) return false;
int previousYear, previousMonth, previousDay;
manager.GetDate(previousYear, previousMonth, previousDay);
if (dateCode != 0 && !manager.SetDate(year, month, day, true)) return false;
if (profile != 0)
{
string previousWeather = manager.GetCurrentWeatherState().GetStateName();
bool previousLooping = manager.IsWeatherLooping();
// Use the native replicated weather route, never local editor previews.
// Loop the selected family; native variants/wind still follow vanilla rules.
manager.ForceWeatherTo(true, weather, 0, 24);
WeatherState current = manager.GetCurrentWeatherState();
WeatherState next = transitions.GetNextState();
if (!manager.IsWeatherLooping() || ((!current || current.GetStateName() != weather) && (!next || next.GetStateName() != weather)))
{
manager.ForceWeatherTo(previousLooping, previousWeather, 0, 24);
if (dateCode != 0) manager.SetDate(previousYear, previousMonth, previousDay, true);
return false;
}
}
m_Manager = manager;
m_DateCode = dateCode;
m_Profile = profile;
m_Started = true;
PrintFormat("Rereintroduced [environment]: date=%1 weather=%2", Date(dateCode), ProfileName(profile));
return true;
}
bool Started() { return m_Started && !m_Stopped; }
int DateCode() { return m_DateCode; }
int Profile() { return m_Profile; }
int CurrentDateCode()
{
if (!Started() || !m_Manager) return -1;
return (m_Manager.GetYear() - 1900) * 10000 + m_Manager.GetMonth() * 100 + m_Manager.GetDay();
}
string CurrentWeather()
{
if (!Started() || !m_Manager) return "unavailable";
WeatherState weather = m_Manager.GetCurrentWeatherState();
if (!weather) return "unavailable";
return weather.GetStateName();
}
string Status()
{
if (!Started()) return "Environment not started.";
return "Current date " + Date(CurrentDateCode()) + ", weather " + CurrentWeather() + " (round profile " + ProfileName(m_Profile) + ").";
}
void Stop() { m_Stopped = true; m_Manager = null; }
}
// Called after the server-owned admin authorization gate. Saving cannot change
// the active service; accepted changes are consumed by the next mission instance.
class ERR_EnvironmentCommands
{
static bool Execute(ERR_Settings settings, ERR_EnvironmentService environment, array<string> parts, array<string> replies)
{
if (!Replication.IsServer() || !settings || parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "date" && parts[1] != "weather")) return false;
if (parts.Count() == 3 && parts[2] == "show")
{
if (environment) replies.Insert(environment.Status());
replies.Insert("Next round: date " + ERR_EnvironmentService.Date(settings.Get("startDateCode")) + ", weather " + ERR_EnvironmentService.ProfileName(settings.Get("weatherProfile")) + ".");
return true;
}
int value;
bool valid;
string key = "startDateCode";
if (parts[1] == "weather") key = "weatherProfile";
if (parts.Count() == 4 && parts[2] == "set")
{
if (key == "startDateCode") valid = ERR_EnvironmentService.ParseDate(parts[3], value);
else valid = ERR_EnvironmentService.ParseProfile(parts[3], value);
}
if (valid && settings.Set(key, value))
replies.Insert("Saved for next round: date " + ERR_EnvironmentService.Date(settings.Get("startDateCode")) + ", weather " + ERR_EnvironmentService.ProfileName(settings.Get("weatherProfile")) + ". Current round unchanged.");
else replies.Insert("Invalid environment or settings could not be saved. Use mission date set YYYY-MM-DD (1900-2050) or authored; mission weather set authored|clear|overcast|rain. Previous setting retained.");
return true;
}
}
@@ -0,0 +1,112 @@
// Initial preparation only. A newly spawned vehicle may start asleep above its
// landing gear contact plane. Wake once, then observe native gravity/contact;
// never correct its transform, apply an impulse, or replenish its supplies.
class ERR_ExtractionPlacement
{
protected ERR_GameMode m_Mode;
protected Vehicle m_Aircraft;
protected bool m_Stopped;
protected int m_WakeCount;
protected float m_Deadline;
protected float m_StableSince = -1;
protected float m_LastObservation = -1;
protected float m_NextDiagnostic;
protected ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING;
protected string m_Failure;
void ERR_ExtractionPlacement(ERR_GameMode mode, Vehicle aircraft)
{
m_Mode = mode; m_Aircraft = aircraft;
m_Deadline = GetGame().GetWorld().GetWorldTime() + 30000;
}
static bool CanWake(bool initialized, int count, float now, float deadline)
{
return initialized && count == 0 && now < deadline;
}
static bool Fresh(float previous, float now)
{
return previous >= 0 && now >= previous && now - previous <= 1500;
}
static bool QualificationAllowed(bool previouslyReady, float now, float deadline)
{
return previouslyReady || now < deadline;
}
static bool Stable(float since, float previous, float now)
{
return since >= 0 && previous >= since && now >= previous && now - previous <= 1500 && now - since >= 1000;
}
int WakeCount() { return m_WakeCount; }
bool Ready() { return !m_Stopped && m_Result == ERR_PrepareResult.READY; }
string FailureReason() { return m_Failure; }
void Stop() { m_Stopped = true; }
protected ERR_PrepareResult Fail(string reason)
{
m_Failure = reason; m_Result = ERR_PrepareResult.FAILED;
Print("Rereintroduced [extraction placement]: " + reason, LogLevel.ERROR);
return m_Result;
}
protected void Diagnostic(bool ready)
{
if (!m_Aircraft) return;
Physics physics = m_Aircraft.GetPhysics();
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
vector entity[4]; m_Aircraft.GetWorldTransform(entity);
PrintFormat("ERR EXTRACTION PLACEMENT: wakes=%1 alive=%2 ready=%3 origin=%4 up=%5", m_WakeCount, ERR_Actors.Alive(m_Aircraft), ready, entity[3], entity[1][1]);
if (physics) PrintFormat("ERR EXTRACTION PHYSICS: dynamic=%1 awake=%2 speed=%3 angularSpeed=%4", physics.IsDynamic(), physics.IsActive(), physics.GetVelocity().Length(), physics.GetAngularVelocity().Length());
if (controller && controller.GetSimulation())
{
vector nativeTransform[4]; controller.GetSimulation().GetWorldTransform(nativeTransform);
PrintFormat("ERR EXTRACTION NATIVE: valid=%1 contact=%2 engine=%3 origin=%4 up=%5 entityDifference=%6", controller.GetSimulation().IsValid(), controller.GetSimulation().HasAnyGroundContact(), controller.GetSimulation().EngineIsOn(), nativeTransform[3], nativeTransform[1][1], vector.Distance(nativeTransform[3], entity[3]));
}
// Diagnostics read every wheel even while physics is awake. The strict parked
// fallback itself still requires sleep and never alters the vehicle.
vector front, left, right;
if (ERR_CasualtyGroundSupport.WheelCenter(m_Aircraft, "v_wheel_F", front, true)) ERR_CasualtyGroundSupport.WheelSupported(m_Aircraft, front, 0.30, true);
if (ERR_CasualtyGroundSupport.WheelCenter(m_Aircraft, "v_wheel_bL", left, true)) ERR_CasualtyGroundSupport.WheelSupported(m_Aircraft, left, 0.45, true);
if (ERR_CasualtyGroundSupport.WheelCenter(m_Aircraft, "v_wheel_bR", right, true)) ERR_CasualtyGroundSupport.WheelSupported(m_Aircraft, right, 0.45, true);
}
ERR_PrepareResult Poll(float now)
{
if (m_Stopped || m_Result == ERR_PrepareResult.FAILED) return ERR_PrepareResult.FAILED;
if (!Replication.IsServer() || !m_Mode || m_Mode.Phase() != ERR_Phase.PREPARING || m_Mode.ExtractionVehicle() != m_Aircraft) return Fail("initial aircraft ownership/phase changed");
if (!ERR_Actors.Alive(m_Aircraft) || !m_Aircraft.GetPrefabData() || !m_Aircraft.GetPrefabData().GetPrefabName().Contains("/Mi8MT/")) return Fail("registered initial Mi8 is missing, destroyed or unsupported");
Physics physics = m_Aircraft.GetPhysics();
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
bool initialized = physics && physics.IsDynamic() && controller && controller.GetSimulation() && controller.GetSimulation().IsValid();
if (CanWake(initialized, m_WakeCount, now, m_Deadline))
{
// Production preparation owns the empty registered aircraft. The count is
// latched BEFORE the native call, preventing repeated wakes on any callback.
m_WakeCount = 1;
physics.SetActive(ActiveState.ACTIVE);
m_LastObservation = now;
Diagnostic(false);
m_NextDiagnostic = now + 1000;
return ERR_PrepareResult.PENDING; // never accept in the mutation frame
}
FuelManagerComponent fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
bool ready = initialized && m_WakeCount == 1 && fuel && fuel.HasFuel() && ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft);
if (now >= m_NextDiagnostic)
{
Diagnostic(ready);
PrintFormat("ERR EXTRACTION FUEL: manager=%1 hasFuel=%2", fuel != null, fuel && fuel.HasFuel());
m_NextDiagnostic = now + 1000;
}
if (!ready || !Fresh(m_LastObservation, now)) m_StableSince = -1;
if (ready)
{
if (m_StableSince < 0) m_StableSince = now;
if (Stable(m_StableSince, m_LastObservation, now))
{
if (!QualificationAllowed(m_Result == ERR_PrepareResult.READY, now, m_Deadline)) return Fail("initial aircraft stable qualification missed the 30-second preparation deadline");
if (m_Result != ERR_PrepareResult.READY) Print("Rereintroduced [extraction placement]: native supported aircraft stable for one second, fuel present, exactly one initial wake");
m_Result = ERR_PrepareResult.READY;
m_LastObservation = now;
return m_Result;
}
}
m_Result = ERR_PrepareResult.PENDING;
m_LastObservation = now;
if (now >= m_Deadline) return Fail("initial aircraft did not become alive, fueled and physically supported/stable within 30 seconds; no replacement or repositioning requested");
return m_Result;
}
}
+46 -1
View File
@@ -16,6 +16,7 @@ class ERR_GameMode : SCR_BaseGameMode
protected ref ERR_FactionCatalog m_Factions;
protected ref ERR_Settings m_Settings;
protected ref ERR_TimeService m_Time = new ERR_TimeService();
protected ref ERR_EnvironmentService m_Environment = new ERR_EnvironmentService();
protected ref ERR_DiscoveryService m_Discovery;
protected ref ERR_Objectives m_Objectives;
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
@@ -27,6 +28,11 @@ class ERR_GameMode : SCR_BaseGameMode
[RplProp()]
protected string m_EndReason;
protected ref ERR_RadioDefenceService m_RadioDefence;
[RplProp()]
protected ERR_RadioFeedback m_RadioFeedback;
protected ref ERR_RecoveryService m_Recovery;
[RplProp()]
protected string m_RecoveryStatus;
protected bool m_StartedPlayers;
protected bool m_Stopped;
protected float m_NoSurvivorsSince = -1;
@@ -46,7 +52,10 @@ class ERR_GameMode : SCR_BaseGameMode
ERR_AdminService Admin() { return m_Admin; }
ERR_Settings Settings() { return m_Settings; }
ERR_TimeService Time() { return m_Time; }
ERR_EnvironmentService Environment() { return m_Environment; }
ERR_RadioDefenceService RadioDefence() { return m_RadioDefence; }
ERR_RecoveryService Recovery() { return m_Recovery; }
string RecoveryStatus() { return m_RecoveryStatus; }
ERR_CommandService Command() { return m_Command; }
ERR_Objectives Objectives() { return m_Objectives; }
ERR_ExtractionService Extraction() { return m_Extraction; }
@@ -57,6 +66,18 @@ class ERR_GameMode : SCR_BaseGameMode
return m_Extraction.VehicleEntity();
}
float EscapeRadius() { return m_EscapeRadius; }
ERR_RadioFeedback RadioFeedback() { return m_RadioFeedback; }
// Called only from existing authority transition/tick paths; no extra timer.
protected bool RefreshRadioFeedback()
{
if (!Replication.IsServer()) return false;
bool defending = m_Phase == ERR_Phase.RADIO_DEFENCE && m_RadioDefence != null && m_RadioDefence.State() == ERR_RadioDefenceState.DEFENDING;
bool occupied = defending && m_RadioDefence.Occupied();
ERR_RadioFeedback next = ERR_RadioFeedbackRules.Next(m_RadioFeedback, defending, occupied);
if (next == m_RadioFeedback) return false;
m_RadioFeedback = next;
return true;
}
bool DefenceOccupied() { return m_RadioDefence && m_RadioDefence.Occupied(); }
int DefenceSecondsRemaining()
{
@@ -97,8 +118,22 @@ class ERR_GameMode : SCR_BaseGameMode
else if (m_Phase == ERR_Phase.EXTRACTION) phase = "Helicopter extraction";
else if (m_Phase == ERR_Phase.VICTORY) phase = "Escape complete";
else phase = "Mission lost";
if (!m_RecoveryStatus.IsEmpty()) phase += " " + m_RecoveryStatus;
return phase;
}
protected void RecoveryChanged(string message)
{
if (!Replication.IsServer() || m_RecoveryStatus == message) return;
m_RecoveryStatus = message;
Replication.BumpMe();
}
bool AbandonRound()
{
if (!Replication.IsServer() || !Active() || m_Stopped || !m_PreparationError.IsEmpty()) return false;
if (!ERR_MissionRules.CanTransition(m_Phase, ERR_Phase.DEFEAT)) return false;
m_EndReason = "An administrator abandoned this round.";
return Transition(ERR_Phase.DEFEAT);
}
protected void FailPreparation(string detail)
{
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
@@ -175,6 +210,7 @@ class ERR_GameMode : SCR_BaseGameMode
if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false;
ERR_Phase previous = m_Phase;
m_Phase = phase;
RefreshRadioFeedback();
if (ERR_MissionRules.Terminal(phase)) PublishEscapeResults();
Replication.BumpMe();
PrintFormat("Rereintroduced [mission]: phase %1 -> %2", previous, phase);
@@ -223,6 +259,8 @@ class ERR_GameMode : SCR_BaseGameMode
m_NoSurvivorsSince = -1;
}
protected string SettingsPath() { return ERR_Settings.PATH; }
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
@@ -230,7 +268,11 @@ class ERR_GameMode : SCR_BaseGameMode
m_Factions = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_FactionDefinition));
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
if (!Replication.IsServer()) return;
m_Settings = new ERR_Settings();
m_Settings = new ERR_Settings(SettingsPath());
OnStop().Insert(m_Environment.Stop);
m_Recovery = new ERR_RecoveryService(this);
m_Recovery.OnChanged().Insert(RecoveryChanged);
OnStop().Insert(m_Recovery.Stop);
m_RadioDefence = new ERR_RadioDefenceService(this);
m_RadioDefence.OnStarted().Insert(RadioStarted);
m_RadioDefence.OnCompleted().Insert(RadioCompleted);
@@ -285,6 +327,7 @@ class ERR_GameMode : SCR_BaseGameMode
FailPreparation("The extraction vehicle must be a helicopter with a pilot position and at least twelve pilot/passenger seats, with a valid escape boundary.");
return;
}
if (!m_Environment.Start(this)) { FailPreparation("The selected date or vanilla weather profile could not be initialized."); return; }
if (!m_Time.Start(this)) { FailPreparation("The world clock could not be initialized."); return; }
Transition(ERR_Phase.PRISON);
}
@@ -311,6 +354,8 @@ class ERR_GameMode : SCR_BaseGameMode
m_Extraction.Tick(m_Phase);
if (!Active()) return;
m_RadioDefence.Tick(now);
if (RefreshRadioFeedback()) Replication.BumpMe();
m_Recovery.Tick();
if (!Active()) return;
if (GetGame().GetPlayerManager().GetPlayerCount() == 0)
{
@@ -0,0 +1,12 @@
// Initial preparation only. A READY site service never consults this rule again.
class ERR_InitialGuardRules
{
static float TimeoutMilliseconds() { return 90000; }
static ERR_PrepareResult State(int requested, int physical, int conscious, bool queueComplete, bool expired)
{
if (requested <= 0 || physical > requested || conscious > physical || conscious < 0 || physical < 0) return ERR_PrepareResult.FAILED;
if (expired) return ERR_PrepareResult.FAILED;
if (physical == requested && conscious == requested && queueComplete) return ERR_PrepareResult.READY;
return ERR_PrepareResult.PENDING;
}
}
@@ -0,0 +1,19 @@
// GetItems may enumerate the same physical entity through multiple storages.
// Identity is the entity reference, never its prefab: distinct rounds still count.
class ERR_Inventory
{
static int AppendUnique(InventoryStorageManagerComponent inventory, notnull array<IEntity> items)
{
if (!inventory) return items.Count();
array<IEntity> raw = {};
inventory.GetItems(raw);
foreach (IEntity item : raw)
if (item && !items.Contains(item)) items.Insert(item);
return items.Count();
}
static int Items(InventoryStorageManagerComponent inventory, out array<IEntity> items)
{
items.Clear();
return AppendUnique(inventory, items);
}
}
@@ -0,0 +1,23 @@
// Attach only to a successor-owned prisoner prefab, with a native action duration.
class ERR_LoadCasualtyAction : ScriptedUserAction
{
override bool CanBeShownScript(IEntity user)
{
return ERR_CasualtyLoadingRules.Nearby(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());
}
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);
}
}
@@ -0,0 +1,76 @@
#ifdef WORKBENCH
// Test adapter exercises the production setting handler with a disposable file.
// Actual mode.Admin below still tests its server authorization and alias dispatch.
class ERR_NativeAdminSettingsAdapter : ERR_AdminService
{
void Probe(ERR_Settings settings, string arguments, array<string> replies)
{
replies.Clear();
arguments = arguments.Trim(); arguments.ToLower();
array<string> parts = {};
arguments.Split(" ", parts, true);
if (parts.Count() != 3 && parts.Count() != 4) return;
string key = Setting(parts[0], parts[1]);
if (!key.IsEmpty()) SettingCommand(settings, key, parts, replies);
}
}
class ERR_NativeAdminSettingsProbe
{
protected static bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: admin settings " + detail); return true; }
Print("ERR SMOKE FAIL: admin settings " + detail, LogLevel.ERROR);
return false;
}
protected static bool Storage(string path)
{
ERR_Settings settings = new ERR_Settings(path);
ERR_NativeAdminSettingsAdapter adapter = new ERR_NativeAdminSettingsAdapter();
array<string> replies = {};
map<string, int> initial = new map<string, int>();
foreach (string key : ERR_Settings.Keys()) initial.Set(key, settings.Get(key));
array<int> values = {0, 100, 15};
foreach (int value : values)
{
adapter.Probe(settings, " INTEL REVEAL SET " + value.ToString() + " ", replies);
if (!Check(replies.Count() == 1 && replies[0] == "intelRevealChancePercent=" + value.ToString() + " saved" && settings.Get("intelRevealChancePercent") == value, "authorized setting handler accepts normalized value " + value.ToString())) return false;
ERR_Settings reloaded = new ERR_Settings(path);
if (!Check(reloaded.Get("intelRevealChancePercent") == value, "discovery probability survives native JSON reload")) return false;
foreach (string key, int original : initial)
if (key != "intelRevealChancePercent" && !Check(reloaded.Get(key) == original, "unrelated setting preserved: " + key)) return false;
}
array<string> invalid = {"-1", "101", "1.5", "100junk", "999999"};
foreach (string argument : invalid)
{
adapter.Probe(settings, "intel reveal set " + argument, replies);
ERR_Settings reloaded = new ERR_Settings(path);
if (!Check(replies.Count() == 1 && replies[0].Contains("Invalid") && replies[0].Contains("0-100") && settings.Get("intelRevealChancePercent") == 15 && reloaded.Get("intelRevealChancePercent") == 15, "invalid input preserves live and stored value: " + argument)) return false;
}
adapter.Probe(settings, "intel reveal help", replies);
if (!Check(replies.Count() == 1 && replies[0].Contains("second document") && replies[0].Contains("newly recovered") && replies[0].Contains("remain unchanged"), "help explains exact versus rough discovery and apply timing")) return false;
adapter.Probe(settings, "mission radio show", replies);
return Check(replies.Count() == 1 && replies[0] == "radioDefenceSeconds=" + initial.Get("radioDefenceSeconds").ToString(), "existing radio alias and response remain intact");
}
static bool Run(ERR_GameMode mode)
{
if (!Check(Replication.IsServer() && mode && mode.Admin() && mode.Settings(), "native Core command service and settings ready")) return false;
int original = mode.Settings().Get("intelRevealChancePercent");
array<string> replies = {};
mode.Admin().Execute(mode, false, "intel reveal set 100", replies);
if (!Check(replies.Count() == 1 && replies[0] == "Administrator access required." && mode.Settings().Get("intelRevealChancePercent") == original, "actual unauthorized command cannot mutate discovery")) return false;
mode.Admin().Execute(mode, true, " INTEL REVEAL SHOW ", replies);
if (!Check(replies.Count() == 1 && replies[0] == "intelRevealChancePercent=" + original.ToString(), "actual Core alias dispatches without Chat")) return false;
mode.Admin().Execute(mode, true, "intel reveal set 101", replies);
if (!Check(replies.Count() == 1 && replies[0].Contains("0-100") && mode.Settings().Get("intelRevealChancePercent") == original, "actual dispatch rejects out-of-range value with guidance")) return false;
mode.Admin().Execute(mode, true, "intel reveal help", replies);
if (!Check(replies.Count() == 1 && replies[0].Contains("second document"), "actual Core help describes physical document progression")) return false;
string path = "$profile:erri-native-admin-settings-probe.json";
if (!Check(!FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "disposable fixture filenames are unused")) return false;
bool result = Storage(path);
FileIO.DeleteFile(path); FileIO.DeleteFile(path + ".bak"); FileIO.DeleteFile(path + ".tmp");
if (!result) return false;
return Check(mode.Settings().Get("intelRevealChancePercent") == original && !FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "fixture removed its files and never changed actual mission settings");
}
}
#endif
@@ -2,6 +2,28 @@
#ifdef WORKBENCH
modded class ERR_GameMode
{
protected ref ERR_NativeEnvironmentRestartProbe m_ERRBaseEnvironmentRestartProbe;
protected override string SettingsPath()
{
string enabled, outcome;
if (System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "environment-restart")
{
m_ERRBaseEnvironmentRestartProbe = new ERR_NativeEnvironmentRestartProbe(this);
return ERR_NativeEnvironmentRestartProbe.PATH;
}
return super.SettingsPath();
}
protected ref ERR_NativeCasualtyServiceProbe m_ERRCasualtyServiceProbe;
protected ref ERR_NativePrisonerProbe m_ERRPrisonerProbe;
protected ref ERR_NativeGuardCancelProbe m_ERRGuardCancelProbe;
protected ref ERR_NativeObjectiveLossProbe m_ERRObjectiveLossProbe;
protected ref ERR_NativeMedicalProbe m_ERRBaseMedicalProbe;
protected ref ERR_NativeCasualtyLoadProbe m_ERRCasualtyLoadProbe;
protected ref ERR_NativeEnvironmentProbe m_ERRBaseEnvironmentProbe;
protected bool m_ERRCatalogChecked;
protected ref ERR_NativePatrolProbe m_ERRPatrolEast;
protected ref ERR_NativePatrolProbe m_ERRPatrolWest;
protected int m_ERRBaseStep;
protected bool m_ERRObjectiveCloseChecked;
protected bool m_ERRStartupFailure;
@@ -10,6 +32,7 @@ modded class ERR_GameMode
protected ref ERR_NativeExtractionProbe m_ERRExtractionProbe;
protected ref ERR_NativeTransportProbe m_ERRTransportProbe;
protected ref ERR_NativeLifecycleProbe m_ERRLifecycleProbe;
protected ref ERR_NativeRecoveryProbe m_ERRRecoveryProbe;
protected ref ERR_NativeRadioDefenceProbe m_ERRBaseRadioProbe;
protected string m_ERRActiveCacheId = "everon-supply-1";
protected int m_ERRMedicalDiscoveries;
@@ -36,8 +59,10 @@ modded class ERR_GameMode
OnLocationDiscovered().Insert(ERR_BaseDiscovered);
OnStop().Insert(ERR_NativeObjectiveProbe.Stop);
OnStop().Insert(ERR_StopRadioProbe);
OnStop().Insert(ERR_StopHealthProbes);
if (m_ERRStartupFailure && m_Factions) m_Factions.EnemyFactionKey = m_Factions.PlayerFactionKey;
m_ERRBaseDeadline = GetWorld().GetWorldTime() + 210000;
if (outcome == "patrols") m_ERRBaseDeadline = GetWorld().GetWorldTime() + 330000;
OnPhaseChanged().Insert(ERR_BaseResult);
GetGame().GetCallqueue().CallLater(ERR_BaseTick, 500, true);
Print("ERR BASE SMOKE: core-only scripted escape; not manual or multiplayer acceptance");
@@ -47,10 +72,25 @@ modded class ERR_GameMode
if (condition) { Print("ERR SMOKE PASS: base " + detail); return true; }
ERR_NativeObjectiveProbe.Stop();
ERR_StopRadioProbe();
ERR_StopHealthProbes();
Print("ERR SMOKE FAIL: base " + detail, LogLevel.ERROR);
GetGame().GetCallqueue().Remove(ERR_BaseTick);
return false;
}
protected void ERR_StopHealthProbes()
{
if (m_ERRPatrolEast) m_ERRPatrolEast.Stop();
if (m_ERRPatrolWest) m_ERRPatrolWest.Stop();
if (m_ERRBaseEnvironmentRestartProbe) m_ERRBaseEnvironmentRestartProbe.Stop();
if (m_ERRBaseEnvironmentProbe) m_ERRBaseEnvironmentProbe.Stop();
if (m_ERRPrisonerProbe) m_ERRPrisonerProbe.Stop();
if (m_ERRGuardCancelProbe) m_ERRGuardCancelProbe.Stop();
if (m_ERRCasualtyServiceProbe) m_ERRCasualtyServiceProbe.Stop();
if (m_ERRObjectiveLossProbe) m_ERRObjectiveLossProbe.Stop();
if (m_ERRBaseMedicalProbe) m_ERRBaseMedicalProbe.Stop();
if (m_ERRCasualtyLoadProbe) m_ERRCasualtyLoadProbe.Cleanup();
if (m_ERRRecoveryProbe) m_ERRRecoveryProbe.Stop();
}
protected void ERR_StopRadioProbe() { if (m_ERRBaseRadioProbe) m_ERRBaseRadioProbe.Stop(); }
protected void ERR_BaseMove(vector position)
{
@@ -124,6 +164,69 @@ modded class ERR_GameMode
Print("ERR SMOKE COMPLETE: early extraction vehicle loss");
return;
}
if (m_ERRExtractionCase == "casualty-service")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1402, "casualty service fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native casualty service with helper-only identity adapter; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "radio-document-loss" || m_ERRExtractionCase == "radio-document-loss-partial")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1302, "radio document-loss fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native " + m_ERRExtractionCase + "; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "guard-cancel")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1502, "guard cancellation fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native guard-cancel; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "prisoner-kit")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 702, "prisoner-kit fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native prisoner-kit; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "medical")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 602, "medical fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native medical; reset requested by fixture");
return;
}
if ((m_ERRExtractionCase == "casualty-load" || m_ERRExtractionCase == "casualty-direct" || m_ERRExtractionCase == "casualty-bench" || m_ERRExtractionCase == "casualty-remove"))
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 802, "casualty-load fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native " + m_ERRExtractionCase + "; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "environment-restart")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1202, "environment restart round finished before explicit test reset")) return;
PrintFormat("ERR SMOKE COMPLETE: environment restart round %1; reset requested by fixture", m_ERRBaseEnvironmentRestartProbe.Round());
return;
}
if (m_ERRExtractionCase == "environment")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1002, "environment fixture finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native environment; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "patrols")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1102, "both patrol routes finished before explicit test reset")) return;
Print("ERR SMOKE COMPLETE: native finite patrol routes and cleanup; reset requested by fixture");
return;
}
if (m_ERRExtractionCase == "recovery")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 502 && EndReason().Contains("administrator abandoned"), "explicit authorized abandonment uses normal defeat result")) return;
if (!ERR_BaseCheck(!RecoveryStatus().IsEmpty() && !AbandonRound(), "terminal event observes active warning and rejects repeated abandonment")) return;
Print("ERR SMOKE COMPLETE: recovery status and explicit administrator abandonment");
return;
}
if (m_ERRExtractionCase == "objective-ui")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 902, "objective UI fixture finished before explicit test reset")) return;
@@ -182,6 +285,89 @@ modded class ERR_GameMode
Print("ERR SMOKE COMPLETE: expected startup-failure");
return;
}
if (m_ERRBaseStep == 1400)
{
int result = m_ERRCasualtyServiceProbe.Tick(now);
if (result < 0) { ERR_BaseCheck(false, "casualty service fixture failed"); return; }
if (result == 1) { m_ERRBaseStep = 1402; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1300)
{
int result = m_ERRObjectiveLossProbe.Tick(now);
if (result < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 1302; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1500)
{
int result = m_ERRGuardCancelProbe.Tick(now);
if (result < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 1502; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 700)
{
int result = m_ERRPrisonerProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 702; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 600)
{
int result = m_ERRBaseMedicalProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 602; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 800)
{
int result = m_ERRCasualtyLoadProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 802; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1200)
{
int result = m_ERRBaseEnvironmentRestartProbe.Tick(now);
if (result < 0) { ERR_BaseCheck(false, "environment restart fixture failed"); return; }
if (result == 1) { m_ERRBaseStep = 1202; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1000)
{
int result = m_ERRBaseEnvironmentProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1) { m_ERRBaseStep = 1002; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 1100)
{
int east = m_ERRPatrolEast.Tick(now);
int west = m_ERRPatrolWest.Tick(now);
if (east < 0 || west < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (east == 1 && west == 1)
{
if (!ERR_NativePatrolProbe.StopChecks(this)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
m_ERRBaseStep = 1102; Transition(ERR_Phase.DEFEAT);
}
return;
}
if (m_ERRBaseStep == 500)
{
int result = m_ERRRecoveryProbe.Tick(now);
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 1)
{
m_ERRBaseStep = 502;
array<string> replies = {};
Admin().Execute(this, true, "mission abandon", replies);
ERR_BaseCheck(Phase() == ERR_Phase.DEFEAT && replies.Count() == 1 && replies[0].Contains("Round abandoned") && RecoveryStatus().IsEmpty() && Recovery().Conscious() == 0 && Recovery().Incapacitated() == 0, "administrator command returns accepted outcome after recovery cleanup");
}
return;
}
if (m_ERRBaseStep == 400)
{
int result = m_ERRLifecycleProbe.Tick(now);
@@ -232,6 +418,83 @@ modded class ERR_GameMode
ERR_Actors.Players(players, true);
if (players.Count() != 1 || Phase() != ERR_Phase.PRISON) return;
m_ERRBasePlayer = players[0];
if (!m_ERRCatalogChecked)
{
if (!ERR_NativeFactionCatalogProbe.Run(this)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
m_ERRCatalogChecked = true;
}
if (m_ERRExtractionCase == "casualty-service")
{
m_ERRCasualtyServiceProbe = new ERR_NativeCasualtyServiceProbe(this);
m_ERRBaseStep = 1400;
return;
}
if (m_ERRExtractionCase == "radio-document-loss" || m_ERRExtractionCase == "radio-document-loss-partial")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRObjectiveLossProbe = new ERR_NativeObjectiveLossProbe(this, m_ERRBasePlayer, m_ERRExtractionCase == "radio-document-loss-partial");
m_ERRBaseStep = 1300;
return;
}
if (m_ERRExtractionCase == "guard-cancel")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRGuardCancelProbe = new ERR_NativeGuardCancelProbe(this);
m_ERRBaseStep = 1500;
return;
}
if (m_ERRExtractionCase == "prisoner-kit")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRPrisonerProbe = new ERR_NativePrisonerProbe(this, m_ERRBasePlayer);
m_ERRBaseStep = 700;
return;
}
if (m_ERRExtractionCase == "medical")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRBaseMedicalProbe = new ERR_NativeMedicalProbe(this);
m_ERRBaseStep = 600;
return;
}
if ((m_ERRExtractionCase == "casualty-load" || m_ERRExtractionCase == "casualty-direct" || m_ERRExtractionCase == "casualty-bench" || m_ERRExtractionCase == "casualty-remove"))
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
if (m_ERRExtractionCase == "casualty-remove") m_ERRCasualtyLoadProbe = new ERR_NativeCasualtyRemoveProbe(this);
else m_ERRCasualtyLoadProbe = new ERR_NativeCasualtyLoadProbe(this, m_ERRExtractionCase != "casualty-load", m_ERRExtractionCase == "casualty-bench");
m_ERRBaseStep = 800;
return;
}
if (m_ERRExtractionCase == "environment-restart")
{
if (!ERR_BaseCheck(m_ERRBaseEnvironmentRestartProbe != null, "environment restart selected isolated settings before mode initialization")) return;
m_ERRBaseStep = 1200;
return;
}
if (m_ERRExtractionCase == "environment")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRBaseEnvironmentProbe = new ERR_NativeEnvironmentProbe(this);
m_ERRBaseStep = 1000;
return;
}
if (m_ERRExtractionCase == "patrols")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRPatrolEast = new ERR_NativePatrolProbe(this, "everon-outpost-1");
m_ERRPatrolWest = new ERR_NativePatrolProbe(this, "everon-supply-2");
m_ERRBaseStep = 1100;
return;
}
if (m_ERRExtractionCase == "recovery")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_ERRRecoveryProbe = new ERR_NativeRecoveryProbe(this, m_ERRBasePlayer);
m_ERRBaseStep = 500;
return;
}
if (m_ERRExtractionCase == "lifecycle")
{
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
@@ -279,14 +542,14 @@ modded class ERR_GameMode
m_ERRBaseStep = 901;
return;
}
if (!ERR_NativeDefinitionProbe.Run() || !ERR_NativeContractsProbe.Run(this, m_ERRBasePlayer) || !ERR_BaseCover() || !ERR_NativeTimeProbe.Run(this)) return;
if (!ERR_NativeAdminSettingsProbe.Run(this) || !ERR_NativeDefinitionProbe.Run() || !ERR_NativeContractsProbe.Run(this, m_ERRBasePlayer) || !ERR_BaseCover() || !ERR_NativeTimeProbe.Run(this)) return;
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
if (!ERR_BaseCheck(Objectives() && Objectives().Current() && Objectives().Current().GetTaskName() == "Escape the prison", "native prison objective exists")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
if (!ERR_BaseCheck(Sites().Count() == 7 && cache && cache.Container && vector.DistanceXZ(cache.Container.GetOrigin(), PrisonPosition()) > 100, "authored supply cache is a separate scavenging location")) return;
array<IEntity> stock = {};
InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)).GetItems(stock);
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)), stock);
PrintFormat("ERR BASE STOCK: cache items=%1", stock.Count());
int expectedStock;
foreach (ERR_SiteStock entry : cache.Definition.Stock) expectedStock += entry.Count;
@@ -360,7 +623,7 @@ modded class ERR_GameMode
{
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
if (!ERR_BaseCheck(medical.Location.Knowledge() == 2 && medical.Location.Label() == "Medical stash" && m_ERRMedicalDiscoveries == 1, "conscious nearby player visually discovers named medical stash once")) return;
if (!ERR_BaseCheck(!Sites().CanObserve(m_ERRBasePlayer, Sites().Find("everon-radio-1")), "proximity cannot bypass radio documents")) return;
if (!ERR_BaseCheck(!Sites().CanObserve(m_ERRBasePlayer, Sites().Find("everon-radio-1")), "distant medical-site discovery does not reveal the radio")) return;
m_ERRBaseStep = 1;
}
else if (m_ERRBaseStep == 1 || m_ERRBaseStep == 2)
@@ -375,11 +638,11 @@ modded class ERR_GameMode
}
else if (m_ERRBaseStep == 3)
{
if (!ERR_BaseCheck(Objectives().Current().GetTaskDescription().Contains("identify the radio station on your map. Reach"), "objective guidance reacts to discovered radio")) return;
if (!ERR_BaseCheck(Objectives().Current().GetTaskDescription().Contains("The radio station is identified on your map. Reach"), "objective guidance reacts to discovered radio")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
array<IEntity> stock = {};
InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)).GetItems(stock);
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)), stock);
foreach (IEntity item : stock) if (item) SCR_EntityHelper.DeleteEntityAndChildren(item);
if (!ERR_BaseCheck(m_ERRMedicalDiscoveries == 1, "discovery ticks do not repeat the location event")) return;
if (m_ERRCacheLoss && !ERR_BaseCheck(!Sites().Find("everon-supply-1").Container, "lost primary cache stays absent while alternate documents identify the radio")) return;
@@ -398,7 +661,9 @@ modded class ERR_GameMode
int result = m_ERRBaseRadioProbe.Tick(now);
if (result < 0) { ERR_StopRadioProbe(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
if (result == 0) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INACTIVE, "radio feedback remains inactive before tuning")) return;
if (!ERR_BaseCheck(TuneRadio(m_ERRBasePlayer, m_ERRBaseRadio), "identified radio accepts transmission")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.WAITING && ERR_ObjectiveText.Description(this, false).Contains("Waiting for"), "radio feedback publishes waiting before first occupancy sample")) return;
m_ERRBaseStep = 31;
m_ERRBaseWait = now + 5000;
}
@@ -406,10 +671,11 @@ modded class ERR_GameMode
{
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
array<IEntity> stock = {};
InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)).GetItems(stock);
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)), stock);
if (!ERR_BaseCheck(stock.IsEmpty() && Sites().Prepare() == ERR_PrepareResult.READY, "depleted stock stays empty when preparation is queried again")) return;
ERR_Actors.DeleteSpawned(cache.Container);
if (!ERR_BaseCheck(DefenceOccupied() && DefenceSecondsRemaining() > 0 && DefenceSecondsRemaining() < 30 && Objectives().Current().GetTaskDescription().Contains("seconds remaining"), "native objective reports advancing radio progress")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.TRANSMITTING && ERR_ObjectiveText.Description(this, false).Contains("Transmission in progress"), "radio guide reports transmitting without querying a countdown")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
m_ERRBaseStep = 310;
@@ -420,6 +686,7 @@ modded class ERR_GameMode
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent));
if (!ERR_BaseCheck(controller.IsUnconscious() && ERR_Actors.Alive(m_ERRBasePlayer) && Players().HasSurvivor() && Phase() == ERR_Phase.RADIO_DEFENCE, "unconscious survivor does not trigger defeat after the full grace period")) return;
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30, "unconscious radio defender cannot advance transmission")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INTERRUPTED && ERR_ObjectiveText.Description(this, false).Contains("Progress has reset"), "radio guide reports interruption after unconsciousness")) return;
SCR_PlayerController playerController = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!ERR_BaseCheck(playerController.GetControlledEntity() == m_ERRBasePlayer && playerController.ERR_ObserverState() == ERR_ObserverReason.NONE && !playerController.ERR_HasObserverCamera(), "unconscious player retains their body and camera")) return;
controller.SetUnconscious(false);
@@ -429,6 +696,7 @@ modded class ERR_GameMode
else if (m_ERRBaseStep == 311)
{
if (!ERR_BaseCheck(ERR_Actors.CanFight(m_ERRBasePlayer) && DefenceOccupied() && DefenceSecondsRemaining() < 30, "regaining consciousness resumes transmission from zero")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.TRANSMITTING && ERR_ObjectiveText.Description(this, false).Contains("Transmission in progress"), "radio guide returns to transmitting after recovery")) return;
int playerId = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_ERRBasePlayer);
if (!ERR_BaseCheck(!Players().CanEnter(playerId, Phase()), "regaining consciousness does not grant another life")) return;
ERR_BaseMove(m_ERRBaseRadio.Position() + "90 0 0");
@@ -439,6 +707,7 @@ modded class ERR_GameMode
{
if (!ERR_BaseCheck(!Sites().Find(m_ERRActiveCacheId).Container, "lost supply cache is not replaced")) return;
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30 && Objectives().Current().GetTaskDescription().Contains("interrupted"), "objective reports interruption and reset")) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INTERRUPTED && ERR_ObjectiveText.Description(this, false).Contains("Return a conscious survivor"), "radio guide reports interruption after leaving the area")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
ERR_BaseMove(m_ERRBaseRadio.Position() + "2 0 0");
m_ERRBaseStep = 4;
@@ -447,6 +716,7 @@ modded class ERR_GameMode
else if (m_ERRBaseStep == 4)
{
if (!ERR_BaseCheck(Phase() == ERR_Phase.EXTRACTION, "radio defence reveals extraction") || !ERR_NativeTimeProbe.VerifyProgression(this)) return;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INACTIVE, "radio feedback clears after completion")) return;
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_ERRBasePlayer.FindComponent(CompartmentAccessComponent));
BaseCompartmentSlot slot = access.FindFreeCompartment(ExtractionVehicle(), ECompartmentType.PILOT);
@@ -0,0 +1,80 @@
#ifdef WORKBENCH
// Positive check observes production initial placement; the fixture never wakes
// the mission helicopter. Negative transforms
// and velocity are applied only to an independent, disposable empty aircraft.
class ERR_NativeCasualtyGroundProbe
{
protected ERR_GameMode m_Mode;
protected Vehicle m_Negative;
protected float m_Wait;
protected float m_Deadline;
protected int m_Step;
protected bool m_Stopped;
void ERR_NativeCasualtyGroundProbe(ERR_GameMode mode) { m_Mode = mode; }
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
ERR_Actors.DeleteSpawned(m_Negative); m_Negative = null;
}
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: casualty ground " + detail); return true; }
Print("ERR SMOKE FAIL: casualty ground " + detail, LogLevel.ERROR); Stop(); return false;
}
int Tick(float now)
{
if (m_Stopped) return -1;
if (m_Deadline > 0 && now > m_Deadline) { Check(false, "bounded ground-support probe deadline"); return -1; }
if (now < m_Wait) return 0;
if (m_Step == 0)
{
Vehicle parked = m_Mode.ExtractionVehicle();
ERR_SiteService sites = m_Mode.Sites();
ERR_ExtractionPlacement placement;
if (sites) placement = sites.ExtractionPlacement();
if (!Check(placement && placement.Ready() && placement.WakeCount() == 1, "production initial placement completed with exactly one wake before player spawn")) return -1;
bool support = ERR_CasualtyGroundSupport.Parked(parked, true);
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(parked.FindComponent(HelicopterControllerComponent));
bool nativeContact = controller && controller.GetSimulation() && controller.GetSimulation().HasAnyGroundContact();
if (!Check((nativeContact || support) && ERR_CasualtyLoadingRules.AircraftReady(parked), "naturally settled mission aircraft has native landing contact or three strict wheel supports")) return -1;
vector raised = parked.GetOrigin() + "0 20 0";
m_Negative = Vehicle.Cast(ERR_Actors.Spawn(m_Mode.Factions().ExtractionPrefab, raised));
if (!Check(m_Negative != null, "separate empty airborne aircraft created for rejection checks")) return -1;
m_Mode.Own(m_Negative);
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Negative.FindComponent(DamageManagerComponent));
if (!Check(damage != null, "negative aircraft damage manager available")) return -1;
damage.EnableDamageHandling(false);
m_Deadline = now + 10000; m_Wait = now + 500; m_Step = 1; return 0;
}
if (m_Step == 1)
{
vector position = m_Negative.GetOrigin();
float height = position[1] - GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
bool support = ERR_CasualtyGroundSupport.Parked(m_Negative, true);
PrintFormat("ERR GROUND NEGATIVE: raisedPosition=%1 terrainClearance=%2", position, height);
if (!Check(ERR_Actors.Alive(m_Negative) && height > 5 && !support && !ERR_CasualtyLoadingRules.AircraftReady(m_Negative), "living aircraft above terrain is rejected even if stationary or sleeping")) return -1;
vector tilted[4]; Math3D.AnglesToMatrix("0 0 90", tilted); tilted[3] = position;
m_Negative.SetWorldTransform(tilted);
m_Wait = now + 500; m_Step = 2; return 0;
}
if (m_Step == 2)
{
vector actual[4]; m_Negative.GetWorldTransform(actual);
Physics physics = m_Negative.GetPhysics();
if (!Check(physics != null, "negative aircraft has actual native physics")) return -1;
PrintFormat("ERR GROUND NEGATIVE: tiltedUp=%1 position=%2", actual[1][1], actual[3]);
if (!Check(ERR_Actors.Alive(m_Negative) && actual[1][1] < 0.85 && !ERR_CasualtyLoadingRules.AircraftMotion(true, actual[1][1], 0, 0) && !ERR_CasualtyLoadingRules.AircraftReady(m_Negative), "actual tilted aircraft is rejected independently by upright policy")) return -1;
// Read back an actual native velocity on the disposable clone. No mission
// aircraft activation, simulation state, engine or velocity is changed.
physics.SetVelocity("2 0 0");
float speed = physics.GetVelocity().Length();
PrintFormat("ERR GROUND NEGATIVE: nativeSpeed=%1", speed);
if (!Check(speed > 0.5 && !ERR_CasualtyLoadingRules.AircraftMotion(true, 1, speed, 0) && !ERR_CasualtyLoadingRules.AircraftReady(m_Negative), "actual native velocity is rejected independently by movement policy")) return -1;
if (!Check(ERR_Actors.Alive(m_Mode.ExtractionVehicle()) && ERR_CasualtyLoadingRules.AircraftReady(m_Mode.ExtractionVehicle()), "mission aircraft remains untouched and eligible after negative checks")) return -1;
Stop(); return 1;
}
return 0;
}
}
#endif
@@ -0,0 +1,255 @@
#ifdef WORKBENCH
// Native mechanism experiment, NOT proof of an available player load/drag action.
// Caller must explicitly opt in and tick this independently of medical fixtures.
class ERR_NativeCasualtyLoadProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Caller;
protected IEntity m_Casualty;
protected Vehicle m_Vehicle;
protected SCR_CompartmentAccessComponent m_Access;
protected SCR_CharacterDamageManagerComponent m_Damage;
protected BaseCompartmentSlot m_Seat;
protected vector m_CallerTransform[4];
protected vector m_GroundPosition;
protected bool m_CallerMoved;
protected bool m_Direct;
protected bool m_Bench;
protected float m_DownSettledAt;
protected float m_NextDiagnostic;
protected int m_Entered;
protected int m_Left;
protected int m_Step;
protected float m_Deadline;
protected float m_Wait;
void ERR_NativeCasualtyLoadProbe(ERR_GameMode mode, bool direct = false, bool bench = false) { m_Mode = mode; m_Direct = direct; m_Bench = bench; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: casualty-load " + detail); return true; }
Print("ERR SMOKE FAIL: casualty-load " + detail, LogLevel.ERROR);
Cleanup();
return false;
}
void Cleanup()
{
if (m_Access)
{
if (m_Access.GetOnCompartmentEntered(false)) m_Access.GetOnCompartmentEntered(false).Remove(Entered);
if (m_Access.GetOnCompartmentLeft(false)) m_Access.GetOnCompartmentLeft(false).Remove(Left);
}
// Restore only the same living local body, never a replacement/reconnected actor.
PlayerController controller = GetGame().GetPlayerController();
if (m_CallerMoved && m_Caller && controller && controller.GetControlledEntity() == m_Caller && ERR_Actors.Alive(m_Caller) && !CompartmentAccessComponent.GetVehicleIn(m_Caller))
ChimeraCharacter.Cast(m_Caller).Teleport(m_CallerTransform);
m_CallerMoved = false;
ERR_Actors.DeleteSpawned(m_Casualty);
m_Casualty = null;
}
protected void Entered(IEntity target, BaseCompartmentManagerComponent manager, int managerId, int slotId, bool move)
{
m_Entered++;
PrintFormat("ERR CASUALTY ENTER EVENT: manager=%1 slot=%2 move=%3 targetRoot=%4 count=%5", managerId, slotId, move, target == m_Vehicle, m_Entered);
}
protected void Left(IEntity target, BaseCompartmentManagerComponent manager, int managerId, int slotId, bool move)
{
m_Left++;
PrintFormat("ERR CASUALTY EXIT EVENT: manager=%1 slot=%2 move=%3 targetRoot=%4 count=%5", managerId, slotId, move, target == m_Vehicle, m_Left);
}
protected bool BelongsToAircraft(IEntity entity)
{
while (entity)
{
if (entity == m_Vehicle) return true;
entity = entity.GetParent();
}
return false;
}
protected BaseCompartmentSlot FindTransportBench()
{
array<BaseCompartmentSlot> seats = {};
m_Mode.Extraction().Seats(seats);
foreach (BaseCompartmentSlot seat : seats)
{
if (!seat || seat.GetType() != ECompartmentType.CARGO || SCR_PatientCompartmentSlot.Cast(seat)) continue;
IEntity owner = seat.GetOwner();
if (!owner || owner == m_Vehicle || !BelongsToAircraft(owner) || !owner.GetPrefabData()) continue;
ResourceName prefab = owner.GetPrefabData().GetPrefabName();
// These installed Mi8 seat-part prefabs own the real transport benches.
// No compartment class, animation, accessibility or health setting is changed.
if (!prefab.Contains("VehPart_MI8_seats")) continue;
if (!seat.IsCompartmentAccessible() || seat.IsGetInLockedFor(m_Casualty) || seat.GetOccupant()) continue;
return seat;
}
return null;
}
protected void Trace(string stage)
{
if (!m_Casualty || !m_Access) return;
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent));
CharacterAnimationComponent animation = CharacterAnimationComponent.Cast(m_Casualty.FindComponent(CharacterAnimationComponent));
if (controller && animation)
PrintFormat("ERR CASUALTY STATE: stage=%1 life=%2 ragdoll=%3 falling=%4 physicsLinked=%5 gettingIn=%6 gettingOut=%7", stage, controller.GetLifeState(), animation.IsRagdollActive(), animation.PhysicsIsFalling(), animation.PhysicsIsLinked(), m_Access.IsGettingIn(), m_Access.IsGettingOut());
if (m_Seat)
{
IEntity owner = m_Seat.GetOwner();
string prefab;
if (owner && owner.GetPrefabData()) prefab = owner.GetPrefabData().GetPrefabName();
PrintFormat("ERR CASUALTY SEAT: stage=%1 name=%2 ownerPrefab=%3 rootOwner=%4 hierarchyValid=%5 slotId=%6 managerId=%7", stage, m_Seat.GetCompartmentUniqueName(), prefab, owner == m_Vehicle, BelongsToAircraft(owner), m_Seat.GetCompartmentSlotID(), m_Seat.GetCompartmentMgrID());
PrintFormat("ERR CASUALTY OCCUPANCY: stage=%1 inCompartment=%2 actualSlot=%3 targetOccupant=%4 vehicleRoot=%5 enterEvents=%6 exitEvents=%7", stage, m_Access.IsInCompartment(), m_Access.GetCompartment() == m_Seat, m_Seat.GetOccupant() == m_Casualty, CompartmentAccessComponent.GetVehicleIn(m_Casualty) == m_Vehicle, m_Entered, m_Left);
}
}
protected bool DownOnGround()
{
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent));
return ERR_Actors.Alive(m_Casualty) && controller && controller.GetLifeState() == ECharacterLifeState.INCAPACITATED && !CompartmentAccessComponent.GetVehicleIn(m_Casualty);
}
protected bool IncapacitateGroundCasualty()
{
if (!Check(m_Damage && ERR_Actors.Alive(m_Casualty), "living fixture casualty has native damage manager before incapacitation")) return false;
HitZone resilience = m_Damage.GetResilienceHitZone();
if (!Check(resilience != null, "fixture casualty has actual native resilience hit zone")) return false;
float beforeResilience = resilience.GetHealth();
bool damageWasEnabled = m_Damage.IsDamageHandlingEnabled();
// ForceUnconsciousness uses HandleDamage(TRUE), which damage protection can
// suppress. Permit only this synchronous operation, then restore the flag
// before any assertion/cleanup path. Never restore health or life state.
m_Damage.EnableDamageHandling(true);
m_Damage.ForceUnconsciousness();
float afterResilience = resilience.GetHealth();
m_Damage.EnableDamageHandling(damageWasEnabled);
PrintFormat("ERR CASUALTY INCAPACITATION: resilienceBefore=%1 resilienceAfter=%2 damageOriginallyEnabled=%3 restored=%4", beforeResilience, afterResilience, damageWasEnabled, m_Damage.IsDamageHandlingEnabled() == damageWasEnabled);
return Check(afterResilience < beforeResilience && ERR_Actors.Alive(m_Casualty) && m_Damage.IsDamageHandlingEnabled() == damageWasEnabled, "single native incapacitation reduces actual resilience, preserves life and restores damage-protection flag");
}
protected bool BeginExit()
{
// Native unloading is a separate mechanism check, not a user-action claim.
vector exitTransform[4];
m_Casualty.GetWorldTransform(exitTransform);
exitTransform[3] = m_GroundPosition;
return Check(m_Access.GetOutVehicle_NoDoor(exitTransform, true, false), "unconscious cargo occupant accepts native ground exit");
}
protected bool ExitSettled()
{
return ERR_Actors.Alive(m_Casualty) && vector.Distance(m_Casualty.GetOrigin(), m_GroundPosition) < 4 && m_Seat.IsCompartmentAccessible();
}
protected vector ExpectedExit() { return m_GroundPosition; }
protected string ExitResultDetail() { return "unloaded survivor on ground and seat accessible"; }
// now is world milliseconds, matching the other native probes. Returns -1/0/1.
int Tick(float now)
{
if (now < m_Wait) return 0;
if (m_Step == 0)
{
PlayerController controller = GetGame().GetPlayerController();
if (!Check(Replication.IsServer() && controller && m_Mode, "server and local real caller available")) return -1;
m_Caller = controller.GetControlledEntity();
m_Vehicle = m_Mode.ExtractionVehicle();
if (!Check(ERR_Actors.CanFight(m_Caller) && !CompartmentAccessComponent.GetVehicleIn(m_Caller) && ERR_Actors.Alive(m_Vehicle), "caller on foot and authored helicopter intact")) return -1;
m_GroundPosition = ERR_Actors.Ground(m_Vehicle.GetOrigin() + "6 0 6");
m_Casualty = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, m_GroundPosition);
if (!Check(m_Casualty != null, "independent casualty spawned on ground")) return -1;
m_Mode.Own(m_Casualty);
m_Access = SCR_CompartmentAccessComponent.Cast(m_Casualty.FindComponent(SCR_CompartmentAccessComponent));
m_Damage = SCR_CharacterDamageManagerComponent.Cast(m_Casualty.FindComponent(SCR_CharacterDamageManagerComponent));
if (!Check(m_Access && m_Damage, "native casualty components present")) return -1;
m_Access.GetOnCompartmentEntered().Insert(Entered);
m_Access.GetOnCompartmentLeft().Insert(Left);
m_Caller.GetWorldTransform(m_CallerTransform);
vector helperTransform[4];
m_Caller.GetWorldTransform(helperTransform);
helperTransform[3] = ERR_Actors.Ground(m_GroundPosition + "2 0 0");
ChimeraCharacter.Cast(m_Caller).Teleport(helperTransform);
m_CallerMoved = true;
m_Step = 1;
m_Wait = now + 1000;
return 0;
}
if (m_Step == 1)
{
if (!IncapacitateGroundCasualty()) return -1;
m_Deadline = now + 8000;
m_Step = 2;
return 0;
}
if (m_Step == 2)
{
if (!DownOnGround())
{
if (now >= m_Deadline) { Check(false, "native health-driven ground incapacitation before boarding deadline"); return -1; }
return 0;
}
if (m_Bench)
{
if (m_DownSettledAt == 0)
{
m_DownSettledAt = now + 5000;
m_Deadline = now + 13000;
Trace("incapacitated; begin five-second bench settling interval");
return 0;
}
if (now < m_DownSettledAt) return 0;
}
PrintFormat("ERR CASUALTY POSITIONS: caller=%1 casualty=%2 helicopter=%3", m_Caller.GetOrigin(), m_Casualty.GetOrigin(), m_Vehicle.GetOrigin());
if (!Check(ERR_Actors.CanFight(m_Caller) && vector.Distance(m_Caller.GetOrigin(), m_Casualty.GetOrigin()) < 4, "conscious real caller physically beside ground casualty")) return -1;
if (m_Bench) m_Seat = FindTransportBench();
else m_Seat = m_Access.FindFreeAndAccessibleCompartment(m_Vehicle, ECompartmentType.CARGO);
if (!Check(m_Seat && !SCR_PatientCompartmentSlot.Cast(m_Seat) && !m_Seat.GetOccupant(), "ordinary free cargo seat selected without patient-seat substitution")) return -1;
if (m_Bench) Print("ERR CASUALTY BENCH SOURCE: VehPart_MI8_seats_base.et sha256=1b9a2e55a9a1cb884886488b6e170f437b6c1a615b584449819b96a6c78ad973");
Trace("before selected boarding request");
PrintFormat("ERR CASUALTY LOAD: CanGetInVehicle=%1 state=%2 targetType=%3", m_Access.CanGetInVehicle(m_Seat.GetOwner()), CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent)).GetLifeState(), m_Seat.GetType());
// Exactly the public server-to-owner primitive used by vanilla ambulance loading.
bool accepted;
if (m_Direct) accepted = m_Access.GetInVehicle(m_Seat.GetOwner(), m_Seat, true, -1, ECloseDoorAfterActions.INVALID, false);
else accepted = m_Access.MoveInVehicle(m_Vehicle, ECompartmentType.CARGO, false, m_Seat);
PrintFormat("ERR CASUALTY MECHANISM: direct=%1 bench=%2 accepted=%3", m_Direct, m_Bench, accepted);
Trace("immediately after selected boarding request");
m_NextDiagnostic = now + 1000;
if (!Check(accepted, "selected loading request accepted while already unconscious")) return -1;
m_Deadline = now + 8000;
m_Step = 3;
return 0;
}
if (m_Step == 3)
{
if (now >= m_NextDiagnostic) { Trace("awaiting physical occupancy"); m_NextDiagnostic = now + 1000; }
if (CompartmentAccessComponent.GetVehicleIn(m_Casualty) != m_Vehicle || m_Seat.GetOccupant() != m_Casualty)
{
if (now >= m_Deadline) { Check(false, "physical cargo occupancy after selected mechanism before deadline"); return -1; }
return 0;
}
if (!Check(ERR_Actors.Alive(m_Casualty) && !ERR_Actors.CanFight(m_Casualty), "living unconscious ground casualty now physically aboard")) return -1;
if (!Check(!m_Access.MoveInVehicle(m_Vehicle, ECompartmentType.CARGO, false, m_Seat), "occupied target seat rejects repeated request")) return -1;
ERR_PlayerRoster roster = new ERR_PlayerRoster();
UUID account = "00e77200-0000-0000-0000-000000000201";
if (!Check(roster.Bind(10201, account), "isolated casualty identity registered")) return -1;
roster.Spawned(10201, m_Casualty);
ERR_ExtractionService service = new ERR_ExtractionService(roster);
if (!Check(service.Assign(m_Vehicle) && service.Begin(m_Vehicle.GetOrigin() + "100 0 0", 50), "isolated boundary service initialized without moving mission helicopter")) return -1;
service.Tick(ERR_Phase.EXTRACTION);
if (!Check(service.DidEscape(account) && service.EscapedCount() == 1, "loaded unconscious identity satisfies physical extraction rule")) return -1;
if (!BeginExit()) return -1;
m_Deadline = now + 8000;
m_Step = 4;
return 0;
}
if (m_Step == 4)
{
if (now >= m_NextDiagnostic) { Trace("awaiting ground exit"); m_NextDiagnostic = now + 1000; }
if (CompartmentAccessComponent.GetVehicleIn(m_Casualty) || m_Seat.GetOccupant())
{
if (now >= m_Deadline) { Check(false, "native exit clears physical vehicle and seat before deadline"); return -1; }
return 0;
}
bool settled = ExitSettled();
if (!settled && now < m_Deadline) return 0;
PrintFormat("ERR CASUALTY EXIT POSITION: actual=%1 requested=%2 alive=%3 accessible=%4", m_Casualty.GetOrigin(), ExpectedExit(), ERR_Actors.Alive(m_Casualty), m_Seat.IsCompartmentAccessible());
if (!Check(settled, ExitResultDetail())) return -1;
Cleanup();
m_Step = 5;
return 1;
}
return 1;
}
}
#endif
@@ -0,0 +1,160 @@
#ifdef WORKBENCH
// Opt-in completion-boundary test of the REAL inherited stock remove action.
// It does not simulate held input, replace the action, or prove remote ownership.
class ERR_NativeCasualtyRemoveProbe : ERR_NativeCasualtyLoadProbe
{
protected SCR_RemoveCasualtyUserAction m_Remove;
protected UserActionContext m_Context;
protected int m_SavedDoor;
protected int m_SelectedDoor;
protected bool m_DoorChanged;
protected bool m_PendingAction;
protected bool m_TestBodyProtected;
protected bool m_RemoveStopped;
protected bool m_RemoveComplete;
protected vector m_AuthoredExit;
protected vector m_SeatedPosition;
protected vector m_HelperPosition;
protected float m_RemoveDeadline;
protected float m_RemoveDiagnostic;
void ERR_NativeCasualtyRemoveProbe(ERR_GameMode mode, bool direct = false, bool bench = false)
{
m_Mode = mode; m_Direct = true; m_Bench = true;
}
override void Cleanup()
{
if (m_Remove && m_DoorChanged) m_Remove.SetCompartmentDoorInfoIndex(m_SavedDoor);
m_DoorChanged = false; m_PendingAction = false; m_RemoveStopped = true;
super.Cleanup();
}
protected void FindAction(IEntity entity)
{
if (!entity || m_Remove) return;
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
if (manager)
{
array<BaseUserAction> actions = {}; manager.GetActionsList(actions);
foreach (BaseUserAction action : actions)
{
SCR_RemoveCasualtyUserAction remove = SCR_RemoveCasualtyUserAction.Cast(action);
if (remove && remove.GetCompartmentSlot() == m_Seat && remove.GetContextIndex("door_l01_door") >= 0)
{
m_Remove = remove; return;
}
}
}
for (IEntity child = entity.GetChildren(); child; child = child.GetSibling()) FindAction(child);
}
protected void FindContext(IEntity entity)
{
if (!entity || m_Context) return;
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
if (manager)
{
UserActionContext context = manager.GetContext("door_l01_door");
if (context)
{
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
if (actions.Contains(m_Remove)) { m_Context = context; return; }
}
}
for (IEntity child = entity.GetChildren(); child; child = child.GetSibling()) FindContext(child);
}
protected CompartmentDoorInfo ResolveDoor()
{
BaseCompartmentManagerComponent manager = m_Seat.GetManager();
if (!manager) return null;
CompartmentDoorInfo info = manager.GetDoorInfo(m_SelectedDoor);
if (info) return info;
CompartmentDoorReference doorReference = manager.GetDoorReference(m_SelectedDoor);
if (!doorReference) return null;
BaseCompartmentManagerComponent referenced = doorReference.GetReferencedDoorCompartmentManager();
int referencedIndex = doorReference.GetAnimDoorIndex();
PrintFormat("ERR CASUALTY REMOVE DOOR REFERENCE: selected=%1 referencedIndex=%2 manager=%3", m_SelectedDoor, referencedIndex, referenced != null);
if (!referenced || referencedIndex < 0) return null;
return referenced.GetDoorInfo(referencedIndex);
}
protected override bool BeginExit()
{
FindAction(m_Vehicle);
if (!Check(m_Remove && BelongsToAircraft(m_Remove.GetOwner()) && m_Remove.GetActionDuration() == 1 && !m_Remove.HasLocalEffectOnly(), "stock bench RemoveCasualty action exists, duration one second, nonlocal effect")) return false;
FindContext(m_Vehicle);
if (!Check(m_Context && m_Context.GetContextName() == "door_l01_door", "actual exterior context contains selected stock seat action")) return false;
// Same native point-to-door selection used by SCR_CompartmentUserAction.
// Explicit cached index substitutes only for selecting a UI context, not input.
m_SelectedDoor = m_Seat.PickDoorIndexForPoint(m_Context.GetOrigin());
if (!Check(m_SelectedDoor >= 0, "actual action context resolves a relevant native compartment door")) return false;
CompartmentDoorInfo door = ResolveDoor();
if (!Check(door && door.GetExitTeleportPointInfo(), "selected native door exposes its authored teleport exit")) return false;
vector exitTransform[4]; door.GetExitTeleportPointInfo().GetWorldTransform(exitTransform);
m_AuthoredExit = exitTransform[3]; m_SeatedPosition = m_Casualty.GetOrigin();
if (!Check(vector.Distance(m_AuthoredExit, m_Vehicle.GetOrigin()) < 15, "authored exit resolves beside this physical helicopter")) return false;
m_SavedDoor = m_Remove.GetCompartmentDoorInfoIndex();
m_Remove.SetCompartmentDoorInfoIndex(m_SelectedDoor); m_DoorChanged = true;
vector outside = m_Context.GetOrigin() - m_Vehicle.GetOrigin(); outside[1] = 0;
if (!Check(outside.Length() > 0.1, "exterior context has a distinct outward direction")) return false;
outside.Normalize();
m_HelperPosition = ERR_Actors.Ground(m_Context.GetOrigin() + outside * 0.75);
vector helper[4]; m_Caller.GetWorldTransform(helper); helper[3] = m_HelperPosition;
ChimeraCharacter.Cast(m_Caller).Teleport(helper);
PrintFormat("ERR CASUALTY REMOVE SETUP: action=%1 context=%2 door=%3 contextOrigin=%4 exit=%5 seatedOrigin=%6 helperRequested=%7", m_Remove.GetActionID(), m_Context.GetContextName(), m_SelectedDoor, m_Context.GetOrigin(), m_AuthoredExit, m_SeatedPosition, m_HelperPosition);
m_PendingAction = true;
m_Wait = GetGame().GetWorld().GetWorldTime() + 1000;
return true;
}
static bool SafeExit(float horizontalDistance, float terrainClearance)
{
return horizontalDistance >= 0 && horizontalDistance <= 1.25 && terrainClearance >= -0.25 && terrainClearance <= 1.5;
}
protected override vector ExpectedExit() { return m_AuthoredExit; }
protected override string ExitResultDetail() { return "stock removal leaves a living incapacitated body beside authored door exit, outside compartment, on terrain, and seat accessible"; }
protected override bool ExitSettled()
{
if (!m_Casualty || !m_Access || !m_Seat) return false;
vector actual = m_Casualty.GetOrigin();
float height = actual[1] - GetGame().GetWorld().GetSurfaceY(actual[0], actual[2]);
CharacterAnimationComponent animation = CharacterAnimationComponent.Cast(m_Casualty.FindComponent(CharacterAnimationComponent));
bool safe = DownOnGround() && !m_Access.IsGettingOut() && !m_Access.IsInCompartment() && !m_Seat.GetOccupant() && m_Seat.IsCompartmentAccessible() && animation && !animation.PhysicsIsFalling() && SafeExit(vector.DistanceXZ(actual, m_AuthoredExit), height);
float now = GetGame().GetWorld().GetWorldTime();
if (now >= m_RemoveDiagnostic || safe)
{
PrintFormat("ERR CASUALTY REMOVE POSITION: actual=%1 aiming=%2 authored=%3 seated=%4 horizontalError=%5 terrainClearance=%6 safe=%7", actual, ChimeraCharacter.Cast(m_Casualty).AimingPosition(), m_AuthoredExit, m_SeatedPosition, vector.DistanceXZ(actual, m_AuthoredExit), height, safe);
Trace("stock door removal observation"); m_RemoveDiagnostic = now + 1000;
}
return safe;
}
override int Tick(float now)
{
if (m_RemoveComplete) return 1;
if (m_RemoveStopped) return -1;
if (m_RemoveDeadline == 0)
{
if (!Check(m_Mode && ERR_CasualtyLoadingRules.AircraftReady(m_Mode.ExtractionVehicle()), "production mission helicopter is physically grounded and stable before fixture")) return -1;
m_RemoveDeadline = now + 45000;
}
if (now > m_RemoveDeadline) { Check(false, "bounded 45-second stock removal fixture deadline"); return -1; }
if (m_Step == 4 && m_PendingAction)
{
if (now < m_Wait) return 0;
if (!Check(ERR_Actors.CanFight(m_Caller) && !CompartmentAccessComponent.GetVehicleIn(m_Caller) && vector.Distance(m_Caller.GetOrigin(), m_HelperPosition) < 1.5, "real conscious rescuer settled outside selected native door")) return -1;
bool shown = m_Remove.CanBeShown(m_Caller);
bool permitted = m_Remove.CanBePerformed(m_Caller);
PrintFormat("ERR CASUALTY REMOVE ACTION: shown=%1 performedAllowed=%2 reason=%3 cachedDoor=%4 caller=%5", shown, permitted, m_Remove.GetCannotPerformReason(), m_Remove.GetCompartmentDoorInfoIndex(), m_Caller.GetOrigin());
if (!Check(shown && permitted && m_Seat.GetOccupant() == m_Casualty && m_Remove.GetCompartmentDoorInfoIndex() == m_SelectedDoor, "stock native action visibility and permission accept actual rescuer and occupied casualty bench")) return -1;
m_PendingAction = false; // exactly one completion invocation, no native retry
m_Remove.PerformAction(m_Remove.GetOwner(), m_Caller);
Print("ERR CASUALTY REMOVE: invoked existing stock action completion once; held input and network dispatch not exercised");
m_Deadline = now + 8000;
}
int result = super.Tick(now);
if (!m_TestBodyProtected && m_Casualty && m_Damage)
{
// Only the fixture-owned casualty is protected from nearby initial guards.
// ForceUnconsciousness still uses its actual native health/life transition.
m_Damage.EnableDamageHandling(false); m_TestBodyProtected = true;
}
if (result == 1) m_RemoveComplete = true;
return result;
}
}
#endif
@@ -0,0 +1,293 @@
#ifdef WORKBENCH
// Only the helper's membership is synthetic. The casualty keeps its real native
// player controller/account/body and exercises the ordinary owner handshake.
class ERR_NativeCasualtyHelperAdapter : ERR_CasualtyLoadingService
{
protected ref ERR_PlayerLife m_HelperLife;
protected bool m_HelperEnabled;
void ERR_NativeCasualtyHelperAdapter(ERR_GameMode mode) { m_Mode = mode; }
void Helper(IEntity actor)
{
m_HelperLife = new ERR_PlayerLife();
m_HelperLife.Actor = actor; m_HelperLife.Connected = true; m_HelperLife.UsedLife = true;
m_HelperLife.ConnectionId = 15401;
m_HelperLife.AccountId = "00e77200-0000-0000-0000-000000001401";
}
void EnableHelper() { m_HelperEnabled = true; }
bool StrictParticipant(IEntity actor) { return super.CurrentLife(actor) != null; }
bool StoppedForProbe() { return m_Stopped; }
protected override ERR_PlayerLife CurrentLife(IEntity actor)
{
if (m_HelperEnabled && m_HelperLife && actor && actor == m_HelperLife.Actor) return m_HelperLife;
return super.CurrentLife(actor);
}
}
class ERR_NativeCasualtyServiceProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Player;
protected IEntity m_Helper;
protected IEntity m_Blocker;
protected Vehicle m_Aircraft;
protected ERR_LoadCasualtyAction m_Action;
protected ref ERR_NativeCasualtyHelperAdapter m_Service;
protected ref ERR_NativeCasualtyGroundProbe m_GroundProbe;
protected bool m_GroundChecked;
protected ref array<BaseCompartmentSlot> m_Reserved = {};
protected BaseCompartmentSlot m_LoadedSeat;
protected float m_Deadline;
protected float m_Wait;
protected float m_PositionDeadline;
protected float m_NextDiagnostic;
protected vector m_RequestedPlayer;
protected vector m_RequestedHelper;
protected bool m_PlayerDamageSnapshot;
protected bool m_PlayerDamageOriginallyEnabled;
protected int m_SettleStart;
protected int m_Step;
protected int m_Resolutions;
protected bool m_Loaded;
protected bool m_Observing;
protected bool m_Stopped;
void ERR_NativeCasualtyServiceProbe(ERR_GameMode mode) { m_Mode = mode; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: casualty service " + detail); return true; }
Print("ERR SMOKE FAIL: casualty service " + detail, LogLevel.ERROR);
Stop(); return false;
}
protected void Resolved(IEntity actor, bool loaded)
{
m_Resolutions++;
m_Loaded = actor == m_Player && loaded;
}
protected void ReleaseFixtureReservations()
{
foreach (BaseCompartmentSlot seat : m_Reserved)
if (seat && m_Helper && seat.IsReservedBy(m_Helper)) seat.SetReserved(null);
m_Reserved.Clear();
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (m_GroundProbe) m_GroundProbe.Stop();
if (m_Service) { m_Service.Stop(); m_Service.OnResolved().Remove(Resolved); }
if (m_Mode) m_Mode.ERR_UseCasualtyFixture(null);
ReleaseFixtureReservations();
// Restore the original damage-handling flag only on this exact still-owned
// living player body. This never restores health or changes another actor.
if (m_PlayerDamageSnapshot && GetGame() && GetGame().GetPlayerController() && GetGame().GetPlayerController().GetControlledEntity() == m_Player && ERR_Actors.Alive(m_Player))
{
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
if (damage) damage.EnableDamageHandling(m_PlayerDamageOriginallyEnabled);
}
m_PlayerDamageSnapshot = false;
ERR_Actors.DeleteSpawned(m_Blocker); m_Blocker = null;
ERR_Actors.DeleteSpawned(m_Helper); m_Helper = null;
// This opt-in case ends by ordinary round reset. Never restore arbitrary
// health, force recovery, teleport the seated player, or delete its body.
}
protected ERR_LoadCasualtyAction FindAction()
{
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(m_Player.FindComponent(BaseActionsManagerComponent));
if (!manager) return null;
array<BaseUserAction> actions = {}; manager.GetActionsList(actions);
bool medical;
ERR_LoadCasualtyAction load;
foreach (BaseUserAction action : actions)
{
if (SCR_BandageUserAction.Cast(action)) medical = true;
ERR_LoadCasualtyAction candidate = ERR_LoadCasualtyAction.Cast(action);
if (candidate) load = candidate;
}
if (!Check(medical, "successor prisoner retains inherited native medicine actions")) return null;
return load;
}
protected void Diagnose()
{
if (!m_Player || !m_Helper || !m_Aircraft || !m_Service) return;
HelicopterControllerComponent aircraftController = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
Physics physics = m_Aircraft.GetPhysics();
bool groundContact;
bool simulationAvailable;
float speed = -1;
float angularSpeed = -1;
if (aircraftController && aircraftController.GetSimulation())
{
simulationAvailable = true;
groundContact = aircraftController.GetSimulation().HasAnyGroundContact();
}
if (physics) { speed = physics.GetVelocity().Length(); angularSpeed = physics.GetAngularVelocity().Length(); }
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
PrintFormat("ERR CASUALTY AIRCRAFT READINESS: alive=%1 controller=%2 simulation=%3 physics=%4 groundContact=%5 up=%6 speed=%7 angularSpeed=%8 position=%9", ERR_Actors.Alive(m_Aircraft), aircraftController != null, simulationAvailable, physics != null, groundContact, transform[1][1], speed, angularSpeed, m_Aircraft.GetOrigin());
CharacterControllerComponent playerController = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
CharacterControllerComponent helperController = CharacterControllerComponent.Cast(m_Helper.FindComponent(CharacterControllerComponent));
int playerLife = -1;
int helperLife = -1;
if (playerController) playerLife = playerController.GetLifeState();
if (helperController) helperLife = helperController.GetLifeState();
PrintFormat("ERR CASUALTY ACTOR READINESS: playerAlive=%1 playerConscious=%2 playerLife=%3 helperAlive=%4 helperConscious=%5 helperLife=%6 pairRange=%7 playerAircraftRange=%8 helperAircraftRange=%9", ERR_Actors.Alive(m_Player), ERR_Actors.CanFight(m_Player), playerLife, ERR_Actors.Alive(m_Helper), ERR_Actors.CanFight(m_Helper), helperLife, vector.Distance(m_Player.GetOrigin(), m_Helper.GetOrigin()), vector.Distance(m_Player.GetOrigin(), m_Aircraft.GetOrigin()), vector.Distance(m_Helper.GetOrigin(), m_Aircraft.GetOrigin()));
PrintFormat("ERR CASUALTY POSITION READINESS: playerRequested=%1 playerActual=%2 playerError=%3 helperRequested=%4 helperActual=%5 helperError=%6", m_RequestedPlayer, m_Player.GetOrigin(), vector.Distance(m_RequestedPlayer, m_Player.GetOrigin()), m_RequestedHelper, m_Helper.GetOrigin(), vector.Distance(m_RequestedHelper, m_Helper.GetOrigin()));
ERR_CasualtyGroundSupport.Parked(m_Aircraft, true);
PrintFormat("ERR CASUALTY SERVICE STATE: step=%1 player=%2 helper=%3 groundedTarget=%4 aircraftReady=%5 sight=%6 pending=%7 resolutions=%8 loaded=%9", m_Step, m_Player.GetOrigin(), m_Helper.GetOrigin(), ERR_CasualtyLoadingRules.GroundSettled(m_Player), ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft), ERR_CasualtyLoadingRules.LineOfSight(m_Helper, m_Player), m_Service.PendingCount(), m_Resolutions, m_Loaded);
}
int Tick(float now)
{
if (m_Stopped) return -1;
if (m_Deadline > 0 && now > m_Deadline) { if (m_Player && m_Helper && m_Service) Diagnose(); Check(false, "bounded native fixture deadline"); return -1; }
if (m_Service && m_Observing) m_Service.Tick(now);
if (now < m_Wait) return 0;
if (m_Step == 0)
{
PlayerController controller = GetGame().GetPlayerController();
if (!Check(Replication.IsServer() && controller && m_Mode.Phase() == ERR_Phase.PRISON, "explicit local server fixture starts with real prisoner")) return -1;
m_Player = controller.GetControlledEntity(); m_Aircraft = m_Mode.ExtractionVehicle();
if (!Check(ERR_Actors.CanFight(m_Player) && m_Aircraft && !CompartmentAccessComponent.GetVehicleIn(m_Player), "real controlled casualty starts conscious on foot")) return -1;
m_Action = FindAction();
if (!Check(m_Action && m_Action.GetOwner() == m_Player && m_Action.GetActionDuration() == 5 && !m_Action.HasLocalEffectOnly(), "real successor prisoner owns five-second server action")) return -1;
DamageManagerComponent playerDamage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
if (!Check(playerDamage != null, "fixture player damage manager available")) return -1;
m_PlayerDamageOriginallyEnabled = playerDamage.IsDamageHandlingEnabled();
m_PlayerDamageSnapshot = true;
playerDamage.EnableDamageHandling(false);
m_RequestedPlayer = ERR_Actors.Ground(m_Aircraft.GetOrigin() + "6 0 6");
m_RequestedHelper = ERR_Actors.Ground(m_RequestedPlayer + "0 0 2");
m_Helper = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, m_RequestedHelper);
if (!Check(m_Helper != null, "separate physical helper spawned")) return -1;
m_Mode.Own(m_Helper);
DamageManagerComponent helperDamage = DamageManagerComponent.Cast(m_Helper.FindComponent(DamageManagerComponent));
if (!Check(helperDamage != null, "fixture helper damage manager available")) return -1;
helperDamage.EnableDamageHandling(false);
vector transform[4]; m_Player.GetWorldTransform(transform); transform[3] = m_RequestedPlayer;
ChimeraCharacter.Cast(m_Player).Teleport(transform);
m_Service = new ERR_NativeCasualtyHelperAdapter(m_Mode); m_Service.Helper(m_Helper);
m_Service.OnResolved().Insert(Resolved);
if (!Check(m_Service.StrictParticipant(m_Player) && !m_Service.StrictParticipant(m_Helper), "actual target passes strict native membership; synthetic helper does not")) return -1;
if (!Check(!m_Mode.CasualtyLoading().Begin(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player), "production and unadapted sample reject out-of-phase helper request")) return -1;
// Deliberate fixture phase setup, not evidence of a completed radio objective.
if (!Check(m_Mode.Transition(ERR_Phase.RADIO_SEARCH) && m_Mode.Transition(ERR_Phase.RADIO_DEFENCE) && m_Mode.Transition(ERR_Phase.EXTRACTION), "fixture enters extraction through explicit legal phase transitions")) return -1;
m_Deadline = now + 45000;
m_PositionDeadline = now + 10000;
m_NextDiagnostic = now;
m_Wait = now + 500; m_Step = 1; return 0;
}
if (m_Step == 1)
{
if (now >= m_NextDiagnostic) { Diagnose(); m_NextDiagnostic = now + 1000; }
bool actorsReady = ERR_Actors.CanFight(m_Player) && ERR_Actors.CanFight(m_Helper) && vector.Distance(m_RequestedPlayer, m_Player.GetOrigin()) <= 1.5 && vector.Distance(m_RequestedHelper, m_Helper.GetOrigin()) <= 1.5 && vector.Distance(m_Player.GetOrigin(), m_Helper.GetOrigin()) <= 3;
bool aircraftReady = ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft);
if ((!actorsReady || !aircraftReady) && now < m_PositionDeadline) return 0;
if (!Check(actorsReady, "actual living conscious actors reached requested nearby ground positions before deadline")) return -1;
if (!Check(aircraftReady, "actual aircraft satisfies unchanged production grounded-motion safety before deadline")) return -1;
if (!m_GroundChecked)
{
if (!m_GroundProbe) m_GroundProbe = new ERR_NativeCasualtyGroundProbe(m_Mode);
int groundResult = m_GroundProbe.Tick(now);
if (groundResult < 0) { Check(false, "native ground-support qualification failed"); return -1; }
if (groundResult == 0) return 0;
m_GroundChecked = true;
}
if (!Check(!m_Service.Begin(m_Helper, m_Player), "conscious target is rejected before native incapacitation")) return -1;
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_Player.FindComponent(SCR_CharacterDamageManagerComponent));
if (!Check(damage != null, "real casualty has native health manager")) return -1;
HitZone resilience = damage.GetResilienceHitZone();
if (!Check(resilience && damage.GetPermitUnconsciousness(), "native resilience and unconsciousness policy available")) return -1;
float beforeResilience = resilience.GetHealth();
// Protection must not swallow the fixture's own true-damage request. Keep
// this synchronous interval limited to that one native health operation.
damage.EnableDamageHandling(true);
damage.ForceUnconsciousness();
float afterResilience = resilience.GetHealth();
damage.EnableDamageHandling(false);
PrintFormat("ERR CASUALTY INCAPACITATION: resilienceBefore=%1 resilienceAfter=%2", beforeResilience, afterResilience);
if (!Check(afterResilience < beforeResilience && ERR_Actors.Alive(m_Player), "single native resilience damage applied before restoring fixture protection")) return -1;
m_Step = 2; m_Wait = now + 500; return 0;
}
if (m_Step == 2)
{
if (!ERR_CasualtyLoadingRules.GroundSettled(m_Player))
{
if (now >= m_NextDiagnostic) { Diagnose(); m_NextDiagnostic = now + 1000; }
return 0;
}
if (!Check(ERR_CasualtyLoadingRules.Nearby(m_Mode, m_Helper, m_Player), "actual incapacitated target is settled with clear line of sight")) return -1;
if (!Check(!m_Mode.CasualtyLoading().CanLoad(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player) && m_Service.PendingCount() == 0, "strict production authorization rejects nonparticipant helper even in valid physical state")) return -1;
m_Service.EnableHelper();
if (!Check(m_Mode.ERR_UseCasualtyFixture(m_Service), "explicit fixture installs helper-only membership adapter")) return -1;
m_Observing = true; m_Service.Tick(now); m_SettleStart = System.GetTickCount();
if (!Check(!m_Service.CanLoad(m_Helper, m_Player), "new observation cannot bypass five-second settling window")) return -1;
m_Step = 3; return 0;
}
if (m_Step == 3)
{
if (System.GetTickCount() - m_SettleStart < 5500) return 0;
Diagnose();
if (!Check(m_Service.CanLoad(m_Helper, m_Player), "settled real casualty is eligible with only helper membership adapted")) return -1;
array<BaseCompartmentSlot> seats = {}; m_Mode.Extraction().Seats(seats);
foreach (BaseCompartmentSlot seat : seats)
{
if (!ERR_CasualtyLoadingRules.TransportBench(seat, m_Aircraft) || seat.GetOccupant() || seat.IsReserved()) continue;
seat.SetReserved(m_Helper); m_Reserved.Insert(seat);
}
if (!Check(!m_Reserved.IsEmpty() && !m_Service.CanLoad(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player), "native external reservations prevent loading when all child benches are reserved")) return -1;
ReleaseFixtureReservations();
vector middle = (ChimeraCharacter.Cast(m_Helper).AimingPosition() + ChimeraCharacter.Cast(m_Player).AimingPosition()) * 0.5;
m_Blocker = ERR_Actors.Spawn("{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et", middle);
if (!Check(m_Blocker != null, "successor-owned physical line-of-sight obstacle spawned")) return -1;
m_Mode.Own(m_Blocker);
vector low, high; m_Blocker.GetWorldBounds(low, high);
vector wall[4]; m_Blocker.GetWorldTransform(wall); wall[3] = wall[3] + middle - (low + high) * 0.5;
m_Blocker.SetWorldTransform(wall);
m_Step = 4; m_Wait = now + 500; return 0;
}
if (m_Step == 4)
{
if (!Check(!ERR_CasualtyLoadingRules.LineOfSight(m_Helper, m_Player) && !m_Service.CanLoad(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player) && m_Service.PendingCount() == 0, "physical wall blocks completion-time rescue authorization")) return -1;
ERR_Actors.DeleteSpawned(m_Blocker); m_Blocker = null;
m_Step = 5; m_Wait = now + 500; return 0;
}
if (m_Step == 5)
{
if (!Check(m_Action.CanBePerformedScript(m_Helper) && m_Service.PendingCount() == 0, "real action becomes performable after obstruction removal")) return -1;
// Invoke the real completion boundary. This does not simulate holding input
// for five seconds or claim a second authenticated network player.
m_Action.PerformAction(m_Player, m_Helper);
if (!Check(m_Service.PendingCount() == 1 && m_Resolutions == 0, "real owner handshake commits one pending physical load")) return -1;
array<BaseCompartmentSlot> seats = {}; m_Mode.Extraction().Seats(seats);
foreach (BaseCompartmentSlot seat : seats)
if (ERR_CasualtyLoadingRules.TransportBench(seat, m_Aircraft) && seat.IsReservedBy(m_Player)) m_LoadedSeat = seat;
if (!Check(m_LoadedSeat != null, "service reserves the actual selected child bench for the casualty")) return -1;
m_Action.PerformAction(m_Player, m_Helper);
if (!Check(m_Service.PendingCount() == 1 && m_Resolutions == 0, "duplicate completion cannot issue a second ticket")) return -1;
m_Step = 6; return 0;
}
if (m_Step == 6)
{
if (m_Service.PendingCount() > 0) return 0;
Diagnose();
if (!Check(m_Loaded && m_Resolutions == 1 && ERR_Actors.Alive(m_Player) && ERR_CasualtyLoadingRules.Down(m_Player) && CompartmentAccessComponent.GetVehicleIn(m_Player) == m_Aircraft && m_LoadedSeat.GetOccupant() == m_Player, "production owner handshake physically loads real living incapacitated player exactly once")) return -1;
if (!Check(!m_LoadedSeat.IsReserved() && m_Service.StrictParticipant(m_Player), "successful loading releases reservation and preserves actual player identity")) return -1;
m_Service.Stop(); m_Service.Stop(); m_Service.Tick(now);
if (!Check(m_Service.StoppedForProbe() && m_Service.PendingCount() == 0 && m_Resolutions == 1 && !m_Service.Begin(m_Helper, m_Player), "Stop is latched, idempotent and cannot repeat completion or restart loading")) return -1;
Stop(); return 1;
}
return 0;
}
}
modded class ERR_GameMode
{
protected ref ERR_NativeCasualtyHelperAdapter m_ERRNativeCasualtyAdapter;
bool ERR_UseCasualtyFixture(ERR_NativeCasualtyHelperAdapter service)
{
string enabled, outcome;
if (!Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1" || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "casualty-service") return false;
m_ERRNativeCasualtyAdapter = service; return true;
}
override ERR_CasualtyLoadingService CasualtyLoading()
{
if (m_ERRNativeCasualtyAdapter) return m_ERRNativeCasualtyAdapter;
return super.CasualtyLoading();
}
}
#endif
@@ -40,9 +40,12 @@ class ERR_NativeDefinitionProbe
if (!Check(!definition.Valid(), "unnamed location rejected")) return false;
alternate.Label = label;
ERR_SiteDefinition radio = definition.Find("everon-radio-1");
radio.DiscoverRadius = 25;
if (!Check(!definition.Valid(), "proximity discovery cannot reveal progression sites")) return false;
radio.DiscoverRadius = 0;
float radioRadius = radio.DiscoverRadius;
radio.DiscoverRadius = 9;
if (!Check(!definition.Valid(), "radio identification beyond eight metres rejected")) return false;
radio.DiscoverRadius = 8;
if (!Check(definition.Valid(), "close physical radio identification allowed")) return false;
radio.DiscoverRadius = radioRadius;
radio.RevealOnEscape = true;
if (!Check(!definition.Valid(), "escape cannot reveal the radio directly")) return false;
radio.RevealOnEscape = false;
@@ -0,0 +1,163 @@
#ifdef WORKBENCH
class ERR_NativeEnvironmentSample : ERR_EnvironmentService
{
bool Apply(TimeAndWeatherManagerEntity manager, int dateCode, int profile) { return StartOn(manager, dateCode, profile); }
}
// Explicit isolated environment smoke outcome. It temporarily changes the private
// native world, restores date/weather family/looping, then requests a round reset.
class ERR_NativeEnvironmentProbe
{
protected ERR_GameMode m_Mode;
protected TimeAndWeatherManagerEntity m_Manager;
protected ref ERR_NativeEnvironmentSample m_Sample;
protected ref ERR_Settings m_TestSettings;
protected bool m_Snapshot;
protected bool m_Restored;
protected bool m_Stopped;
protected bool m_OwnsSettings;
protected bool m_RestoreDateAccepted;
protected int m_Year;
protected int m_Month;
protected int m_Day;
protected string m_Weather;
protected bool m_Looping;
protected float m_Clock;
protected float m_DayDuration;
protected bool m_DayAdvances;
protected float m_Wait;
protected float m_Deadline;
protected int m_Step;
protected int m_Result;
void ERR_NativeEnvironmentProbe(ERR_GameMode mode) { m_Mode = mode; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: environment " + detail); return true; }
Print("ERR SMOKE FAIL: environment " + detail, LogLevel.ERROR);
m_Result = -1;
Stop();
return false;
}
protected string CurrentWeather()
{
if (!m_Manager || !m_Manager.GetCurrentWeatherState()) return "";
return m_Manager.GetCurrentWeatherState().GetStateName();
}
protected void Restore()
{
if (!m_Snapshot || m_Restored || !m_Manager) return;
m_Restored = true;
if (m_Sample) m_Sample.Stop();
m_RestoreDateAccepted = m_Manager.SetDate(m_Year, m_Month, m_Day, true);
m_Manager.ForceWeatherTo(m_Looping, m_Weather, 0, 24);
}
void Stop()
{
if (m_Stopped) return;
Restore();
m_Stopped = true;
if (m_OwnsSettings)
{
string path = "$profile:erri-native-environment-probe.json";
FileIO.DeleteFile(path); FileIO.DeleteFile(path + ".bak"); FileIO.DeleteFile(path + ".tmp");
m_OwnsSettings = false;
}
}
protected bool SettingsChecks()
{
string path = "$profile:erri-native-environment-probe.json";
if (!Check(!FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "environment fixture filenames are unused")) return false;
m_OwnsSettings = true;
JsonSaveContext json = new JsonSaveContext();
json.EnableTypeDiscriminator(false);
foreach (string key : ERR_Settings.Keys())
if (key != "startDateCode" && key != "weatherProfile") json.WriteValue(key, m_Mode.Settings().Get(key));
if (!Check(json.SaveToFile(path), "owned legacy settings fixture written")) return false;
m_TestSettings = new ERR_Settings(path);
JsonLoadContext unchanged = new JsonLoadContext();
if (!Check(m_TestSettings.Get("startDateCode") == 0 && m_TestSettings.Get("weatherProfile") == 0 && unchanged.LoadFromFile(path) && !unchanged.DoesKeyExist("startDateCode") && !unchanged.DoesKeyExist("weatherProfile"), "missing environment settings preserve authored defaults without rewriting legacy file")) return false;
if (!Check(m_TestSettings.Set("startDateCode", 1501231) && m_TestSettings.Set("weatherProfile", 2), "date and weather persist in owned fixture settings")) return false;
ERR_Settings reloaded = new ERR_Settings(path);
if (!Check(reloaded.Get("startDateCode") == 1501231 && reloaded.Get("weatherProfile") == 2 && reloaded.Get("radioDefenceSeconds") == m_Mode.Settings().Get("radioDefenceSeconds"), "odd maximum date code round-trips exactly and old settings survive")) return false;
if (!Check(!m_TestSettings.Set("startDateCode", 229) && !m_TestSettings.Set("weatherProfile", 4), "non-leap-century date and unknown profile cannot be saved")) return false;
int originalDate = m_Mode.Settings().Get("startDateCode");
int originalProfile = m_Mode.Settings().Get("weatherProfile");
array<string> replies = {};
m_Mode.Admin().Execute(m_Mode, false, "mission weather set rain", replies);
if (!Check(replies.Count() == 1 && replies[0].Contains("Administrator") && m_Mode.Settings().Get("startDateCode") == originalDate && m_Mode.Settings().Get("weatherProfile") == originalProfile, "unauthorized environment command preserves real settings")) return false;
replies.Clear();
ERR_EnvironmentCommands.Execute(m_TestSettings, m_Mode.Environment(), {"mission", "date", "set", "2000-02-29"}, replies);
if (!Check(m_TestSettings.Get("startDateCode") == 1000229 && replies.Count() == 1 && replies[0].Contains("next round"), "natural leap-date command saves only next-round settings")) return false;
replies.Clear();
ERR_EnvironmentCommands.Execute(m_TestSettings, m_Mode.Environment(), {"mission", "weather", "set", "clear"}, replies);
if (!Check(m_TestSettings.Get("weatherProfile") == 1 && replies.Count() == 1 && replies[0].Contains("Current round unchanged"), "named weather command saves only next-round settings")) return false;
replies.Clear();
ERR_EnvironmentCommands.Execute(m_TestSettings, m_Mode.Environment(), {"mission", "date", "set", "1900-00-00"}, replies);
if (!Check(m_TestSettings.Get("startDateCode") == 1000229 && replies[0].Contains("Invalid"), "zero month/day cannot masquerade as authored date")) return false;
return true;
}
int Tick(float now)
{
if (m_Result != 0) return m_Result;
if (m_Stopped) return -1;
if (!Replication.IsServer() || !m_Mode || !m_Mode.Active()) { Check(false, "requires active server authority"); return -1; }
if (m_Deadline > 0 && now > m_Deadline) { Check(false, "native environment request timed out"); return -1; }
if (now < m_Wait) return 0;
if (m_Step == 0)
{
ChimeraWorld world = ChimeraWorld.CastFrom(m_Mode.GetWorld());
if (world) m_Manager = world.GetTimeAndWeatherManager();
if (!Check(m_Manager && m_Manager.GetCurrentWeatherState() && m_Mode.Environment().Started(), "native environment manager and round service available")) return -1;
m_Manager.GetDate(m_Year, m_Month, m_Day);
m_Weather = CurrentWeather(); m_Looping = m_Manager.IsWeatherLooping();
m_Clock = m_Manager.GetTimeOfTheDay(); m_DayDuration = m_Manager.GetDayDuration(); m_DayAdvances = m_Manager.GetIsDayAutoAdvanced();
m_Snapshot = true;
m_Deadline = now + 15000;
if (!SettingsChecks()) return -1;
if (!Check(CurrentWeather() == m_Weather && m_Manager.GetYear() == m_Year && m_Manager.GetMonth() == m_Month && m_Manager.GetDay() == m_Day, "saving fixture environment settings does not mutate native world")) return -1;
for (int profile = 1; profile <= 3; profile++)
if (!Check(ERR_EnvironmentService.HasWeatherState(m_Manager, ERR_EnvironmentService.WeatherStateName(profile)), "selected vanilla weather family exists: " + ERR_EnvironmentService.ProfileName(profile))) return -1;
m_Sample = new ERR_NativeEnvironmentSample();
if (!Check(!m_Sample.Apply(m_Manager, 229, 1) && !m_Sample.Apply(m_Manager, 1000229, 4) && !m_Sample.Started(), "invalid date/profile rejected before native changes")) return -1;
if (!Check(m_Sample.Apply(m_Manager, 890601, 1), "clear profile and specific date requested once")) return -1;
m_Step = 1; m_Wait = now + 500;
return 0;
}
if (m_Step == 1)
{
if (!Check(CurrentWeather() == "Clear" && m_Manager.IsWeatherLooping() && m_Manager.GetYear() == 1989 && m_Manager.GetMonth() == 6 && m_Manager.GetDay() == 1, "native clear weather and requested date applied after frame")) return -1;
if (!Check(m_Sample.Apply(m_Manager, 1000229, 3) && m_Sample.DateCode() == 890601 && m_Sample.Profile() == 1 && CurrentWeather() == "Clear", "repeated start cannot apply another date or profile")) return -1;
m_Sample.Stop();
if (!Check(!m_Sample.Apply(m_Manager, 1000229, 3), "stopped environment cannot restart")) return -1;
m_Sample = new ERR_NativeEnvironmentSample();
if (!Check(m_Sample.Apply(m_Manager, 1000229, 2), "overcast and leap date requested on fresh service")) return -1;
m_Step = 2; m_Wait = now + 500;
return 0;
}
if (m_Step == 2)
{
if (!Check(CurrentWeather() == "Overcast" && m_Manager.GetYear() == 2000 && m_Manager.GetMonth() == 2 && m_Manager.GetDay() == 29, "native overcast and leap date accepted")) return -1;
m_Sample.Stop(); m_Sample = new ERR_NativeEnvironmentSample();
if (!Check(m_Sample.Apply(m_Manager, 0, 3), "rain profile preserves authored/current date when date override is disabled")) return -1;
m_Step = 3; m_Wait = now + 500;
return 0;
}
if (m_Step == 3)
{
if (!Check(CurrentWeather() == "Rainy" && m_Manager.GetYear() == 2000 && m_Manager.GetMonth() == 2 && m_Manager.GetDay() == 29, "native rainy family applied without changing date")) return -1;
float clockAdvance = m_Manager.GetTimeOfTheDay() - m_Clock;
if (clockAdvance < 0) clockAdvance += 24;
if (!Check(m_Manager.GetDayDuration() == m_DayDuration && m_Manager.GetIsDayAutoAdvanced() == m_DayAdvances && clockAdvance < 0.1, "environment does not reset clock or progression")) return -1;
Restore();
m_Step = 4; m_Wait = now + 500;
return 0;
}
if (m_Step == 4)
{
if (!Check(m_RestoreDateAccepted && m_Manager.GetYear() == m_Year && m_Manager.GetMonth() == m_Month && m_Manager.GetDay() == m_Day && CurrentWeather() == m_Weather && m_Manager.IsWeatherLooping() == m_Looping, "fixture restores original native date, weather family and looping")) return -1;
m_Result = 1; Stop();
return 1;
}
return 0;
}
}
#endif
@@ -0,0 +1,118 @@
#ifdef WORKBENCH
// Only environment-restart opts into this fixed private path. The runner must
// refuse all preexisting fixture files and require exactly two real rounds.
class ERR_NativeEnvironmentRestartProbe
{
static const string PATH = "$profile:erri-environment-restart.json";
static const string MARKER = "$profile:erri-environment-restart-round.json";
protected ERR_GameMode m_Mode;
protected TimeAndWeatherManagerEntity m_Manager;
protected bool m_Failed;
protected bool m_Stopped;
protected bool m_Complete;
protected int m_Round = 1;
protected int m_InitialDate;
protected string m_InitialWeather;
protected bool m_InitialLoop;
protected float m_Wait;
protected float m_Hour;
protected float m_Duration;
void ERR_NativeEnvironmentRestartProbe(ERR_GameMode mode)
{
m_Mode = mode;
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
if (world) m_Manager = world.GetTimeAndWeatherManager();
if (!Check(m_Manager && m_Manager.GetCurrentWeatherState(), "world environment exists before production settings and service initialization")) return;
m_InitialDate = DateCode();
m_InitialWeather = Weather();
m_InitialLoop = m_Manager.IsWeatherLooping();
if (FileIO.FileExists(MARKER))
{
JsonLoadContext marker = new JsonLoadContext();
int stage;
if (!Check(marker.LoadFromFile(MARKER) && marker.ReadValue("completedRound", stage) && stage == 1 && FileIO.FileExists(PATH), "second instance sees first round's completed marker and saved settings")) return;
m_Round = 2;
}
else
{
if (!Check(!FileIO.FileExists(PATH) && !FileIO.FileExists(PATH + ".bak") && !FileIO.FileExists(PATH + ".tmp"), "first instance owns previously unused fixture settings paths")) return;
}
PrintFormat("ERR ENVIRONMENT RESTART INSTANCE: round=%1 authoredDate=%2 authoredWeather=%3", m_Round, m_InitialDate, m_InitialWeather);
}
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: environment restart " + detail); return true; }
m_Failed = true;
Print("ERR SMOKE FAIL: environment restart " + detail, LogLevel.ERROR);
return false;
}
protected int DateCode() { return (m_Manager.GetYear() - 1900) * 10000 + m_Manager.GetMonth() * 100 + m_Manager.GetDay(); }
protected string Weather()
{
WeatherState state = m_Manager.GetCurrentWeatherState();
if (!state) return "unavailable";
return state.GetStateName();
}
int Round() { return m_Round; }
void Stop() { m_Stopped = true; }
int Tick(float now)
{
if (m_Failed || m_Stopped) return -1;
if (m_Complete) return 1;
if (m_Wait == 0)
{
if (!Check(m_Mode.Environment().Started() && m_Mode.Time().Started(), "production round services started")) return -1;
if (!Check(m_Mode.Settings().Get("startTimeMinutes") == 840 && m_Mode.Settings().Get("timeMultiplier") == 1 && m_Mode.Time().StartMinutes() == 840 && m_Mode.Time().Multiplier() == 1, "both rounds retain original clock defaults and active progression settings")) return -1;
m_Hour = m_Manager.GetTimeOfTheDay();
m_Duration = m_Manager.GetDayDuration();
if (m_Round == 1)
{
if (!Check(m_Mode.Settings().Get("startDateCode") == 0 && m_Mode.Settings().Get("weatherProfile") == 0 && m_Mode.Environment().DateCode() == 0 && m_Mode.Environment().Profile() == 0, "first instance uses authored environment defaults")) return -1;
if (!Check(DateCode() == m_InitialDate && Weather() == m_InitialWeather && m_Manager.IsWeatherLooping() == m_InitialLoop, "default production startup preserves authored date and weather")) return -1;
array<string> replies = {};
if (!Check(ERR_EnvironmentCommands.Execute(m_Mode.Settings(), m_Mode.Environment(), {"mission", "date", "set", "2000-02-29"}, replies), "production date command handled")) return -1;
if (!Check(ERR_EnvironmentCommands.Execute(m_Mode.Settings(), m_Mode.Environment(), {"mission", "weather", "set", "overcast"}, replies), "production weather command handled")) return -1;
if (!Check(replies.Count() == 2 && replies[0].Contains("Saved for next round") && replies[1].Contains("Saved for next round"), "both commands report durable next-round saves")) return -1;
// This is the actual mode's settings owner, not a detached sample service.
ERR_Settings disk = new ERR_Settings(PATH);
if (!Check(disk.Get("startDateCode") == 1000229 && disk.Get("weatherProfile") == 2 && disk.Get("radioDefenceSeconds") == 180 && disk.Get("timeMultiplier") == 1, "production saves persist environment and preserve unrelated defaults")) return -1;
}
else
{
if (!Check(m_Mode.Settings().Get("startDateCode") == 1000229 && m_Mode.Settings().Get("weatherProfile") == 2 && m_Mode.Environment().DateCode() == 1000229 && m_Mode.Environment().Profile() == 2, "second real mode loads saved date and weather into production services")) return -1;
}
m_Wait = now + 2500;
return 0;
}
if (now < m_Wait) return 0;
if (!Check(m_Mode.Settings().Get("startDateCode") == 1000229 && m_Mode.Settings().Get("weatherProfile") == 2, "saved next-round settings remain intact")) return -1;
if (m_Round == 1)
{
if (!Check(m_Mode.Environment().DateCode() == 0 && m_Mode.Environment().Profile() == 0 && DateCode() == m_InitialDate && Weather() == m_InitialWeather && m_Manager.IsWeatherLooping() == m_InitialLoop, "saving real mode settings leaves current environment unchanged across native ticks")) return -1;
}
else if (!Check(DateCode() == 1000229 && Weather() == "Overcast" && m_Manager.IsWeatherLooping(), "second round applies exact leap date and selected native weather family")) return -1;
float advance = m_Manager.GetTimeOfTheDay() - m_Hour;
if (advance < 0) advance += 24;
if (!Check(advance > 0 && advance < 0.02 && m_Manager.GetIsDayAutoAdvanced() && m_Duration == ERR_TimeService.DayDuration(1) && m_Manager.GetDayDuration() == m_Duration, "native clock continues advancing without an environment-induced reset or rate change")) return -1;
if (m_Round == 1)
{
JsonSaveContext marker = new JsonSaveContext();
marker.EnableTypeDiscriminator(false);
marker.WriteValue("completedRound", 1);
if (!Check(marker.SaveToFile(MARKER), "first completed round leaves restart handoff marker")) return -1;
}
else
{
if (FileIO.FileExists(PATH)) FileIO.DeleteFile(PATH);
if (FileIO.FileExists(PATH + ".bak")) FileIO.DeleteFile(PATH + ".bak");
if (FileIO.FileExists(PATH + ".tmp")) FileIO.DeleteFile(PATH + ".tmp");
if (FileIO.FileExists(MARKER)) FileIO.DeleteFile(MARKER);
if (!Check(!FileIO.FileExists(PATH) && !FileIO.FileExists(PATH + ".bak") && !FileIO.FileExists(PATH + ".tmp") && !FileIO.FileExists(MARKER), "successful second round removes only fixture settings and marker files")) return -1;
}
m_Complete = true;
return 1;
}
}
#endif
@@ -0,0 +1,75 @@
#ifdef WORKBENCH
// Read-only native assertions. Run after site preparation and faction init,
// before destructive vehicle fixtures. Does not load legacy full faction configs.
class ERR_NativeFactionCatalogProbe
{
protected static ResourceName UAZ() { return "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et"; }
protected static ResourceName Mi8() { return "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et"; }
protected static bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: faction-catalog " + detail); return true; }
Print("ERR SMOKE FAIL: faction-catalog " + detail, LogLevel.ERROR);
return false;
}
protected static bool Catalogs(SCR_Faction faction)
{
array<SCR_EntityCatalog> catalogs = {};
if (!Check(faction.GetAllFactionEntityCatalogs(catalogs) == 6, faction.GetFactionKey() + " retains all six catalog types")) return false;
SCR_EntityCatalog vehicles = faction.GetFactionEntityCatalogOfType(EEntityCatalogType.VEHICLE);
array<SCR_EntityCatalogEntry> entries = {};
if (!Check(vehicles && vehicles.GetEntityList(entries) == 2, faction.GetFactionKey() + " has exactly two supported vehicle entries")) return false;
bool uaz, mi8;
foreach (SCR_EntityCatalogEntry entry : entries)
{
if (!Check(entry != null, "vehicle entry exists")) return false;
ResourceName prefab = entry.GetPrefab();
PrintFormat("ERR FACTION VEHICLE: faction=%1 prefab=%2", faction.GetFactionKey(), prefab);
if (prefab == UAZ()) uaz = true;
else if (prefab == Mi8()) mi8 = true;
else return Check(false, "unexpected vehicle catalog entry " + prefab);
}
if (!Check(uaz && mi8 && vehicles.GetEntryWithPrefab(UAZ()) && vehicles.GetEntryWithPrefab(Mi8()), "catalog lookup resolves both unique mission vehicles")) return false;
array<EEntityCatalogType> retained = {EEntityCatalogType.CHARACTER, EEntityCatalogType.GROUP, EEntityCatalogType.WEAPONS_TRIPOD, EEntityCatalogType.ITEM, EEntityCatalogType.SUPPLY_CONTAINER_ITEM};
foreach (EEntityCatalogType kind : retained)
{
SCR_EntityCatalog catalog = faction.GetFactionEntityCatalogOfType(kind);
if (!Check(catalog != null, faction.GetFactionKey() + " non-vehicle catalog exists " + typename.EnumToString(EEntityCatalogType, kind))) return false;
catalog.GetEntityList(entries);
PrintFormat("ERR FACTION RETAINED: faction=%1 type=%2 entries=%3", faction.GetFactionKey(), typename.EnumToString(EEntityCatalogType, kind), entries.Count());
// Installed US/USSR supply-container catalogs are intentionally empty.
if (kind != EEntityCatalogType.SUPPLY_CONTAINER_ITEM && !Check(!entries.IsEmpty(), "retained native content catalog is initialized and nonempty")) return false;
}
return true;
}
protected static bool Items(SCR_Faction faction, ResourceName dressing, ResourceName saline, ResourceName radio)
{
SCR_EntityCatalog items = faction.GetFactionEntityCatalogOfType(EEntityCatalogType.ITEM);
return Check(items && items.GetEntryWithPrefab(dressing) && items.GetEntryWithPrefab(saline) && items.GetEntryWithPrefab(radio), faction.GetFactionKey() + " native dressing saline and backpack radio remain catalogued");
}
static bool Run(ERR_GameMode mode)
{
if (!Check(Replication.IsServer() && mode && mode.Active(), "active authority available")) return false;
FactionManager manager = GetGame().GetFactionManager();
if (!Check(manager != null, "native faction manager exists")) return false;
SCR_Faction us = SCR_Faction.Cast(manager.GetFactionByKey("US"));
SCR_Faction ussr = SCR_Faction.Cast(manager.GetFactionByKey("USSR"));
if (!Check(us && ussr && mode.PlayerFaction() == us && mode.EnemyFaction() == ussr, "mission retains US and USSR faction identities")) return false;
if (!Check(us.IsFactionEnemy(ussr) && ussr.IsFactionEnemy(us) && !us.IsFactionEnemy(us) && !ussr.IsFactionEnemy(ussr), "mutual enemy and same-faction relations preserved")) return false;
if (!Check(us.GetFactionRadioEncryptionKey() == "chickenNuggets" && ussr.GetFactionRadioEncryptionKey() == "coldBorscht" && us.GetFactionRadioFrequency() == 48000 && ussr.GetFactionRadioFrequency() == 42000, "native radio encryption and frequencies preserved")) return false;
if (!Check(us.GetCallsignInfo() && ussr.GetCallsignInfo(), "native faction callsign configurations resolve")) return false;
if (!Catalogs(us) || !Catalogs(ussr)) return false;
if (!Items(us, "{A81F501D3EF6F38E}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et", "{00E36F41CA310E2A}Prefabs/Items/Medicine/SalineBag_01/SalineBag_US_01.et", "{9B6B61BB3FE3DFB0}Prefabs/Items/Equipment/Radios/Radio_ANPRC77.et")) return false;
if (!Items(ussr, "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et", "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et", "{54C68E438DD34265}Prefabs/Items/Equipment/Radios/Radio_R107M.et")) return false;
SCR_EntityCatalogManagerComponent catalogManager = SCR_EntityCatalogManagerComponent.GetInstance();
if (!Check(catalogManager && catalogManager.GetFactionEntityCatalogOfType(EEntityCatalogType.VEHICLE, "US") && catalogManager.GetFactionEntityCatalogOfType(EEntityCatalogType.ITEM, "USSR"), "global catalog manager retains faction vehicle and inventory access")) return false;
ERR_SiteRuntime depot = mode.Sites().Find("everon-supply-2");
Vehicle helicopter = mode.ExtractionVehicle();
if (!Check(depot && depot.Transport && ERR_Actors.Alive(depot.Transport) && depot.Transport.GetPrefabData().GetPrefabName() == UAZ(), "physical finite depot vehicle is supported UAZ")) return false;
if (!Check(helicopter && ERR_Actors.Alive(helicopter) && helicopter.GetPrefabData().GetPrefabName() == Mi8() && helicopter.FindComponent(HelicopterControllerComponent), "physical extraction aircraft is supported Mi8")) return false;
array<BaseCompartmentSlot> seats = {};
if (!Check(mode.Extraction().Seats(seats) >= 12, "physical aircraft still has required capacity")) return false;
Print("ERR FACTION RESULT: successor faction catalog scope and retained native support; no driving or multiplayer claim");
return true;
}
}
#endif
@@ -0,0 +1,247 @@
#ifdef WORKBENCH
// Native queue length is not exposed: SCR_AIGroup.GetSpawnQueueSize() is a
// compatibility stub returning zero. Observe dispatch on explicitly tagged
// disposable groups instead; every engine call still reaches unchanged super.
class ERR_NativeGuardDispatchTrace
{
static int Total;
}
modded class SCR_AIGroup
{
protected bool m_ERRGuardCancelTracked;
protected int m_ERRGuardCancelDispatches;
protected int m_ERRGuardCancelInitCalls;
protected int m_ERRGuardCancelRequestCalls;
void ERR_TrackGuardCancel()
{
m_ERRGuardCancelTracked = true;
PrintFormat("ERR GUARD CANCEL TRACK: group=%1 initCalls=%2 requests=%3 agents=%4 spawnImmediately=%5 slots=%6 lifecycle=%7", this, m_ERRGuardCancelInitCalls, m_ERRGuardCancelRequestCalls, GetAgentsCount(), GetSpawnImmediately(), m_aUnitPrefabSlots.Count(), GetLifecyclePolicy());
}
override void EOnInit(IEntity owner)
{
m_ERRGuardCancelInitCalls++;
if (m_ERRGuardCancelTracked) PrintFormat("ERR GUARD CANCEL LATE INIT: group=%1 initCalls=%2", this, m_ERRGuardCancelInitCalls);
super.EOnInit(owner);
}
override void RequestSpawn(int slotsWanted = -1, float observerRange = 0)
{
m_ERRGuardCancelRequestCalls++;
if (m_ERRGuardCancelTracked) PrintFormat("ERR GUARD CANCEL REQUEST: group=%1 request=%2 wanted=%3 range=%4 slots=%5", this, m_ERRGuardCancelRequestCalls, slotsWanted, observerRange, m_aUnitPrefabSlots.Count());
super.RequestSpawn(slotsWanted, observerRange);
}
int ERR_GuardCancelDispatches() { return m_ERRGuardCancelDispatches; }
// EOnInit assigns the cached utility before requesting members. The ignore-
// spawning branch instead invokes all-members init immediately; exclude it.
bool ERR_GuardCancelPendingInit() { return GetGroupUtilityComponent() && !m_bOnInitInvoked; }
override bool ExpandOneMember()
{
if (m_ERRGuardCancelTracked)
{
m_ERRGuardCancelDispatches++;
ERR_NativeGuardDispatchTrace.Total++;
PrintFormat("ERR GUARD CANCEL DISPATCH: group=%1 dispatch=%2 agents=%3 slots=%4 complete=%5", this, m_ERRGuardCancelDispatches, GetAgentsCount(), m_aUnitPrefabSlots.Count(), IsExpandComplete());
}
return super.ExpandOneMember();
}
}
// Assemble only the owned-group portion of PENDING preparation. No authored
// mission sites, stock, waypoints or live mission service are cloned or changed.
class ERR_NativeGuardCancelOwner : ERR_SiteService
{
protected int m_Cancellations;
void ERR_NativeGuardCancelOwner(ERR_GameMode mode) { m_Mode = mode; }
int Cancellations() { return m_Cancellations; }
protected override void CancelInitialGuardRequests()
{
m_Cancellations++;
PrintFormat("ERR GUARD CANCEL OWNER: pendingState=%1 calls=%2 ownedSites=%3", m_State, m_Cancellations, m_Sites.Count());
super.CancelInitialGuardRequests();
foreach (ERR_SiteRuntime site : m_Sites)
if (site.Group) PrintFormat("ERR GUARD CANCEL RETIRED: site=%1 group=%2 slots=%3 spawnImmediately=%4 agents=%5 complete=%6", site.Definition.Id, site.Group, site.Group.m_aUnitPrefabSlots.Count(), site.Group.GetSpawnImmediately(), site.Group.GetAgentsCount(), site.Group.IsExpandComplete());
}
bool Attach(SCR_AIGroup group, string label)
{
if (!group || m_Begun) return false;
m_Begun = true;
ERR_SiteRuntime site = new ERR_SiteRuntime();
site.Definition = new ERR_SiteDefinition();
site.Definition.Id = label;
site.Definition.Guarded = true;
site.Group = group;
if (group.m_aUnitPrefabSlots) site.InitialGuardCount = group.m_aUnitPrefabSlots.Count();
m_Sites.Insert(site);
m_Entities.Insert(group);
return site.InitialGuardCount == 6 && group.GetSpawnImmediately();
}
}
class ERR_NativeGuardCancelProbe
{
protected ERR_GameMode m_Mode;
protected ref ERR_NativeGuardCancelOwner m_EmptyOwner;
protected ref ERR_NativeGuardCancelOwner m_PartialOwner;
protected ref ERR_NativeGuardCancelOwner m_ControlOwner;
protected SCR_AIGroup m_Empty;
protected SCR_AIGroup m_Partial;
protected SCR_AIGroup m_Control;
protected ref array<IEntity> m_PartialMembers = {};
protected ref array<IEntity> m_DeletedMembers = {};
protected ref array<SCR_AIGroup> m_MainGroups = {};
protected int m_PartialDispatches;
protected int m_TotalBeforeDelete;
protected int m_Step;
protected float m_Deadline;
protected float m_ObserveUntil;
protected bool m_RolledBack;
void ERR_NativeGuardCancelProbe(ERR_GameMode mode) { m_Mode = mode; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: guard-cancel " + detail); return true; }
Print("ERR SMOKE FAIL: guard-cancel " + detail, LogLevel.ERROR);
Stop();
return false;
}
protected int Members(SCR_AIGroup group, out array<IEntity> members)
{
members.Clear();
if (!group) return 0;
array<AIAgent> agents = {};
group.GetAgents(agents);
foreach (AIAgent agent : agents)
{
if (!agent) continue;
IEntity actor = agent.GetControlledEntity();
if (actor && !members.Contains(actor)) members.Insert(actor);
}
return members.Count();
}
protected SCR_AIGroup NewGroup(vector offset)
{
ERR_SiteRuntime outpost = m_Mode.Sites().Find("everon-outpost-1");
if (!outpost || !outpost.Group) return null;
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn(m_Mode.Factions().GuardGroupPrefab, ERR_Actors.Ground(outpost.Group.GetOrigin() + offset)));
if (group) group.ERR_TrackGuardCancel();
return group;
}
protected bool OriginalSitesIntact()
{
if (!m_Mode.Sites() || m_Mode.Sites().Prepare() != ERR_PrepareResult.READY) return false;
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
array<SCR_AIGroup> groups = {};
foreach (ERR_LocationComponent location : locations)
{
ERR_SiteRuntime site = m_Mode.Sites().Find(location.Id());
if (site && site.Group) groups.Insert(site.Group);
}
if (groups.Count() != m_MainGroups.Count()) return false;
foreach (SCR_AIGroup group : m_MainGroups)
if (!group || !groups.Contains(group)) return false;
return true;
}
protected bool CancelledStable()
{
array<IEntity> emptyMembers = {};
array<IEntity> partialMembers = {};
if (!m_Empty || Members(m_Empty, emptyMembers) != 0 || m_Empty.ERR_GuardCancelDispatches() != 0) return false;
if (!m_Partial || Members(m_Partial, partialMembers) != m_PartialMembers.Count() || m_Partial.ERR_GuardCancelDispatches() != m_PartialDispatches) return false;
foreach (IEntity actor : m_PartialMembers)
if (!actor || !partialMembers.Contains(actor)) return false;
return true;
}
void Stop()
{
// Each owner rolls back at most once. ERR_Actors.DeleteSpawned retains its
// stock active-player protection; no player is assigned to these groups.
if (m_RolledBack) return;
m_RolledBack = true;
if (m_EmptyOwner) m_EmptyOwner.Rollback();
if (m_PartialOwner) m_PartialOwner.Rollback();
if (m_ControlOwner) m_ControlOwner.Rollback();
}
int Tick(float now)
{
if (m_Deadline == 0) m_Deadline = now + 90000;
if (now > m_Deadline || !m_Mode.Active()) { Check(false, "bounded fixture deadline or active mission lost"); return -1; }
if (m_Step == 0)
{
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
foreach (ERR_LocationComponent location : locations)
{
ERR_SiteRuntime site = m_Mode.Sites().Find(location.Id());
if (site && site.Group) m_MainGroups.Insert(site.Group);
}
ERR_NativeGuardDispatchTrace.Total = 0;
m_EmptyOwner = new ERR_NativeGuardCancelOwner(m_Mode);
m_PartialOwner = new ERR_NativeGuardCancelOwner(m_Mode);
m_ControlOwner = new ERR_NativeGuardCancelOwner(m_Mode);
m_Empty = NewGroup("50 0 60");
if (!Check(m_EmptyOwner.Attach(m_Empty, "guard-cancel-empty"), "empty case owns one six-slot automatic native group")) return -1;
array<IEntity> initial = {};
if (!Check(m_Empty.ERR_GuardCancelPendingInit() && Members(m_Empty, initial) == 0 && m_Empty.ERR_GuardCancelDispatches() == 0 && !m_Empty.IsExpandComplete(), "empty case is cancelled before its first native member dispatch")) return -1;
m_EmptyOwner.Stop();
m_EmptyOwner.Stop();
if (!Check(m_EmptyOwner.Cancellations() == 1 && !m_Empty.GetSpawnImmediately() && m_Empty.m_aUnitPrefabSlots.IsEmpty(), "empty cancellation retires only this instance's spawn intent once")) return -1;
if (!Check(m_Empty && m_EmptyOwner.Owns(m_Empty) && m_EmptyOwner.Prepare() == ERR_PrepareResult.FAILED && !m_Mode.Sites().Owns(m_Empty), "repeated actual preparation stop retains only the disposable empty group for observation")) return -1;
m_Partial = NewGroup("-50 0 60");
if (!Check(m_PartialOwner.Attach(m_Partial, "guard-cancel-partial"), "partial case owns a separate six-slot automatic native group")) return -1;
m_Control = NewGroup("0 0 100");
if (!Check(m_ControlOwner.Attach(m_Control, "guard-cancel-control"), "uncancelled six-slot control uses the same native queue")) return -1;
m_Step = 1;
return 0;
}
if (m_Step == 1)
{
array<IEntity> emptyMembers = {};
if (!m_Empty || Members(m_Empty, emptyMembers) != 0 || m_Empty.ERR_GuardCancelDispatches() != 0)
{ Check(false, "empty group received late dispatch or members after cancellation"); return -1; }
if (!m_Partial || !m_Control) { Check(false, "disposable observation group disappeared before cancellation"); return -1; }
if (Members(m_Partial, m_PartialMembers) == 0) return 0;
if (!Check(m_PartialMembers.Count() < 6 && !m_Partial.IsExpandComplete() && m_Partial.ERR_GuardCancelDispatches() > 0, "native queue produced a real partial squad before cancellation")) return -1;
// The fixture tick runs after native dispatch returns, so the queue has
// finished re-enqueuing this partial fill. Do not purge from inside AddAgent.
m_PartialOwner.Stop();
m_PartialOwner.Stop();
if (!Check(m_PartialOwner.Cancellations() == 1 && !m_Partial.GetSpawnImmediately() && m_Partial.m_aUnitPrefabSlots.IsEmpty(), "partial cancellation retires future spawn intent without deleting present actors")) return -1;
m_PartialDispatches = m_Partial.ERR_GuardCancelDispatches();
if (!Check(m_PartialOwner.Prepare() == ERR_PrepareResult.FAILED && m_PartialOwner.Owns(m_Partial), "partial preparation stops idempotently while existing members remain")) return -1;
PrintFormat("ERR GUARD CANCEL: partialMembers=%1 dispatches=%2 observationSeconds=15", m_PartialMembers.Count(), m_PartialDispatches);
m_ObserveUntil = now + 15000;
m_Step = 2;
return 0;
}
if (m_Step == 2)
{
if (!CancelledStable()) { Check(false, "cancelled groups changed member identity/count or received another native dispatch"); return -1; }
array<IEntity> controlMembers = {};
int controlCount = Members(m_Control, controlMembers);
if (!m_Control || controlCount > 6) { Check(false, "uncancelled control vanished or exceeded its requested size"); return -1; }
if (now < m_ObserveUntil || controlCount != 6 || !m_Control.IsExpandComplete()) return 0;
if (!Check(m_Control.ERR_GuardCancelDispatches() >= 6 && m_Control.m_aUnitPrefabSlots.Count() == 6 && m_Control.GetSpawnImmediately(), "uncancelled native control reached six while cancelled groups stayed unchanged for at least fifteen seconds")) return -1;
if (!Check(OriginalSitesIntact(), "original mission site groups and ready service remain intact")) return -1;
foreach (IEntity partialMember : m_PartialMembers) m_DeletedMembers.Insert(partialMember);
foreach (IEntity controlMember : controlMembers) m_DeletedMembers.Insert(controlMember);
foreach (IEntity actor : m_DeletedMembers)
if (!Check(!ERR_Actors.Player(actor), "rollback target is a disposable non-player character")) return -1;
m_TotalBeforeDelete = ERR_NativeGuardDispatchTrace.Total;
Stop();
m_ObserveUntil = now + 6000;
m_Step = 3;
return 0;
}
if (m_Step == 3)
{
if (ERR_NativeGuardDispatchTrace.Total != m_TotalBeforeDelete) { Check(false, "native dispatch occurred after rollback"); return -1; }
if (now < m_ObserveUntil) return 0;
if (!Check(!m_Empty && !m_Partial && !m_Control, "each disposable group was deleted once by its real owner rollback")) return -1;
foreach (IEntity deleted : m_DeletedMembers)
if (!Check(!deleted, "each physical partial/control member is actually deleted after rollback")) return -1;
if (!Check(OriginalSitesIntact(), "original mission groups remain intact after rollback and six-second observation")) return -1;
return 1;
}
return 0;
}
}
#endif
@@ -117,7 +117,7 @@ class ERR_NativeLifecycleProbe
reservations.Set(13, second);
vector position;
int count = 2;
while (count < 25 && m_Placement.Prison(m_Mode.PrisonPosition(), reservations, position))
while (count < 16 && m_Placement.Prison(m_Mode.PrisonPosition(), reservations, position))
{
reservations.Set(100 + count, position);
count++;
@@ -126,7 +126,7 @@ class ERR_NativeLifecycleProbe
if (!Check(count >= 11 && !m_Placement.Prison(m_Mode.PrisonPosition(), reservations, position), "occupied and pending slots exhaust a bounded grid without overlap or outside fallback")) return -1;
vector prison = m_Mode.PrisonPosition();
foreach (int id, vector reserved : reservations)
if (!Check(Math.AbsFloat(reserved[0] - prison[0]) <= 3.1 && Math.AbsFloat(reserved[2] - prison[2]) <= 3.1, "candidate remains inside authored prison")) return -1;
if (!Check(Math.AbsFloat(reserved[0] - prison[0]) <= 4.6 && Math.AbsFloat(reserved[2] - prison[2]) <= 4.6, "candidate remains inside authored prison")) return -1;
reservations.Remove(1);
if (!Check(m_Placement.Prison(m_Mode.PrisonPosition(), reservations, position) && vector.DistanceXZ(position, first) < 0.1, "released pending reservation becomes available again")) return -1;
m_Position = first;
@@ -0,0 +1,225 @@
#ifdef WORKBENCH
// Opt-in native medical-effects fixture. Treatment enters at the real item's
// completion boundary; manual user actions/animations and remote clients are not
// simulated. Only fixture-owned characters are injured or deleted.
class ERR_NativeMedicalProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Casualty;
protected IEntity m_Healer;
protected IEntity m_Dressing;
protected IEntity m_Saline;
protected IEntity m_SpareSaline;
protected SCR_CharacterDamageManagerComponent m_Damage;
protected CharacterControllerComponent m_Controller;
protected SCR_CharacterBloodHitZone m_Blood;
protected SCR_SalineStorageComponent m_SalineStorage;
protected SCR_InventoryStorageManagerComponent m_Inventory;
protected ref array<IEntity> m_Owned = {};
protected int m_Step;
protected int m_Result;
protected bool m_Stopped;
protected bool m_Recovered;
protected float m_Wait;
protected float m_Deadline;
protected float m_InfusionStarted;
protected float m_SeedBlood;
protected float m_PreInfusionBlood;
protected float m_RegenSpeed;
protected float m_NextDiagnostic;
void ERR_NativeMedicalProbe(ERR_GameMode mode) { m_Mode = mode; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: medical " + detail); return true; }
Print("ERR SMOKE FAIL: medical " + detail, LogLevel.ERROR);
Trace("failure");
m_Result = -1;
Stop();
return false;
}
protected IEntity Spawn(ResourceName prefab, vector position)
{
IEntity entity = ERR_Actors.Spawn(prefab, ERR_Actors.Ground(position));
if (entity) { m_Owned.Insert(entity); m_Mode.Own(entity); }
return entity;
}
protected int SalineEffects()
{
if (!m_Damage) return 0;
array<ref SCR_PersistentDamageEffect> effects = {};
m_Damage.GetPersistentEffects(effects);
int count;
foreach (SCR_PersistentDamageEffect effect : effects)
if (SCR_SalineDamageEffect.Cast(effect)) count++;
return count;
}
protected int AttachedBags()
{
if (!m_SalineStorage) return 0;
array<IEntity> items = {};
m_SalineStorage.GetAll(items);
return items.Count();
}
protected void Trace(string stage)
{
if (!m_Blood || !m_Controller || !m_Damage) return;
PrintFormat("ERR MEDICAL STATE: stage=%1 blood=%2 bloodState=%3 unconscious=%4 bleeding=%5 salineEffects=%6 attachedBags=%7", stage, m_Blood.GetHealth(), m_Blood.GetDamageState(), m_Controller.IsUnconscious(), m_Damage.IsBleeding(), SalineEffects(), AttachedBags());
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
// Items first, then characters; native scheduled saline deletion is null-safe.
for (int i = m_Owned.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_Owned[i]);
m_Owned.Clear();
m_Casualty = null;
m_Healer = null;
m_Dressing = null;
m_Saline = null;
m_SpareSaline = null;
m_Damage = null;
m_Controller = null;
m_Blood = null;
m_SalineStorage = null;
m_Inventory = null;
}
int Tick(float now)
{
if (m_Result != 0) return m_Result;
if (m_Stopped) return -1;
if (!Replication.IsServer() || !m_Mode || !m_Mode.Active())
{
Check(false, "fixture requires the active authority");
return -1;
}
if (m_Deadline > 0 && now > m_Deadline)
{
Check(false, "native medical state timed out at step " + m_Step.ToString());
return -1;
}
if (now < m_Wait) return 0;
if (m_Step == 0)
{
// Keep fixture actors separate from the real prisoner spawn.
vector anchor = m_Mode.PrisonPosition() + "100 0 100";
m_Casualty = Spawn(m_Mode.Factions().PrisonerPrefab, anchor);
m_Healer = Spawn(m_Mode.Factions().PrisonerPrefab, anchor + "3 0 0");
m_Dressing = Spawn("{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et", anchor + "4 0 0");
m_Saline = Spawn("{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et", anchor + "5 0 0");
m_SpareSaline = Spawn("{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et", anchor + "6 0 0");
if (!Check(m_Casualty && m_Healer && m_Dressing && m_Saline && m_SpareSaline, "two isolated characters and finite stock-type medicine spawned")) return -1;
if (!Check(!ERR_Actors.Player(m_Casualty) && !ERR_Actors.Player(m_Healer), "neither medical actor is a real player")) return -1;
m_Deadline = now + 120000;
m_Wait = now + 1000;
m_Step = 1;
return 0;
}
if (!ERR_Actors.Alive(m_Casualty) || !ERR_Actors.Alive(m_Healer))
{
Check(false, "owned casualty or healer died unexpectedly");
return -1;
}
if (m_Step == 1)
{
m_Damage = SCR_CharacterDamageManagerComponent.Cast(m_Casualty.FindComponent(SCR_CharacterDamageManagerComponent));
m_Controller = CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent));
m_SalineStorage = SCR_SalineStorageComponent.Cast(m_Casualty.FindComponent(SCR_SalineStorageComponent));
m_Inventory = SCR_InventoryStorageManagerComponent.Cast(m_Healer.FindComponent(SCR_InventoryStorageManagerComponent));
if (!Check(m_Damage && m_Controller && m_SalineStorage && m_Inventory, "native health, controller and medical storage components available")) return -1;
m_Blood = m_Damage.GetBloodHitZone();
if (!Check(m_Blood && !m_Controller.IsUnconscious() && !m_Damage.IsBleeding(), "casualty begins healthy and conscious")) return -1;
if (!Check(m_Inventory.TryInsertItem(m_Dressing) && m_Inventory.TryInsertItem(m_Saline) && m_Inventory.TryInsertItem(m_SpareSaline), "finite medical items enter healer inventory")) return -1;
SCR_ConsumableItemComponent dressing = SCR_ConsumableItemComponent.Cast(m_Dressing.FindComponent(SCR_ConsumableItemComponent));
SCR_ConsumableItemComponent saline = SCR_ConsumableItemComponent.Cast(m_Saline.FindComponent(SCR_ConsumableItemComponent));
if (!Check(dressing && saline, "stock items contain actual native consumable components")) return -1;
SCR_ConsumableBandage bandageEffect = SCR_ConsumableBandage.Cast(dressing.GetConsumableEffect());
SCR_ConsumableSalineBag salineEffect = SCR_ConsumableSalineBag.Cast(saline.GetConsumableEffect());
SCR_EConsumableFailReason reason;
if (!Check(bandageEffect && salineEffect && !bandageEffect.CanApplyEffect(m_Casualty, m_Healer, reason) && !salineEffect.CanApplyEffect(m_Casualty, m_Healer, reason), "healthy casualty rejects bandage and saline")) return -1;
array<HitZone> zones = {};
m_Damage.GetHitZonesOfGroupFromOwner(ECharacterHitZoneGroup.LEFTARM, zones);
SCR_CharacterHitZone wound;
foreach (HitZone zone : zones)
{
SCR_CharacterHitZone candidate = SCR_CharacterHitZone.Cast(zone);
if (candidate && candidate.GetMaxBleedingRate() > 0) { wound = candidate; break; }
}
if (!Check(wound != null, "native left-arm hit zone can bleed")) return -1;
wound.SetHealthScaled(0.5);
m_Damage.AddBleedingEffectOnHitZone(wound);
float threshold = m_Blood.GetDamageStateThreshold(ECharacterBloodState.UNCONSCIOUS) * m_Blood.GetMaxHealth();
float destroyed = m_Blood.GetDamageStateThreshold(ECharacterBloodState.DESTROYED) * m_Blood.GetMaxHealth();
if (!Check(threshold - destroyed > 200, "native blood thresholds permit a survivable recovery fixture")) return -1;
// Initial casualty condition only. All subsequent blood changes are native effects.
m_Blood.SetHealth(threshold - 100);
m_SeedBlood = m_Blood.GetHealth();
m_Wait = now + 5000;
m_Step = 2;
return 0;
}
if (m_Step == 2)
{
Trace("untreated blood loss");
if (!Check(m_Damage.IsBleeding() && m_Blood.GetHealth() < m_SeedBlood && m_Controller.IsUnconscious(), "native bleeding reduces real blood and causes unconsciousness")) return -1;
SCR_ConsumableItemComponent dressing = SCR_ConsumableItemComponent.Cast(m_Dressing.FindComponent(SCR_ConsumableItemComponent));
SCR_ConsumableBandage effect = SCR_ConsumableBandage.Cast(dressing.GetConsumableEffect());
SCR_EConsumableFailReason reason;
if (!Check(effect.CanApplyEffectToHZ(m_Casualty, m_Healer, ECharacterHitZoneGroup.LEFTARM, reason), "real dressing accepts bleeding arm")) return -1;
ItemUseParameters parameters = effect.GetAnimationParameters(m_Dressing, m_Casualty, ECharacterHitZoneGroup.LEFTARM);
if (!Check(parameters && effect.GetDeleteOnUse(), "native dressing parameters and finite consumption policy are valid")) return -1;
dressing.ApplyItemEffect(m_Casualty, m_Healer, parameters, m_Dressing, effect.GetDeleteOnUse());
m_Wait = now + 500;
m_Step = 3;
return 0;
}
if (m_Step == 3)
{
if (!Check(!m_Dressing && !m_Damage.IsBleeding() && m_Damage.GetGroupDamageOverTime(ECharacterHitZoneGroup.LEFTARM, EDamageType.BLEEDING) == 0, "dressing is consumed and native bleeding is stopped")) return -1;
if (!Check(m_Controller.IsUnconscious(), "bandage alone has not fabricated blood recovery")) return -1;
SCR_ConsumableItemComponent saline = SCR_ConsumableItemComponent.Cast(m_Saline.FindComponent(SCR_ConsumableItemComponent));
SCR_ConsumableSalineBag effect = SCR_ConsumableSalineBag.Cast(saline.GetConsumableEffect());
SCR_EConsumableFailReason reason;
if (!Check(effect && effect.CanApplyEffect(m_Casualty, m_Healer, reason) && !effect.GetDeleteOnUse(), "native saline accepts real blood loss and retains bag for infusion")) return -1;
ItemUseParameters parameters = effect.GetAnimationParameters(m_Saline, m_Casualty);
if (!Check(parameters != null, "native saline item parameters available")) return -1;
m_PreInfusionBlood = m_Blood.GetHealth();
m_RegenSpeed = effect.GetItemRegenSpeed();
saline.ApplyItemEffect(m_Casualty, m_Healer, parameters, m_Saline, effect.GetDeleteOnUse());
m_InfusionStarted = now;
m_Wait = now + 1000;
m_Step = 4;
return 0;
}
if (m_Step == 4)
{
array<IEntity> attached = {};
m_SalineStorage.GetAll(attached);
if (!Check(m_Saline && attached.Count() == 1 && attached.Contains(m_Saline) && m_Damage.GetGroupSalineBagged(ECharacterHitZoneGroup.RIGHTARM) && SalineEffects() == 1, "real saline bag occupies native treatment slot with one healing effect")) return -1;
SCR_ConsumableItemComponent spare = SCR_ConsumableItemComponent.Cast(m_SpareSaline.FindComponent(SCR_ConsumableItemComponent));
SCR_EConsumableFailReason reason;
if (!Check(!spare.GetConsumableEffect().CanApplyEffect(m_Casualty, m_Healer, reason) && reason == SCR_EConsumableFailReason.ALREADY_APPLIED && m_SpareSaline, "occupied treatment slot rejects another bag without consuming spare")) return -1;
m_NextDiagnostic = now;
m_Step = 5;
}
if (m_Step == 5)
{
if (now >= m_NextDiagnostic) { Trace("native infusion"); m_NextDiagnostic = now + 10000; }
if (!m_Controller.IsUnconscious()) m_Recovered = true;
if (m_Saline || AttachedBags() != 0 || SalineEffects() != 0 || m_Damage.GetGroupSalineBagged(ECharacterHitZoneGroup.RIGHTARM)) return 0;
float requiredGain = Math.Min(m_RegenSpeed * 60, (m_Blood.GetMaxHealth() - m_PreInfusionBlood) * 0.8);
if (!Check(m_RegenSpeed > 0 && m_Blood.GetHealth() - m_PreInfusionBlood >= requiredGain, "native infusion restores substantial blood without setting health again")) return -1;
if (!Check(m_Recovered && !m_Controller.IsUnconscious() && ERR_Actors.CanFight(m_Casualty), "casualty regains consciousness through native health recovery")) return -1;
if (!Check(now - m_InfusionStarted >= 85000 && !m_Saline && AttachedBags() == 0 && SalineEffects() == 0, "finite saline expires after native duration and releases treatment state")) return -1;
if (!Check(m_SpareSaline != null, "unused spare survives the completed infusion")) return -1;
InventoryItemComponent spareItem = InventoryItemComponent.Cast(m_SpareSaline.FindComponent(InventoryItemComponent));
if (!Check(spareItem && spareItem.GetParentSlot(), "rejected spare bag remains a physical inventory item")) return -1;
m_Result = 1;
Stop();
Print("ERR MEDICAL RESULT: native blood loss, finite treatment and recovery; not manual action or multiplayer acceptance");
return 1;
}
return 0;
}
}
#endif
@@ -0,0 +1,183 @@
#ifdef WORKBENCH
// Explicit disposable-world fixture: destroy all four authored radio documents,
// then move the real participant to the station. No replacement clues or actors
// are used to grant discovery; the normal site tick must identify the station.
class ERR_NativeObjectiveLossProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Player;
protected ERR_SiteRuntime m_Radio;
protected ref array<IEntity> m_Documents = {};
protected IEntity m_NonParticipant;
protected IEntity m_Blocker;
protected bool m_Partial;
protected bool m_SettingSaved;
protected int m_RevealChance;
protected int m_Step;
protected int m_Identifications;
protected float m_Wait;
protected float m_Deadline;
void ERR_NativeObjectiveLossProbe(ERR_GameMode mode, IEntity player, bool partial)
{
m_Mode = mode; m_Player = player; m_Partial = partial;
m_Mode.OnLocationDiscovered().Insert(Discovered);
}
protected void Discovered(ERR_LocationComponent location, int previous, int current)
{
if (m_Radio && location == m_Radio.Location && current == 2) m_Identifications++;
}
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: objective-loss " + detail); return true; }
Print("ERR SMOKE FAIL: objective-loss " + detail, LogLevel.ERROR);
Stop();
return false;
}
protected void Move(vector position)
{
vector transform[4];
m_Player.GetWorldTransform(transform);
transform[3] = ERR_Actors.Ground(position);
ChimeraCharacter.Cast(m_Player).Teleport(transform);
}
protected void CollectDocuments(out array<IEntity> documents)
{
documents.Clear();
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
array<IEntity> stock = {};
foreach (ERR_LocationComponent location : locations)
{
ERR_SiteRuntime site = m_Mode.Sites().Find(location.Id());
if (!site || !site.Container) continue;
ERR_Inventory.AppendUnique(InventoryStorageManagerComponent.Cast(site.Container.FindComponent(InventoryStorageManagerComponent)), stock);
}
ERR_Inventory.AppendUnique(InventoryStorageManagerComponent.Cast(m_Player.FindComponent(InventoryStorageManagerComponent)), stock);
foreach (IEntity item : stock)
{
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(item.FindComponent(ERR_DocumentComponent));
if (document && document.TargetId() == m_Radio.Location.Id()) documents.Insert(item);
}
}
void Stop()
{
if (m_Mode)
{
m_Mode.OnLocationDiscovered().Remove(Discovered);
if (m_SettingSaved) m_Mode.Settings().Set("intelRevealChancePercent", m_RevealChance);
}
m_SettingSaved = false;
ERR_Actors.DeleteSpawned(m_NonParticipant);
ERR_Actors.DeleteSpawned(m_Blocker);
}
int Tick(float now)
{
if (m_Deadline == 0) m_Deadline = now + 60000;
if (now > m_Deadline || !m_Mode.Active() || !ERR_Actors.CanFight(m_Player))
{
Check(false, "fixture lost its active conscious participant or exceeded its deadline");
return -1;
}
if (now < m_Wait) return 0;
if (m_Step == 0)
{
m_Radio = m_Mode.Sites().Find("everon-radio-1");
if (!Check(m_Radio && m_Radio.Location && m_Radio.Location.Knowledge() == 0 && m_Radio.Definition.DiscoverRadius == 8, "authored station is initially unknown with bounded eight-metre discovery")) return -1;
CollectDocuments(m_Documents);
if (!Check(m_Documents.Count() == 4, "four distinct physical documents exist before deliberate loss")) return -1;
if (!Check(m_Mode.UnlockPrison(m_Player) && m_Mode.Phase() == ERR_Phase.RADIO_SEARCH, "real prisoner enters radio search through ordinary mission interface")) return -1;
if (m_Partial)
{
m_RevealChance = m_Mode.Settings().Get("intelRevealChancePercent");
m_SettingSaved = true;
if (!Check(m_Mode.Settings().Set("intelRevealChancePercent", 0), "isolated first document produces only partial knowledge")) return -1;
ERR_SiteRuntime cache = m_Mode.Sites().Find("everon-supply-1");
Move(cache.Container.GetOrigin() + "0 0 2");
InventoryStorageManagerComponent source = InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent));
SCR_InventoryStorageManagerComponent target = SCR_InventoryStorageManagerComponent.Cast(m_Player.FindComponent(SCR_InventoryStorageManagerComponent));
array<IEntity> stock = {};
ERR_Inventory.Items(source, stock);
IEntity selected;
foreach (IEntity item : stock) if (m_Documents.Contains(item)) { selected = item; break; }
BaseInventoryStorageComponent destination;
if (selected && target) destination = target.FindStorageForItem(selected);
if (!Check(destination && source.TryMoveItemToStorage(selected, destination), "one real document transfer accepted before the remaining clues are lost")) return -1;
m_Wait = now + 8000;
}
m_Step = 1;
return 0;
}
if (m_Step == 1)
{
int expected;
if (m_Partial) expected = 1;
if (!Check(m_Radio.Location.Knowledge() == expected, "ordinary clue processing leaves the requested unknown or partial station state")) return -1;
foreach (IEntity document : m_Documents) if (document) SCR_EntityHelper.DeleteEntityAndChildren(document);
m_Step = 2;
m_Wait = now + 1500;
return 0;
}
if (m_Step == 2)
{
foreach (IEntity removed : m_Documents)
if (!Check(!removed, "each original physical document is deleted")) return -1;
array<IEntity> remaining = {};
CollectDocuments(remaining);
if (!Check(remaining.IsEmpty() && m_Mode.Sites().Prepare() == ERR_PrepareResult.READY, "all targeted documents are absent and preparation does not replenish them")) return -1;
Move(m_Radio.Location.Position() + "0 0 20");
m_NonParticipant = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, ERR_Actors.Ground(m_Radio.Location.Position() + "3 0 0"));
if (!Check(m_NonParticipant != null, "disposable unpossessed character created for authorization rejection")) return -1;
m_Mode.Own(m_NonParticipant);
if (!Check(!m_Mode.Sites().CanObserve(m_NonParticipant, m_Radio), "unpossessed character cannot identify the station")) return -1;
m_Step = 3;
m_Wait = now + 6000;
return 0;
}
if (m_Step == 3)
{
int expected;
if (m_Partial) expected = 1;
if (!Check(m_Radio.Location.Knowledge() == expected && !m_Mode.Sites().CanObserve(m_Player, m_Radio), "distant participant and nearby nonparticipant do not reveal the station")) return -1;
ERR_Actors.DeleteSpawned(m_NonParticipant);
m_Blocker = ERR_Actors.Spawn("{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et", ERR_Actors.Ground(m_Radio.Location.Position() + "-1.5 0 2"));
if (!Check(m_Blocker != null, "physical obstruction created before close approach")) return -1;
m_Mode.Own(m_Blocker);
Move(m_Radio.Location.Position() + "0 0 4");
m_Step = 4;
m_Wait = now + 6000;
return 0;
}
if (m_Step == 4)
{
int expected;
if (m_Partial) expected = 1;
PrintFormat("ERR OBJECTIVE LOSS: player=%1 radio=%2 knowledge=%3 visible=%4", m_Player.GetOrigin(), m_Radio.Location.Position(), m_Radio.Location.Knowledge(), m_Mode.Sites().CanObserve(m_Player, m_Radio));
if (!Check(m_Radio.Location.Knowledge() == expected && !m_Mode.Sites().CanObserve(m_Player, m_Radio), "a nearby station behind a wall remains unidentified")) return -1;
ERR_Actors.DeleteSpawned(m_Blocker);
m_Step = 5;
m_Wait = now + 6000;
return 0;
}
if (m_Step == 5)
{
TraceParam sight = new TraceParam();
sight.Start = m_Player.GetOrigin() + "0 1.5 0";
sight.End = m_Radio.Location.Position() + "0 1.3 0";
sight.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
sight.LayerMask = TRACE_LAYER_CAMERA;
sight.Exclude = m_Player;
float fraction = m_Mode.GetWorld().TraceMove(sight, null);
PrintFormat("ERR OBJECTIVE LOSS FINAL: player=%1 radio=%2 knowledge=%3 identifications=%4 visible=%5", m_Player.GetOrigin(), m_Radio.Location.Position(), m_Radio.Location.Knowledge(), m_Identifications, m_Mode.Sites().CanObserve(m_Player, m_Radio));
PrintFormat("ERR OBJECTIVE LOSS SIGHT: fraction=%1 hit=%2 blockerRemaining=%3", fraction, sight.TraceEnt, m_Blocker);
if (!Check(m_Mode.Sites().CanObserve(m_Player, m_Radio) && m_Radio.Location.Knowledge() == 2 && m_Identifications == 1, "actual participant with clear close approach identifies the station once through normal discovery")) return -1;
array<IEntity> remaining = {};
CollectDocuments(remaining);
if (!Check(remaining.IsEmpty(), "physical discovery creates no replacement documents")) return -1;
if (!Check(m_Mode.RadioDefence().CanBegin(m_Player, m_Radio.Location) && m_Mode.TuneRadio(m_Player, m_Radio.Location) && m_Mode.Phase() == ERR_Phase.RADIO_DEFENCE, "identified station permits ordinary authorized tuning after complete document loss")) return -1;
Stop();
return 1;
}
return 0;
}
}
#endif
@@ -0,0 +1,203 @@
#ifdef WORKBENCH
// Observe existing physical guards. No teleporting, spawned soldiers or forced
// completion: a full ordered loop and physical movement must happen natively.
class ERR_NativePatrolProbe
{
protected ERR_GameMode m_Mode;
protected string m_SiteId;
protected ERR_SiteRuntime m_Site;
protected IEntity m_Actor;
protected vector m_Origin;
protected float m_Deadline;
protected int m_Completions;
protected int m_LastIndex = -1;
protected bool m_BadSequence;
protected bool m_Moved;
protected bool m_Started;
protected bool m_Prepared;
protected float m_AcquireDeadline;
protected bool m_Finished;
protected int m_LastQueueSample = -1;
protected ref map<string, int> m_QueueBounds = new map<string, int>();
void ERR_NativePatrolProbe(ERR_GameMode mode, string siteId = "everon-outpost-1") { m_Mode = mode; m_SiteId = siteId; }
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: patrol " + detail); return true; }
Print("ERR SMOKE FAIL: patrol " + detail, LogLevel.ERROR);
return false;
}
protected void Completed(AIWaypoint waypoint)
{
if (!m_Site) return;
int index = m_Site.PatrolWaypoints.Find(waypoint);
if (index < 0) return;
if (m_LastIndex >= 0 && index != (m_LastIndex + 1) % m_Site.PatrolWaypoints.Count()) m_BadSequence = true;
m_LastIndex = index;
m_Completions++;
PrintFormat("ERR PATROL COMPLETED: site=%1 index=%2 count=%3", m_Site.Definition.Id, index, m_Completions);
}
protected bool DefinitionChecks()
{
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"));
if (!Check(definition && definition.Valid(), "authored definition resolves")) return false;
ERR_SiteDefinition site = definition.Find("everon-outpost-1");
if (!Check(site && site.Patrol && site.Patrol.Count() == 4, "four ordered outpost points authored")) return false;
ref array<ref ERR_SitePatrolPoint> savedRoute = site.Patrol;
site.Patrol = new array<ref ERR_SitePatrolPoint>();
site.Patrol.Insert(savedRoute[0]);
if (!Check(!definition.Valid(), "single-point route rejected")) return false;
for (int extra = 1; extra < 7; extra++) site.Patrol.Insert(savedRoute[extra % savedRoute.Count()]);
if (!Check(!definition.Valid(), "over-six-point route rejected")) return false;
site.Patrol.Clear();
site.Patrol.Insert(null);
site.Patrol.Insert(savedRoute[1]);
if (!Check(!definition.Valid(), "missing point rejected")) return false;
site.Patrol = savedRoute;
vector saved = site.Patrol[0].Offset;
site.Patrol[0].Offset = "61 0 0";
if (!Check(!definition.Valid(), "unbounded route rejected")) return false;
site.Patrol[0].Offset = "0 1 0";
if (!Check(!definition.Valid(), "floating route rejected")) return false;
site.Patrol[0].Offset = site.Patrol[1].Offset;
if (!Check(!definition.Valid(), "duplicate route points rejected")) return false;
site.Patrol[0].Offset = saved;
site.Guarded = false;
if (!Check(!definition.Valid(), "route without a physical guard rejected")) return false;
site.Guarded = true;
ERR_SiteDefinition prison = definition.Find("everon-prison-1");
prison.Patrol = site.Patrol;
if (!Check(!definition.Valid(), "prison patrol rejected; gate guards remain stationary")) return false;
prison.Patrol = null;
return Check(definition.Valid(), "isolated authoring mutations restored");
}
protected bool PreparedRoute(string id, bool deferDuplicateFailure = false)
{
ERR_SiteRuntime site = m_Mode.Sites().Find(id);
if (!Check(site && site.Group && site.PatrolCycle && site.PatrolWaypoints.Count() == 4, id + " existing group has one cycle and four native moves")) return false;
array<AIWaypoint> route = {};
site.PatrolCycle.GetWaypoints(route);
if (!Check(route.Count() == 4 && site.PatrolCycle.GetRerunCounter() == -1 && m_Mode.Sites().Owns(site.PatrolCycle), id + " finite owner holds repeating cycle")) return false;
for (int i = 0; i < route.Count(); i++)
{
vector expected = site.Definition.Position + site.Definition.GuardOffset + site.Definition.Patrol[i].Offset;
if (!Check(route[i] == site.PatrolWaypoints[i] && m_Mode.Sites().Owns(route[i]) && vector.DistanceXZ(route[i].GetOrigin(), expected) < 0.1, id + " authored point " + i.ToString())) return false;
}
array<AIWaypoint> queued = {};
site.Group.GetWaypoints(queued);
PrintFormat("ERR PATROL QUEUE: site=%1 count=%2 current=%3", id, queued.Count(), site.Group.GetCurrentWaypoint());
for (int diagnosticIndex = 0; diagnosticIndex < Math.Min(queued.Count(), 20); diagnosticIndex++)
{
AIWaypoint entry = queued[diagnosticIndex];
PrintFormat("ERR PATROL QUEUE ENTRY: site=%1 index=%2 point=%3 cycle=%4 current=%5 entity=%6", id, diagnosticIndex, site.PatrolWaypoints.Find(entry), entry == site.PatrolCycle, entry == site.Group.GetCurrentWaypoint(), entry);
}
// Diagnostic only: retain duplicate rejection at final acceptance, but first
// observe whether native completion events execute the apparent repetitions.
// GetWaypoints has no documented raw-queue/expanded-cycle distinction.
int repeated;
for (int j = 0; j < queued.Count(); j++)
{
if (!Check(queued[j] && (queued[j] == site.PatrolCycle || site.PatrolWaypoints.Contains(queued[j])), id + " queue contains only owned patrol references")) return false;
for (int k = 0; k < j; k++) if (queued[j] == queued[k]) repeated++;
}
int currentIndex = site.PatrolWaypoints.Find(site.Group.GetCurrentWaypoint());
string stateKey = id + ":" + currentIndex.ToString();
if (m_QueueBounds.Contains(stateKey))
{
if (!Check(queued.Count() <= m_QueueBounds.Get(stateKey), id + " queue does not grow at the same current route point")) return false;
}
else m_QueueBounds.Insert(stateKey, queued.Count());
PrintFormat("ERR PATROL QUEUE DIAGNOSTIC: site=%1 repeatedPairs=%2 deferred=%3 completions=%4", id, repeated, deferDuplicateFailure, m_Completions);
if (!deferDuplicateFailure && !Check(repeated == 0, id + " no duplicate queue references")) return false;
return true;
}
int Tick(float now)
{
if (m_Finished) return 1;
if (!m_Started)
{
if (!m_Prepared)
{
if (!DefinitionChecks() || !PreparedRoute("everon-supply-2", true) || !PreparedRoute("everon-outpost-1", true)) return -1;
ERR_SiteRuntime prison = m_Mode.Sites().Find("everon-prison-1");
if (!Check(prison && prison.Group && !prison.PatrolCycle && prison.PatrolWaypoints.IsEmpty(), "prison guards retain stationary orders")) return -1;
m_Site = m_Mode.Sites().Find(m_SiteId);
if (!Check(m_Site && m_Site.Group && m_Site.InitialGuardCount == 6, "selected native guard profile requests six physical members")) return -1;
m_AcquireDeadline = now + 15000;
m_Prepared = true;
}
if (!m_Site || !m_Site.Group) { Check(false, "original guard group disappeared before observation"); return -1; }
array<AIAgent> agents = {};
m_Site.Group.GetAgents(agents);
array<IEntity> ready = {};
foreach (AIAgent agent : agents)
{
if (!agent) continue;
IEntity actor = agent.GetControlledEntity();
if (ERR_Actors.CanFight(actor) && !ready.Contains(actor)) ready.Insert(actor);
}
if (ready.Count() > m_Site.InitialGuardCount) { Check(false, "original group contains excess living physical guards"); return -1; }
if (ready.Count() < m_Site.InitialGuardCount)
{
if (now < m_AcquireDeadline) return 0;
PrintFormat("ERR PATROL ACTOR TIMEOUT: site=%1 agents=%2 requested=%3 nativeComplete=%4", m_SiteId, agents.Count(), m_Site.InitialGuardCount, m_Site.Group.IsExpandComplete());
Check(false, "existing living physical guard unavailable after bounded readiness wait");
return -1;
}
m_Actor = ready[0];
if (!Check(ready.Count() == 6, "six distinct original living physical guards available")) return -1;
m_Origin = m_Actor.GetOrigin();
m_Site.Group.GetOnWaypointCompleted().Insert(Completed);
m_Deadline = now + 180000;
m_Started = true;
return 0;
}
if (m_Actor && vector.DistanceXZ(m_Origin, m_Actor.GetOrigin()) >= 8) m_Moved = true;
if (m_BadSequence) { Stop(); Check(false, "native route completion order"); return -1; }
if (m_Completions != m_LastQueueSample)
{
m_LastQueueSample = m_Completions;
if (!PreparedRoute(m_SiteId, true)) { Stop(); return -1; }
}
// Five completions prove traversal across a cycle boundary, from any starting point.
if (m_Completions >= 5 && m_Moved)
{
Stop();
if (!Check(true, "existing physical guard moved at least 8m and five native moves completed in authored cyclic order")) return -1;
if (!PreparedRoute(m_SiteId)) return -1;
m_Finished = true;
return 1;
}
if (now >= m_Deadline)
{
PrintFormat("ERR PATROL TIMEOUT: completions=%1 moved=%2 actor=%3 current=%4", m_Completions, m_Moved, m_Actor, m_Site.Group.GetCurrentWaypoint());
Stop(); Check(false, "native route traversal completed within 180s"); return -1;
}
return 0;
}
// Run last in a disposable mission: deliberately stops both route owners.
static bool StopChecks(ERR_GameMode mode)
{
ERR_SiteRuntime site = mode.Sites().Find("everon-outpost-1");
if (!site || !site.Group || !site.PatrolCycle) return false;
AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), ERR_Actors.Ground(site.Definition.Position + "40 0 0")));
if (!search) return false;
mode.Own(search);
site.Group.AddWaypointAt(search, 0);
mode.Sites().Stop();
mode.Sites().Stop();
array<AIWaypoint> queued = {};
site.Group.GetWaypoints(queued);
bool valid = !site.PatrolCycle && site.PatrolWaypoints.IsEmpty() && queued.Contains(search) && mode.Sites().Prepare() == ERR_PrepareResult.FAILED;
site.Group.RemoveWaypoint(search);
ERR_Actors.DeleteSpawned(search);
if (valid) Print("ERR SMOKE PASS: patrol stop clears owned routes once and preserves foreign Search");
else Print("ERR SMOKE FAIL: patrol stop ownership", LogLevel.ERROR);
return valid;
}
void Stop()
{
if (m_Site && m_Site.Group) m_Site.Group.GetOnWaypointCompleted().Remove(Completed);
}
void ~ERR_NativePatrolProbe() { Stop(); }
}
#endif
@@ -0,0 +1,210 @@
#ifdef WORKBENCH
// One real controller plus eleven disposable physical characters. This probes
// native inventory and transfer compatibility, not twelve clients or UI pickup.
class ERR_NativePrisonerProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Player;
protected ref array<IEntity> m_Actors = {};
protected ref map<int, vector> m_Reservations = new map<int, vector>();
protected ref ERR_SpawnPlacement m_Placement = new ERR_SpawnPlacement();
protected ref map<string, int> m_Expected = new map<string, int>();
protected ERR_SiteRuntime m_Cache;
protected InventoryStorageManagerComponent m_Source;
protected SCR_InventoryStorageManagerComponent m_Inventory;
protected IEntity m_Document;
protected vector m_Original[4];
protected int m_Step;
protected int m_Checked;
protected float m_Wait;
void ERR_NativePrisonerProbe(ERR_GameMode mode, IEntity player)
{
m_Mode = mode; m_Player = player;
m_Expected.Set("Prefabs/Characters/HeadGear/Hat_Patrol_US_01/Hat_Patrol_US_01.et", 1);
m_Expected.Set("Prefabs/Characters/Uniforms/Jacket_US_BDU_rolledup.et", 1);
m_Expected.Set("Prefabs/Characters/Uniforms/Pants_US_BDU.et", 1);
m_Expected.Set("Prefabs/Characters/Footwear/CombatBoots_US_01.et", 1);
m_Expected.Set("Prefabs/Items/Equipment/Watches/Watch_SandY184A.et", 1);
m_Expected.Set("Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et", 2);
m_Expected.Set("Prefabs/Items/Medicine/Tourniquet_01/Tourniquet_US_01.et", 1);
m_Expected.Set("Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et", 1);
}
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: prisoner " + detail); return true; }
Print("ERR SMOKE FAIL: prisoner " + detail, LogLevel.ERROR);
Stop();
return false;
}
protected void Positions(string stage)
{
foreach (int index, IEntity actor : m_Actors)
{
if (!actor) continue;
vector lower, upper;
actor.GetWorldBounds(lower, upper);
PrintFormat("ERR PRISONER POSITION: stage=%1 index=%2 requested=%3 actual=%4 boundsMin=%5 boundsMax=%6", stage, index, m_Reservations.Get(index), actor.GetOrigin(), lower, upper);
}
}
protected bool Kit(IEntity actor, int index)
{
if (!Check(actor && actor.GetPrefabData() && actor.GetPrefabData().GetPrefabName() == "{3FF693715C9F8928}Prefabs/ERR/Prisoner.et", "character " + index.ToString() + " uses successor resource")) return false;
if (!Check(actor.FindComponent(SCR_CharacterDamageManagerComponent) && actor.FindComponent(CharacterControllerComponent) && actor.FindComponent(SCR_SalineStorageComponent) && actor.FindComponent(SCR_TourniquetStorageComponent) && actor.FindComponent(CompartmentAccessComponent), "character " + index.ToString() + " retains vanilla health, treatment and vehicle components")) return false;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
if (!Check(inventory != null, "character inventory exists")) return false;
array<IEntity> items = {};
inventory.GetItems(items);
map<string, int> counts = new map<string, int>();
int identityItems;
array<IEntity> seen = {};
foreach (IEntity item : items)
{
if (!item || !item.GetPrefabData()) return Check(false, "inventory item has no resource identity");
ResourceName resource = item.GetPrefabData().GetPrefabName();
PrintFormat("ERR PRISONER ITEM: actor=%1 item=%2 resource=%3 duplicateReference=%4", index, item, resource, seen.Contains(item));
if (seen.Contains(item)) continue;
seen.Insert(item);
// Vanilla creates identification on possession, death or unconsciousness.
// Preserve this native identity record; it is not mission navigation equipment.
if (resource.EndsWith("Prefabs/Items/PersonalBelongings/PersonalBelongings_US.et"))
{
if (!Check(ERR_Actors.Player(actor) && item.FindComponent(SCR_IdentityInventoryItemComponent), "possessed prisoner retains native identification")) return false;
identityItems++;
continue;
}
// A non-equipment belt is authored by the rolled-up vanilla jacket itself.
if (resource.EndsWith("Prefabs/Characters/Vests/Vest_ALICE/Vest_ALICE_belt_dummy.et")) continue;
bool allowed = false;
foreach (string path, int expected : m_Expected)
{
if (!resource.EndsWith(path)) continue;
counts.Set(path, counts.Get(path) + 1);
allowed = true;
break;
}
if (!allowed) return Check(false, "unexpected starter inventory resource: " + resource);
}
int expectedIdentity;
if (ERR_Actors.Player(actor)) expectedIdentity = 1;
if (!Check(identityItems == expectedIdentity, "native identification is created exactly once for the possessed prisoner")) return false;
foreach (string path, int expected : m_Expected)
if (counts.Get(path) != expected) return Check(false, string.Format("character %1 expected %2 of %3, found %4", index, expected, path, counts.Get(path)));
PrintFormat("ERR PRISONER INVENTORY: character=%1 storedItems=%2", index, items.Count());
return Check(true, "character " + index.ToString() + " has exact clothing/watch/medical kit and no inherited navigation, radio, binoculars, tools, flares or weapons");
}
protected bool Contains(InventoryStorageManagerComponent inventory, IEntity item)
{
if (!inventory || !item) return false;
array<IEntity> items = {};
inventory.GetItems(items);
return items.Contains(item);
}
void Stop()
{
// Return borrowed stock when its source still exists; never spawn replacements.
if (m_Document && m_Cache && m_Cache.Container && m_Inventory && Contains(m_Inventory, m_Document))
{
BaseInventoryStorageComponent destination = BaseInventoryStorageComponent.Cast(m_Cache.Container.FindComponent(BaseInventoryStorageComponent));
if (destination) m_Inventory.TryMoveItemToStorage(m_Document, destination);
}
for (int i = 1; i < m_Actors.Count(); i++) ERR_Actors.DeleteSpawned(m_Actors[i]);
m_Reservations.Clear();
}
int Tick(float now)
{
if (now < m_Wait) return 0;
if (m_Step == 0)
{
if (!Check(m_Mode && m_Mode.Phase() == ERR_Phase.PRISON && ERR_Actors.Player(m_Player) && ERR_Actors.CanFight(m_Player), "fixture begins with a real conscious prisoner")) return -1;
m_Player.GetWorldTransform(m_Original);
m_Actors.Insert(m_Player);
m_Reservations.Set(0, m_Player.GetOrigin());
Positions("initial");
m_Step = 1;
m_Wait = now + 1000;
return 0;
}
if (m_Step == 1)
{
if (m_Actors.Count() < 12)
{
vector position;
bool available = m_Placement.Prison(m_Mode.PrisonPosition(), m_Reservations, position);
if (!available) Positions("no-clear-slot");
if (!Check(available, "bounded clear prison slot exists for next test character")) return -1;
IEntity actor = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, position);
if (!Check(actor != null, "physical test prisoner created")) return -1;
m_Mode.Own(actor);
m_Reservations.Set(m_Actors.Count(), position);
m_Actors.Insert(actor);
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
if (!Check(damage != null, "test prisoner damage component exists")) return -1;
damage.EnableDamageHandling(false);
m_Wait = now + 500;
return 0;
}
Positions("all-spawned");
m_Step = 2; m_Wait = now + 1500;
return 0;
}
if (m_Step == 2)
{
if (m_Checked == 0) Positions("settled");
if (!Kit(m_Actors[m_Checked], m_Checked)) return -1;
m_Checked++;
if (m_Checked < 12) return 0;
m_Cache = m_Mode.Sites().Find("everon-supply-1");
if (!Check(m_Cache && m_Cache.Container, "authored clue source remains available")) return -1;
m_Source = InventoryStorageManagerComponent.Cast(m_Cache.Container.FindComponent(InventoryStorageManagerComponent));
m_Inventory = SCR_InventoryStorageManagerComponent.Cast(m_Player.FindComponent(SCR_InventoryStorageManagerComponent));
if (!Check(m_Source && m_Inventory, "source and player inventory managers are usable")) return -1;
array<IEntity> stock = {};
m_Source.GetItems(stock);
foreach (IEntity item : stock)
{
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(item.FindComponent(ERR_DocumentComponent));
if (document && document.TargetId() == "everon-radio-1") { m_Document = item; break; }
}
if (!Check(m_Document && m_Inventory, "real finite radio document and player storage found")) return -1;
vector transform[4]; m_Player.GetWorldTransform(transform);
transform[3] = ERR_Actors.Ground(m_Cache.Container.GetOrigin() + "0 0 2");
ChimeraCharacter.Cast(m_Player).Teleport(transform);
m_Step = 3; m_Wait = now + 1000;
return 0;
}
if (m_Step == 3)
{
BaseInventoryStorageComponent destination = m_Inventory.FindStorageForItem(m_Document);
if (!Check(destination && m_Source.TryMoveItemToStorage(m_Document, destination), "minimal starter clothing accepts a real authored clue transfer")) return -1;
m_Step = 4; m_Wait = now + 1000;
return 0;
}
if (m_Step == 4)
{
if (!Check(Contains(m_Inventory, m_Document) && !Contains(m_Source, m_Document) && m_Mode.Phase() == ERR_Phase.PRISON, "document physically moved without duplication while prison phase remains active")) return -1;
BaseInventoryStorageComponent destination = BaseInventoryStorageComponent.Cast(m_Cache.Container.FindComponent(BaseInventoryStorageComponent));
if (!Check(destination && m_Inventory.TryMoveItemToStorage(m_Document, destination), "borrowed document returns to finite source")) return -1;
m_Step = 5; m_Wait = now + 1000;
return 0;
}
if (m_Step == 5)
{
if (!Check(Contains(m_Source, m_Document) && !Contains(m_Inventory, m_Document), "borrowed clue restored exactly once")) return -1;
Stop();
ChimeraCharacter.Cast(m_Player).Teleport(m_Original);
m_Step = 6; m_Wait = now + 1000;
return 0;
}
if (m_Step == 6)
{
for (int i = 1; i < m_Actors.Count(); i++)
if (!Check(!m_Actors[i], "disposable prisoner removed after native cleanup")) return -1;
ERR_PlayerLife life = m_Mode.Players().Get(GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_Player));
if (!Check(ERR_Actors.Player(m_Player) && ERR_Actors.Alive(m_Player) && life && life.UsedLife, "real prisoner and spent life preserved")) return -1;
m_Step = 7;
return 1;
}
return 0;
}
}
#endif
@@ -0,0 +1,96 @@
#ifdef WORKBENCH
class ERR_NativeRecoveryProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Player;
protected CharacterControllerComponent m_Controller;
protected float m_Wait;
protected float m_RecoveryDeadline;
protected bool m_Stopped;
protected bool m_ChangedConsciousness;
protected bool m_InitiallyUnconscious;
protected int m_Step;
protected int m_Changes;
void ERR_NativeRecoveryProbe(ERR_GameMode mode, IEntity player)
{
m_Mode = mode; m_Player = player;
m_Controller = CharacterControllerComponent.Cast(player.FindComponent(CharacterControllerComponent));
if (m_Controller) m_InitiallyUnconscious = m_Controller.IsUnconscious();
mode.Recovery().OnChanged().Insert(Changed);
mode.OnStop().Insert(Stop);
}
protected void Changed(string message) { m_Changes++; }
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (m_Mode) m_Mode.OnStop().Remove(Stop);
if (m_Mode && m_Mode.Recovery()) m_Mode.Recovery().OnChanged().Remove(Changed);
if (m_ChangedConsciousness && m_Controller && ERR_Actors.Alive(m_Player)) m_Controller.SetUnconscious(m_InitiallyUnconscious);
}
protected bool Check(bool ok, string detail)
{
if (ok) { Print("ERR SMOKE PASS: recovery " + detail); return true; }
Stop();
Print("ERR SMOKE FAIL: recovery " + detail, LogLevel.ERROR);
return false;
}
int Tick(float now)
{
if (m_Stopped) return -1;
if (now < m_Wait) return 0;
if (m_Step == 0)
{
m_Mode.Recovery().Tick();
if (!Check(m_Controller && m_Mode.Recovery().Conscious() == 1 && m_Mode.RecoveryStatus().IsEmpty(), "real conscious prisoner is available without recovery warning")) return -1;
array<string> replies = {};
m_Mode.Admin().Execute(m_Mode, false, "mission abandon", replies);
if (!Check(replies.Count() == 1 && replies[0].Contains("Administrator") && m_Mode.Phase() == ERR_Phase.PRISON && m_Mode.EndReason().IsEmpty(), "unauthorized abandonment leaves round and result intact")) return -1;
m_Mode.Admin().Execute(m_Mode, true, "mission abandon now", replies);
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Mode.EndReason().IsEmpty(), "malformed abandonment does not end a round")) return -1;
m_ChangedConsciousness = true;
m_Controller.SetUnconscious(true);
m_Step = 1; m_Wait = now + 1000;
return 0;
}
if (m_Step == 1)
{
m_Mode.Recovery().Tick();
if (!Check(ERR_Actors.Alive(m_Player) && !ERR_Actors.CanFight(m_Player) && m_Mode.Recovery().Incapacitated() == 1 && m_Mode.Recovery().Conscious() == 0, "native unconscious prisoner is counted as incapacitated, not dead")) return -1;
if (!Check(m_Mode.RecoveryStatus().Contains("No conscious prisoner") && m_Mode.Status().Contains("mission abandon") && ERR_ObjectiveText.Description(m_Mode).Contains(m_Mode.RecoveryStatus()), "server status and native objective copy explain the recovery state")) return -1;
if (!Check(m_Changes == 1, "recovery state publishes exactly one change")) return -1;
m_Mode.Recovery().Tick();
if (!Check(m_Changes == 1, "unchanged sampling emits no duplicate recovery event")) return -1;
m_Step = 2; m_Wait = now + 35000;
return 0;
}
if (m_Step == 2)
{
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && ERR_Actors.Alive(m_Player) && !ERR_Actors.CanFight(m_Player) && m_Mode.Players().HasSurvivor(), "all-incapacitated player survives beyond ordinary defeat grace")) return -1;
m_Controller.SetUnconscious(false);
m_RecoveryDeadline = now + 12000;
m_Step = 3; m_Wait = now + 1000;
return 0;
}
if (m_Step == 3)
{
if (!ERR_Actors.CanFight(m_Player) && now < m_RecoveryDeadline) return 0;
m_Mode.Recovery().Tick();
if (!Check(ERR_Actors.CanFight(m_Player) && m_Mode.RecoveryStatus().IsEmpty() && m_Mode.Recovery().Conscious() == 1 && m_Changes == 2, "native recovery clears the warning once")) return -1;
int id = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_Player);
ERR_PlayerLife life = m_Mode.Players().Get(id);
if (!Check(life && life.UsedLife && life.Actor == m_Player, "recovery preserves original actor and spent life")) return -1;
m_Controller.SetUnconscious(true);
m_Step = 4; m_Wait = now + 1000;
return 0;
}
if (m_Step == 4)
{
m_Mode.Recovery().Tick();
if (!Check(!m_Mode.RecoveryStatus().IsEmpty() && m_Changes == 3, "second incapacitation restores warning before abandonment")) return -1;
return 1;
}
return 0;
}
}
#endif
@@ -3,9 +3,40 @@
modded class SCR_BaseItemSupportStationComponent
{
protected static int s_ERRStationTeardownLines;
protected static int s_ERRStationInitLines;
protected bool m_ERRStationInitSeen;
protected bool m_ERRStationInitModePresent;
protected bool m_ERRStationInitMainWorld;
protected bool m_ERRStationInitSiteOwned;
protected bool m_ERRStationInitInInventory;
protected ResourceName m_ERRStationInitParentPrefab;
protected bool m_ERRStationInDelete;
protected IEntity m_ERRStationDeleteOwner;
protected override void DelayedInit(IEntity owner)
{
// Preserve initialization and every native validation before observation.
super.DelayedInit(owner);
string enabled;
if (!System.GetCLIParam("errStationTeardownDiag", enabled) || enabled != "1" || !owner) return;
m_ERRStationInitSeen = true;
ERR_GameMode mode = ERR_GameMode.Get();
m_ERRStationInitModePresent = mode != null;
if (GetGame()) m_ERRStationInitMainWorld = owner.GetWorld() == GetGame().GetWorld();
ERR_SiteService sites;
if (mode) sites = mode.Sites();
m_ERRStationInitSiteOwned = sites && sites.Owns(owner);
InventoryItemComponent item = InventoryItemComponent.Cast(owner.FindComponent(InventoryItemComponent));
m_ERRStationInitInInventory = item && item.GetParentSlot();
IEntity parent = owner.GetParent();
if (parent && parent.GetPrefabData()) m_ERRStationInitParentPrefab = parent.GetPrefabData().GetPrefabName();
if (s_ERRStationInitLines >= 64) return;
s_ERRStationInitLines++;
ResourceName prefab;
if (owner.GetPrefabData()) prefab = owner.GetPrefabData().GetPrefabName();
PrintFormat("ERR STATION INIT OWNERSHIP: n=%1 prefab=%2 modePresent=%3 mainWorld=%4 siteOwned=%5 inInventory=%6 parentPrefab=%7", s_ERRStationInitLines, prefab, m_ERRStationInitModePresent, m_ERRStationInitMainWorld, m_ERRStationInitSiteOwned, m_ERRStationInitInInventory, m_ERRStationInitParentPrefab);
}
override void OnDelete(IEntity owner)
{
string enabled;
@@ -51,6 +82,24 @@ modded class SCR_BaseItemSupportStationComponent
}
string detail = string.Format("ERR STATION TEARDOWN DIAG: n=%1 owner=%2 prefab=%3 cachedCatalog=%4 globalCatalog=%5", s_ERRStationTeardownLines, ownerName, prefab, m_EntityCatalogManager != null, catalog != null);
detail += string.Format(" modePresent=%1 gameState=%2 errPhase=%3 endData=%4 call=OnDelete/InitValidSetup", baseMode != null, gameState, phase, hasEndData);
detail += string.Format(" initSeen=%1 initMode=%2 initMainWorld=%3 initSiteOwned=%4 initInInventory=%5 initParentPrefab=%6", m_ERRStationInitSeen, m_ERRStationInitModePresent, m_ERRStationInitMainWorld, m_ERRStationInitSiteOwned, m_ERRStationInitInInventory, m_ERRStationInitParentPrefab);
bool deleteMainWorld;
bool deleteSiteOwned;
bool deleteInInventory;
ResourceName deleteParentPrefab;
if (owner)
{
if (GetGame()) deleteMainWorld = owner.GetWorld() == GetGame().GetWorld();
ERR_GameMode deleteMode = ERR_GameMode.Cast(baseMode);
ERR_SiteService deleteSites;
if (deleteMode) deleteSites = deleteMode.Sites();
deleteSiteOwned = deleteSites && deleteSites.Owns(owner);
InventoryItemComponent deleteItem = InventoryItemComponent.Cast(owner.FindComponent(InventoryItemComponent));
deleteInInventory = deleteItem && deleteItem.GetParentSlot();
IEntity deleteParent = owner.GetParent();
if (deleteParent && deleteParent.GetPrefabData()) deleteParentPrefab = deleteParent.GetPrefabData().GetPrefabName();
}
detail += string.Format(" deleteMainWorld=%1 deleteSiteOwned=%2 deleteInInventory=%3 deleteParentPrefab=%4", deleteMainWorld, deleteSiteOwned, deleteInInventory, deleteParentPrefab);
Print(detail);
if (s_ERRStationTeardownLines == 64) Print("ERR STATION TEARDOWN DIAG: 64-line diagnostic limit reached; vanilla validation and cleanup continue unchanged.");
}
@@ -11,9 +11,25 @@ class ERR_ObjectiveText
}
static string Extraction(float radius)
{
return string.Format("Reach the helicopter marked on your map. Board and fly beyond %1 kilometres from the prison. Living prisoners aboard escape, including unconscious passengers. Dead or absent prisoners do not.", radius / 1000.0);
return string.Format("The map marks the initial helicopter pickup site. Board the helicopter and fly beyond %1 kilometres from the prison. Living prisoners aboard escape, including unconscious passengers. Dead or absent prisoners do not.", radius / 1000.0);
}
static string RadioFeedbackMessage(ERR_RadioFeedback feedback)
{
if (feedback == ERR_RadioFeedback.WAITING)
return "Waiting for a conscious defender near the radio. Keep someone within 60 metres to start the transmission.";
if (feedback == ERR_RadioFeedback.TRANSMITTING)
return "Transmission in progress. Keep a conscious survivor within 60 metres of the radio. Leaving the area resets progress.";
if (feedback == ERR_RadioFeedback.INTERRUPTED)
return "Transmission interrupted. Progress has reset. Return a conscious survivor to within 60 metres of the radio to resume.";
return "Keep a conscious survivor within 60 metres of the radio until the transmission finishes. Leaving the area resets the defence timer.";
}
static string Description(ERR_GameMode mode, bool progress = true)
{
string description = PhaseDescription(mode, progress);
if (mode && mode.Active() && !mode.RecoveryStatus().IsEmpty()) description += "\n" + mode.RecoveryStatus();
return description;
}
protected static string PhaseDescription(ERR_GameMode mode, bool progress)
{
if (!mode) return "";
ERR_Phase phase = mode.Phase();
@@ -30,12 +46,12 @@ class ERR_ObjectiveText
mode.Locations(locations);
foreach (ERR_LocationComponent location : locations)
if (location.Kind() == ERR_LocationKind.RADIO && location.Knowledge() == 2)
return "The recovered documents identify the radio station on your map. Reach the station and tune its radio to start the transmission.";
return "Choose either marked supply cache. Each has equipment and enough radio documents to identify the station. If one cache is lost, use the other. Nearby medical and guard sites can be discovered by approaching with a clear view.";
return "The radio station is identified on your map. Reach the station and tune its radio to start the transmission.";
return "Choose either marked supply cache. Each has equipment and enough radio documents to identify the station. If one cache is lost, use the other. If every document is lost, a close approach with a clear view can identify the station itself. Nearby medical and guard sites can be discovered by approaching with a clear view.";
}
if (phase == ERR_Phase.RADIO_DEFENCE)
{
if (!progress) return "Keep a conscious survivor within 60 metres of the radio until the transmission finishes. Leaving the area resets the defence timer.";
if (!progress) return RadioFeedbackMessage(mode.RadioFeedback());
int seconds = mode.DefenceSecondsRemaining();
if (!mode.DefenceOccupied())
return string.Format("Transmission interrupted. Keep a conscious survivor within 60 metres of the radio for %1 seconds. Leaving resets progress.", seconds);
@@ -0,0 +1,114 @@
// Explicit local diagnostic launcher. Normal games and Workbench never request
// transitions from this file. No player, identity or audit state is fabricated.
#ifndef WORKBENCH
// Native Game is not Managed; this script-owned helper owns the callback.
class ERR_OrdinaryScenarioLaunchRequest : Managed
{
protected string m_Project;
protected ResourceName m_Header;
protected string m_Addon;
protected bool m_Queued;
protected bool m_Finished;
void Queue(string project, ResourceName header, string addon)
{
if (m_Queued || m_Finished) return;
m_Queued = true;
m_Project = project;
m_Header = header;
m_Addon = addon;
GetGame().GetCallqueue().CallLater(Attempt, 0, false);
}
void Cancel()
{
m_Finished = true;
if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(Attempt);
}
protected void Attempt()
{
if (m_Finished) return;
m_Finished = true;
if (!GameStateTransitions.IsInsideMainMenu() || ERR_GameMode.Get() || GameStateTransitions.IsTransitionRequestedOrInProgress())
{
Print("ERR ORDINARY: header launch refused outside idle main menu", LogLevel.ERROR);
return;
}
string addons;
if (!System.GetCLIParam("addons", addons) || addons != m_Addon)
{
Print("ERR ORDINARY: header launch addon selection mismatch", LogLevel.ERROR);
return;
}
ResourceName expected = m_Header;
MissionHeader header = MissionHeader.ReadMissionHeader(expected);
if (!header || header.GetWorldResourceName() != "{B6C516F86302C679}Worlds/ERR_Everon.ent" || header.GetWorldSystemsConfig() != "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf")
{
Print("ERR ORDINARY: diagnostic mission header is invalid", LogLevel.ERROR);
return;
}
PrintFormat("ERR ORDINARY: requesting header project=%1", m_Project);
if (!GameStateTransitions.RequestScenarioChangeTransition(expected, string.Empty, addons))
Print("ERR ORDINARY: native header transition refused", LogLevel.ERROR);
}
}
modded class ArmaReforgerScripted
{
protected ref ERR_OrdinaryScenarioLaunchRequest m_ERRDiagnosticLaunchRequest;
protected bool m_ERRDiagnosticLaunchAttempted;
protected string m_ERRDiagnosticProject;
protected ResourceName ERR_DiagnosticHeader(string project)
{
if (project == "core") return "{AD62322D5213C440}Missions/ERR_Everon_Base.conf";
return string.Empty;
}
protected string ERR_DiagnosticAddon(string project)
{
if (project == "core") return "2528466DE220D91C";
return string.Empty;
}
override bool OnGameStart()
{
bool started = super.OnGameStart();
if (!started || !System.GetCLIParam("errOrdinaryScenario", m_ERRDiagnosticProject)) return started;
ResourceName expected = ERR_DiagnosticHeader(m_ERRDiagnosticProject);
if (expected.IsEmpty())
{
Print("ERR ORDINARY: invalid diagnostic project", LogLevel.ERROR);
return started;
}
// A loaded successor is inspected only; never replace an active mission.
if (ERR_GameMode.Get())
{
MissionHeader active = GetMissionHeader();
ResourceName systems = "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf";
if (!active || active.GetHeaderResourceName() != expected || active.GetWorldSystemsConfig() != systems || GetSystemsConfig() != systems)
Print("ERR ORDINARY: active scenario header/systems mismatch", LogLevel.ERROR);
else
PrintFormat("ERR ORDINARY: header verified project=%1 systems=MissionSystems", m_ERRDiagnosticProject);
return started;
}
if (m_ERRDiagnosticLaunchAttempted) return started;
m_ERRDiagnosticLaunchAttempted = true;
// OnGameStart runs while the native state is finishing initialization. The
// one queued attempt requires that it actually reached the main menu.
m_ERRDiagnosticLaunchRequest = new ERR_OrdinaryScenarioLaunchRequest();
m_ERRDiagnosticLaunchRequest.Queue(m_ERRDiagnosticProject, expected, ERR_DiagnosticAddon(m_ERRDiagnosticProject));
return started;
}
override protected void OnBeforeWorldCleanup()
{
if (m_ERRDiagnosticLaunchRequest) m_ERRDiagnosticLaunchRequest.Cancel();
m_ERRDiagnosticLaunchRequest = null;
super.OnBeforeWorldCleanup();
}
}
#endif
@@ -49,6 +49,18 @@ class ERR_PlayerRoster
if (!m_Connections.Find(playerId, identity)) return null;
return m_Lives.Get(identity);
}
// The roster identity was bound by the audited connection lifecycle. Consumers
// validate its current body rather than requesting a second backend identity.
ERR_PlayerLife CurrentActor(IEntity actor)
{
if (!Replication.IsServer() || !actor || !GetGame() || !GetGame().GetPlayerManager()) return null;
PlayerManager manager = GetGame().GetPlayerManager();
int connection = manager.GetPlayerIdFromControlledEntity(actor);
if (connection <= 0 || manager.GetPlayerControlledEntity(connection) != actor) return null;
ERR_PlayerLife life = Get(connection);
if (!life || life.AccountId.IsNull() || !life.Connected || !life.UsedLife || life.ConnectionId != connection || life.Actor != actor) return null;
return life;
}
bool CanEnter(int playerId, int phase)
{
ERR_PlayerLife life = Get(playerId);
@@ -67,10 +67,7 @@ class ERR_RadioDefenceService
}
protected bool IsPlayer(IEntity actor)
{
if (!m_Mode || !ERR_Actors.Player(actor)) return false;
int id = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor);
ERR_PlayerLife life = m_Mode.Players().Get(id);
return life && life.Connected && life.UsedLife && life.Actor == actor;
return m_Mode && m_Mode.Players() && m_Mode.Players().CurrentActor(actor) != null;
}
protected bool Friendly(IEntity actor)
{
@@ -0,0 +1,19 @@
// Presentation only. The defence service remains the sole owner of progress.
enum ERR_RadioFeedback
{
INACTIVE,
WAITING,
TRANSMITTING,
INTERRUPTED
}
class ERR_RadioFeedbackRules
{
static ERR_RadioFeedback Next(ERR_RadioFeedback previous, bool defending, bool occupied)
{
if (!defending) return ERR_RadioFeedback.INACTIVE;
if (occupied) return ERR_RadioFeedback.TRANSMITTING;
if (previous == ERR_RadioFeedback.TRANSMITTING || previous == ERR_RadioFeedback.INTERRUPTED) return ERR_RadioFeedback.INTERRUPTED;
return ERR_RadioFeedback.WAITING;
}
}
@@ -0,0 +1,74 @@
// Observation only: unconsciousness never consumes a life or ends a round here.
class ERR_RecoveryRules
{
// 0 absent/dead, 1 reserved or awaiting handoff, 2 conscious, 3 incapacitated.
static int SurvivorKind(bool alive, bool connected, bool sameActor, bool canFight)
{
if (!alive) return 0;
if (!connected || !sameActor) return 1;
if (canFight) return 2;
return 3;
}
static bool NeedsRescuer(int conscious, int incapacitated)
{
return conscious == 0 && incapacitated > 0;
}
}
class ERR_RecoveryService
{
protected ERR_GameMode m_Mode;
protected bool m_Stopped;
protected int m_Conscious;
protected int m_Incapacitated;
protected int m_Reserved;
protected string m_Message;
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
ScriptInvoker OnChanged() { return m_OnChanged; }
int Conscious() { return m_Conscious; }
int Incapacitated() { return m_Incapacitated; }
int Reserved() { return m_Reserved; }
string Message() { return m_Message; }
void Tick()
{
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
int conscious, incapacitated, reserved;
array<ref ERR_PlayerLife> lives = {};
m_Mode.Players().Participants(lives);
foreach (ERR_PlayerLife life : lives)
{
IEntity controlled = null;
if (life.Connected) controlled = GetGame().GetPlayerManager().GetPlayerControlledEntity(life.ConnectionId);
int kind = ERR_RecoveryRules.SurvivorKind(ERR_Actors.Alive(life.Actor), life.Connected, controlled == life.Actor, ERR_Actors.CanFight(life.Actor));
if (kind == 1) reserved++;
else if (kind == 2) conscious++;
else if (kind == 3) incapacitated++;
}
m_Conscious = conscious;
m_Incapacitated = incapacitated;
m_Reserved = reserved;
string message;
if (ERR_RecoveryRules.NeedsRescuer(conscious, incapacitated))
{
message = "No conscious prisoner is currently available to help. Natural recovery and treatment can still continue.";
if (reserved > 0) message += " A surviving prisoner's reconnection may provide help.";
message += " An administrator can abandon this round with /esc mission abandon.";
}
Publish(message);
}
protected void Publish(string message)
{
if (m_Message == message) return;
m_Message = message;
m_OnChanged.Invoke(message);
}
void Stop()
{
if (!Replication.IsServer() || m_Stopped) return;
m_Stopped = true;
m_Conscious = 0; m_Incapacitated = 0; m_Reserved = 0;
Publish("");
}
}
@@ -3,6 +3,8 @@ class ERR_Resources
{
static ResourceName ObjectiveDocument() { return "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et"; }
static ResourceName SearchWaypoint() { return "{B3E7B8DC2BAB8ACC}Prefabs/AI/Waypoints/AIWaypoint_SearchAndDestroy.et"; }
static ResourceName MoveWaypoint() { return "{750A8D1695BD6998}Prefabs/AI/Waypoints/AIWaypoint_Move.et"; }
static ResourceName CycleWaypoint() { return "{35BD6541CBB8AC08}Prefabs/AI/Waypoints/AIWaypoint_Cycle.et"; }
static ResourceName DefendWaypoint() { return "{93291E72AC23930F}Prefabs/AI/Waypoints/AIWaypoint_Defend.et"; }
static ResourceName IntelFolder() { return "{6D56FED1E55A8F84}Prefabs/Items/Misc/IntelligenceFolder_E_01/IntelligenceFolder_E_01.et"; }
}
+23 -1
View File
@@ -8,7 +8,7 @@ class ERR_Settings
static array<string> Keys()
{
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier"};
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile"};
}
static bool Valid(string key, int value)
@@ -20,6 +20,25 @@ class ERR_Settings
if (key == "radioDefenceSeconds") return value >= 30 && value <= 1800;
if (key == "startTimeMinutes") return value >= 0 && value <= 1439;
if (key == "timeMultiplier") return value >= 1 && value <= 24;
if (key == "weatherProfile") return value >= 0 && value <= 3;
if (key == "startDateCode")
{
if (value == 0) return true;
// Compact (year-1900)MMDD stays exactly representable by JSON floats.
if (value < 101 || value > 1501231) return false;
int year = 1900 + value / 10000;
int month = (value % 10000) / 100;
int day = value % 100;
if (month < 1 || month > 12 || day < 1) return false;
int days = 31;
if (month == 4 || month == 6 || month == 9 || month == 11) days = 30;
if (month == 2)
{
days = 28;
if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) days = 29;
}
return day <= days;
}
return false;
}
@@ -27,6 +46,7 @@ class ERR_Settings
{
if (key == "startTimeMinutes") return 840;
if (key == "timeMultiplier") return 1;
if (key == "startDateCode" || key == "weatherProfile") return 0;
return -1;
}
@@ -41,6 +61,8 @@ class ERR_Settings
m_Values.Set("radioDefenceSeconds", 180);
m_Values.Set("startTimeMinutes", AddedDefault("startTimeMinutes"));
m_Values.Set("timeMultiplier", AddedDefault("timeMultiplier"));
m_Values.Set("startDateCode", AddedDefault("startDateCode"));
m_Values.Set("weatherProfile", AddedDefault("weatherProfile"));
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
map<string, int> loaded;
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Values = loaded;
@@ -34,13 +34,20 @@ class ERR_SiteTransport
float MaximumTerrainRange;
}
[BaseContainerProps()]
class ERR_SitePatrolPoint
{
[Attribute("0 0 0", UIWidgets.Coords, "Ground waypoint offset from the guard anchor")]
vector Offset;
}
[BaseContainerProps()]
class ERR_SiteDefinition
{
[Attribute()]
string Id;
[Attribute()] string Label;
[Attribute("0", UIWidgets.EditBox, "Conscious visual discovery radius for optional sites") ]
[Attribute("0", UIWidgets.EditBox, "Conscious visual discovery radius: radio up to 8m, optional sites up to 60m") ]
float DiscoverRadius;
[Attribute()] ref array<ref ERR_SiteProp> Props;
[Attribute()] ref ERR_SiteTransport Transport;
@@ -52,6 +59,7 @@ class ERR_SiteDefinition
vector Position;
[Attribute("", UIWidgets.ResourceNamePicker, "Optional physical guard group", "et")]
ResourceName Guards;
[Attribute()] ref array<ref ERR_SitePatrolPoint> Patrol;
[Attribute("0", UIWidgets.CheckBox, "Populate a finite squad from the faction catalog") ]
bool Guarded;
[Attribute("20 0 0", UIWidgets.Coords)]
@@ -92,9 +100,9 @@ class ERR_ScenarioDefinition
{
if (!site || site.Id.IsEmpty() || ids.Contains(site.Id) || site.Prefab.IsEmpty()) return false;
if (site.Label.IsEmpty() || site.Label.Length() > 64) return false;
if (site.DiscoverRadius < 0 || site.DiscoverRadius > 60) return false;
if (!ERR_SiteDiscoveryRules.ValidRadius(site.Kind, site.DiscoverRadius)) return false;
bool optionalSite = site.Kind == ERR_LocationKind.DEPOT || site.Kind == ERR_LocationKind.OUTPOST;
if (!optionalSite && (site.DiscoverRadius > 0 || site.RevealOnEscape)) return false;
if (!optionalSite && site.RevealOnEscape) return false;
if (site.Transport)
{
ERR_SiteTransport transport = site.Transport;
@@ -116,6 +124,18 @@ class ERR_ScenarioDefinition
if (site.Kind == ERR_LocationKind.RADIO) radios++;
if (site.Kind == ERR_LocationKind.EXTRACTION) extractions++;
if (site.Guarded || !site.Guards.IsEmpty()) groups++;
if (site.Patrol && !site.Patrol.IsEmpty())
{
// Native Move completes within 5m: separate every point by at least 12m.
if (!(site.Guarded || !site.Guards.IsEmpty()) || !optionalSite || site.Patrol.Count() < 2 || site.Patrol.Count() > 6) return false;
for (int pointIndex = 0; pointIndex < site.Patrol.Count(); pointIndex++)
{
ERR_SitePatrolPoint point = site.Patrol[pointIndex];
if (!point || point.Offset.Length() > 60 || Math.AbsFloat(point.Offset[1]) > 0.01) return false;
for (int previous = 0; previous < pointIndex; previous++)
if (vector.DistanceXZ(point.Offset, site.Patrol[previous].Offset) < 12) return false;
}
}
int documents = 0;
if (!site.Stock) continue;
foreach (ERR_SiteStock stock : site.Stock)
@@ -0,0 +1,17 @@
// Authoring bounds separate close identification of the objective radio from
// broader discovery of optional supply sites. Neither reveals a remote site.
class ERR_SiteDiscoveryRules
{
static float RadioRadiusLimit() { return 8; }
static bool ValidRadius(int kind, float radius)
{
if (radius < 0 || radius > 60) return false;
if (kind == ERR_LocationKind.RADIO) return radius <= RadioRadiusLimit();
if (kind == ERR_LocationKind.DEPOT || kind == ERR_LocationKind.OUTPOST) return true;
return (kind == ERR_LocationKind.PRISON || kind == ERR_LocationKind.EXTRACTION) && radius == 0;
}
static bool InRange(float radius, float horizontalDistance, float verticalDistance)
{
return radius > 0 && horizontalDistance >= 0 && horizontalDistance <= radius && verticalDistance >= 0 && verticalDistance <= 8;
}
}
+154 -13
View File
@@ -13,18 +13,26 @@ class ERR_SiteRuntime
ERR_LocationComponent Location;
IEntity Container;
SCR_AIGroup Group;
int InitialGuardCount;
int LastInitialPhysical = -1;
int LastInitialConscious = -1;
Vehicle Transport;
AIWaypointCycle PatrolCycle;
ref array<AIWaypoint> PatrolWaypoints = {};
}
// One owner for finite physical site contents. No population or stock refill tick.
class ERR_SiteService
{
protected ERR_GameMode m_Mode;
protected ref ERR_ExtractionPlacement m_ExtractionPlacement;
ERR_ExtractionPlacement ExtractionPlacement() { return m_ExtractionPlacement; }
protected ref array<ref ERR_SiteRuntime> m_Sites = {};
protected ref array<IEntity> m_Entities = {};
protected ref array<ref ERR_StockPlacement> m_Placements = {};
protected ERR_PrepareResult m_State = ERR_PrepareResult.PENDING;
protected float m_Deadline;
protected float m_GuardDeadline;
protected bool m_Begun;
protected bool m_Stopped;
protected bool m_Activated;
@@ -62,6 +70,49 @@ class ERR_SiteService
PrintFormat("Rereintroduced [sites]: cache clearance position=%1 clear=%2", ground, !m_Obstructed);
return !m_Obstructed;
}
protected bool CreateGuardOrders(ERR_SiteRuntime site, vector anchor)
{
if (!site.Definition.Patrol || site.Definition.Patrol.IsEmpty())
{
AIWaypoint defend = AIWaypoint.Cast(Spawn(ERR_Resources.DefendWaypoint(), anchor));
if (!defend) return false;
site.Group.AddWaypoint(defend);
return true;
}
foreach (ERR_SitePatrolPoint point : site.Definition.Patrol)
{
AIWaypoint move = AIWaypoint.Cast(Spawn(ERR_Resources.MoveWaypoint(), anchor + point.Offset));
if (!move) return false;
site.PatrolWaypoints.Insert(move);
}
site.PatrolCycle = AIWaypointCycle.Cast(Spawn(ERR_Resources.CycleWaypoint(), anchor));
if (!site.PatrolCycle) return false;
site.PatrolCycle.SetRerunCounter(-1);
site.PatrolCycle.SetWaypoints(site.PatrolWaypoints);
// The native cycle inserts its own children and itself. Adding children here
// would duplicate the route. HQ inserts Search at index 0 above this queue.
site.Group.AddWaypoint(site.PatrolCycle);
return true;
}
protected void StopPatrols()
{
foreach (ERR_SiteRuntime site : m_Sites)
{
array<AIWaypoint> empty = {};
if (site.PatrolCycle) site.PatrolCycle.SetWaypoints(empty);
if (site.Group)
{
array<AIWaypoint> queued = {};
site.Group.GetWaypoints(queued);
foreach (AIWaypoint waypoint : queued)
if (waypoint && (waypoint == site.PatrolCycle || site.PatrolWaypoints.Contains(waypoint))) site.Group.RemoveWaypoint(waypoint);
}
foreach (AIWaypoint move : site.PatrolWaypoints) ERR_Actors.DeleteSpawned(move);
ERR_Actors.DeleteSpawned(site.PatrolCycle);
site.PatrolWaypoints.Clear();
site.PatrolCycle = null;
}
}
protected IEntity Spawn(ResourceName resource, vector position)
{
IEntity entity = ERR_Actors.Spawn(resource, ERR_Actors.Ground(position));
@@ -159,6 +210,7 @@ class ERR_SiteService
m_Begun = true;
if (!definition || !definition.Valid()) return Fail("invalid scenario definition");
m_Deadline = GetGame().GetWorld().GetWorldTime() + 15000;
m_GuardDeadline = GetGame().GetWorld().GetWorldTime() + ERR_InitialGuardRules.TimeoutMilliseconds();
foreach (ERR_SiteDefinition spec : definition.Sites)
{
vector position = ERR_Actors.Ground(spec.Position);
@@ -176,6 +228,7 @@ class ERR_SiteService
Vehicle helicopter = Vehicle.Cast(Spawn(m_Mode.Factions().ExtractionPrefab, position + "12 0 0"));
if (!helicopter) return Fail("extraction vehicle");
if (!m_Mode.SetExtractionVehicle(helicopter)) return Fail("extraction vehicle registration");
m_ExtractionPlacement = new ERR_ExtractionPlacement(m_Mode, helicopter);
}
if (!spec.Container.IsEmpty())
{
@@ -231,20 +284,100 @@ class ERR_SiteService
ResourceName groupPrefab = spec.Guards;
if (groupPrefab.IsEmpty()) groupPrefab = m_Mode.Factions().GuardGroupPrefab;
site.Group = SCR_AIGroup.Cast(Spawn(groupPrefab, guardPosition));
AIWaypoint waypoint = AIWaypoint.Cast(Spawn(ERR_Resources.DefendWaypoint(), guardPosition));
if (!site.Group || !waypoint) return Fail("guard population: " + spec.Id);
site.Group.AddWaypoint(waypoint);
if (!site.Group) return Fail("guard group resource: " + spec.Id);
if (!site.Group.m_aUnitPrefabSlots || site.Group.m_aUnitPrefabSlots.IsEmpty() || !site.Group.GetSpawnImmediately()) return Fail("guard profile must request an initial physical squad: " + spec.Id);
site.InitialGuardCount = site.Group.m_aUnitPrefabSlots.Count();
foreach (ResourceName member : site.Group.m_aUnitPrefabSlots)
if (member.IsEmpty()) return Fail("guard profile has an empty member resource: " + spec.Id);
if (!CreateGuardOrders(site, guardPosition)) return Fail("guard initial orders: " + spec.Id);
}
PrintFormat("Rereintroduced [sites]: authored %1 at %2", spec.Id, position);
}
return true;
}
protected ERR_PrepareResult InitialGuards(ERR_SiteRuntime site)
{
bool requested = site.Definition.Guarded || !site.Definition.Guards.IsEmpty();
if (!requested) return ERR_PrepareResult.READY;
if (!site.Group || site.InitialGuardCount <= 0)
{
Fail("initial guard group disappeared or has no configured members: " + site.Definition.Id);
return m_State;
}
if (site.Group.GetFaction() != m_Mode.EnemyFaction())
{
Fail("guard group faction disagrees with catalog: " + site.Definition.Id);
return m_State;
}
array<AIAgent> agents = {};
array<IEntity> physical = {};
site.Group.GetAgents(agents);
int conscious;
foreach (AIAgent agent : agents)
{
if (!agent) continue;
IEntity actor = agent.GetControlledEntity();
if (!actor || physical.Contains(actor)) continue;
physical.Insert(actor);
if (SCR_FactionManager.SGetFaction(actor) != m_Mode.EnemyFaction())
{
Fail("initial guard actor faction disagrees with catalog: " + site.Definition.Id);
return m_State;
}
if (ERR_Actors.CanFight(actor)) conscious++;
}
bool complete = site.Group.IsExpandComplete();
if (site.LastInitialPhysical != physical.Count() || site.LastInitialConscious != conscious)
{
PrintFormat("Rereintroduced [guards]: initial site=%1 requested=%2 physical=%3 conscious=%4 nativeComplete=%5", site.Definition.Id, site.InitialGuardCount, physical.Count(), conscious, complete);
site.LastInitialPhysical = physical.Count();
site.LastInitialConscious = conscious;
}
ERR_PrepareResult result = ERR_InitialGuardRules.State(site.InitialGuardCount, physical.Count(), conscious, complete, GetGame().GetWorld().GetWorldTime() > m_GuardDeadline);
if (result == ERR_PrepareResult.FAILED)
{
AIPathfindingComponent path = AIPathfindingComponent.Cast(site.Group.FindComponent(AIPathfindingComponent));
NavmeshWorldComponent navmesh;
if (path) navmesh = path.GetNavmeshComponent();
bool tileLoaded, tileRequested;
if (navmesh)
{
tileLoaded = navmesh.IsTileLoaded(site.Group.GetOrigin());
tileRequested = navmesh.IsTileRequested(site.Group.GetOrigin());
}
PrintFormat("Rereintroduced [guards]: timeout detail site=%1 navmesh=%2 tileLoaded=%3 tileRequested=%4 dormantAlive=%5 dormantDead=%6", site.Definition.Id, navmesh != null, tileLoaded, tileRequested, site.Group.GetDormantAliveCount(), site.Group.GetDormantDeadCount());
Fail(string.Format("initial guard population incomplete or invalid: site=%1 requested=%2 physical=%3 conscious=%4 nativeComplete=%5; limit=90s; no replacement requested", site.Definition.Id, site.InitialGuardCount, physical.Count(), conscious, complete));
}
return result;
}
protected void CancelInitialGuardRequests()
{
// Stock DespawnMembers returns early for an empty group; explicitly remove
// its queued initial request before a failed/stopped preparation deletes it.
if (!GetGame()) return;
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
if (!world) return;
foreach (ERR_SiteRuntime site : m_Sites)
{
if (!site.Group || site.InitialGuardCount <= 0) continue;
// Retire this instance's initial spawn intent as well as its current queue
// entries. A native expansion pass may otherwise schedule the empty capacity
// again. Existing physical members remain until the owner rolls back.
site.Group.SetSpawnImmediately(false);
site.Group.m_aUnitPrefabSlots = new array<ResourceName>();
world.PurgeSpawnRequestsForGroup(site.Group);
}
}
ERR_PrepareResult Prepare()
{
if (!Replication.IsServer() || !m_Begun || m_Stopped) return ERR_PrepareResult.FAILED;
if (m_State != ERR_PrepareResult.PENDING) return m_State;
int pending, accepted;
if (!m_ExtractionPlacement) { Fail("extraction placement owner missing"); return m_State; }
ERR_PrepareResult aircraftPlacement = m_ExtractionPlacement.Poll(GetGame().GetWorld().GetWorldTime());
if (aircraftPlacement == ERR_PrepareResult.FAILED) { Fail(m_ExtractionPlacement.FailureReason()); return m_State; }
int pending, accepted, pendingGuards;
foreach (ERR_SiteRuntime site : m_Sites)
{
if (!TransportReady(site))
@@ -252,12 +385,9 @@ class ERR_SiteService
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) Fail("transport needs fuel, driver/passenger seats and a fitting parking footprint: " + site.Definition.Id);
return m_State;
}
if (!site.Group) continue;
if (site.Group.GetFaction() != m_Mode.EnemyFaction())
{
Fail("guard group faction disagrees with catalog: " + site.Definition.Id);
return m_State;
}
ERR_PrepareResult guards = InitialGuards(site);
if (guards == ERR_PrepareResult.FAILED) return m_State;
if (guards == ERR_PrepareResult.PENDING) pendingGuards++;
}
string firstRejected;
foreach (ERR_StockPlacement placement : m_Placements)
@@ -276,6 +406,7 @@ class ERR_SiteService
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) Fail(string.Format("physical stock insertion timed out: pending=%1 accepted=%2 sample=%3", pending, accepted, firstRejected));
return m_State;
}
if (pendingGuards > 0 || aircraftPlacement == ERR_PrepareResult.PENDING) return m_State;
foreach (IEntity entity : m_Entities) m_Mode.Own(entity);
m_State = ERR_PrepareResult.READY;
PrintFormat("Rereintroduced [sites]: %1 sites and %2 physical stock items ready", m_Sites.Count(), m_Placements.Count());
@@ -302,10 +433,12 @@ class ERR_SiteService
bool CanObserve(IEntity player, ERR_SiteRuntime site)
{
if (!Replication.IsServer() || m_Stopped || !m_Mode.Active() || m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return false;
if (!site || !site.Location || site.Definition.DiscoverRadius <= 0 || !ERR_Actors.CanFight(player)) return false;
if (!site || !site.Definition || !m_Sites.Contains(site) || !site.Location || !m_Mode.HasLocation(site.Location) || !ERR_Actors.CanFight(player)) return false;
if (!ERR_Actors.Player(player) || !m_Mode.Players() || !m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) return false;
if (!m_Mode.Players().CurrentActor(player)) return false;
vector position = site.Location.Position();
vector observer = player.GetOrigin();
if (vector.DistanceXZ(observer, position) > site.Definition.DiscoverRadius || Math.AbsFloat(observer[1] - position[1]) > 8) return false;
if (!ERR_SiteDiscoveryRules.InRange(site.Definition.DiscoverRadius, vector.DistanceXZ(observer, position), Math.AbsFloat(observer[1] - position[1]))) return false;
TraceParam trace = new TraceParam();
trace.Start = player.GetOrigin() + "0 1.5 0";
trace.End = position + "0 1.3 0";
@@ -333,11 +466,19 @@ class ERR_SiteService
}
void Rollback()
{
Stop();
// Children/items are listed after parents; delete them first.
for (int i = m_Entities.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_Entities[i]);
m_Entities.Clear();
m_Placements.Clear();
m_Sites.Clear();
}
void Stop() { m_Stopped = true; }
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
if (m_ExtractionPlacement) m_ExtractionPlacement.Stop();
if (m_State == ERR_PrepareResult.PENDING) CancelInitialGuardRequests();
StopPatrols();
}
}
@@ -41,6 +41,11 @@ class ERR_SpawnLogic : SCR_SpawnLogic
BindLifecycle(mode);
SCR_PlayerController resultController = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
if (resultController) resultController.ERR_ResetEscapeResult();
#ifndef WORKBENCH
string diagnosticProject;
if (System.GetCLIParam("errOrdinaryScenario", diagnosticProject))
PrintFormat("ERR ORDINARY ADMISSION: server=%1 sessionMode=%2 modePresent=%3 controllerPresent=%4 identityPresent=%5 nameLength=%6", Replication.IsServer(), RplSession.Mode(), mode != null, resultController != null, !identity.IsNull(), GetGame().GetPlayerManager().GetPlayerName(playerId).Length());
#endif
if (!mode || !mode.Players().Bind(playerId, identity))
{
Print("Rereintroduced: player identity could not be bound; spawn withheld", LogLevel.ERROR);
@@ -38,15 +38,23 @@ class ERR_SpawnPlacement
}
bool Prison(vector center, map<int, vector> reservations, out vector position)
{
// 25 bounded candidates lie inside the authored 12 by 12 metre prison.
for (int z = -2; z <= 2; z++)
for (int x = -2; x <= 2; x++)
{
vector candidate = ERR_Actors.Ground(center + Vector(x * 1.5, 0, z * 1.5));
if (!Clear(candidate, reservations)) continue;
position = candidate;
return true;
}
// Native character query bounds are about 4m wide. A 3m lattice leaves
// clearance between that conservative bound and the 0.9m query column.
// Search the four inner positions, then eight edge positions, then corners.
// All sixteen centers remain at least 1.5m inside the authored +/-6m walls.
for (int band = 0; band <= 2; band++)
for (int row = 0; row < 4; row++)
for (int column = 0; column < 4; column++)
{
int outerAxes = 0;
if (row == 0 || row == 3) outerAxes++;
if (column == 0 || column == 3) outerAxes++;
if (outerAxes != band) continue;
vector candidate = ERR_Actors.Ground(center + Vector(-4.5 + column * 3, 0, -4.5 + row * 3));
if (!Clear(candidate, reservations)) continue;
position = candidate;
return true;
}
return false;
}
bool Near(IEntity survivor, map<int, vector> reservations, out vector position)
@@ -0,0 +1,26 @@
// Honor noSplash before Game-module widget classes exist, only for the
// explicit ordinary diagnostic. Visible/unflagged startup stays vanilla.
#ifndef WORKBENCH
modded class SplashScreen
{
protected bool m_ERRDiagnosticSplashSkipped;
override void Load()
{
string project;
if (!System.IsCLIParam("noSplash") || !System.GetCLIParam("errOrdinaryScenario", project) || (project != "core" && project != "chat"))
{
super.Load();
return;
}
// BaseLoadingAnim.Show may request Load again while the deliberately absent
// root is null. Preserve the splash sequence counter once per instance.
if (!m_ERRDiagnosticSplashSkipped)
{
m_ERRDiagnosticSplashSkipped = true;
s_iSplashShown++;
Print("ERR ORDINARY: diagnostic noSplash honored before Game compilation");
}
}
}
#endif