Fix inventory layout integration and qualify ordinary UI and radio guidance

This commit is contained in:
2026-09-19 18:17:51 +02:00
parent 5c7719c19c
commit 67de3752fb
125 changed files with 18310 additions and 42 deletions
@@ -0,0 +1,112 @@
// Successor-only compatibility for the installed stock inventory storage list.
// Its stock HandlerAttached uses LayoutSlot on a root inserted under a grid.
// Supply a real linear-layout parent; retain the complete stock handler unchanged.
class ERR_InventoryGridPlacement
{
protected int m_Row;
protected int m_Column;
protected int m_RowSpan;
protected int m_ColumnSpan;
protected LayoutHorizontalAlign m_Horizontal;
protected LayoutVerticalAlign m_Vertical;
protected float m_Left, m_Top, m_Right, m_Bottom;
bool Capture(Widget widget)
{
if (!widget || !GridLayoutWidget.Cast(widget.GetParent())) return false;
m_Row = GridSlot.GetRow(widget); m_Column = GridSlot.GetColumn(widget);
m_RowSpan = GridSlot.GetRowSpan(widget); m_ColumnSpan = GridSlot.GetColumnSpan(widget);
m_Horizontal = AlignableSlot.GetHorizontalAlign(widget);
m_Vertical = AlignableSlot.GetVerticalAlign(widget);
AlignableSlot.GetPadding(widget, m_Left, m_Top, m_Right, m_Bottom);
return true;
}
bool Apply(Widget widget)
{
if (!widget || !GridLayoutWidget.Cast(widget.GetParent())) return false;
GridSlot.SetRow(widget, m_Row); GridSlot.SetColumn(widget, m_Column);
GridSlot.SetRowSpan(widget, m_RowSpan); GridSlot.SetColumnSpan(widget, m_ColumnSpan);
AlignableSlot.SetHorizontalAlign(widget, m_Horizontal);
AlignableSlot.SetVerticalAlign(widget, m_Vertical);
AlignableSlot.SetPadding(widget, m_Left, m_Top, m_Right, m_Bottom);
return true;
}
}
modded class SCR_InventoryMenuUI
{
// Weak widget identities belong to this exact menu's native hierarchy.
// The empty wrapper survives stock storage-list refresh, then dies with the menu.
protected Widget m_ERRStorageLayoutRoot;
protected GridLayoutWidget m_ERRStorageLayoutGrid;
protected VerticalLayoutWidget m_ERRStorageLayoutHost;
protected bool m_ERRStorageLayoutBound;
bool ERR_PrepareStorageLayout(Widget child)
{
if (!GetGame() || !GetGame().GetWorkspace() || !GetGame().GetMenuManager()) return false;
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || !mode.Active() || GetGame().GetWorld() != mode.GetWorld()) return false;
if (SCR_InventoryMenuUI.GetInventoryMenu() != this) return false;
Widget root = GetRootWidget();
if (!root || !child || child.GetName() != "Frame0") return false;
GridLayoutWidget grid = GridLayoutWidget.Cast(root.FindAnyWidget("StoragesListSlot"));
if (!grid || child.GetParent() != grid) return false;
if (m_ERRStorageLayoutBound && (m_ERRStorageLayoutRoot != root || m_ERRStorageLayoutGrid != grid || !m_ERRStorageLayoutHost)) return false;
if (m_ERRStorageLayoutHost && (m_ERRStorageLayoutHost.GetParent() != grid || m_ERRStorageLayoutHost.GetChildren())) return false;
// Never adopt a foreign widget that happens to use our private name.
Widget named = grid.FindAnyWidget("ERR_InventoryStorageLayout");
if (named && named != m_ERRStorageLayoutHost) return false;
ref ERR_InventoryGridPlacement placement = new ERR_InventoryGridPlacement();
if (!placement.Capture(child)) return false;
bool created;
ref ERR_InventoryGridPlacement previousHost = new ERR_InventoryGridPlacement();
if (m_ERRStorageLayoutHost)
{
if (!previousHost.Capture(m_ERRStorageLayoutHost)) return false;
}
else
{
Widget host = GetGame().GetWorkspace().CreateWidget(WidgetType.VerticalLayoutWidgetTypeID, WidgetFlags.VISIBLE, null, 0, grid);
m_ERRStorageLayoutHost = VerticalLayoutWidget.Cast(host);
if (!m_ERRStorageLayoutHost)
{
if (host) host.RemoveFromHierarchy();
return false;
}
m_ERRStorageLayoutHost.SetName("ERR_InventoryStorageLayout");
created = true;
}
if (!placement.Apply(m_ERRStorageLayoutHost))
{
if (created) { m_ERRStorageLayoutHost.RemoveFromHierarchy(); m_ERRStorageLayoutHost = null; }
return false;
}
m_ERRStorageLayoutHost.AddChild(child);
if (!child || !m_ERRStorageLayoutHost) return false;
if (child.GetParent() != m_ERRStorageLayoutHost)
{
// Do not move a widget which an intervening callback moved elsewhere.
if (child.GetParent() == grid) placement.Apply(child);
if (created && !m_ERRStorageLayoutHost.GetChildren()) { m_ERRStorageLayoutHost.RemoveFromHierarchy(); m_ERRStorageLayoutHost = null; }
else if (!created) previousHost.Apply(m_ERRStorageLayoutHost);
return false;
}
// Outer grid placement retains padding/alignment; the sole inner child fills it.
AlignableSlot.SetPadding(child, 0, 0, 0, 0);
AlignableSlot.SetHorizontalAlign(child, LayoutHorizontalAlign.Stretch);
AlignableSlot.SetVerticalAlign(child, LayoutVerticalAlign.Stretch);
m_ERRStorageLayoutRoot = root; m_ERRStorageLayoutGrid = grid;
m_ERRStorageLayoutBound = true;
return true;
}
}
modded class SCR_InventoryStoragesListUI
{
override void HandlerAttached(Widget w)
{
// m_MenuHandler is assigned by the stock constructor before AddHandler.
// All mismatches/failures retain stock behavior, including any original error.
if (GetGame() && GetGame().GetMenuManager() && m_MenuHandler && m_MenuHandler == SCR_InventoryMenuUI.GetInventoryMenu())
m_MenuHandler.ERR_PrepareStorageLayout(w);
super.HandlerAttached(w);
}
}
@@ -783,6 +783,7 @@ modded class ERR_GameMode
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;
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.WAITING && Objectives().Current() && Objectives().Current().GetTaskDescription().Contains("Waiting for a conscious defender") && !Objectives().Current().GetTaskDescription().Contains("interrupted"), "native radio task reports waiting before first occupancy sample")) return;
m_ERRBaseStep = 31;
m_ERRBaseWait = now + 5000;
}
@@ -62,7 +62,11 @@ class ERR_ObjectiveText
if (!progress) return RadioFeedbackMessage(mode.RadioFeedback());
int seconds = mode.DefenceSecondsRemaining();
if (!mode.DefenceOccupied())
{
if (mode.RadioFeedback() == ERR_RadioFeedback.WAITING)
return string.Format("Waiting for a conscious defender near the radio. Keep someone within 60 metres for %1 seconds to finish the transmission. Leaving resets progress.", seconds);
return string.Format("Transmission interrupted. Keep a conscious survivor within 60 metres of the radio for %1 seconds. Leaving resets progress.", seconds);
}
int displayed = Math.Ceil(seconds / 5.0) * 5;
return string.Format("Transmission in progress: up to %1 seconds remaining. Keep a conscious survivor within 60 metres of the radio. Leaving resets progress.", displayed);
}
@@ -1,4 +1,19 @@
#ifndef WORKBENCH
// Opt-in observer calls this read-only snapshot only after exact menu ownership checks.
modded class SCR_InventoryMenuUI
{
string ERR_InputObserverInventoryState()
{
int hitZones = -1;
if (m_aHitZonePoints) hitZones = m_aHitZonePoints.Count();
Widget root = GetRootWidget();
Widget overlay;
if (root) overlay = root.FindAnyWidget("LoadingOverlay");
bool overlayPresent = overlay != null;
bool overlayVisible = overlay && overlay.IsVisibleInHierarchy();
return string.Format("pending=%1 stage=%2 hitZones=%3 overlay=%4 overlayVisible=%5", m_bProcessInitQueue, m_eInitStage, hitZones, overlayPresent, overlayVisible);
}
}
// Explicit ordinary-game observation only. No input, actor, menu or camera writes.
class ERR_OrdinaryInputObserverRules
{
@@ -37,6 +52,16 @@ class ERR_OrdinaryInputObserver : Managed
protected bool m_Opened;
protected bool m_Closed;
protected SCR_InventoryMenuUI m_Menu;
protected string m_CaptureSuffix;
protected SCR_InventoryMenuUI m_CaptureMenu;
protected Widget m_CaptureRoot;
protected int m_CaptureDue;
protected int m_CaptureExpires;
protected int m_CaptureLoadedAt = -1;
protected int m_InventoryStateLines;
protected bool m_BaselineCaptureQueued;
protected bool m_OpenCaptureQueued;
protected bool m_ClosedCaptureQueued;
protected int m_Start;
protected int m_Previous;
protected int m_Samples;
@@ -90,9 +115,98 @@ class ERR_OrdinaryInputObserver : Managed
{
PrintFormat("ERR INPUT OBSERVE: run=%1 sample=%2 real=%3 world=%4 %5", m_Nonce, m_Samples, System.GetTickCount(), m_LastWorldTime, detail);
}
protected void CancelCapture()
{
if (m_CaptureSuffix != "" && GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(CaptureLater);
m_CaptureSuffix = ""; m_CaptureMenu = null; m_CaptureRoot = null; m_CaptureDue = 0; m_CaptureExpires = 0; m_CaptureLoadedAt = -1;
}
protected void Capture(string suffix)
{
if (!m_Screenshots || !Valid()) return;
if (!m_Screenshots || m_Finished || !Valid() || !GetGame().GetCallqueue() || !GetGame().GetMenuManager()) return;
if (suffix == "baseline") { if (m_BaselineCaptureQueued) return; m_BaselineCaptureQueued = true; }
else if (suffix == "open") { if (m_OpenCaptureQueued) return; m_OpenCaptureQueued = true; }
else if (suffix == "closed") { if (m_ClosedCaptureQueued) return; m_ClosedCaptureQueued = true; }
else return;
// One owned pending capture. A later transition supersedes its old image;
// never label a later UI state as the earlier transition.
CancelCapture();
SCR_InventoryMenuUI menu = SCR_InventoryMenuUI.GetInventoryMenu();
if (suffix == "open")
{
if (!menu || menu.GetCharacterController() != m_Character || GetGame().GetMenuManager().GetTopMenu() != menu) return;
Widget root = menu.GetRootWidget();
if (!root || !root.IsVisibleInHierarchy()) return;
m_CaptureMenu = menu; m_CaptureRoot = root;
}
else if (menu || GetGame().GetMenuManager().GetTopMenu()) return;
m_CaptureSuffix = suffix;
int captureStart = System.GetTickCount();
m_CaptureDue = captureStart + 300;
m_CaptureExpires = captureStart + 3000;
GetGame().GetCallqueue().CallLater(CaptureLater, 300, false);
}
protected void CaptureLater()
{
string suffix = m_CaptureSuffix;
SCR_InventoryMenuUI expectedMenu = m_CaptureMenu;
Widget expectedRoot = m_CaptureRoot;
int due = m_CaptureDue;
int expires = m_CaptureExpires;
int loadedAt = m_CaptureLoadedAt;
CancelCapture();
if (suffix == "" || !m_Screenshots || m_Finished || !Valid() || !GetGame().GetMenuManager()) return;
int now = System.GetTickCount();
if (now < due || !ERR_OrdinaryInputObserverRules.Within(m_Start, m_Previous, now, m_Samples)) return;
float worldTime = m_World.GetWorldTime();
if (worldTime < m_LastWorldTime) return;
SCR_InventoryMenuUI menu = SCR_InventoryMenuUI.GetInventoryMenu();
if (suffix == "open")
{
if (!m_Opened || m_Closed || !expectedMenu || !expectedRoot || menu != expectedMenu || menu != m_Menu) return;
if (menu.GetCharacterController() != m_Character || menu.GetRootWidget() != expectedRoot || !expectedRoot.IsVisibleInHierarchy()) return;
if (GetGame().GetMenuManager().GetTopMenu() != menu) return;
// Separate prefix preserves the input-observer parser contract. At most
// thirty read-only state lines for this already opt-in owned capture.
if (m_InventoryStateLines < 30)
{
string stateReason = "retry";
if (now + 100 >= expires) stateReason = "timeout";
PrintFormat("ERR INVENTORY OBSERVE: run=%1 real=%2 reason=%3 %4", m_Nonce, now, stateReason, menu.ERR_InputObserverInventoryState());
m_InventoryStateLines++;
}
if (now >= expires) return;
// Stock OnQueueProcessed removes this widget only after inventory init.
// Retain one pending request, never restart its original three-second bound.
bool loading = expectedRoot.FindAnyWidget("LoadingOverlay") != null;
if (loading) loadedAt = -1;
else if (loadedAt < 0) loadedAt = now;
// Require a later revalidated observation after native overlay removal,
// avoiding a screenshot request in the same update that removed it.
if (loading || now - loadedAt < 100)
{
if (now + 100 >= expires || !GetGame().GetCallqueue()) return;
m_CaptureSuffix = suffix; m_CaptureMenu = expectedMenu; m_CaptureRoot = expectedRoot;
m_CaptureDue = now + 100; m_CaptureExpires = expires; m_CaptureLoadedAt = loadedAt;
GetGame().GetCallqueue().CallLater(CaptureLater, 100, false);
return;
}
}
else
{
if (now >= expires) return;
if (menu || GetGame().GetMenuManager().GetTopMenu()) return;
if (suffix == "baseline")
{
if (!m_Ready || m_Opened || m_Closed || !GetGame().GetInputManager()) return;
float forward = GetGame().GetInputManager().GetActionValue("CharacterForward");
vector input = m_Character.GetMovementInput();
vector velocity = m_Character.GetVelocity();
float displacement = vector.Distance(m_Actor.GetOrigin(), m_BaselineOrigin);
if (!ERR_OrdinaryInputObserverRules.Stationary(forward, input, velocity, displacement)) return;
}
if (suffix == "closed" && !m_Closed) return;
}
m_LastWorldTime = worldTime;
System.MakeScreenshot("$logs:err-input-" + m_Nonce + "-" + suffix);
Record("CAPTURE_REQUEST " + suffix);
}
@@ -145,6 +259,7 @@ class ERR_OrdinaryInputObserver : Managed
vector input = m_Character.GetMovementInput();
vector velocity = m_Character.GetVelocity();
float forward = GetGame().GetInputManager().GetActionValue("CharacterForward");
if (m_CaptureSuffix == "baseline" && !ERR_OrdinaryInputObserverRules.Stationary(forward, input, velocity, vector.Distance(origin, m_BaselineOrigin))) CancelCapture();
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);
@@ -157,6 +272,7 @@ class ERR_OrdinaryInputObserver : Managed
{
if (m_Finished) return;
m_Finished = true;
CancelCapture();
Record(reason);
if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(Sample);
if (m_Mode) m_Mode.OnStop().Remove(Cancel);