Improve base guidance and qualify bounded aircraft route and hidden test lifecycle

This commit is contained in:
2026-09-19 16:41:32 +02:00
parent 6f0e4c5978
commit 5a24eea2f5
192 changed files with 27066 additions and 46 deletions
@@ -318,6 +318,10 @@ modded class ERR_GameMode
if (m_ERRStartupFailure)
{
if (PreparationError().IsEmpty()) return;
int presentation = ERR_NativePreparationStatusProbe.Tick(this, now);
if (presentation < 0) { ERR_BaseCheck(false, "actual persistent startup-failure loading presentation within30seconds"); return; }
if (presentation == 0) return;
if (!ERR_BaseCheck(true, "actual failed-start loading widgets persist beyond17seconds with spinner hidden; diagnostic screenshot requested")) return;
if (!ERR_BaseCheck(Phase() == ERR_Phase.PREPARING && !Active() && m_Stopped, "invalid faction setup stops services without starting the mission")) return;
array<IEntity> players = {};
ERR_Actors.Players(players);
@@ -0,0 +1,38 @@
#ifdef WORKBENCH
// Only the explicit startup-failure fixture calls this read-only observation.
// No placeholder/widget creation, presentation calls or preparation-state writes.
class ERR_NativePreparationStatusProbe
{
protected static ERR_GameMode s_Mode;
protected static float s_Deadline;
protected static float s_Since = -1;
protected static float s_Last = -1;
protected static float s_CaptureAt = -1;
static int Tick(ERR_GameMode mode, float now)
{
if (s_Mode != mode)
{
s_Mode = mode; s_Deadline = now + 30000;
s_Since = -1; s_Last = -1; s_CaptureAt = -1;
}
if (!mode || mode != ERR_GameMode.Get() || mode.Phase() != ERR_Phase.PREPARING || mode.PreparationError().IsEmpty() || now < 0 || now >= s_Deadline) return -1;
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
bool visible = respawn && respawn.ERR_ProbePreparationVisible(mode, true);
if (s_Last >= 0 && (now < s_Last || now - s_Last > 1500)) s_Since = -1;
s_Last = now;
if (!visible) { s_Since = -1; return 0; }
if (s_Since < 0) s_Since = now;
// Exceed the old15-second hint lifetime; inspect the real widget every tick.
if (now - s_Since < 17000) return 0;
if (s_CaptureAt < 0)
{
System.MakeScreenshot("$logs:err-preparation-failure");
s_CaptureAt = now;
Print("ERR PREPARATION UI: actual failed-start title/body and hidden native spinner observed continuously for17seconds; screenshot requested");
return 0;
}
if (now - s_CaptureAt < 500) return 0;
return 1;
}
}
#endif
@@ -44,9 +44,17 @@ class ERR_ObjectiveText
{
array<ERR_LocationComponent> locations = {};
mode.Locations(locations);
bool roughRadio;
foreach (ERR_LocationComponent location : locations)
if (location.Kind() == ERR_LocationKind.RADIO && location.Knowledge() == 2)
{
if (!location || location.Kind() != ERR_LocationKind.RADIO) continue;
if (location.Knowledge() == 2)
return "The radio station is identified on your map. Reach its radio and complete the 'Contact extraction HQ' action to start the transmission.";
if (location.Knowledge() == 1) roughRadio = true;
}
// Scan all sites before acknowledging rough intel: an identified radio wins.
if (roughRadio)
return "Intel recovered: a 500 m search square is now marked on the map. Use a carried field map to view it. Take a different intelligence folder into your inventory to pinpoint the radio; a folder already recovered cannot provide another clue. Try the other marked supply cache if needed. A close approach with a clear view can also identify the station.";
return "Choose either marked supply cache. Take its intelligence folders into your own inventory to reveal the station on your map. If only a search area appears, take another folder. Each cache has enough folders; use the other if one is lost. If every folder is lost, a close approach with a clear view can identify the station itself. Nearby medical and guard sites can also be discovered by approaching with a clear view.";
}
if (phase == ERR_Phase.RADIO_DEFENCE)
+1 -1
View File
@@ -229,7 +229,7 @@ modded class SCR_PlayerController
{
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (!hints) return; // retry on the existing monitor; do not lose the first hint
hints.ShowCustom(ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable), "Spectating", 12);
if (!hints.ShowCustom(ERR_ObserverGuidance.Message(m_ERRObserverReason, survivorAvailable), "Spectating", 12)) return;
m_ERRObserverReady = true;
m_ERRShownObserverReason = m_ERRObserverReason;
m_ERRShownObserverAvailable = survivorAvailable;
@@ -0,0 +1,174 @@
#ifndef WORKBENCH
// Explicit ordinary-game observation only. No input, actor, menu or camera writes.
class ERR_OrdinaryInputObserverRules
{
static bool Nonce(string nonce)
{
if (nonce.Length() < 1 || nonce.Length() > 32) return false;
string allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for (int i = 0; i < nonce.Length(); i++) if (!allowed.Contains(nonce[i])) return false;
return true;
}
static bool Within(int start, int previous, int now, int samples)
{
return now >= start && now >= previous && now - start < 45000 && samples < 450;
}
static bool Stationary(float forward, vector input, vector velocity, float displacement)
{
return Math.AbsFloat(forward) <= 0.01 && input.Length() <= 0.01 && velocity.Length() <= 0.05 && displacement >= 0 && displacement <= 0.05;
}
}
class ERR_OrdinaryInputObserver : Managed
{
protected static ref ERR_OrdinaryInputObserver s_Request;
protected static bool s_Attempted;
protected ERR_GameMode m_Mode;
protected BaseWorld m_World;
protected PlayerController m_Controller;
protected IEntity m_Actor;
protected CharacterControllerComponent m_Character;
protected ref ERR_PlayerLife m_Life;
protected UUID m_Account;
protected int m_PlayerId;
protected string m_Nonce;
protected bool m_Screenshots;
protected bool m_Finished;
protected bool m_Ready;
protected bool m_Opened;
protected bool m_Closed;
protected SCR_InventoryMenuUI m_Menu;
protected int m_Start;
protected int m_Previous;
protected int m_Samples;
protected int m_BaselineSince = -1;
protected vector m_BaselineOrigin;
protected float m_LastWorldTime;
protected static bool Enabled(out string nonce)
{
string project;
if (!Replication.IsServer() || System.IsConsoleApp()) return false;
if (!System.GetCLIParam("errOrdinaryScenario", project) || (project != "core" && project != "chat")) return false;
return System.GetCLIParam("errOrdinaryInputObserve", nonce) && ERR_OrdinaryInputObserverRules.Nonce(nonce);
}
static void Queue(ERR_GameMode mode, PlayerController controller, IEntity actor)
{
string nonce;
if (s_Attempted || !Enabled(nonce) || !GetGame() || !mode || !controller || !actor || !mode.Players()) return;
ERR_PlayerLife life = mode.Players().CurrentActor(actor);
if (!life) return;
ERR_OrdinaryInputObserver request = new ERR_OrdinaryInputObserver();
request.m_Mode = mode; request.m_World = GetGame().GetWorld();
request.m_Controller = controller; request.m_Actor = actor;
request.m_Character = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
request.m_Life = life; request.m_Account = life.AccountId;
request.m_PlayerId = controller.GetPlayerId(); request.m_Nonce = nonce;
if (!request.Valid()) return;
string screenshots;
request.m_Screenshots = System.GetCLIParam("errOrdinaryInputScreenshots", screenshots) && screenshots == "1";
request.m_Start = System.GetTickCount(); request.m_Previous = request.m_Start;
request.m_LastWorldTime = request.m_World.GetWorldTime();
s_Attempted = true; s_Request = request;
mode.OnStop().Insert(request.Cancel);
request.Record("QUEUED");
GetGame().GetCallqueue().CallLater(request.Sample, 100, false);
}
protected bool Valid()
{
string nonce;
if (!Enabled(nonce) || nonce != m_Nonce || !GetGame() || !m_Mode || m_Mode != ERR_GameMode.Get()) return false;
if (!m_World || GetGame().GetWorld() != m_World || m_Mode.GetWorld() != m_World || !m_Mode.Active() || m_Mode.Phase() != ERR_Phase.PRISON) return false;
if (!m_Controller || m_Controller != GetGame().GetPlayerController() || m_Controller.GetPlayerId() != m_PlayerId || m_PlayerId <= 0) return false;
PlayerManager manager = GetGame().GetPlayerManager();
if (!manager || manager.GetPlayerController(m_PlayerId) != m_Controller || manager.GetPlayerControlledEntity(m_PlayerId) != m_Actor || m_Controller.GetControlledEntity() != m_Actor) return false;
if (!ERR_Actors.CanFight(m_Actor) || m_Actor.GetWorld() != m_World || !m_Mode.Players()) return false;
if (!m_Character || m_Actor.FindComponent(CharacterControllerComponent) != m_Character || m_Character.IsUnconscious()) return false;
ERR_PlayerLife current = m_Mode.Players().CurrentActor(m_Actor);
return current && current == m_Life && current.AccountId == m_Account;
}
protected void Record(string detail)
{
PrintFormat("ERR INPUT OBSERVE: run=%1 sample=%2 real=%3 world=%4 %5", m_Nonce, m_Samples, System.GetTickCount(), m_LastWorldTime, detail);
}
protected void Capture(string suffix)
{
if (!m_Screenshots || !Valid()) return;
System.MakeScreenshot("$logs:err-input-" + m_Nonce + "-" + suffix);
Record("CAPTURE_REQUEST " + suffix);
}
// Snapshot sequencing is separate from native reads so its policy is executable.
protected void Observe(int now, vector origin, float forward, vector input, vector velocity, bool inventoryOpen)
{
if (!m_Ready)
{
float displacement = vector.Distance(origin, m_BaselineOrigin);
if (m_BaselineSince < 0) displacement = 0;
if (inventoryOpen || now - m_Previous > 500 || !ERR_OrdinaryInputObserverRules.Stationary(forward, input, velocity, displacement))
{
m_BaselineSince = -1;
return;
}
if (m_BaselineSince < 0) { m_BaselineSince = now; m_BaselineOrigin = origin; }
if (now - m_BaselineSince < 2000) return;
m_Ready = true; Record("READY"); Capture("baseline");
}
if (inventoryOpen && !m_Opened)
{
m_Opened = true; Record("INVENTORY_OPEN"); Capture("open");
}
else if (!inventoryOpen && m_Opened && !m_Closed)
{
m_Closed = true; Record("INVENTORY_CLOSED"); Capture("closed");
}
}
protected void Sample()
{
if (m_Finished) return;
if (!Valid()) { End("CANCELLED original-admission-lost"); return; }
int now = System.GetTickCount();
float worldTime = m_World.GetWorldTime();
if (worldTime < m_LastWorldTime) { End("CANCELLED world-clock-reversed"); return; }
m_LastWorldTime = worldTime;
if (!ERR_OrdinaryInputObserverRules.Within(m_Start, m_Previous, now, m_Samples)) { End("FINISHED observation-bound"); return; }
if (!GetGame().GetInputManager() || !GetGame().GetMenuManager()) { End("CANCELLED native-input-or-menu-unavailable"); return; }
SCR_InventoryMenuUI menu = SCR_InventoryMenuUI.GetInventoryMenu();
bool open;
if (menu)
{
Widget root = menu.GetRootWidget();
if (menu.GetCharacterController() != m_Character || !root || !root.IsVisible() || GetGame().GetMenuManager().GetTopMenu() != menu)
{ End("CANCELLED inventory-context-mismatch"); return; }
if (m_Opened && (m_Closed || menu != m_Menu)) { End("CANCELLED inventory-instance-replaced"); return; }
open = true;
}
vector origin = m_Actor.GetOrigin();
vector input = m_Character.GetMovementInput();
vector velocity = m_Character.GetVelocity();
float forward = GetGame().GetInputManager().GetActionValue("CharacterForward");
m_Samples++;
PrintFormat("ERR INPUT OBSERVE: run=%1 sample=%2 real=%3 world=%4 SAMPLE origin=%5 forward=%6 input=%7 velocity=%8 inventory=%9", m_Nonce, m_Samples, now, m_LastWorldTime, origin, forward, input, velocity, open);
Observe(now, origin, forward, input, velocity, open);
if (m_Opened && !m_Closed && menu) m_Menu = menu;
m_Previous = now;
if (m_Samples >= 450) { End("FINISHED sample-bound"); return; }
GetGame().GetCallqueue().CallLater(Sample, 100, false);
}
protected void End(string reason)
{
if (m_Finished) return;
m_Finished = true;
Record(reason);
if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(Sample);
if (m_Mode) m_Mode.OnStop().Remove(Cancel);
m_Mode = null; m_World = null; m_Controller = null; m_Actor = null;
m_Character = null; m_Life = null; m_Menu = null;
}
void Cancel() { End("CANCELLED mission-or-world-stopped"); }
static void ClearWorld()
{
if (s_Request) s_Request.Cancel();
s_Request = null;
// Process-wide single attempt: a restart cannot silently create new evidence.
}
}
#endif
@@ -248,6 +248,7 @@ modded class ArmaReforgerScripted
override protected void OnBeforeWorldCleanup()
{
ERR_OrdinaryScreenshotRequest.ClearWorld();
ERR_OrdinaryInputObserver.ClearWorld();
if (m_ERRDiagnosticLaunchRequest) m_ERRDiagnosticLaunchRequest.Cancel();
m_ERRDiagnosticLaunchRequest = null;
super.OnBeforeWorldCleanup();
@@ -267,6 +268,7 @@ modded class ERR_GameMode
if (!Players().CurrentActor(actor) || Phase() != ERR_Phase.PRISON) return;
PrintFormat("ERR ORDINARY: local participant verified project=%1 phase=prison", project);
ERR_OrdinaryScreenshotRequest.Queue(this, local, actor);
ERR_OrdinaryInputObserver.Queue(this, local, actor);
}
}
@@ -25,12 +25,13 @@ modded class SCR_PlayerController
{
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode) return;
if (mode.Phase() != ERR_Phase.PREPARING && !GetControlledEntity()) return;
if (mode.Phase() != ERR_Phase.PREPARING && (ERR_ObserverState() != ERR_ObserverReason.NONE || !ERR_Actors.Alive(GetControlledEntity()))) return;
string message = ERR_ObjectiveText.Description(mode, false);
if (message.IsEmpty()) return;
if (m_ERRGuidePhase == mode.Phase() && m_ERRGuideMessage == message) return;
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (!hints) return;
if (!message.IsEmpty()) hints.ShowCustom(message, ERR_ObjectiveText.Title(mode.Phase()), 15);
if (!hints.ShowCustom(message, ERR_ObjectiveText.Title(mode.Phase()), 15)) return;
m_ERRGuidePhase = mode.Phase();
m_ERRGuideMessage = message;
}
@@ -0,0 +1,105 @@
// Local presentation attached to the native respawn loading placeholder.
// No timer, camera, admission, or mission-state changes.
modded class SCR_RespawnSystemComponent
{
protected Widget m_ERRPreparationParent;
protected Widget m_ERRPreparationRoot;
protected TextWidget m_ERRPreparationTitle;
protected TextWidget m_ERRPreparationDescription;
protected Widget m_ERRPreparationSpinner;
protected bool m_ERRPreparationSpinnerHidden;
protected bool m_ERRPreparationSpinnerWasVisible;
protected string m_ERRPreparationLastTitle;
protected string m_ERRPreparationLastDescription;
protected void ERR_ClearPreparationStatus()
{
if (m_ERRPreparationSpinnerHidden && m_ERRPreparationSpinner)
m_ERRPreparationSpinner.SetVisible(m_ERRPreparationSpinnerWasVisible);
m_ERRPreparationSpinnerHidden = false;
m_ERRPreparationSpinner = null;
if (m_ERRPreparationRoot) m_ERRPreparationRoot.RemoveFromHierarchy();
m_ERRPreparationRoot = null;
m_ERRPreparationParent = null;
m_ERRPreparationTitle = null;
m_ERRPreparationDescription = null;
m_ERRPreparationLastTitle = "";
m_ERRPreparationLastDescription = "";
}
override void UpdateLoadingPlaceholder(float dt)
{
super.UpdateLoadingPlaceholder(dt);
// Vanilla OnInit(IEntity owner) stores its owning mode in m_pGameMode.
ERR_GameMode mode = ERR_GameMode.Get();
if (System.IsConsoleApp() || !mode || m_pGameMode != mode || mode.Phase() != ERR_Phase.PREPARING || !m_wLoadingPlaceholder)
{
ERR_ClearPreparationStatus();
return;
}
if (m_ERRPreparationParent != m_wLoadingPlaceholder || !m_ERRPreparationRoot || m_ERRPreparationRoot.GetParent() != m_wLoadingPlaceholder)
{
ERR_ClearPreparationStatus();
if (!GetGame() || !GetGame().GetWorkspace()) return;
m_ERRPreparationRoot = GetGame().GetWorkspace().CreateWidgets("{BCCAE287751344D9}UI/layouts/ERR/PreparationStatus.layout", m_wLoadingPlaceholder);
if (!m_ERRPreparationRoot) return;
m_ERRPreparationParent = m_wLoadingPlaceholder;
m_ERRPreparationTitle = TextWidget.Cast(m_ERRPreparationRoot.FindAnyWidget("ERR_PreparationTitle"));
m_ERRPreparationDescription = TextWidget.Cast(m_ERRPreparationRoot.FindAnyWidget("ERR_PreparationDescription"));
if (!m_ERRPreparationTitle || !m_ERRPreparationDescription) { ERR_ClearPreparationStatus(); return; }
m_ERRPreparationSpinner = m_wLoadingPlaceholder.FindAnyWidget("Spinner");
}
bool failed = !mode.PreparationError().IsEmpty();
string title = "Preparing mission";
if (failed) title = "Mission could not start";
string description = ERR_ObjectiveText.Description(mode, false);
if (title != m_ERRPreparationLastTitle)
{
m_ERRPreparationTitle.SetText(title);
m_ERRPreparationLastTitle = title;
}
if (description != m_ERRPreparationLastDescription)
{
m_ERRPreparationDescription.SetText(description);
m_ERRPreparationLastDescription = description;
}
if (failed && m_ERRPreparationSpinner && !m_ERRPreparationSpinnerHidden)
{
m_ERRPreparationSpinnerWasVisible = m_ERRPreparationSpinner.IsVisible();
m_ERRPreparationSpinner.SetVisible(false);
m_ERRPreparationSpinnerHidden = true;
}
else if (!failed && m_ERRPreparationSpinnerHidden)
{
if (m_ERRPreparationSpinner) m_ERRPreparationSpinner.SetVisible(m_ERRPreparationSpinnerWasVisible);
m_ERRPreparationSpinnerHidden = false;
}
}
override void DestroyLoadingPlaceholder()
{
ERR_ClearPreparationStatus();
super.DestroyLoadingPlaceholder();
}
#ifdef WORKBENCH
protected bool ERR_PreparationWidgetVisible(Widget widget)
{
if (!widget || !widget.IsVisible()) return false;
float width, height;
widget.GetScreenSize(width, height);
return width > 0 && height > 0;
}
// Inspect the actual production-created widgets, without creating or changing them.
bool ERR_ProbePreparationVisible(ERR_GameMode mode, bool failed)
{
if (!mode || mode != ERR_GameMode.Get() || m_pGameMode != mode || !m_wLoadingPlaceholder || !m_wLoadingPlaceholder.IsVisible()) return false;
if (!m_ERRPreparationRoot || m_ERRPreparationParent != m_wLoadingPlaceholder || m_ERRPreparationRoot.GetParent() != m_wLoadingPlaceholder || !m_ERRPreparationRoot.IsVisible()) return false;
if (!ERR_PreparationWidgetVisible(m_ERRPreparationTitle) || !ERR_PreparationWidgetVisible(m_ERRPreparationDescription) || !ERR_PreparationWidgetVisible(m_ERRPreparationRoot)) return false;
string title = "Preparing mission"; if (failed) title = "Mission could not start";
if (m_ERRPreparationTitle.GetText() != title || m_ERRPreparationDescription.GetText() != ERR_ObjectiveText.Description(mode, false)) return false;
if (failed) return m_ERRPreparationSpinner && m_ERRPreparationSpinnerHidden && !m_ERRPreparationSpinner.IsVisible();
return !m_ERRPreparationSpinnerHidden;
}
#endif
}