Embed RHS 1.9 compatibility; record Workshop 0.0.141
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-11-05 16:39:35 +01:00
parent 9418b6950a
commit 9caed3f12d
325 changed files with 76722 additions and 25 deletions
@@ -0,0 +1,16 @@
ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
ID "17B3CC49610F4ADE"
components {
SCR_GameOverScreenManagerComponent "{56DEC646298AD219}" {
m_GameOverScreenConfig SCR_GameOverScreenConfig "{56DEC6462FFDD82D}" : "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf" {}
}
ERR_ScenarioComponent "{B78E910E88DE30D2}" {
m_Definition "{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"
}
SCR_RespawnSystemComponent "{56B2B4793051E7C9}" {
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
}
}
m_fAutoReloadTime 120
m_bUseSpawnPreload 1
}
@@ -0,0 +1,546 @@
enum ERR_ObserverReason
{
NONE,
SPENT_LIFE,
LATE_EXTRACTION,
SPAWN_UNAVAILABLE,
UNCONSCIOUS
}
class ERR_ObserverRules
{
static int Cycle(bool enabled, bool left, bool right, bool previousLeft, bool previousRight)
{
if (!enabled || left == right) return 0;
if (left && !previousLeft) return -1;
if (right && !previousRight) return 1;
return 0;
}
static bool Toggle(bool enabled, bool pressed, bool previous)
{
return enabled && pressed && !previous;
}
static bool Casualty(bool active, bool currentBody, bool alive, bool conscious)
{
return active && currentBody && alive && !conscious;
}
static bool View(int reason, bool active, bool alive, bool conscious)
{
if (!active || reason == ERR_ObserverReason.NONE) return false;
if (reason == ERR_ObserverReason.UNCONSCIOUS) return alive && !conscious;
return !alive;
}
}
// Presentation only. Existing server admission and observer selection own state.
class ERR_ObserverGuidance
{
static string Message(int reason, bool survivorAvailable)
{
if (reason == ERR_ObserverReason.NONE) return "";
string message = "You are spectating until the next round.";
if (reason == ERR_ObserverReason.SPENT_LIFE)
message = "Your life for this round has been used. You can play again next round.";
else if (reason == ERR_ObserverReason.LATE_EXTRACTION)
message = "Extraction has started. New prisoners can join next round.";
else if (reason == ERR_ObserverReason.SPAWN_UNAVAILABLE)
message = "Your prisoner could not be spawned. You are spectating until the next round.";
else if (reason == ERR_ObserverReason.UNCONSCIOUS)
{
message = "You are unconscious. Your body remains where you fell and teammates can still treat you. Your view returns automatically when you wake up.";
if (!survivorAvailable) return message + " No conscious teammate is currently available; the camera stays near your body. Use camera look controls to orbit and zoom controls to change distance.";
}
if (!survivorAvailable)
return message + " No teammate or body is currently available to follow. Use camera look controls to orbit the last known position and zoom controls to change distance.";
return message + " Press Left / Right arrow to change teammate or body, including your own (or /esc spectate next / /esc spectate previous). Press Enter to switch between third-person and the living teammate's camera. Player view waits in third-person if no live camera stream is available. Use camera look controls to orbit and zoom controls to change distance in third-person.";
}
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
{
return reason != ERR_ObserverReason.NONE && (!shown || previousReason != reason || previousAvailable != survivorAvailable);
}
}
class ERR_ObserverCameraClass : SCR_ManualCameraClass {}
// Retain the native config registration when extending this serialized core.
[BaseContainerProps(configRoot: true)]
modded class SCR_ChatPanelManager
{
bool ERR_HasOpenChatPanel()
{
foreach (SCR_ChatPanel panel : m_aChatPanels)
if (panel && panel.IsOpen()) return true;
return false;
}
}
class ERR_ObserverChatInput
{
static bool Capture()
{
SCR_ChatPanelManager manager = SCR_ChatPanelManager.GetInstance();
bool chatOpen = manager && manager.ERR_HasOpenChatPanel();
bool editing;
Widget focused = GetGame().GetWorkspace().GetFocusedWidget();
while (focused)
{
// A HUD stopping drawing can unregister a panel before its focused edit
// widget closes. Keep that still-open native panel's send action available.
SCR_ChatPanel panel = SCR_ChatPanel.Cast(focused.FindHandler(SCR_ChatPanel));
if (panel && panel.IsOpen()) chatOpen = true;
if (EditBoxWidget.Cast(focused)) editing = true;
focused = focused.GetParent();
}
InputManager input = GetGame().GetInputManager();
// IsOpen is immediate; vanilla refreshes ChatContext later in its HUD update.
// Renew only for an actual open chat, never for an unrelated password box.
if (chatOpen) input.ActivateContext("ChatContext", 1);
return chatOpen || editing || input.IsContextActive("ChatContext");
}
}
// Gate before SCR_ManualCamera activates its context: disabling only the follow
// component is too late, since the camera context can already consume chat keys.
class ERR_ObserverCamera : SCR_ManualCamera
{
override protected bool IsDisabledByMenu()
{
if (super.IsDisabledByMenu()) return true;
return ERR_ObserverChatInput.Capture();
}
}
// A local follow camera. It never possesses its target or offers free flight.
[BaseContainerProps(), SCR_BaseManualCameraComponentTitle()]
class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
{
protected IEntity m_Target;
protected IEntity m_Vehicle;
protected float m_Yaw = 180;
protected float m_Pitch = 20;
protected float m_Distance = 5;
protected bool m_LeftDown, m_RightDown, m_EnterDown;
protected int m_InputResumeFrames;
protected bool m_PlayerView;
protected ref ERR_ObserverHeadVisibility m_ViewHead = new ERR_ObserverHeadVisibility();
protected float m_OrbitFov, m_OrbitNear;
protected ref ERR_ObserverScopeDisplay m_ScopeDisplay = new ERR_ObserverScopeDisplay();
protected ref ERR_ObserverPIPDisplay m_PIPDisplay = new ERR_ObserverPIPDisplay();
protected bool m_InputContextActive;
protected bool m_SpaceDown;
bool InputContextActive() { return m_InputContextActive; }
bool IsPlayerView() { return m_PlayerView; }
// Process one edge per press; menus still consume the state so holding a key
// while closing a menu cannot unexpectedly change the followed teammate.
void Input(bool enabled, bool left, bool right, bool enter, bool space = false)
{
// An inactive context reports zero even while a physical key is held. Let
// the reactivated context settle, then require release before accepting keys.
if (!enabled) m_InputResumeFrames = 2;
else if (m_InputResumeFrames > 0)
{
enabled = false;
if (m_InputResumeFrames > 1 || (!left && !right && !enter && !space)) m_InputResumeFrames--;
}
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
int cycle = ERR_ObserverRules.Cycle(enabled, left, right, m_LeftDown, m_RightDown);
if (controller && cycle != 0) controller.ERR_CycleObserver(cycle);
if (ERR_ObserverRules.Toggle(enabled, enter, m_EnterDown)) m_PlayerView = !m_PlayerView;
if (controller && ERR_ObserverRules.Toggle(enabled, space, m_SpaceDown)) controller.ERR_UseSelfEpinephrine();
m_SpaceDown = space;
m_LeftDown = left; m_RightDown = right; m_EnterDown = enter;
}
override bool EOnCameraInit()
{
// Release a held jump key after knockout before accepting self-injection.
m_InputResumeFrames = 2;
m_OrbitFov = GetCameraEntity().GetVerticalFOV();
m_OrbitNear = GetCameraEntity().GetNearPlane();
return true;
}
override void EOnCameraExit() { m_PIPDisplay.Close(); m_ScopeDisplay.Close(); m_ViewHead.Restore(); }
override void EOnCameraFrame(SCR_ManualCameraParam param)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!controller || GetGame().GetCameraManager().CurrentCamera() != GetCameraEntity()) { m_PIPDisplay.Close(); m_ScopeDisplay.Close(); m_ViewHead.Restore(); return; }
m_InputContextActive = GetInputManager().IsContextActive("ERR_ObserverContext");
Input(param.isManualInputEnabled, GetInputManager().GetActionValue("ERR_ObserverPrevious") > 0.5, GetInputManager().GetActionValue("ERR_ObserverNext") > 0.5, GetInputManager().GetActionValue("ERR_ObserverView") > 0.5, GetInputManager().GetActionValue("ERR_SelfEpinephrine") > 0.5);
IEntity target = controller.ERR_ObservedEntity();
float fov, nearPlane;
if (m_PlayerView && controller.ERR_ReadPlayerView(target, param.timeSlice, param.transform, fov, nearPlane))
{
param.fov = fov; GetCameraEntity().SetNearPlane(nearPlane);
RplId scopeWeapon; vector scopeData;
controller.ERR_ReadScope(scopeWeapon, scopeData);
SCR_2DOpticsComponent optic = ERR_ObserverScope.Resolve(target, scopeWeapon, scopeData);
IEntity weapon;
if (optic)
{
RplComponent weaponRpl = RplComponent.Cast(Replication.FindItem(scopeWeapon));
if (weaponRpl) weapon = weaponRpl.GetEntity();
}
vector eye[4], geometry;
float eyeFov, eyeNear, pipFov, vignette;
// Read both timelines every camera frame, including the 2D fallback, so
// enabling PiP cannot resume an eye camera from a different time.
bool haveEye = controller.ERR_ReadPlayerEye(target, param.timeSlice, eye, eyeFov, eyeNear, pipFov, geometry, vignette);
bool pip = haveEye && param.isManualInputEnabled && optic && weapon;
if (pip) pip = m_PIPDisplay.Render(optic, param.transform, eye, fov, pipFov, scopeData, geometry, vignette);
if (pip)
{
m_ScopeDisplay.Close();
Math3D.MatrixCopy(eye, param.transform);
param.fov = eyeFov; GetCameraEntity().SetNearPlane(eyeNear);
}
else
{
m_PIPDisplay.Close();
if (!param.isManualInputEnabled || !optic || !weapon || !m_ScopeDisplay.RenderScope(optic, weapon, fov, scopeData)) m_ScopeDisplay.Close();
}
m_ViewHead.Update(target, param.transform[3]);
param.isDirty = true;
return;
}
// No estimated head camera: corpses, streaming gaps and expired samples use
// the stable follow view until an authenticated live camera stream arrives.
m_PIPDisplay.Close(); m_ScopeDisplay.Close();
m_ViewHead.Restore();
param.fov = m_OrbitFov; GetCameraEntity().SetNearPlane(m_OrbitNear);
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
IEntity exclude = target;
IEntity vehicle;
if (target)
{
vehicle = CompartmentAccessComponent.GetVehicleIn(target);
vector transform[4];
if (vehicle)
{
vehicle.GetWorldTransform(transform);
vector lower, upper;
vehicle.GetWorldBounds(lower, upper);
aim = (lower + upper) * 0.5;
exclude = vehicle;
}
else
{
target.GetWorldTransform(transform);
aim = transform[3] + "0 1.5 0";
if (!ERR_Actors.CanFight(target)) aim = transform[3] + "0 0.4 0";
}
if (target != m_Target || vehicle != m_Vehicle)
{
vector angles = transform[2].VectorToAngles();
m_Yaw = angles[0] + 180; m_Pitch = 20; m_Distance = 5;
if (vehicle) m_Distance = 12;
m_Target = target; m_Vehicle = vehicle;
}
}
else
{
// A lone casualty, an all-down squad or an entity still streaming can have
// only an anchor. Keep orbit controls and forget the previous vehicle limits.
m_Target = null; m_Vehicle = null;
m_Distance = Math.Clamp(m_Distance, 2, 12);
}
if ((!m_PlayerView || !target || !ERR_Actors.CanFight(target)) && param.isManualInputEnabled)
{
m_Yaw += GetInputManager().GetActionValue("ManualCameraRotateYaw") * 90 * param.timeSlice;
m_Pitch = Math.Clamp(m_Pitch - GetInputManager().GetActionValue("ManualCameraRotatePitch") * 90 * param.timeSlice, 5, 75);
float scale = Math.Clamp(1 + GetInputManager().GetActionValue("ManualCameraZoom") * 12 * param.timeSlice, 0.5, 1.5);
float minimum = 2, maximum = 12;
if (vehicle) { minimum = 6; maximum = 25; }
m_Distance = Math.Clamp(m_Distance * scale, minimum, maximum);
}
float yaw = m_Yaw * Math.DEG2RAD, pitch = m_Pitch * Math.DEG2RAD;
vector offset = Vector(Math.Sin(yaw) * Math.Cos(pitch), Math.Sin(pitch), Math.Cos(yaw) * Math.Cos(pitch));
vector position = aim + offset * m_Distance;
TraceParam trace = new TraceParam();
trace.Start = aim;
trace.End = position;
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
trace.LayerMask = TRACE_LAYER_CAMERA;
trace.Exclude = exclude;
float fraction = GetCameraEntity().GetWorld().TraceMove(trace, null);
if (fraction < 1) position = vector.Lerp(aim, position, Math.Max(0.05, fraction - 0.05));
Math3D.DirectionAndUpMatrix(vector.Direction(position, aim), "0 1 0", param.transform);
param.transform[3] = position;
param.isDirty = true;
}
}
modded class SCR_PlayerController
{
[RplProp()]
protected ERR_ObserverReason m_ERRObserverReason;
[RplProp()]
protected RplId m_ERRObservedId = RplId.Invalid();
[RplProp()]
protected vector m_ERRObserverAnchor;
protected IEntity m_ERRServerObserved;
protected IEntity m_ERRServerObservedVehicle;
protected SCR_ManualCamera m_ERRObserverCamera;
protected bool m_ERRObserverMonitor;
protected bool m_ERRObserverReady;
protected ERR_ObserverReason m_ERRShownObserverReason;
protected bool m_ERRShownObserverAvailable;
protected string m_ERRShownRecoveryStatus;
protected string m_ERRPendingRecoveryStatus;
protected int m_ERRRecoveryStatusSince;
protected float m_ERRLastCycle = -1000;
protected IEntity m_ERRRecoveryBody;
protected float m_ERRRecoveryDeadline = -1;
// Optional casualty action is implemented by the self-epinephrine extension.
string ERR_SelfEpinephrineHint() { return ""; }
void ERR_UseSelfEpinephrine() {}
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
IEntity ERR_ObservedEntity()
{
RplComponent component = RplComponent.Cast(Replication.FindItem(m_ERRObservedId));
if (!component) return null;
return component.GetEntity();
}
bool ERR_HasObserverCamera() { return m_ERRObserverCamera != null; }
void ERR_SetObserver(ERR_ObserverReason reason)
{
if (!Replication.IsServer()) return;
if (reason != ERR_ObserverReason.NONE && ERR_Actors.Alive(GetControlledEntity()) && reason != ERR_ObserverReason.UNCONSCIOUS) return;
m_ERRObserverReason = reason;
ERR_GameMode mode = ERR_GameMode.Get();
if (mode && reason != ERR_ObserverReason.NONE) m_ERRObserverAnchor = mode.PrisonPosition();
if (reason == ERR_ObserverReason.NONE) ERR_ReleaseObserved();
Replication.BumpMe();
ERR_UpdateObserverServer();
}
protected void ERR_StreamObserved(IEntity entity, bool normalStreaming)
{
RplIdentity identity = GetRplIdentity();
if (!identity.IsValid() || identity == RplIdentity.Local() || !entity) return;
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
if (component) component.EnableStreamingConNode(identity, normalStreaming);
}
protected void ERR_ReleaseObserved()
{
ERR_StreamObserved(m_ERRServerObserved, true);
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
m_ERRServerObserved = null;
m_ERRServerObservedVehicle = null;
m_ERRObservedId = RplId.Invalid();
}
void ERR_UpdateObserverServer(int direction = 0)
{
if (!Replication.IsServer()) return;
ERR_GameMode mode = ERR_GameMode.Get();
IEntity body = GetControlledEntity();
bool casualty = mode && ERR_ObserverRules.Casualty(mode.Active(), mode.Players() && mode.Players().CurrentActor(body) != null, ERR_Actors.Alive(body), ERR_Actors.CanFight(body));
if (casualty && m_ERRObserverReason != ERR_ObserverReason.UNCONSCIOUS)
{
m_ERRObserverReason = ERR_ObserverReason.UNCONSCIOUS;
m_ERRObserverAnchor = body.GetOrigin();
}
if (m_ERRObserverReason == ERR_ObserverReason.UNCONSCIOUS && !ERR_Actors.Alive(body)) m_ERRObserverReason = ERR_ObserverReason.SPENT_LIFE;
// Death can leave the controller attached to its corpse, so entity-lost is
// not a reliable trigger (especially with the last/only player).
ERR_PlayerLife life;
if (mode && mode.Players()) life = mode.Players().Get(GetPlayerId());
if (mode && mode.Active() && life && life.UsedLife && life.Actor && !ERR_Actors.Alive(life.Actor) && !ERR_Actors.Alive(body) && m_ERRObserverReason == ERR_ObserverReason.NONE)
{
m_ERRObserverReason = ERR_ObserverReason.SPENT_LIFE;
m_ERRObserverAnchor = life.Actor.GetOrigin();
}
if (m_ERRObserverReason == ERR_ObserverReason.NONE) return;
if (!mode || !mode.Active() || (ERR_Actors.Alive(body) && !casualty) || (m_ERRObserverReason == ERR_ObserverReason.UNCONSCIOUS && !casualty))
{
m_ERRObserverReason = ERR_ObserverReason.NONE;
ERR_ReleaseObserved();
Replication.BumpMe();
return;
}
array<IEntity> survivors = {};
ERR_Actors.Players(survivors);
Faction friendly = mode.PlayerFaction();
for (int i = survivors.Count() - 1; i >= 0; i--)
if (survivors[i] == body || (casualty && !ERR_Actors.CanFight(survivors[i])) || !friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
// Dead teammates remain selectable alongside survivors, including our own body.
ERR_ObserverTargets.Bodies(mode, survivors);
// Lethal damage normally leaves a treatable casualty, not a native corpse.
// Keep that player's own canonical body in the cycle without possessing it.
if (casualty && !survivors.Contains(body)) survivors.Insert(body);
int index = survivors.Find(m_ERRServerObserved);
if (index < 0)
{
if (!casualty && life && life.Actor) index = survivors.Find(life.Actor);
if (index < 0) index = 0;
}
else index += direction;
if (index < 0) index = survivors.Count() - 1;
if (index >= survivors.Count()) index = 0;
IEntity target;
if (!survivors.IsEmpty()) target = survivors[index];
if (target != m_ERRServerObserved)
{
ERR_ReleaseObserved();
m_ERRServerObserved = target;
if (target)
{
RplComponent component = RplComponent.Cast(target.FindComponent(RplComponent));
if (component) m_ERRObservedId = component.Id();
ERR_StreamObserved(target, false);
}
}
if (target)
{
m_ERRObserverAnchor = target.GetOrigin();
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(target);
if (vehicle != m_ERRServerObservedVehicle)
{
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
m_ERRServerObservedVehicle = vehicle;
ERR_StreamObserved(vehicle, false);
}
}
else if (casualty) m_ERRObserverAnchor = body.GetOrigin();
Replication.BumpMe();
}
void ERR_CycleObserver(int direction)
{
if (this == GetGame().GetPlayerController()) Rpc(ERR_RequestObserverCycle, direction);
}
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
protected void ERR_RequestObserverCycle(int direction)
{
if (!Replication.IsServer() || (direction != -1 && direction != 1)) return;
float now = GetGame().GetWorld().GetWorldTime();
if (now - m_ERRLastCycle < 250) return;
m_ERRLastCycle = now;
ERR_UpdateObserverServer(direction);
}
void ERR_StartObserverMonitor()
{
if (this != GetGame().GetPlayerController() || System.IsConsoleApp() || m_ERRObserverMonitor) return;
m_ERRObserverMonitor = true;
ERR_OnObserverMonitorStarted();
GetGame().GetCallqueue().CallLater(ERR_UpdateObserverClient, 250, true);
}
protected void ERR_OnObserverMonitorStarted() {}
protected void ERR_UpdateObserverClient()
{
if (!GetGame()) return;
if (this != GetGame().GetPlayerController())
{
// A replaced controller may retain its timer until native destruction.
// Release only its presentation; never restore its old body camera.
GetGame().GetCallqueue().Remove(ERR_UpdateObserverClient);
m_ERRObserverMonitor = false;
m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1;
ERR_CloseObserverCamera();
return;
}
ERR_GameMode mode = ERR_GameMode.Get();
ERR_UpdateRecoveryCamera(mode);
bool observe = mode && ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
if (!observe)
{
ERR_CloseObserverCamera();
return;
}
SCR_EditorManagerEntity editor = SCR_EditorManagerEntity.GetInstance();
if (editor && editor.IsOpened()) return;
if (!m_ERRObserverCamera)
{
Resource resource = Resource.Load("{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et");
if (!resource || !resource.IsValid()) return;
EntitySpawnParams params = new EntitySpawnParams();
params.TransformMode = ETransformMode.WORLD;
params.Transform[3] = m_ERRObserverAnchor + "0 5 -6";
m_ERRObserverCamera = SCR_ManualCamera.Cast(GetGame().SpawnEntityPrefabLocal(resource, GetGame().GetWorld(), params));
if (!m_ERRObserverCamera) return;
}
CameraManager manager = GetGame().GetCameraManager();
if (!manager) return;
// Native death/respawn camera changes can arrive after our camera is created.
if (manager.CurrentCamera() != m_ERRObserverCamera) manager.SetCamera(m_ERRObserverCamera);
if (manager.CurrentCamera() != m_ERRObserverCamera) return;
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
if (respawn) respawn.DestroyLoadingPlaceholder();
// The server-selected replicated ID distinguishes no target from a target
// whose local entity is still streaming. Never imply no survivors from delay.
bool survivorAvailable = m_ERRObservedId != RplId.Invalid();
string recovery = mode.RecoveryStatus();
string selfEpiHint = ERR_SelfEpinephrineHint();
if (!selfEpiHint.IsEmpty()) recovery += "\n" + selfEpiHint;
bool contextChanged = ERR_ObserverGuidance.Refresh(m_ERRObserverReady, m_ERRShownObserverReason, m_ERRObserverReason, m_ERRShownObserverAvailable, survivorAvailable);
int now = System.GetTickCount();
// Medical samples can alternate between improving and stalled. Keep the
// accepted hint readable until the new status has remained stable for 3s.
// This affects presentation only, never the authoritative defeat timer.
if (m_ERRPendingRecoveryStatus != recovery || now < m_ERRRecoveryStatusSince)
{
m_ERRPendingRecoveryStatus = recovery;
m_ERRRecoveryStatusSince = now;
}
if (!contextChanged && m_ERRShownRecoveryStatus != recovery && now - m_ERRRecoveryStatusSince < 3000) return;
if (contextChanged || m_ERRShownRecoveryStatus != recovery)
{
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (!hints) return; // retry on the existing monitor; do not lose the first hint
string message = ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable);
if (!recovery.IsEmpty()) message += "\n" + recovery;
if (!hints.ShowCustom(message, "Spectating", 12, m_ERRObserverReady)) return;
m_ERRObserverReady = true;
m_ERRShownObserverReason = m_ERRObserverReason;
m_ERRShownObserverAvailable = survivorAvailable;
m_ERRShownRecoveryStatus = recovery;
}
}
// Track the actual local body even when an editor/other camera owns the view.
// Run once per recovery, with retries for asynchronous editor close/camera setup.
protected void ERR_UpdateRecoveryCamera(ERR_GameMode mode)
{
IEntity body = GetControlledEntity();
if (!mode || !mode.Active() || !ERR_Actors.Alive(body))
{ m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1; return; }
if (!ERR_Actors.CanFight(body))
{ m_ERRRecoveryBody = body; m_ERRRecoveryDeadline = -1; return; }
if (!m_ERRRecoveryBody) return;
if (body != m_ERRRecoveryBody)
{ m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1; return; }
float now = GetGame().GetWorld().GetWorldTime();
if (m_ERRRecoveryDeadline < 0) m_ERRRecoveryDeadline = now + 10000;
SCR_EditorManagerEntity editor = SCR_EditorManagerEntity.GetInstance();
if (editor && editor.IsOpened()) editor.Close(false);
CameraManager manager = GetGame().GetCameraManager();
CameraBase playerCamera = GetPlayerCamera();
if (manager && playerCamera) manager.SetCamera(playerCamera);
if ((manager && playerCamera && manager.CurrentCamera() == playerCamera && (!editor || !editor.IsOpened())) || now >= m_ERRRecoveryDeadline)
{
if (now >= m_ERRRecoveryDeadline) Print("Reintroduced [spectator]: recovery camera return timed out waiting for editor/camera; player body remains controlled", LogLevel.WARNING);
m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1;
}
}
protected void ERR_CloseObserverCamera()
{
m_ERRObserverReady = false;
m_ERRShownRecoveryStatus = "";
m_ERRPendingRecoveryStatus = "";
m_ERRRecoveryStatusSince = 0;
if (!m_ERRObserverCamera) return;
if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera();
m_ERRObserverCamera.Terminate();
delete m_ERRObserverCamera;
m_ERRObserverCamera = null;
}
void ~SCR_PlayerController()
{
if (GetGame())
{
GetGame().GetCallqueue().Remove(ERR_UpdateObserverClient);
ERR_CloseObserverCamera();
if (Replication.IsServer()) ERR_ReleaseObserved();
}
}
}
@@ -0,0 +1,148 @@
"""Actual observer monitor/recovery/cleanup; native camera lifecycle is doubled."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
p=Path(os.environ.get('ERR_OBSERVER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'))
game_mode=Path(os.environ.get('ERR_GAME_MODE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'))
binder=body(game_mode,'protected void BindLocalObserver(')
binder=binder.replace('protected ', '').replace('SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());','Controller* controller = static_cast<Controller*>(GetGame()->GetPlayerController());')
binder=binder.replace('GetGame().','GetGame()->').replace('GetCallqueue().','GetCallqueue()->').replace('Remove(BindLocalObserver)','Remove(this)').replace('controller.','controller->')
def cpp(s):
s=s.replace('protected ','').replace('null','nullptr')
for n in ('ERR_GameMode','ERR_Actors','ERR_ObserverRules','ERR_ObserverGuidance','SCR_HintManagerComponent','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','Resource','SCR_ManualCamera','RplId','ETransformMode','LogLevel','System'):
s=s.replace(n+'.',n+'::')
for n in ('ERR_GameMode','SCR_HintManagerComponent','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','Resource','CameraManager','CameraBase'):
s=re.sub(r'\b'+n+r' (\w+)',n+r'* \1',s)
for n in ('mode','hints','respawn','editor','resource','manager','m_ERRObserverCamera'):
s=s.replace(n+'.',n+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('GetCallqueue().','GetCallqueue()->')
s=s.replace('Remove(ERR_UpdateObserverClient)','Remove(this)').replace('CallLater(ERR_UpdateObserverClient, 250, true)','CallLater(this, 250, true)')
s=s.replace('EntitySpawnParams params = new EntitySpawnParams();','EntitySpawnParams params;')
return s
methods='\n'.join(cpp(body(p,n)) for n in ('void ERR_StartObserverMonitor(', 'protected void ERR_UpdateObserverClient(', 'protected void ERR_UpdateRecoveryCamera(', 'protected void ERR_CloseObserverCamera('))
run(r'''#include <cassert>
#include <set>
#include <string>
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}};
struct Actor {bool alive=true,conscious=true;};using IEntity=Actor*;
struct ERR_Actors {static bool Alive(IEntity a){return a&&a->alive;}static bool CanFight(IEntity a){return Alive(a)&&a->conscious;}};
struct ERR_GameMode {string RecoveryStatus(){return "";}bool active=true;static ERR_GameMode*current;static ERR_GameMode*Get(){return current;}bool Active(){return active;}} mode;
ERR_GameMode*ERR_GameMode::current=&mode;
struct ERR_ObserverRules {static bool View(int reason,bool active,bool alive,bool conscious){return active&&((reason==4&&alive&&!conscious)||(reason!=0&&!alive));}};
struct ERR_ObserverGuidance {static bool Refresh(bool ready,int oldReason,int reason,bool oldAvailable,bool available){return !ready||oldReason!=reason||oldAvailable!=available;}static string Message(int,bool){return "spectating";}};
struct Vec{Vec operator+(const char*)const{return {};}};
struct EntitySpawnParams{int TransformMode=0;Vec Transform[4];};namespace ETransformMode{enum{WORLD};}
struct Resource{static Resource*Load(const char*){static Resource r;return &r;}bool IsValid(){return true;}};
struct CameraBase{virtual ~CameraBase()=default;};
struct CameraManager{CameraBase*current=nullptr;int sets=0;CameraBase*CurrentCamera(){return current;}void SetCamera(CameraBase*c){current=c;sets++;}} manager;
int terminated=0,deleted=0,explicitSwitch=0;
struct SCR_ManualCamera:CameraBase{
CameraBase*previous=nullptr;bool ended=false;
static SCR_ManualCamera*Cast(SCR_ManualCamera*c){return c;}
void TrySwitchToControlledEntityCamera(){explicitSwitch++;}
void Terminate(){if(!ended){ended=true;terminated++;}}
// SDK SCR_ManualCamera destructor invokes SwitchToPreviousCamera, which
// changes only its own currently selected camera, never a foreign current one.
~SCR_ManualCamera(){Terminate();if(manager.current==this&&previous)manager.SetCamera(previous);deleted++;}
};
struct SCR_HintManagerComponent{int attempts=0;bool silent=false;static SCR_HintManagerComponent*GetInstance(){static SCR_HintManagerComponent h;return &h;}bool ShowCustom(string,string,int,bool quiet=false){attempts++;silent=quiet;return true;}};
struct SCR_RespawnSystemComponent{int clears=0;static SCR_RespawnSystemComponent*GetInstance(){static SCR_RespawnSystemComponent r;return &r;}void DestroyLoadingPlaceholder(){clears++;}};
struct SCR_EditorManagerEntity{bool opened=false;int closes=0;static SCR_EditorManagerEntity*GetInstance(){static SCR_EditorManagerEntity e;return &e;}bool IsOpened(){return opened;}void Close(bool){opened=false;closes++;}};
struct RplId{static int Invalid(){return -1;}};namespace LogLevel{enum{WARNING};}void Print(const char*,int){}
namespace System{int now=0;int GetTickCount(){return now;}bool IsConsoleApp(){return false;}}
struct Queue{std::set<void*>owners;int added=0;void CallLater(void*owner,int,bool){owners.insert(owner);added++;}void Remove(void*owner){owners.erase(owner);}} queue;
struct World{float now=1;float GetWorldTime(){return now;}} world;
struct Game{void*local=nullptr;int spawns=0;void*GetPlayerController(){return local;}Queue*GetCallqueue(){return &queue;}World*GetWorld(){return &world;}CameraManager*GetCameraManager(){return &manager;}
SCR_ManualCamera*SpawnEntityPrefabLocal(Resource*,World*,EntitySpawnParams){spawns++;auto*c=new SCR_ManualCamera();c->previous=manager.current;manager.SetCamera(c);return c;}} game;
bool gameAvailable=true;Game*GetGame(){return gameAvailable?&game:nullptr;}
struct Controller{
string ERR_SelfEpinephrineHint(){return "";}
IEntity actor=nullptr,m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1;
bool m_ERRObserverMonitor=false,m_ERRObserverReady=false,m_ERRShownObserverAvailable=false;
string m_ERRShownRecoveryStatus,m_ERRPendingRecoveryStatus;
int m_ERRRecoveryStatusSince=0;
int m_ERRObserverReason=0,m_ERRShownObserverReason=0,m_ERRObservedId=1,starts=0;
Vec m_ERRObserverAnchor;SCR_ManualCamera*m_ERRObserverCamera=nullptr;CameraBase*playerCamera=nullptr;
IEntity GetControlledEntity(){return actor;}CameraBase*GetPlayerCamera(){return playerCamera;}
void ERR_OnObserverMonitorStarted(){starts++;}
'''+methods+r'''
};
struct Binder {bool m_Stopped=false,m_NativeEndHandled=false;
'''+binder+r'''
};
int main(){
// The real repeating binder must survive the first controller and gaps.
Binder binder;Controller first,replacement;queue.owners.insert(&binder);
game.local=nullptr;binder.BindLocalObserver();assert(queue.owners.count(&binder)&&queue.added==0);
game.local=&first;binder.BindLocalObserver();
assert(first.m_ERRObserverMonitor&&first.starts==1&&queue.owners.count(&first));
assert(queue.owners.count(&binder));
binder.BindLocalObserver();assert(first.starts==1&&queue.added==1);
game.local=nullptr;binder.BindLocalObserver();assert(queue.owners.count(&binder));
game.local=&replacement;binder.BindLocalObserver();
assert(replacement.m_ERRObserverMonitor&&replacement.starts==1&&queue.owners.count(&replacement));
first.ERR_UpdateObserverClient();
assert(!first.m_ERRObserverMonitor&&!queue.owners.count(&first)&&queue.owners.count(&replacement));
binder.BindLocalObserver();assert(replacement.starts==1&&queue.added==2);
binder.m_Stopped=true;binder.BindLocalObserver();assert(!queue.owners.count(&binder)&&queue.owners.count(&replacement));
binder.m_Stopped=false;binder.m_NativeEndHandled=true;queue.owners.insert(&binder);binder.BindLocalObserver();
assert(!queue.owners.count(&binder)&&queue.owners.count(&replacement)&&queue.added==2);
queue.owners.clear();queue.added=0;game.local=nullptr;
Actor body;CameraBase formerBodyCamera,newBodyCamera;
Controller former,current;former.actor=&body;former.playerCamera=&formerBodyCamera;
game.local=&former;manager.current=&formerBodyCamera;former.ERR_StartObserverMonitor();
body.conscious=false;former.m_ERRObserverReason=4;former.ERR_UpdateObserverClient();
assert(former.m_ERRRecoveryBody==&body&&former.m_ERRObserverCamera&&queue.owners.count(&former));
SCR_ManualCamera*oldOwned=former.m_ERRObserverCamera;
// New controller owns a separate spectator camera; old body wakes meanwhile.
current.playerCamera=&newBodyCamera;current.m_ERRObserverReason=1;game.local=&current;current.ERR_StartObserverMonitor();current.ERR_UpdateObserverClient();
SCR_ManualCamera*newOwned=current.m_ERRObserverCamera;assert(newOwned&&newOwned!=oldOwned&&manager.current==newOwned);
auto*editor=SCR_EditorManagerEntity::GetInstance();editor->opened=true;body.conscious=true;
int beforeClears=SCR_RespawnSystemComponent::GetInstance()->clears;
int beforeSets=manager.sets,beforeHints=SCR_HintManagerComponent::GetInstance()->attempts,beforeSpawns=game.spawns,beforeDeleted=deleted;
former.ERR_UpdateObserverClient();
assert(manager.current==newOwned&&manager.sets==beforeSets&&editor->opened&&editor->closes==0);
assert(!former.m_ERRRecoveryBody&&former.m_ERRRecoveryDeadline==-1&&!former.m_ERRObserverMonitor&&!former.m_ERRObserverCamera);
assert(deleted==beforeDeleted+1&&terminated==deleted&&explicitSwitch==0);
assert(current.m_ERRObserverCamera==newOwned&&queue.owners.count(&current)==1&&queue.owners.count(&former)==0);
assert(SCR_HintManagerComponent::GetInstance()->attempts==beforeHints&&game.spawns==beforeSpawns&&SCR_RespawnSystemComponent::GetInstance()->clears==beforeClears);
// Even a queued stale spectator callback cannot create a camera or a hint.
former.actor=nullptr;former.m_ERRObserverReason=1;former.ERR_UpdateObserverClient();
assert(game.spawns==beforeSpawns&&manager.current==newOwned&&SCR_HintManagerComponent::GetInstance()->attempts==beforeHints);
// Start is still local-only; reacquisition restarts exactly one owned timer.
int added=queue.added;former.ERR_StartObserverMonitor();assert(queue.added==added);
game.local=&former;former.actor=&body;former.m_ERRObserverReason=0;former.ERR_StartObserverMonitor();former.ERR_StartObserverMonitor();
assert(queue.added==added+1&&former.starts==2&&queue.owners.count(&current)==1);
// Normal recovery still closes the editor and returns this controller's body.
body.conscious=false;former.ERR_UpdateObserverClient();assert(former.m_ERRRecoveryBody==&body);
body.conscious=true;former.ERR_UpdateObserverClient();assert(manager.current==&formerBodyCamera&&!editor->opened&&editor->closes==1&&!former.m_ERRRecoveryBody);
// Foreign current camera survives this controller's retained camera cleanup.
game.local=&current;manager.current=&newBodyCamera;current.ERR_CloseObserverCamera();assert(manager.current==&newBodyCamera&&deleted==beforeDeleted+2);
// If the old owned camera is still current at replacement, its native
// destructor may restore its own previous camera; no new-controller camera is deleted.
Controller abandoned;abandoned.m_ERRObserverReason=1;game.local=&abandoned;manager.current=&newBodyCamera;
abandoned.ERR_StartObserverMonitor();abandoned.ERR_UpdateObserverClient();assert(manager.current==abandoned.m_ERRObserverCamera);
game.local=&current;beforeSets=manager.sets;beforeDeleted=deleted;abandoned.ERR_UpdateObserverClient();
assert(!abandoned.m_ERRObserverCamera&&manager.current==&newBodyCamera&&manager.sets==beforeSets+1&&deleted==beforeDeleted+1);
assert(queue.owners.count(&abandoned)==0&&queue.owners.count(&current)==1);
// Solo death may leave the controlled corpse assigned; restore the orbit
// camera if the native death camera replaces it on a later tick.
Controller solo;Actor corpse;corpse.alive=false;corpse.conscious=false;
solo.actor=&corpse;solo.m_ERRObserverReason=1;game.local=&solo;editor->opened=false;
solo.ERR_UpdateObserverClient();auto*corpseCamera=solo.m_ERRObserverCamera;
assert(corpseCamera&&manager.current==corpseCamera);
manager.current=&newBodyCamera;beforeClears=SCR_RespawnSystemComponent::GetInstance()->clears;beforeSpawns=game.spawns;
solo.ERR_UpdateObserverClient();
assert(manager.current==corpseCamera&&game.spawns==beforeSpawns&&SCR_RespawnSystemComponent::GetInstance()->clears==beforeClears+1);
// Game Master remains usable during spectating, including before first camera creation.
editor->opened=true;manager.current=&newBodyCamera;solo.ERR_UpdateObserverClient();
assert(manager.current==&newBodyCamera&&game.spawns==beforeSpawns);
solo.ERR_CloseObserverCamera();solo.ERR_UpdateObserverClient();
assert(!solo.m_ERRObserverCamera&&manager.current==&newBodyCamera&&game.spawns==beforeSpawns);
editor->opened=false;solo.ERR_UpdateObserverClient();assert(solo.m_ERRObserverCamera&&manager.current==solo.m_ERRObserverCamera);
solo.ERR_CloseObserverCamera();
gameAvailable=false;former.ERR_UpdateObserverClient();gameAvailable=true;
}
''')
print('PASS: actual observer monitor/recovery/cleanup rejects retained old controller, preserves new camera/editor/hints/timer, restarts once and retains normal recovery; native effects modeled from SDK, no runtime claim')
@@ -0,0 +1,127 @@
"""Execute loading presentation/cleanup and native observation bodies with UI adapters.
No native layout rendering, multiplayer, or real startup-failure claim.
"""
from policy_harness import ROOT, body, run
SOURCE=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PreparationStatus.c'
def cpp(s):
s=s.replace('protected ','').replace('override ','').replace('null','nullptr')
s=s.replace('ERR_GameMode mode = ERR_GameMode.Get();','ERR_GameMode* mode = ERR_GameMode::Get();')
s=s.replace('ERR_GameMode current = ERR_GameMode::Get();','ERR_GameMode* current = ERR_GameMode::Get();')
s=s.replace('ERR_GameMode current = ERR_GameMode.Get();','ERR_GameMode* current = ERR_GameMode::Get();')
s=s.replace('current.Phase()', 'current->Phase()')
s=s.replace('ERR_GameMode mode,','ERR_GameMode* mode,')
s=s.replace('ERR_GameMode mode)', 'ERR_GameMode* mode)')
for name in ['System','ERR_GameMode','ERR_Phase','ERR_ObjectiveText','ERR_MissionRules','ERR_PreparationFailureMenu']:s=s.replace(name+'.',name+'::')
s=s.replace('TextWidget.Cast(','CastText(').replace('Widget widget)', 'Widget* widget)').replace('widget.', 'widget->')
for name in ['mode','m_wLoadingPlaceholder','m_ERRPreparationSpinner','m_ERRPreparationRoot','m_ERRPreparationTitle','m_ERRPreparationDescription']:
s=s.replace(name+'.',name+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorkspace().','GetWorkspace()->').replace('.IsEmpty()','.empty()')
return s.replace('super.','Native::')
methods='\n'.join(cpp(body(SOURCE,key)) for key in ('void ERR_KeepSpawnSceneLoading(', 'void ERR_ShowSpawnSceneLoading(', 'protected string ERR_PreparationDescription(', 'protected void ERR_ClearPreparationStatus(', 'override void UpdateLoadingPlaceholder(', 'override void DestroyLoadingPlaceholder(', 'protected bool ERR_PreparationWidgetVisible(', 'bool ERR_ProbePreparationVisible('))
run(r'''
#include <cassert>
#include <string>
#include <map>
using string=std::string;
namespace System{bool console=false;bool IsConsoleApp(){return console;}}
namespace ERR_Phase{enum{PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
namespace ERR_MissionRules{bool Terminal(int p){return p==ERR_Phase::VICTORY||p==ERR_Phase::DEFEAT;}}
struct ERR_GameMode{static ERR_GameMode* current;int phase=ERR_Phase::PREPARING;string error;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}bool Active(){return phase>=ERR_Phase::PRISON&&phase<=ERR_Phase::EXTRACTION;}string PreparationError(){return error;}};ERR_GameMode* ERR_GameMode::current=nullptr;
namespace ERR_PreparationFailureMenu{bool shown=false;bool Show(ERR_GameMode*){return shown;}bool Visible(ERR_GameMode*){return shown;}};
namespace ERR_ObjectiveText{string Description(ERR_GameMode* mode,bool){return mode->error.empty()?"Preparing sites":"Mission failed; ask an administrator to correct and restart";}}
struct Widget{Widget* parent=nullptr;bool visible=true,removed=false,textWidget=false;string text;int textWrites=0,visibilityWrites=0;float width=900,height=170;std::map<string,Widget*> children;
void GetScreenSize(float& w,float& h){w=width;h=height;}Widget* GetParent(){return parent;}void RemoveFromHierarchy(){removed=true;parent=nullptr;}void SetVisible(bool value){visible=value;visibilityWrites++;}bool IsVisible(){return visible;}
Widget* FindAnyWidget(string name){return children.count(name)?children[name]:nullptr;}void SetText(string value){text=value;textWrites++;}string GetText(){return text;}};
Widget* CastText(Widget* w){return w&&w->textWidget?w:nullptr;}
struct Workspace{int creates=0;bool fail=false,missingText=false;Widget* CreateWidgets(const char*,Widget* parent){creates++;if(fail)return nullptr;auto root=new Widget;root->parent=parent;for(string name:{"ERR_PreparationTitle","ERR_PreparationDescription"}){auto child=new Widget;child->parent=root;child->textWidget=true;root->children[name]=child;}if(missingText)root->children.erase("ERR_PreparationTitle");return root;}};
struct Game{Workspace workspace;Workspace* GetWorkspace(){return &workspace;}};Game game;Game* GetGame(){return &game;}
struct Native{Widget* m_wLoadingPlaceholder=nullptr;ERR_GameMode* m_pGameMode=nullptr;int nativeUpdates=0,destroys=0;void CreateLoadingPlaceholder(){m_wLoadingPlaceholder=new Widget;}void UpdateLoadingPlaceholder(float){nativeUpdates++;}void DestroyLoadingPlaceholder(){destroys++;if(m_wLoadingPlaceholder)m_wLoadingPlaceholder->RemoveFromHierarchy();m_wLoadingPlaceholder=nullptr;}};
struct Probe:Native{
Widget *m_ERRPreparationParent=nullptr,*m_ERRPreparationRoot=nullptr,*m_ERRPreparationTitle=nullptr,*m_ERRPreparationDescription=nullptr,*m_ERRPreparationSpinner=nullptr;
bool m_ERRKeepSpawnSceneLoading=false,m_ERRPreparationSpinnerHidden=false,m_ERRPreparationSpinnerWasVisible=false;string m_ERRPreparationLastTitle,m_ERRPreparationLastDescription;
'''+methods+r'''
};
struct Fixture{ERR_GameMode mode;Widget parent,spinner;Probe p;Fixture(){game.workspace={};System::console=false;ERR_PreparationFailureMenu::shown=false;ERR_GameMode::current=&mode;p.m_pGameMode=&mode;p.m_wLoadingPlaceholder=&parent;parent.children["Spinner"]=&spinner;spinner.parent=&parent;}};
int main(){
{Fixture f;f.mode.error="bad";ERR_PreparationFailureMenu::shown=true;f.p.UpdateLoadingPlaceholder(.1f);assert(!f.parent.visible&&f.p.ERR_ProbePreparationVisible(&f.mode,true));}
{Fixture f;f.mode.phase=ERR_Phase::PRISON;f.p.ERR_ShowSpawnSceneLoading();auto root=f.p.m_ERRPreparationRoot;f.p.ERR_KeepSpawnSceneLoading(true);f.p.DestroyLoadingPlaceholder();assert(!root->removed&&!f.parent.removed&&f.p.destroys==0);f.p.ERR_KeepSpawnSceneLoading(false);f.p.DestroyLoadingPlaceholder();assert(root->removed&&f.p.destroys==1);f.p.ERR_ShowSpawnSceneLoading();assert(f.p.m_wLoadingPlaceholder&&f.p.ERR_ProbePreparationVisible(&f.mode,false));}
{Fixture f;f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.nativeUpdates==1&&game.workspace.creates==1&&f.p.ERR_ProbePreparationVisible(&f.mode,false));
Widget* root=f.p.m_ERRPreparationRoot;auto title=f.p.m_ERRPreparationTitle;auto description=f.p.m_ERRPreparationDescription;
for(int i=0;i<200;i++){f.p.UpdateLoadingPlaceholder(.1f);}assert(game.workspace.creates==1&&title->textWrites==1&&description->textWrites==1);
f.mode.error="invalid catalog";f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.ERR_ProbePreparationVisible(&f.mode,true)&&!f.spinner.visible);
for(int i=0;i<200;i++){f.p.UpdateLoadingPlaceholder(.1f);}assert(f.p.m_ERRPreparationRoot==root&&title->textWrites==2&&description->textWrites==2&&f.spinner.visibilityWrites==1);
title->width=0;assert(!f.p.ERR_ProbePreparationVisible(&f.mode,true));title->width=900;f.mode.error.clear();f.p.UpdateLoadingPlaceholder(.1f);assert(f.spinner.visible&&f.p.ERR_ProbePreparationVisible(&f.mode,false));
f.mode.phase=ERR_Phase::PRISON;f.p.UpdateLoadingPlaceholder(.1f);assert(!root->removed&&!f.parent.removed&&f.spinner.visible&&f.p.ERR_ProbePreparationVisible(&f.mode,false));
assert(description->text=="Please wait while your spawn area and equipment finish loading. You will take control when preparation is complete.");
for(int phase=ERR_Phase::PRISON;phase<=ERR_Phase::EXTRACTION;phase++){f.mode.phase=phase;for(int i=0;i<200;i++)f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.m_ERRPreparationRoot==root&&f.p.ERR_ProbePreparationVisible(&f.mode,false));}
f.mode.phase=ERR_Phase::VICTORY;f.p.UpdateLoadingPlaceholder(.1f);assert(root->removed&&f.parent.removed&&f.spinner.visible&&!f.p.m_ERRPreparationRoot&&f.p.destroys==1);
}
for(int phase : {ERR_Phase::VICTORY,ERR_Phase::DEFEAT}){Fixture f;f.p.UpdateLoadingPlaceholder(.1f);f.p.ERR_KeepSpawnSceneLoading(true);f.mode.phase=phase;f.p.UpdateLoadingPlaceholder(.1f);assert(f.parent.removed&&!f.p.m_ERRKeepSpawnSceneLoading&&f.p.destroys==1);}
{Fixture f;f.p.ERR_KeepSpawnSceneLoading(true);ERR_GameMode replacement;ERR_GameMode::current=&replacement;f.p.DestroyLoadingPlaceholder();assert(f.parent.removed&&f.p.destroys==1);}
{Fixture f;f.p.ERR_KeepSpawnSceneLoading(true);ERR_GameMode::current=nullptr;f.p.DestroyLoadingPlaceholder();assert(f.parent.removed&&f.p.destroys==1);}
for(int mismatch=0;mismatch<6;mismatch++){Fixture f;ERR_GameMode other;if(mismatch==0)ERR_GameMode::current=nullptr;if(mismatch==1)f.p.m_pGameMode=&other;if(mismatch==2)f.mode.phase=-1;if(mismatch==3)System::console=true;if(mismatch==4)f.p.m_wLoadingPlaceholder=nullptr;if(mismatch==5)f.p.m_pGameMode=nullptr;f.p.UpdateLoadingPlaceholder(.1f);assert(game.workspace.creates==0&&f.p.nativeUpdates==1&&!f.parent.removed&&f.spinner.visibilityWrites==0);}
{Fixture f;f.mode.phase=ERR_Phase::PRISON;f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.ERR_ProbePreparationVisible(&f.mode,false));Widget* root=f.p.m_ERRPreparationRoot;f.p.DestroyLoadingPlaceholder();assert(root->removed&&f.parent.removed&&!f.p.m_ERRPreparationRoot&&f.p.destroys==1);f.p.UpdateLoadingPlaceholder(.1f);assert(game.workspace.creates==1);}
{Fixture f;f.mode.error="bad";f.spinner.visible=false;f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.ERR_ProbePreparationVisible(&f.mode,true));f.p.ERR_ClearPreparationStatus();assert(!f.spinner.visible);}
{Fixture f;f.mode.error="bad";f.p.UpdateLoadingPlaceholder(.1f);Widget* old=f.p.m_ERRPreparationRoot;Widget newParent,newSpinner;newParent.children["Spinner"]=&newSpinner;newSpinner.parent=&newParent;f.p.m_wLoadingPlaceholder=&newParent;f.p.UpdateLoadingPlaceholder(.1f);assert(old->removed&&f.spinner.visible&&!newSpinner.visible&&f.p.m_ERRPreparationParent==&newParent&&game.workspace.creates==2);f.p.DestroyLoadingPlaceholder();assert(newParent.removed&&!f.parent.removed&&newSpinner.visible&&!f.p.m_ERRPreparationRoot&&f.p.destroys==1);}
{Fixture f;f.mode.error="bad";f.p.UpdateLoadingPlaceholder(.1f);f.p.m_ERRPreparationParent=nullptr;f.p.m_ERRPreparationRoot=nullptr;f.p.m_ERRPreparationTitle=nullptr;f.p.m_ERRPreparationDescription=nullptr;f.p.m_ERRPreparationSpinner=nullptr;f.p.m_wLoadingPlaceholder=nullptr;f.p.UpdateLoadingPlaceholder(.1f);assert(!f.p.m_ERRPreparationSpinnerHidden);}
{Fixture f;game.workspace.fail=true;f.p.UpdateLoadingPlaceholder(.1f);assert(!f.p.m_ERRPreparationRoot&&f.spinner.visibilityWrites==0);game.workspace.fail=false;f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.ERR_ProbePreparationVisible(&f.mode,false));}
{Fixture f;game.workspace.missingText=true;f.p.UpdateLoadingPlaceholder(.1f);assert(!f.p.m_ERRPreparationRoot&&f.spinner.visibilityWrites==0);}
{Fixture f;f.p.UpdateLoadingPlaceholder(.1f);Widget other;Widget* moved=f.p.m_ERRPreparationRoot;moved->parent=&other;f.p.UpdateLoadingPlaceholder(.1f);assert(moved->removed&&f.p.m_ERRPreparationRoot!=moved&&f.p.m_ERRPreparationParent==&f.parent);}
}
''')
probe=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativePreparationStatusProbe.c'
s=body(probe,'static int Tick(').replace('ERR_GameMode mode,','ERR_GameMode* mode,').replace('ERR_GameMode.Get()','ERR_GameMode::Get()').replace('ERR_Phase.PREPARING','PREPARING').replace('mode.','mode->').replace('.IsEmpty()','.empty()').replace('SCR_RespawnSystemComponent respawn','SCR_RespawnSystemComponent* respawn').replace('SCR_RespawnSystemComponent.GetInstance()','SCR_RespawnSystemComponent::GetInstance()').replace('respawn.','respawn->').replace('System.MakeScreenshot','System::MakeScreenshot')
run(r'''
#include <cassert>
#include <string>
using string=std::string;const int PREPARING=0;
struct ERR_GameMode{static ERR_GameMode* current;int phase=0;string error="bad";static ERR_GameMode* Get(){return current;}int Phase(){return phase;}string PreparationError(){return error;}};ERR_GameMode* ERR_GameMode::current=nullptr;
struct SCR_RespawnSystemComponent{bool visible=true;static SCR_RespawnSystemComponent instance;static SCR_RespawnSystemComponent* GetInstance(){return &instance;}bool ERR_ProbePreparationVisible(ERR_GameMode*,bool failed){assert(failed);return visible;}};SCR_RespawnSystemComponent SCR_RespawnSystemComponent::instance;
namespace System{int captures=0;void MakeScreenshot(const char*){captures++;}}void Print(const char*){}
struct Probe{static ERR_GameMode* s_Mode;static float s_Deadline,s_Since,s_Last,s_CaptureAt;
'''+s+r'''
};ERR_GameMode* Probe::s_Mode=nullptr;float Probe::s_Deadline=0,Probe::s_Since=-1,Probe::s_Last=-1,Probe::s_CaptureAt=-1;
int main(){ERR_GameMode mode;ERR_GameMode::current=&mode;
for(int i=0;i<=33;i++){assert(Probe::Tick(&mode,i*500)==0);}assert(System::captures==0);assert(Probe::Tick(&mode,17000)==0&&System::captures==1);assert(Probe::Tick(&mode,17500)==1&&System::captures==1);
ERR_GameMode next;ERR_GameMode::current=&next;assert(Probe::Tick(&next,20000)==0);assert(Probe::Tick(&next,22000)==0&&Probe::s_Since==22000);SCR_RespawnSystemComponent::instance.visible=false;assert(Probe::Tick(&next,22500)==0&&Probe::s_Since==-1);assert(Probe::Tick(&next,50000)==-1);
}
''')
text=SOURCE.read_text()
prefab=(ROOT/'addons/core/Prefabs/ERR/GameMode.et').read_text()
assert 'm_bUseSpawnPreload 1' in prefab and 'm_bUseSpawnPreload 0' not in prefab
assert 'GetOwner(' not in text
assert text.count('m_pGameMode != mode') == 3
# m_pGameMode is the installed vanilla protected owner field, assigned by
# SCR_RespawnSystemComponent.OnInit(IEntity owner). The adapter deliberately
# exposes that field rather than inventing a ScriptComponent.GetOwner API.
for forbidden in ('CallLater(', 'SetCamera(', 'SpawnEntity', 'SetControlledEntity(', 'Transition(', 'PreparationError() ='):
assert forbidden not in text,forbidden
native=probe.read_text()
for forbidden in ('CreateWidgets(', 'UpdateLoadingPlaceholder(', 'SetText(', 'SetVisible('):assert forbidden not in native
base=(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c').read_text()
assert 'ERR_NativePreparationStatusProbe.Tick(this, now)' in base
assert 'native-end-active' in base and 'native-end-preparing' in base
print('PASS: actual preparation status scope, persistent updates, spinner restoration, placeholder replacement/deletion/destruction, native observation persistence/timeout; rendering remains native-pending')
# Execute the production world-system rebind that restores frame delivery.
source=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverManagers.c'
bind=body(source,'void ERR_BindEscapeRespawn(').replace('SCR_RespawnSystemComponent respawn','Respawn* respawn').replace('ERR_GameMode mode = ERR_GameMode.Get();','Mode* mode = Mode::current;').replace('SCR_RespawnSystemComponent.Cast(mode.FindComponent(SCR_RespawnSystemComponent))','mode->respawn')
run(r"""
#include <cassert>
struct Respawn{int frames=0;};struct Mode{inline static Mode* current=nullptr;Respawn* respawn=nullptr;};void Print(const char*){}
struct System{Respawn* m_RespawnSystem=nullptr;bool m_bReady=true,enabled=false;int enables=0;void Enable(bool value){enabled=value;enables++;}
"""+bind+r"""
void Frame(){if(enabled&&m_RespawnSystem)m_RespawnSystem->frames++;}
};
int main(){System system;Mode mode;Respawn original,replacement,unrelated;Mode::current=&mode;mode.respawn=&replacement;system.m_RespawnSystem=&original;
system.ERR_BindEscapeRespawn(&unrelated);assert(system.enables==0&&system.m_RespawnSystem==&original);
system.ERR_BindEscapeRespawn(nullptr);assert(system.enables==0);
system.ERR_BindEscapeRespawn(&replacement);assert(system.enabled&&!system.m_bReady&&system.m_RespawnSystem==&replacement);system.Frame();assert(replacement.frames==1&&original.frames==0);
system.m_bReady=true;system.ERR_BindEscapeRespawn(&replacement);assert(system.m_bReady); // Idempotent: keep current deploy readiness.
Mode::current=nullptr;system.ERR_BindEscapeRespawn(&original);assert(system.m_RespawnSystem==&replacement);
}
""")
create=body(SOURCE,'protected override void CreateLoadingPlaceholder(')
assert create.index('super.CreateLoadingPlaceholder()') < create.index('ERR_BindEscapeRespawn(this)')
assert 'm_fAutoReloadTime 120' in [line.strip() for line in prefab.splitlines()]
print('PASS: replaced respawn system receives frame updates, disabled updater wakes, stale/non-Escape binding refused; terminal and stale preload leases release black overlay')