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,8 @@
ERRIC_InventoryMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
components {
SCR_RespawnSystemComponent "{56B2B4793051E7C9}" {
m_SpawnLogic SCR_SpawnLogic {}
}
}
m_bUseSpawnPreload 0
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{86FBBCE45AF62682}Prefabs/ERRIC_InventoryMode.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS : PC {}
}
}
+19
View File
@@ -0,0 +1,19 @@
# Isolated stock inventory runtime control
This diagnostic addon depends only on installed vanilla Reforger (`58D0FB3206B6F859`). It must run without successor Core, AI, Intel, Map or Chat loaded. It has its own project, world and prefab identities. It is not a player mission or a multiplayer/admission test.
Copy this directory alone as the hidden runner's `addons/core`; select `Worlds/ERRIC_Inventory.ent` and command-line Workbench plugin `ERRIC_InventoryControlPlugin`, with `-erricInventoryControl 1 -errPlay 1`. Keep the separate private profile, muted hidden Gamescope containment, prefix lock and source-bound archive. The fixture emits `ERR SMOKE PASS/FAIL/COMPLETE` with explicit vanilla-control scope. No automatic reload; required completion count is one. Native compilation/runtime execution is pending.
The fixture waits at most 30 seconds in actual local `RplMode.None` for `SCR_PlayerController`. It creates one stock `{2F912ED6E399FF47}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Unarmed.et`, assigns that body once through native `SetControlledEntity`, waits for settling, then calls the controller's actual `ActionOpenInventory`. It verifies the exact body, native menu/top menu, root, native `GridLayoutWidget` named `StoragesListSlot` and its direct `Frame0` child. The open screenshot is requested after 500 ms. Public native `SCR_InventoryStorageManagerComponent.CloseInventory` closes it; another 500 ms later the fixture verifies closure and requests a closed screenshot. World teardown disposes the fixture actor; the diagnostic does not delete a possessed actor or modify its inventory, health, or controls.
`Action_OpenInventory` is not a toggle: the installed implementation always invokes `OpenInventory`. The menu's `Action_CloseInventory` is protected. Therefore this control deliberately uses the public native close method instead of incorrectly invoking the open method twice. No keyboard input or user-input usability is claimed.
Retain all raw GUI errors. The suspected vanilla source path is:
* `scripts/Game/Player/SCR_PlayerController.c:965`: `ActionOpenInventory` delegates to the body's native inventory manager.
* `scripts/Game/Inventory/SCR_InventoryStorageManagerComponent.c:1756,1824`: normal inventory-open path uses `ChimeraMenuPreset.Inventory20Menu`.
* `scripts/Game/UI/Inventory/SCR_InventoryMenuUI.c:1694`: `ShowStoragesList` creates `{FC579324F5E4B3A4}UI/layouts/Menus/Inventory/InventoryCharacterGrid.layout` beneath `StoragesListSlot`, then attaches `SCR_InventoryStoragesListUI`.
* The installed `InventoryMain.layout` defines `StoragesListSlot` as a `GridLayoutWidget`; `InventoryCharacterGrid.layout` defines its root as `OverlayWidget Frame0`.
* `scripts/Game/UI/Inventory/SCR_InventoryStoragesListUI.c:301–302` unconditionally calls two `LayoutSlot` operations on that root, which now has a native grid slot.
Successor ordinary runs 16:55:12 and 16:58:32 recorded exactly two `OverlayWidget Frame0 has invalid slot GridWidgetSlot ... expected LayoutSlot` errors when opening inventory. This source chain is a concrete hypothesis; this control must reproduce it without successor addons before attributing runtime errors to vanilla. A completed fixture alongside retained GUI errors is a failed clean-runtime report, not an error-free qualification. Workbench control evidence does not establish ordinary-game module parity.
@@ -0,0 +1,126 @@
// Diagnostic addon only. No successor scripts/resources or player audit policy.
#ifdef WORKBENCH
class ERRIC_InventoryModeClass : SCR_BaseGameModeClass {}
class ERRIC_InventoryMode : SCR_BaseGameMode
{
protected SCR_PlayerController m_Controller;
protected IEntity m_Character;
protected SCR_InventoryStorageManagerComponent m_Inventory;
protected SCR_InventoryMenuUI m_Menu;
protected int m_Deadline;
protected int m_Wait;
protected int m_Step;
protected bool m_Finished;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string enabled;
if (SCR_Global.IsEditMode() || !System.GetCLIParam("erricInventoryControl", enabled) || enabled != "1") return;
if (!Replication.IsServer() || RplSession.Mode() != RplMode.None)
{
Fail("requires isolated native singleplayer Workbench mode");
return;
}
m_Deadline = System.GetTickCount() + 30000;
GetGame().GetCallqueue().CallLater(Probe, 100, true);
}
protected void Fail(string reason)
{
if (m_Finished) return;
m_Finished = true;
if (GetGame()) GetGame().GetCallqueue().Remove(Probe);
Print("ERR SMOKE FAIL: vanilla inventory control: " + reason, LogLevel.ERROR);
}
protected bool Check(bool condition, string reason)
{
if (!condition) { Fail(reason); return false; }
Print("ERR SMOKE PASS: vanilla inventory control: " + reason);
return true;
}
protected bool SameBody()
{
if (!GetGame() || !m_Controller || !m_Character) return false;
return GetGame().GetPlayerController() == m_Controller && m_Controller.GetControlledEntity() == m_Character;
}
protected void Probe()
{
if (m_Finished || !GetGame() || !GetGame().GetWorld()) return;
int now = System.GetTickCount();
if (now > m_Deadline) { Fail("bounded native controller/menu deadline"); return; }
if (now < m_Wait) return;
if (m_Step == 0)
{
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!m_Controller) return;
if (!Check(!m_Controller.GetControlledEntity(), "real local controller initially unassigned")) return;
Resource resource = Resource.Load("{2F912ED6E399FF47}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Unarmed.et");
if (!Check(resource && resource.IsValid(), "installed unmodified US_Unarmed resource")) return;
EntitySpawnParams params = new EntitySpawnParams();
params.TransformMode = ETransformMode.WORLD;
vector origin = "6090 0 7160";
origin[1] = GetGame().GetWorld().GetSurfaceY(origin[0], origin[2]) + 0.1;
params.Transform[3] = origin;
m_Character = GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params);
if (!Check(m_Character != null, "one stock body spawned")) return;
if (!Check(m_Controller.SetControlledEntity(m_Character), "native fixture-only body assignment succeeded")) return;
m_Inventory = SCR_InventoryStorageManagerComponent.Cast(m_Character.FindComponent(SCR_InventoryStorageManagerComponent));
if (!Check(m_Inventory != null, "stock inventory component present")) return;
m_Step = 1;
m_Wait = now + 1000;
return;
}
if (!SameBody()) { Fail("original local controller/body changed"); return; }
if (m_Step == 1)
{
if (!Check(SCR_InventoryMenuUI.GetInventoryMenu() == null, "inventory initially closed")) return;
m_Controller.ActionOpenInventory();
m_Step = 2;
m_Wait = now + 500;
return;
}
if (m_Step == 2)
{
m_Menu = SCR_InventoryMenuUI.GetInventoryMenu();
if (!Check(m_Menu != null, "actual native Inventory20Menu opened")) return;
ChimeraCharacter character = ChimeraCharacter.Cast(m_Character);
if (!Check(character && m_Menu.GetCharacterController() == character.GetCharacterController(), "menu bound to exact stock body")) return;
Widget root = m_Menu.GetRootWidget();
if (!Check(root != null && GetGame().GetMenuManager().GetTopMenu() == m_Menu, "native inventory root and top menu")) return;
Widget parent = root.FindAnyWidget("StoragesListSlot");
if (!Check(parent && GridLayoutWidget.Cast(parent), "stock storage-list parent is native GridLayoutWidget")) return;
Widget frame = parent.GetChildren();
if (!Check(frame && frame.GetName() == "Frame0" && frame.GetParent() == parent, "stock Frame0 is direct child of native storage grid")) return;
PrintFormat("ERRIC INVENTORY GEOMETRY: parent=%1 type=%2 child=%3 type=%4", parent.GetName(), parent.Type(), frame.GetName(), frame.Type());
System.MakeScreenshot("$logs:erric-inventory-open");
m_Step = 3;
m_Wait = now + 500;
return;
}
if (m_Step == 3)
{
if (!Check(SCR_InventoryMenuUI.GetInventoryMenu() == m_Menu, "same native menu retained for delayed capture")) return;
// Action_OpenInventory opens only; public native CloseInventory is its counterpart.
m_Inventory.CloseInventory();
m_Step = 4;
m_Wait = now + 500;
return;
}
if (!Check(SCR_InventoryMenuUI.GetInventoryMenu() == null, "native inventory closed with original body retained")) return;
System.MakeScreenshot("$logs:erric-inventory-closed");
m_Finished = true;
GetGame().GetCallqueue().Remove(Probe);
Print("ERR SMOKE COMPLETE: vanilla inventory control; direct native API experiment only, no successor or input qualification");
}
void ~ERRIC_InventoryMode()
{
if (GetGame()) GetGame().GetCallqueue().Remove(Probe);
// World teardown owns the fixture entity; do not delete a possessed actor.
}
}
#endif
@@ -0,0 +1,11 @@
[WorkbenchPluginAttribute(name: "Isolated vanilla inventory control", wbModules: {"WorldEditor"})]
class ERRIC_InventoryControlPlugin : WorldEditorPlugin
{
override void RunCommandline()
{
string enabled, play;
WorldEditor editor = Workbench.GetModule(WorldEditor);
if (editor && System.GetCLIParam("erricInventoryControl", enabled) && enabled == "1" && System.GetCLIParam("errPlay", play) && play == "1")
editor.SwitchToGameMode();
}
}
@@ -0,0 +1,3 @@
SubScene {
Parent "{853E92315D1D9EFE}worlds/Eden/Eden.ent"
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{2AACCD1FDA9EB6E9}Worlds/ERRIC_Inventory.ent"
Configurations {
WorldResourceClass PC {}
WorldResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,3 @@
ERRIC_InventoryMode InventoryControl : "{86FBBCE45AF62682}Prefabs/ERRIC_InventoryMode.et" {}
SCR_FactionManager Factions : "{589540B35EBA8CDC}Prefabs/MP/Managers/Factions/FactionManager_USxUSSR.et" {}
SCR_LoadoutManager Loadouts : "{97EF4C1A41F63DDB}Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et" {}
@@ -0,0 +1,6 @@
GameProject {
ID "ERRIC_InventoryControl"
GUID "0FFD909E91FC329B"
TITLE "Vanilla inventory diagnostic control (isolated)"
Dependencies { "58D0FB3206B6F859" }
}
+1 -1
View File
@@ -57,7 +57,7 @@ class SequenceTests(unittest.TestCase):
self.setup_sequence(); self.run_sequence()
self.assertEqual(self.keyboard.events, [('W', True), ('W', False), ('Tab', True), ('Tab', False), ('Tab', True), ('Tab', False)])
self.assertTrue(self.report['sequence_completed'])
self.assertAlmostEqual(self.clock(), .9)
self.assertAlmostEqual(self.clock(), 3.4)
self.assertGreater(self.guard.calls, len(self.keyboard.events))
def test_focus_loss_aborts_and_attempts_release_without_foreign_event(self):
self.setup_sequence()
+91
View File
@@ -0,0 +1,91 @@
"""Isolated-control provenance/evidence helper tests. Never invoke native runner."""
import hashlib
from pathlib import Path
import sys
import tempfile
import unittest
from unittest.mock import patch
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
import headless_inventory_control as control
GUID='0FFD909E91FC329B'
PROJECT='GameProject { ID "ERRIC_Control" GUID "'+GUID+'" Dependencies { "58D0FB3206B6F859" } }\n'
def loaded(guids=('5614BBCCBB55ED1C','58D0FB3206B6F859',GUID)):
return '00 ENGINE : Loaded addons:\n'+''.join("00 ENGINE : gproj: '/private/%s/addon.gproj' guid: '%s'\n"%(i,g) for i,g in enumerate(guids))+'00 SCRIPT : Module: Game; defines: "WORKBENCH"\n'
def complete(errors=2):
return loaded()+''.join('00 GUI (E): '+control.EXPECTED_ERROR+'\n' for _ in range(errors))+'00 SCRIPT : ERR SMOKE PASS: vanilla inventory control: actual native Inventory20Menu opened\n00 SCRIPT : ERR SMOKE COMPLETE: vanilla inventory control\n'
class InventoryControl(unittest.TestCase):
def test_authored_fixture_has_exact_vanilla_only_dependency_and_hashes(self):
files,guid=control.fixture_inventory(control.FIXTURE)
self.assertEqual(guid,GUID);self.assertIn('README.md',files)
self.assertIn('Scripts/Game/ERRIC/ERRIC_InventoryControl.c',files)
for path,digest in files.items():self.assertEqual(digest,hashlib.sha256((control.FIXTURE/path).read_bytes()).hexdigest())
def test_fixture_refuses_extra_projects_dependencies_or_identity_aliases(self):
with tempfile.TemporaryDirectory() as temp:
root=Path(temp);project=root/'addon.gproj';project.write_text(PROJECT)
control.fixture_inventory(root)
bads=[PROJECT.replace('58D0FB3206B6F859','2528466DE220D91C'), PROJECT.replace('"58D0FB3206B6F859"','"58D0FB3206B6F859" "2528466DE220D91C"'),PROJECT+'Dependencies { "58D0FB3206B6F859" }',PROJECT+'GUID "ABCDABCDABCDABCD"',PROJECT.replace('"58D0FB3206B6F859"','"58D0FB3206B6F859" arbitrary'),PROJECT.replace('Dependencies','Dependencies { Dependencies')]
bads.extend(PROJECT.replace(GUID,guid) for guid in ('5614BBCCBB55ED1C','58D0FB3206B6F859','2528466DE220D91C','5192138CB9D5B911','D31039C7C53E348A','FA804F411BDDF2C6','9AAEE9041D79809E'))
for bad in bads:
project.write_text(bad)
with self.subTest(bad=bad),self.assertRaises(ValueError):control.fixture_inventory(root)
project.write_text(PROJECT);(root/'nested').mkdir();extra=root/'nested/hidden.gproj';extra.write_text(PROJECT)
with self.assertRaises(ValueError):control.fixture_inventory(root)
extra.unlink();extra=root/'runtime.exe';extra.write_bytes(b'not source')
with self.assertRaises(ValueError):control.fixture_inventory(root)
def test_symlink_roots_files_and_directories_refused(self):
with tempfile.TemporaryDirectory() as temp:
root=Path(temp)/'fixture';root.mkdir();(root/'addon.gproj').write_text(PROJECT)
alias=Path(temp)/'alias';alias.symlink_to(root,target_is_directory=True)
with self.assertRaises(ValueError):control.fixture_inventory(alias)
link=root/'linked.c';link.symlink_to(root/'addon.gproj')
with self.assertRaises(ValueError):control.fixture_inventory(root)
link.unlink();link.symlink_to(Path(temp),target_is_directory=True)
with self.assertRaises(ValueError):control.fixture_inventory(root)
def test_known_hidden_harness_remapping_does_not_launch(self):
source=(control.ROOT/'tools/headless_workbench.py').read_text()
with patch.object(control.subprocess,'run',side_effect=AssertionError('No native launch')):
mapped=control.harness_source(source)
self.assertIn('ERRIC_InventoryControlPlugin',mapped);self.assertIn('Worlds/ERRIC_Inventory.ent',mapped)
self.assertIn("'-errPlay', '1', '-erricInventoryControl', '1'",mapped)
self.assertNotIn('ERR_ValidateBaseWorldPlugin',mapped);self.assertNotIn('Worlds/ERR_Everon.ent',mapped)
compile(mapped,'isolated-harness','exec')
for token in ('ERR_ValidateBaseWorldPlugin','Worlds/ERR_Everon.ent',"'-errPlay', '1'"):
with self.assertRaises(ValueError):control.harness_source(source.replace(token,'CHANGED'))
def test_reproduction_keeps_both_raw_errors_and_never_claims_clean_runtime(self):
evidence=control.control_evidence(complete(),GUID)
self.assertTrue(evidence['only_vanilla_and_control_loaded']);self.assertTrue(evidence['stock_slot_error_reproduced'])
self.assertFalse(evidence['native_clean']);self.assertFalse(evidence['successor_or_release_qualified'])
self.assertEqual(evidence['target_gui_error_count'],2);self.assertTrue(evidence['target_gui_pair_observed']);self.assertEqual(evidence['other_engine_errors'],[])
self.assertEqual(evidence['native_report']['engine_errors'],['00 GUI (E): '+control.EXPECTED_ERROR]*2)
self.assertEqual(evidence['native_report']['smoke_completions'],1)
for count in (0,1,3):self.assertFalse(control.control_evidence(complete(count),GUID)['stock_slot_error_reproduced'])
def test_target_pair_is_additive_and_does_not_hide_other_engine_errors(self):
extra=['00 WORLD (E): stock vehicle '+str(i) for i in range(7)]
evidence=control.control_evidence(complete()+'\n'.join(extra)+'\n',GUID)
self.assertTrue(evidence['target_gui_pair_observed']);self.assertEqual(evidence['target_gui_error_count'],2)
self.assertEqual(evidence['other_engine_errors'],extra)
self.assertFalse(evidence['stock_slot_error_reproduced']);self.assertFalse(evidence['native_clean'])
for text in (complete(1),complete(3),complete().replace('Loaded addons:','Available addons:'),complete()+'ERR SMOKE FAIL: bad',complete()+'ERR SMOKE COMPLETE: duplicate'):
self.assertFalse(control.control_evidence(text,GUID)['target_gui_pair_observed'])
wrong_channel=control.control_evidence(complete().replace('GUI (E)','WORLD (E)'),GUID)
self.assertEqual(wrong_channel['target_gui_error_count'],0);self.assertFalse(wrong_channel['target_gui_pair_observed']);self.assertEqual(len(wrong_channel['other_engine_errors']),2)
def test_addon_isolation_uses_all_exact_loaded_blocks(self):
bads=[complete().replace('Loaded addons:','Available addons:'),complete().replace(loaded(),loaded(('5614BBCCBB55ED1C','58D0FB3206B6F859'))),complete().replace(loaded(),loaded(('5614BBCCBB55ED1C','58D0FB3206B6F859',GUID, '2528466DE220D91C'))),complete().replace(loaded(),loaded(('58D0FB3206B6F859','58D0FB3206B6F859',GUID))),complete()+loaded(('5614BBCCBB55ED1C','58D0FB3206B6F859','2528466DE220D91C')),complete().replace("00 ENGINE : gproj:","00 SCRIPT : gproj:",1)]
for bad in bads:
evidence=control.control_evidence(bad,GUID);self.assertFalse(evidence['only_vanilla_and_control_loaded']);self.assertFalse(evidence['stock_slot_error_reproduced'])
self.assertTrue(control.control_evidence(complete()+loaded(),GUID)['stock_slot_error_reproduced'])
def test_completion_cannot_hide_native_script_or_fixture_failure(self):
for suffix in ('\n00 SCRIPT : ERR SMOKE FAIL: vanilla inventory control: wrong body','\n00 SCRIPT (E): invalid operation','\n00 SCRIPT : Virtual Machine Exception','\n00 RESOURCES (E): unexpected resource error','\nCould not find endscreen','\nERR SMOKE COMPLETE: duplicate'):
self.assertFalse(control.control_evidence(complete()+suffix,GUID)['stock_slot_error_reproduced'],suffix)
self.assertFalse(control.control_evidence(complete().replace('ERR SMOKE COMPLETE:','NOT COMPLETE:'),GUID)['stock_slot_error_reproduced'])
if __name__=='__main__':unittest.main()
+86
View File
@@ -0,0 +1,86 @@
"""Execute candidate hierarchy adapter; native slot regeneration/rendering unqualified."""
from policy_harness import ROOT, body, run
P=ROOT/'addons/core/Scripts/Game/ERR/ERR_InventoryLayout.c'
def cpp(s):
s=s.replace('protected ', '').replace('override ', '').replace('ref ', '')
for t in ('Widget','ERR_GameMode','GridLayoutWidget','ERR_InventoryGridPlacement'):
for n in ('widget','child','w','mode','root','grid','named','placement','previousHost','host'):
s=s.replace(t+' '+n+'(',t+'* '+n+'(').replace(t+' '+n+')',t+'* '+n+')').replace(t+' '+n+' =',t+'* '+n+' =')
s=s.replace('GridLayoutWidget.Cast(', 'dynamic_cast<GridLayoutWidget*>(').replace('VerticalLayoutWidget.Cast(', 'dynamic_cast<VerticalLayoutWidget*>(')
for n in ('widget','child','w','mode','root','grid','named','placement','previousHost','host','m_ERRStorageLayoutRoot','m_ERRStorageLayoutGrid','m_ERRStorageLayoutHost','m_MenuHandler'):
s=s.replace(n+'.',n+'->')
for n in ('ERR_GameMode','SCR_InventoryMenuUI','WidgetType','WidgetFlags','GridSlot','AlignableSlot','LayoutHorizontalAlign','LayoutVerticalAlign'):
s=s.replace(n+'.',n+'::')
s=s.replace('GetGame().','GetGame()->').replace('GetWorkspace().','GetWorkspace()->')
s=s.replace('null','nullptr').replace('bool created;', 'bool created = false;')
return s.replace('super.', 'Native::')
placement='\n'.join(cpp(body(P,k)) for k in ('bool Capture(', 'bool Apply('))
prepare=cpp(body(P,'bool ERR_PrepareStorageLayout('))
attach=cpp(body(P,'override void HandlerAttached('))
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
using string=std::string;
namespace LayoutHorizontalAlign{enum{Left,Center,Right,Stretch};}
namespace LayoutVerticalAlign{enum{Top,Center,Bottom,Stretch};}
namespace WidgetType{const int VerticalLayoutWidgetTypeID=7;}
namespace WidgetFlags{const int VISIBLE=1;}
struct Widget {virtual ~Widget(){}Widget* parent=nullptr;std::vector<Widget*> children;string name;bool removed=false,refuseAdd=false;int row=0,col=0,rows=1,cols=1,h=0,v=0;float l=0,t=0,r=0,b=0;int handlers=0;
Widget* GetParent(){return parent;}Widget* GetChildren(){return children.empty()?nullptr:children[0];}string GetName(){return name;}void SetName(string n){name=n;}
void AddChild(Widget* child){if(refuseAdd)return;if(child->parent){auto& a=child->parent->children;a.erase(std::remove(a.begin(),a.end(),child),a.end());}child->parent=this;children.push_back(child);child->row=child->col=0;child->rows=child->cols=1;child->l=child->t=child->r=child->b=0;child->h=child->v=0;}
Widget* FindAnyWidget(string n){if(name==n)return this;for(auto c:children)if(auto result=c->FindAnyWidget(n))return result;return nullptr;}
void RemoveFromHierarchy(){removed=true;if(parent){auto& a=parent->children;a.erase(std::remove(a.begin(),a.end(),this),a.end());}parent=nullptr;}
};
struct GridLayoutWidget:Widget{};struct VerticalLayoutWidget:Widget{};
namespace GridSlot {void Check(Widget* w){assert(w&&dynamic_cast<GridLayoutWidget*>(w->parent));}int GetRow(Widget*w){Check(w);return w->row;}int GetColumn(Widget*w){Check(w);return w->col;}int GetRowSpan(Widget*w){Check(w);return w->rows;}int GetColumnSpan(Widget*w){Check(w);return w->cols;}void SetRow(Widget*w,int n){Check(w);w->row=n;}void SetColumn(Widget*w,int n){Check(w);w->col=n;}void SetRowSpan(Widget*w,int n){Check(w);w->rows=n;}void SetColumnSpan(Widget*w,int n){Check(w);w->cols=n;}}
namespace AlignableSlot {int GetHorizontalAlign(Widget*w){return w->h;}int GetVerticalAlign(Widget*w){return w->v;}void GetPadding(Widget*w,float&l,float&t,float&r,float&b){l=w->l;t=w->t;r=w->r;b=w->b;}void SetHorizontalAlign(Widget*w,int n){w->h=n;}void SetVerticalAlign(Widget*w,int n){w->v=n;}void SetPadding(Widget*w,float l,float t,float r,float b){w->l=l;w->t=t;w->r=r;w->b=b;}}
struct Workspace {int creates=0;bool fail=false,wrongType=false,refuseAdd=false;Widget* last=nullptr;Widget* CreateWidget(int type,int flags,void*,int,Widget* parent){assert(type==7&&flags==1);creates++;if(fail)return nullptr;Widget* host=wrongType?new Widget:new VerticalLayoutWidget;parent->AddChild(host);host->refuseAdd=refuseAdd;last=host;return host;}};
struct ERR_GameMode {static ERR_GameMode* current;bool active=true;int world=1;static ERR_GameMode* Get(){return current;}bool Active(){return active;}int GetWorld(){return world;}};ERR_GameMode* ERR_GameMode::current=nullptr;
struct Game {Workspace workspace;bool menu=true;int world=1;Workspace* GetWorkspace(){return &workspace;}bool GetMenuManager(){return menu;}int GetWorld(){return world;}} game;Game* GetGame(){return &game;}
struct ERR_InventoryGridPlacement {int m_Row=0,m_Column=0,m_RowSpan=0,m_ColumnSpan=0,m_Horizontal=0,m_Vertical=0;float m_Left=0,m_Top=0,m_Right=0,m_Bottom=0;
'''+placement+r'''
};
struct SCR_InventoryMenuUI {static SCR_InventoryMenuUI* current;Widget* root=nullptr;Widget* m_ERRStorageLayoutRoot=nullptr;GridLayoutWidget* m_ERRStorageLayoutGrid=nullptr;VerticalLayoutWidget* m_ERRStorageLayoutHost=nullptr;bool m_ERRStorageLayoutBound=false;static SCR_InventoryMenuUI* GetInventoryMenu(){return current;}Widget* GetRootWidget(){return root;}
'''+prepare+r'''
};SCR_InventoryMenuUI* SCR_InventoryMenuUI::current=nullptr;
struct Native {int calls=0,baseInit=0,refresh=0,invalidLayoutCalls=0;void HandlerAttached(Widget* w){calls++;baseInit++;if(!w||!dynamic_cast<VerticalLayoutWidget*>(w->parent))invalidLayoutCalls+=2;refresh++;}};
struct Handler:Native {SCR_InventoryMenuUI* m_MenuHandler=nullptr;
'''+attach+r'''
};
struct Fixture {ERR_GameMode mode;Widget root,child;GridLayoutWidget grid;SCR_InventoryMenuUI menu;Handler handler;
Fixture(){game={};ERR_GameMode::current=&mode;SCR_InventoryMenuUI::current=&menu;menu.root=&root;grid.name="StoragesListSlot";root.AddChild(&grid);child.name="Frame0";grid.AddChild(&child);child.row=2;child.col=3;child.rows=4;child.cols=5;child.l=1;child.t=2;child.r=3;child.b=4;child.h=2;child.v=1;handler.m_MenuHandler=&menu;}
void attach(){handler.HandlerAttached(&child);assert(handler.calls==1&&handler.baseInit==1&&handler.refresh==1);}
};
int main(){
{Fixture f;f.attach();auto host=f.menu.m_ERRStorageLayoutHost;assert(host&&host->parent==&f.grid&&f.child.parent==host&&game.workspace.creates==1&&f.handler.invalidLayoutCalls==0);assert(host->row==2&&host->col==3&&host->rows==4&&host->cols==5&&host->l==1&&host->t==2&&host->r==3&&host->b==4&&host->h==2&&host->v==1);assert(f.child.l==0&&f.child.t==0&&f.child.r==0&&f.child.b==0&&f.child.h==3&&f.child.v==3);
// Native refresh removes the old root; the same menu reuses its empty host.
f.child.RemoveFromHierarchy();Widget next;next.name="Frame0";f.grid.AddChild(&next);Handler second;second.m_MenuHandler=&f.menu;second.HandlerAttached(&next);assert(second.calls==1&&second.baseInit==1&&second.refresh==1&&second.invalidLayoutCalls==0&&game.workspace.creates==1&&next.parent==host&&f.grid.children.size()==1);
}
for(int fault=0;fault<8;fault++){Fixture f;SCR_InventoryMenuUI other;Widget foreignRoot;GridLayoutWidget foreignGrid;
if(fault==0)ERR_GameMode::current=nullptr;
if(fault==1)f.mode.active=false;
if(fault==2)game.world=2;
if(fault==3)SCR_InventoryMenuUI::current=&other;
if(fault==4)f.handler.m_MenuHandler=&other;
if(fault==5)f.child.name="Different";
if(fault==6)f.menu.root=&foreignRoot;
if(fault==7){foreignGrid.name="OtherGrid";foreignGrid.AddChild(&f.child);}
f.attach();assert(game.workspace.creates==0&&!f.menu.m_ERRStorageLayoutBound&&f.handler.invalidLayoutCalls==2);
}
{Fixture f;Widget named;named.name="ERR_InventoryStorageLayout";f.grid.AddChild(&named);f.attach();assert(game.workspace.creates==0&&f.child.parent==&f.grid&&!named.removed);}
for(int fault=0;fault<3;fault++){Fixture f;game.workspace.fail=fault==0;game.workspace.wrongType=fault==1;game.workspace.refuseAdd=fault==2;f.attach();assert(f.child.parent==&f.grid&&!f.menu.m_ERRStorageLayoutHost&&!f.menu.m_ERRStorageLayoutBound&&f.handler.invalidLayoutCalls==2);assert(f.child.row==2&&f.child.col==3&&f.child.l==1);if(game.workspace.last)assert(game.workspace.last->removed);}
{Fixture f;f.attach();auto host=f.menu.m_ERRStorageLayoutHost;f.child.RemoveFromHierarchy();Widget foreign;host->AddChild(&foreign);Widget next;next.name="Frame0";f.grid.AddChild(&next);assert(!f.menu.ERR_PrepareStorageLayout(&next));assert(next.parent==&f.grid&&foreign.parent==host&&game.workspace.creates==1);}
{Fixture f;f.attach();auto host=f.menu.m_ERRStorageLayoutHost;f.child.RemoveFromHierarchy();host->row=7;host->l=9;host->refuseAdd=true;Widget next;next.name="Frame0";f.grid.AddChild(&next);next.row=6;next.l=8;assert(!f.menu.ERR_PrepareStorageLayout(&next));assert(next.parent==&f.grid&&next.row==6&&next.l==8&&host->row==7&&host->l==9&&game.workspace.creates==1);}
{Fixture f;f.attach();f.child.RemoveFromHierarchy();Widget replacementRoot;replacementRoot.AddChild(&f.grid);f.menu.root=&replacementRoot;Widget next;next.name="Frame0";f.grid.AddChild(&next);assert(!f.menu.ERR_PrepareStorageLayout(&next));assert(game.workspace.creates==1&&next.parent==&f.grid);}
{Fixture f;f.attach();auto host=f.menu.m_ERRStorageLayoutHost;f.child.RemoveFromHierarchy();host->RemoveFromHierarchy();f.menu.m_ERRStorageLayoutHost=nullptr;Widget next;next.name="Frame0";f.grid.AddChild(&next);assert(!f.menu.ERR_PrepareStorageLayout(&next));assert(game.workspace.creates==1);}
{ERR_InventoryGridPlacement p;Widget child;assert(!p.Capture(&child)&&!p.Apply(&child));}
}
''')
s=P.read_text();handler=body(P,'override void HandlerAttached(')
assert handler.count('super.HandlerAttached(w)')==1
assert 'return' not in handler
for forbidden in ('Init(', 'RefreshList(', 'SetFillWeight(', 'SetSizeMode(', 'CallLater(', 'RemoveHandler(', 'CreateWidgets(', 'OpenMenu('):assert forbidden not in s,forbidden
assert 'class SCR_InventoryStorageBaseUI' not in s
print('PASS: actual inventory adapter scope, grid placement, sole compatible host, refresh reuse, rollback, foreign hierarchy refusal and exactly-once native delegation; native compatibility pending')
+74 -10
View File
@@ -4,8 +4,8 @@ P=ROOT/'addons/core/Scripts/Game/ERR/ERR_OrdinaryInputObserver.c'
def cpp(s):
s=s.replace('protected ', '').replace('out string nonce','string& nonce')
for t in ('ERR_GameMode','PlayerController','IEntity','ERR_PlayerLife','ERR_OrdinaryInputObserver','PlayerManager','SCR_InventoryMenuUI','Widget'):
s=s.replace(t+' mode',t+'* mode').replace(t+' controller',t+'* controller').replace(t+' actor',t+'* actor').replace(t+' life',t+'* life').replace(t+' request',t+'* request').replace(t+' current',t+'* current').replace(t+' manager',t+'* manager').replace(t+' menu',t+'* menu').replace(t+' root',t+'* root')
for v in ('mode','controller','actor','request','life','current','manager','menu','root','m_Mode','m_World','m_Controller','m_Actor','m_Character','m_Menu','s_Request'):
s=s.replace(t+' mode',t+'* mode').replace(t+' controller',t+'* controller').replace(t+' actor',t+'* actor').replace(t+' life',t+'* life').replace(t+' request',t+'* request').replace(t+' current',t+'* current').replace(t+' manager',t+'* manager').replace(t+' menu',t+'* menu').replace(t+' root',t+'* root').replace(t+' expectedMenu',t+'* expectedMenu').replace(t+' expectedRoot',t+'* expectedRoot').replace(t+' overlay',t+'* overlay')
for v in ('mode','controller','actor','request','life','current','manager','menu','root','m_Mode','m_World','m_Controller','m_Actor','m_Character','m_Menu','s_Request','expectedMenu','expectedRoot','m_CaptureMenu','m_CaptureRoot','overlay','m_aHitZonePoints'):
s=s.replace(v+'.',v+'->')
for scope in ('System','Replication','ERR_Phase','ERR_GameMode','ERR_Actors','SCR_InventoryMenuUI','ERR_OrdinaryInputObserverRules','vector','Math'):
s=s.replace(scope+'.',scope+'::')
@@ -13,11 +13,14 @@ def cpp(s):
s=s.replace('CharacterControllerComponent.Cast(actor->FindComponent(CharacterControllerComponent))','actor->FindComponent(0)').replace('FindComponent(CharacterControllerComponent)','FindComponent(0)')
s=s.replace('.Insert(request->Cancel)', '.Insert()').replace('.Remove(Cancel)', '.Remove()')
s=s.replace('.CallLater(request->Sample, 100, false)', '.CallLater()').replace('->CallLater(request->Sample, 100, false)', '->CallLater()').replace('->CallLater(Sample, 100, false)', '->CallLater()').replace('->Remove(Sample)', '->Remove()')
s=s.replace('->CallLater(CaptureLater, 300, false)', '->QueueCapture()').replace('->Remove(CaptureLater)', '->RemoveCapture()').replace('->CallLater(CaptureLater, 100, false)', '->QueueCapture()')
s=s.replace('string.Format', 'Format').replace('Widget* overlay;', 'Widget* overlay=null;')
s=s.replace('null','nullptr').replace('bool open;', 'bool open = false;')
s=s.replace('nonce.Length()', 'int(nonce.size())').replace('allowed.Contains(nonce[i])', '(allowed.find(nonce[i]) != string::npos)')
return s
state=cpp(body(P,'string ERR_InputObserverInventoryState('))
rules='\n'.join(cpp(body(P,x)) for x in ('static bool Nonce(', 'static bool Within(', 'static bool Stationary('))
methods='\n'.join(cpp(body(P,x)) for x in ('protected static bool Enabled(', 'static void Queue(', 'protected bool Valid(', 'protected void Record(', 'protected void Capture(', 'protected void Observe(', 'protected void Sample(', 'protected void End(', 'void Cancel(', 'static void ClearWorld('))
methods='\n'.join(cpp(body(P,x)) for x in ('protected static bool Enabled(', 'static void Queue(', 'protected bool Valid(', 'protected void Record(', 'protected void CancelCapture(', 'protected void Capture(', 'protected void CaptureLater(', 'protected void Observe(', 'protected void Sample(', 'protected void End(', 'void Cancel(', 'static void ClearWorld('))
run(r'''
#include <cassert>
#include <cmath>
@@ -25,6 +28,7 @@ run(r'''
#include <vector>
#include <map>
using string=std::string;using UUID=string;
string Format(string,bool pending,int stage,int points,bool present,bool visible){return std::to_string(pending)+","+std::to_string(stage)+","+std::to_string(points)+","+std::to_string(present)+","+std::to_string(visible);}
struct vector {float x=0,y=0,z=0;float Length(){return std::sqrt(x*x+y*y+z*z);}static float Distance(vector a,vector b){return vector{a.x-b.x,a.y-b.y,a.z-b.z}.Length();}};
namespace Math {float AbsFloat(float x){return std::abs(x);}}
namespace System {bool console=false;int now=0;std::map<string,string> cli;std::vector<string> shots;bool IsConsoleApp(){return console;}int GetTickCount(){return now;}bool GetCLIParam(string key,string& value){if(!cli.count(key))return false;value=cli[key];return true;}void MakeScreenshot(string path){shots.push_back(path);}}
@@ -40,13 +44,17 @@ struct Roster {IEntity* actor=nullptr;ERR_PlayerLife* life=nullptr;ERR_PlayerLif
struct Invoker {int inserted=0,removed=0;void Insert(){inserted++;}void Remove(){removed++;}};
struct ERR_GameMode {static ERR_GameMode* current;BaseWorld* world=nullptr;bool active=true;int phase=ERR_Phase::PRISON;Roster roster;Invoker stop;static ERR_GameMode* Get(){return current;}BaseWorld* GetWorld(){return world;}bool Active(){return active;}int Phase(){return phase;}Roster* Players(){return &roster;}Invoker& OnStop(){return stop;}};ERR_GameMode* ERR_GameMode::current=nullptr;
struct PlayerManager {PlayerController* controller=nullptr;IEntity* actor=nullptr;PlayerController* GetPlayerController(int){return controller;}IEntity* GetPlayerControlledEntity(int){return actor;}};
struct Widget {bool visible=true;bool IsVisible(){return visible;}};
struct SCR_InventoryMenuUI {static SCR_InventoryMenuUI* current;CharacterControllerComponent* character=nullptr;Widget* root=nullptr;static SCR_InventoryMenuUI* GetInventoryMenu(){return current;}CharacterControllerComponent* GetCharacterController(){return character;}Widget* GetRootWidget(){return root;}};SCR_InventoryMenuUI* SCR_InventoryMenuUI::current=nullptr;
struct Widget {bool visible=true,hierarchy=true;Widget* loading=nullptr;Widget* FindAnyWidget(string name){assert(name=="LoadingOverlay");return loading;}bool IsVisible(){return visible;}bool IsVisibleInHierarchy(){return visible&&hierarchy;}};
struct HitPoints {int count=0;int Count(){return count;}};
struct SCR_InventoryMenuUI {bool m_bProcessInitQueue=true;int m_eInitStage=7;HitPoints points;HitPoints* m_aHitZonePoints=&points;static SCR_InventoryMenuUI* current;CharacterControllerComponent* character=nullptr;Widget* root=nullptr;static SCR_InventoryMenuUI* GetInventoryMenu(){return current;}CharacterControllerComponent* GetCharacterController(){return character;}Widget* GetRootWidget(){return root;}
'''+state+r'''
};SCR_InventoryMenuUI* SCR_InventoryMenuUI::current=nullptr;
struct MenuManager {SCR_InventoryMenuUI* top=nullptr;SCR_InventoryMenuUI* GetTopMenu(){return top;}};
struct InputManager {float forward=0;float GetActionValue(string name){assert(name=="CharacterForward");return forward;}};
struct Callqueue {int queued=0,removed=0;void CallLater(){queued++;}void Remove(){removed++;}};
struct Callqueue {int queued=0,removed=0,captures=0,capturesRemoved=0;void CallLater(){queued++;}void Remove(){removed++;}void QueueCapture(){captures++;}void RemoveCapture(){capturesRemoved++;}};
struct Game {BaseWorld* world=nullptr;PlayerController* controller=nullptr;PlayerManager manager;MenuManager menus;InputManager input;Callqueue queue;BaseWorld* GetWorld(){return world;}PlayerController* GetPlayerController(){return controller;}PlayerManager* GetPlayerManager(){return &manager;}MenuManager* GetMenuManager(){return &menus;}InputManager* GetInputManager(){return &input;}Callqueue* GetCallqueue(){return &queue;}} game;Game* GetGame(){return &game;}
std::vector<string> events;int samples=0;
std::vector<string> events,inventoryStates,inventoryReasons;int samples=0;
void PrintFormat(const char*,string nonce,int,string reason,string state){assert(nonce=="Run42");inventoryStates.push_back(state);inventoryReasons.push_back(reason);}
void PrintFormat(const char*,string nonce,int,int,float,string detail){assert(nonce=="Run42");events.push_back(detail);}
void PrintFormat(const char*,string nonce,int,int,float,vector,float,vector,vector,bool){assert(nonce=="Run42");samples++;}
struct ERR_OrdinaryInputObserverRules {
@@ -56,14 +64,16 @@ struct ERR_OrdinaryInputObserver {
static ERR_OrdinaryInputObserver* s_Request;static bool s_Attempted;
ERR_GameMode* m_Mode=nullptr;BaseWorld* m_World=nullptr;PlayerController* m_Controller=nullptr;IEntity* m_Actor=nullptr;CharacterControllerComponent* m_Character=nullptr;ERR_PlayerLife* m_Life=nullptr;
UUID m_Account;int m_PlayerId=0;string m_Nonce;bool m_Screenshots=false,m_Finished=false,m_Ready=false,m_Opened=false,m_Closed=false;SCR_InventoryMenuUI* m_Menu=nullptr;
string m_CaptureSuffix;SCR_InventoryMenuUI* m_CaptureMenu=nullptr;Widget* m_CaptureRoot=nullptr;int m_CaptureDue=0,m_CaptureExpires=0,m_CaptureLoadedAt=-1,m_InventoryStateLines=0;bool m_BaselineCaptureQueued=false,m_OpenCaptureQueued=false,m_ClosedCaptureQueued=false;
int m_Start=0,m_Previous=0,m_Samples=0,m_BaselineSince=-1;vector m_BaselineOrigin;float m_LastWorldTime=0;
'''+methods+r'''
};
ERR_OrdinaryInputObserver* ERR_OrdinaryInputObserver::s_Request=nullptr;bool ERR_OrdinaryInputObserver::s_Attempted=false;
struct Fixture {ERR_GameMode mode;BaseWorld world;PlayerController controller;IEntity actor;CharacterControllerComponent character;ERR_PlayerLife life;Widget widget;SCR_InventoryMenuUI menu;
Fixture(){game={};System::cli={{"errOrdinaryScenario","core"},{"errOrdinaryInputObserve","Run42"},{"errOrdinaryInputScreenshots","1"}};System::now=0;System::console=false;System::shots.clear();Replication::server=true;events.clear();samples=0;ERR_OrdinaryInputObserver::s_Request=nullptr;ERR_OrdinaryInputObserver::s_Attempted=false;SCR_InventoryMenuUI::current=nullptr;ERR_GameMode::current=&mode;mode.world=&world;mode.roster.actor=&actor;mode.roster.life=&life;actor.world=&world;actor.component=&character;controller.actor=&actor;game.world=&world;game.controller=&controller;game.manager.controller=&controller;game.manager.actor=&actor;menu.character=&character;menu.root=&widget;}
Fixture(){game={};System::cli={{"errOrdinaryScenario","core"},{"errOrdinaryInputObserve","Run42"},{"errOrdinaryInputScreenshots","1"}};System::now=0;System::console=false;System::shots.clear();Replication::server=true;events.clear();inventoryStates.clear();inventoryReasons.clear();samples=0;ERR_OrdinaryInputObserver::s_Request=nullptr;ERR_OrdinaryInputObserver::s_Attempted=false;SCR_InventoryMenuUI::current=nullptr;ERR_GameMode::current=&mode;mode.world=&world;mode.roster.actor=&actor;mode.roster.life=&life;actor.world=&world;actor.component=&character;controller.actor=&actor;game.world=&world;game.controller=&controller;game.manager.controller=&controller;game.manager.actor=&actor;menu.character=&character;menu.root=&widget;}
ERR_OrdinaryInputObserver* queue(){ERR_OrdinaryInputObserver::Queue(&mode,&controller,&actor);return ERR_OrdinaryInputObserver::s_Request;}
void sample(int ms){System::now=ms;world.time=ms;ERR_OrdinaryInputObserver::s_Request->Sample();}
void capture(int ms){System::now=ms;world.time=ms;ERR_OrdinaryInputObserver::s_Request->CaptureLater();}
void baseline(){for(int t=100;t<=2100;t+=100)sample(t);assert(ERR_OrdinaryInputObserver::s_Request->m_Ready);}
void open(){SCR_InventoryMenuUI::current=&menu;game.menus.top=&menu;}
void close(){SCR_InventoryMenuUI::current=nullptr;game.menus.top=nullptr;}
@@ -96,15 +106,66 @@ int main(){
if(fault==12)f.mode.roster.life=&otherLife;
if(fault==13)f.life.AccountId="changed";
assert(!p->Valid());f.sample(100);assert(p->m_Finished&&samples==0&&game.queue.removed==1&&f.mode.stop.removed==1);p->Cancel();assert(game.queue.removed==1);}
{Fixture f;auto p=f.queue();f.baseline();assert(count("READY")==1&&System::shots.size()==1);game.input.forward=1;f.character.input={0,0,1};f.actor.origin={0,0,1};f.sample(2200);assert(samples==22&&!p->m_Opened);game.input.forward=0;f.character.input={};f.character.disabled=true;f.open();f.sample(2300);assert(p->m_Opened&&count("INVENTORY_OPEN")==1&&System::shots.size()==2);f.sample(2400);assert(System::shots.size()==2);f.close();f.sample(2500);assert(p->m_Closed&&count("INVENTORY_CLOSED")==1&&System::shots.size()==3);f.sample(2600);assert(System::shots.size()==3);for(auto path:System::shots)assert(path.find("$logs:err-input-Run42-")==0);p->Cancel();int writes=samples;f.sample(2700);assert(samples==writes);ERR_OrdinaryInputObserver::ClearWorld();assert(ERR_OrdinaryInputObserver::s_Attempted&&!ERR_OrdinaryInputObserver::s_Request);assert(!f.queue());}
{Fixture f;auto p=f.queue();f.baseline();assert(count("READY")==1&&System::shots.empty()&&game.queue.captures==1);f.capture(2400);assert(System::shots.size()==1);game.input.forward=1;f.character.input={0,0,1};f.actor.origin={0,0,1};f.sample(2450);assert(samples==22&&!p->m_Opened);game.input.forward=0;f.character.input={};f.character.disabled=true;f.open();f.sample(2500);assert(p->m_Opened&&count("INVENTORY_OPEN")==1&&System::shots.size()==1);f.capture(2800);assert(System::shots.size()==1&&p->m_CaptureLoadedAt==2800&&p->m_CaptureDue==2900);f.capture(2900);assert(System::shots.size()==2);f.close();f.sample(3000);assert(p->m_Closed&&count("INVENTORY_CLOSED")==1&&System::shots.size()==2);f.capture(3300);assert(System::shots.size()==3);f.sample(3400);assert(System::shots.size()==3);for(auto path:System::shots)assert(path.find("$logs:err-input-Run42-")==0);p->Capture("baseline");p->Capture("open");p->Capture("closed");assert(game.queue.captures==4);p->Cancel();int writes=samples;f.sample(3500);assert(samples==writes);ERR_OrdinaryInputObserver::ClearWorld();assert(ERR_OrdinaryInputObserver::s_Attempted&&!ERR_OrdinaryInputObserver::s_Request);assert(!f.queue());}
{Fixture f;auto p=f.queue();System::cli["errOrdinaryInputScreenshots"]="0";p->m_Screenshots=false;f.open();for(int t=100;t<2300;t+=100)f.sample(t);assert(!p->m_Ready);f.close();for(int t=2300;t<=4300;t+=100)f.sample(t);assert(p->m_Ready&&System::shots.empty());}
for(int fault=0;fault<4;fault++){Fixture f;auto p=f.queue();f.baseline();f.open();
if(fault==0)f.menu.character=nullptr;
if(fault==1)f.menu.root=nullptr;
if(fault==2)f.widget.visible=false;
if(fault==3)game.menus.top=nullptr;
f.sample(2200);assert(p->m_Finished&&!p->m_Opened&&System::shots.size()==1);}
f.sample(2200);assert(p->m_Finished&&!p->m_Opened&&System::shots.empty());}
{Fixture f;auto p=f.queue();f.baseline();f.open();f.sample(2200);SCR_InventoryMenuUI replacement=f.menu;SCR_InventoryMenuUI::current=&replacement;game.menus.top=&replacement;f.sample(2300);assert(p->m_Finished&&!p->m_Closed);}
for(int fault=0;fault<14;fault++){Fixture f;auto p=f.queue();f.baseline();f.open();f.sample(2200);assert(p->m_CaptureMenu==&f.menu&&p->m_CaptureRoot==&f.widget);SCR_InventoryMenuUI otherMenu=f.menu;Widget otherRoot;IEntity otherActor;
if(fault==0)SCR_InventoryMenuUI::current=nullptr;
if(fault==1){SCR_InventoryMenuUI::current=&otherMenu;game.menus.top=&otherMenu;}
if(fault==2)f.menu.root=nullptr;
if(fault==3)f.menu.root=&otherRoot;
if(fault==4)f.widget.hierarchy=false;
if(fault==5)game.menus.top=nullptr;
if(fault==6)game.manager.actor=&otherActor;
if(fault==7)f.life.AccountId="changed";
if(fault==8)f.mode.phase=ERR_Phase::DEFEAT;
if(fault==9)p->m_CaptureMenu=nullptr;
if(fault==10)p->m_CaptureRoot=nullptr;
if(fault==11)p->m_Screenshots=false;
if(fault==12)f.menu.character=nullptr;
if(fault==13)p->m_LastWorldTime=3000;
f.capture(2500);assert(System::shots.empty()&&p->m_CaptureSuffix.empty()&&inventoryStates.empty());}
{Fixture f;auto p=f.queue();f.baseline();f.open();Widget loading;f.widget.loading=&loading;f.sample(2200);int expires=p->m_CaptureExpires;assert(expires==5200);f.capture(2500);assert(System::shots.empty()&&p->m_CaptureSuffix=="open"&&p->m_CaptureDue==2600&&p->m_CaptureExpires==expires);f.widget.loading=nullptr;f.capture(2600);assert(System::shots.empty()&&p->m_CaptureLoadedAt==2600&&p->m_CaptureExpires==expires);f.capture(2700);assert(System::shots.size()==1&&System::shots[0].find("-open")!=string::npos&&p->m_CaptureSuffix.empty());f.capture(2800);assert(System::shots.size()==1);}
{Fixture f;auto p=f.queue();f.baseline();f.open();Widget loading;f.widget.loading=&loading;f.sample(2200);for(int t=2500;t<=5100;t+=100){f.capture(t);assert(System::shots.empty());assert(p->m_CaptureExpires==5200||p->m_CaptureSuffix.empty());}assert(p->m_CaptureSuffix.empty()&&game.queue.captures<=28&&inventoryStates.size()<=30&&inventoryReasons.back()=="timeout");f.widget.loading=nullptr;f.capture(5200);assert(System::shots.empty());}
{Fixture f;auto p=f.queue();f.baseline();f.open();Widget loading;f.sample(2200);f.capture(2500);assert(System::shots.empty()&&p->m_CaptureLoadedAt==2500);f.widget.loading=&loading;f.capture(2600);assert(p->m_CaptureLoadedAt==-1&&p->m_CaptureExpires==5200);f.widget.loading=nullptr;f.capture(2700);assert(System::shots.empty()&&p->m_CaptureLoadedAt==2700);f.capture(2800);assert(System::shots.size()==1&&p->m_CaptureLoadedAt==-1);}
{Fixture f;auto p=f.queue();f.baseline();f.open();f.sample(2200);f.capture(5100);assert(System::shots.empty()&&p->m_CaptureSuffix.empty()&&p->m_CaptureLoadedAt==-1);}
{Fixture f;auto p=f.queue();f.baseline();f.open();f.sample(2200);f.capture(2500);assert(p->m_CaptureLoadedAt==2500);p->Cancel();assert(p->m_CaptureLoadedAt==-1&&p->m_CaptureSuffix.empty());}
for(int fault=0;fault<8;fault++){Fixture f;auto p=f.queue();f.baseline();f.open();Widget loading,otherRoot;SCR_InventoryMenuUI otherMenu=f.menu;IEntity otherActor;f.widget.loading=&loading;f.sample(2200);f.capture(2500);assert(p->m_CaptureSuffix=="open");
if(fault==0){SCR_InventoryMenuUI::current=&otherMenu;game.menus.top=&otherMenu;}
if(fault==1)f.menu.root=&otherRoot;
if(fault==2)game.menus.top=nullptr;
if(fault==3)f.widget.hierarchy=false;
if(fault==4)game.manager.actor=&otherActor;
if(fault==5)f.life.AccountId="changed";
if(fault==6)p->Cancel();
if(fault==7)ERR_OrdinaryInputObserver::ClearWorld();
f.widget.loading=nullptr;System::now=2600;f.world.time=2600;p->CaptureLater();assert(System::shots.empty()&&p->m_CaptureSuffix.empty()&&inventoryStates.size()==1);}
{Fixture f;auto p=f.queue();f.baseline();f.open();Widget loading;f.widget.loading=&loading;f.sample(2200);f.capture(2500);f.close();f.sample(2550);assert(p->m_CaptureSuffix=="closed"&&p->m_CaptureDue==2850&&System::shots.empty());f.capture(2850);assert(System::shots.size()==1&&System::shots[0].find("-closed")!=string::npos);}
{Fixture f;Widget loading;f.widget.loading=&loading;f.menu.points.count=3;assert(f.menu.ERR_InputObserverInventoryState()=="1,7,3,1,1");loading.hierarchy=false;assert(f.menu.ERR_InputObserverInventoryState()=="1,7,3,1,0");f.menu.m_aHitZonePoints=nullptr;f.menu.m_bProcessInitQueue=false;f.widget.loading=nullptr;assert(f.menu.ERR_InputObserverInventoryState()=="0,7,-1,0,0");}
{Fixture f;auto p=f.queue();f.baseline();f.open();Widget loading;f.widget.loading=&loading;f.sample(2200);p->m_InventoryStateLines=29;f.capture(2500);assert(p->m_InventoryStateLines==30&&inventoryStates.size()==1);f.capture(2600);assert(p->m_InventoryStateLines==30&&inventoryStates.size()==1&&p->m_CaptureSuffix=="open");}
{Fixture f;auto p=f.queue();f.baseline();f.open();f.sample(2200);f.capture(5200);assert(System::shots.empty()&&p->m_CaptureSuffix.empty()&&inventoryReasons.size()==1&&inventoryReasons[0]=="timeout");}
for(int fault=0;fault<4;fault++){Fixture f;auto p=f.queue();f.baseline();
if(fault==0)game.input.forward=1;
if(fault==1)f.character.input={0,0,1};
if(fault==2)f.character.velocity={0,0,1};
if(fault==3)f.actor.origin={0,0,.2};
f.capture(2400);assert(System::shots.empty()&&p->m_CaptureSuffix.empty());}
{Fixture f;auto p=f.queue();f.baseline();game.input.forward=1;f.sample(2200);assert(p->m_CaptureSuffix.empty());game.input.forward=0;f.capture(2400);assert(System::shots.empty());}
{Fixture f;f.queue();f.baseline();f.capture(2399);assert(System::shots.empty());}
{Fixture f;f.queue();f.baseline();f.capture(45000);assert(System::shots.empty());}
{Fixture f;f.queue();f.baseline();f.open();f.sample(2200);f.close();f.sample(2300);assert(game.queue.captures==3);f.capture(2600);assert(System::shots.size()==1&&System::shots[0].find("-closed")!=string::npos);}
{Fixture f;f.queue();f.baseline();f.open();f.capture(2400);assert(System::shots.empty());}
{Fixture f;f.queue();f.baseline();game.menus.top=&f.menu;f.capture(2400);assert(System::shots.empty());}
{Fixture f;auto p=f.queue();f.baseline();assert(!p->m_CaptureSuffix.empty());p->Cancel();assert(p->m_CaptureSuffix.empty()&&game.queue.capturesRemoved==1);f.capture(2500);assert(System::shots.empty());}
{Fixture f;auto p=f.queue();f.baseline();ERR_OrdinaryInputObserver::ClearWorld();assert(p->m_Finished&&p->m_CaptureSuffix.empty()&&game.queue.capturesRemoved==1);System::now=2500;p->CaptureLater();assert(System::shots.empty());}
{Fixture f;auto p=f.queue();f.sample(100);f.sample(200);f.sample(1000);assert(p->m_BaselineSince==-1);f.sample(1100);assert(!p->m_Ready);f.sample(45000);assert(p->m_Finished&&count("FINISHED observation-bound")==1);}
{Fixture f;auto p=f.queue();for(int t=1;t<=450;t++)f.sample(t);assert(p->m_Finished&&samples==450&&count("FINISHED sample-bound")==1);}
{Fixture f;auto p=f.queue();f.sample(100);System::now=200;f.world.time=99;p->Sample();assert(p->m_Finished&&count("CANCELLED world-clock-reversed")==1);}
@@ -117,6 +178,9 @@ for forbidden in ('SetActionValue(', 'ActivateContext(', 'OpenMenu(', 'CloseMenu
assert s.startswith('#ifndef WORKBENCH')
assert 's_Attempted = false' not in body(P,'static void ClearWorld(')
assert s.count('System.MakeScreenshot(')==1
assert 'ERR INVENTORY OBSERVE:' in s
assert 'm_InventoryStateLines < 30' in s
assert 'override' not in body(P,'string ERR_InputObserverInventoryState(')
for line in s.splitlines():
if 'PrintFormat(' in line:assert all(x not in line for x in ('m_Account','m_PlayerId','m_Life','m_Controller','m_Actor'))
print('PASS: actual observer nonce, admission pins, baseline/menu sequencing, snapshots, bounded clocks/samples, cleanup and one-attempt latch; native untested')
+41 -5
View File
@@ -19,11 +19,14 @@ def records(nonce=NONCE, menus=False):
return f'SCRIPT : ERR INPUT OBSERVE: run={nonce} sample={index} real={1000+real} world={5000+real} {event}\n'
result=line(0,0,'QUEUED')
for i in range(1,450):
opened=menus and 22<=i<=24
opened=menus and 22<=i<=25
result+=line(i,i*100,f'SAMPLE origin=<1, 2, 3> forward=0 input=<0, 0, 0> velocity=<0, 0, 0> inventory={int(opened)}')
if i==21:result+=line(i,i*100,'READY')+line(i,i*100,'CAPTURE_REQUEST baseline')
if menus and i==22:result+=line(i,i*100,'INVENTORY_OPEN')+line(i,i*100,'CAPTURE_REQUEST open')
if menus and i==25:result+=line(i,i*100,'INVENTORY_CLOSED')+line(i,i*100,'CAPTURE_REQUEST closed')
if i==21:result+=line(i,i*100,'READY')
if not menus and i==24:result+=line(i,i*100,'CAPTURE_REQUEST baseline')
if menus and i==22:result+=line(i,i*100,'INVENTORY_OPEN')
if menus and i==25:result+=line(i,i*100,'CAPTURE_REQUEST open')
if menus and i==26:result+=line(i,i*100,'INVENTORY_CLOSED')
if menus and i==29:result+=line(i,i*100,'CAPTURE_REQUEST closed')
return result+line(449,45000,'FINISHED observation-bound')
class InputRunnerTests(unittest.TestCase):
@@ -59,7 +62,7 @@ class InputRunnerTests(unittest.TestCase):
menu=runner.input_observer_evidence(records(menus=True),NONCE)
self.assertTrue(menu['sampler_complete'],menu)
self.assertTrue(menu['inventory_open_observed'] and menu['inventory_close_observed'])
self.assertEqual(menu['capture_requests'],['baseline','open','closed'])
self.assertEqual(menu['capture_requests'],['open','closed'])
def test_malformed_missing_reordered_unbounded_and_foreign_records_refused(self):
good=records()
@@ -79,6 +82,39 @@ class InputRunnerTests(unittest.TestCase):
for bad in (good.replace('INVENTORY_OPEN','INVENTORY_CLOSED'),good.replace('inventory=1','inventory=0'),good.replace('CAPTURE_REQUEST open','CAPTURE_REQUEST baseline')):
self.assertFalse(runner.input_observer_evidence(bad,NONCE)['sampler_complete'])
def test_delayed_capture_requires_delay_current_sample_and_transition_interval(self):
def add_after(text, anchor, suffix):
lines=text.splitlines()
index=next(i for i,line in enumerate(lines) if anchor in line)
prefix=lines[index].split(' SAMPLE ',1)[0] if ' SAMPLE ' in lines[index] else lines[index].rsplit(' ',1)[0]
lines.insert(index+1,prefix+' CAPTURE_REQUEST '+suffix)
return '\n'.join(lines)+'\n'
baseline=records()
without='\n'.join(line for line in baseline.splitlines() if 'CAPTURE_REQUEST' not in line)+'\n'
# A skipped capture is valid; a same-tick or 200ms request is not.
self.assertTrue(runner.input_observer_evidence(without,NONCE)['sampler_complete'])
for anchor in (' READY','sample=23 real=3300 world=7300 SAMPLE'):
self.assertFalse(runner.input_observer_evidence(add_after(without,anchor,'baseline'),NONCE)['sampler_complete'])
duplicate=baseline.replace('CAPTURE_REQUEST baseline','CAPTURE_REQUEST baseline\n'+next(line for line in baseline.splitlines() if 'CAPTURE_REQUEST baseline' in line))
stale=baseline.replace('sample=24 real=3400 world=7400 CAPTURE','sample=23 real=3400 world=7400 CAPTURE')
self.assertFalse(runner.input_observer_evidence(duplicate,NONCE)['sampler_complete'])
self.assertFalse(runner.input_observer_evidence(stale,NONCE)['sampler_complete'])
menus=records(menus=True)
no_caps='\n'.join(line for line in menus.splitlines() if 'CAPTURE_REQUEST' not in line)+'\n'
for anchor,suffix in (('sample=25 real=3500 world=7500 SAMPLE','baseline'),(' INVENTORY_CLOSED','open')):
self.assertFalse(runner.input_observer_evidence(add_after(no_caps,anchor,suffix),NONCE)['sampler_complete'])
wrong_state=menus.replace('sample=25 real=3500 world=7500 SAMPLE origin=<1, 2, 3> forward=0 input=<0, 0, 0> velocity=<0, 0, 0> inventory=1',
'sample=25 real=3500 world=7500 SAMPLE origin=<1, 2, 3> forward=0 input=<0, 0, 0> velocity=<0, 0, 0> inventory=0')
self.assertFalse(runner.input_observer_evidence(wrong_state,NONCE)['sampler_complete'])
def test_delayed_baseline_rejects_intervening_motion_even_if_returned(self):
good=records()
original='sample=22 real=3200 world=7200 SAMPLE origin=<1, 2, 3> forward=0 input=<0, 0, 0> velocity=<0, 0, 0>'
for changed in (original.replace('origin=<1, 2, 3>','origin=<1.2, 2, 3>'),original.replace('forward=0','forward=1'),
original.replace('input=<0, 0, 0>','input=<0, 0, 1>'),original.replace('velocity=<0, 0, 0>','velocity=<0, 0, 1>')):
with self.subTest(changed=changed):
self.assertFalse(runner.input_observer_evidence(good.replace(original,changed),NONCE)['sampler_complete'])
def test_prior_failure_results_are_never_replaced(self):
for previous in ('game-failed','cleanup-failed','source-changed','display-probe-incomplete','timeout','process-exited'):
for content in ('',records()):self.assertEqual(runner.input_observer_result(previous,content,NONCE)[0],previous)
+72
View File
@@ -0,0 +1,72 @@
"""Execute actual task/guide copy for initial waiting and subsequent radio states."""
from policy_harness import ROOT, body, run
source = ROOT / 'addons/core/Scripts/Game/ERR/ERR_ObjectiveText.c'
def translate(s):
s = s.replace('protected ', '').replace('ERR_GameMode mode,', 'ERR_GameMode* mode,')
s = s.replace('ERR_Phase phase', 'int phase').replace('mode.', 'mode->')
s = s.replace('ERR_RadioFeedback feedback', 'int feedback')
s = s.replace('array<ERR_LocationComponent>', 'std::vector<ERR_LocationComponent*>')
s = s.replace('foreach (ERR_LocationComponent location : locations)', 'for (ERR_LocationComponent* location : locations)')
s = s.replace('location.', 'location->').replace('bool roughRadio;', 'bool roughRadio = false;')
s = s.replace('.IsEmpty()', '.empty()').replace('string.Format(', 'Format(')
for name in ('ERR_Phase', 'ERR_LocationKind', 'ERR_RadioFeedback', 'Math'):
s = s.replace(name + '.', name + '::')
return s
methods = '\n'.join(translate(body(source, signature)) for signature in (
'static string RadioFeedbackMessage(', 'protected static string PhaseDescription('))
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <cmath>
using string=std::string;
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT};}
namespace ERR_RadioFeedback {enum {INACTIVE,WAITING,TRANSMITTING,INTERRUPTED};}
namespace Math {float Ceil(float n){return std::ceil(n);}}
struct ERR_LocationComponent {int Kind(){return 0;}int Knowledge(){return 0;}};
struct ERR_GameMode {
int phase=ERR_Phase::RADIO_DEFENCE, feedback=ERR_RadioFeedback::WAITING, remaining=180;
bool occupied=false;int Phase(){return phase;}string PreparationError(){return "";}
void Locations(std::vector<ERR_LocationComponent*>& locations){locations.clear();}
int RadioFeedback(){return feedback;}int DefenceSecondsRemaining(){return remaining;}
bool DefenceOccupied(){return occupied;}float EscapeRadius(){return 9000;}
};
string Format(string s,int value){auto p=s.find("%1");assert(p!=string::npos);return s.replace(p,2,std::to_string(value));}
string Extraction(float radius){assert(radius==9000);return "extraction";}
struct Actual {
''' + methods + r'''
};
bool has(string text,string needle){return text.find(needle)!=string::npos;}
int main(){
ERR_GameMode mode;
for(int duration : {30,180,1800}) {
mode.remaining=duration;
auto task=Actual::PhaseDescription(&mode,true);
assert(has(task,"Waiting for a conscious defender")&&!has(task,"interrupted"));
assert(has(task,std::to_string(duration)+" seconds")&&has(task,"Leaving resets progress"));
auto guide=Actual::PhaseDescription(&mode,false);
assert(has(guide,"Waiting for")&&!has(guide,"interrupted"));
}
mode.feedback=ERR_RadioFeedback::TRANSMITTING;mode.occupied=true;mode.remaining=179;
auto active=Actual::PhaseDescription(&mode,true);
assert(has(active,"up to 180 seconds remaining")&&!has(active,"Waiting")&&!has(active,"interrupted"));
assert(has(Actual::PhaseDescription(&mode,false),"Transmission in progress"));
mode.feedback=ERR_RadioFeedback::INTERRUPTED;mode.occupied=false;mode.remaining=180;
auto interrupted=Actual::PhaseDescription(&mode,true);
assert(has(interrupted,"Transmission interrupted")&&has(interrupted,"180 seconds"));
assert(!has(interrupted,"Waiting")&&has(Actual::PhaseDescription(&mode,false),"Progress has reset"));
mode.feedback=ERR_RadioFeedback::TRANSMITTING;mode.occupied=true;mode.remaining=174;
assert(has(Actual::PhaseDescription(&mode,true),"up to 175 seconds remaining"));
// Occupancy remains the progress authority; WAITING only selects unoccupied copy.
mode.feedback=ERR_RadioFeedback::WAITING;
assert(has(Actual::PhaseDescription(&mode,true),"seconds remaining"));
mode.occupied=false;
assert(has(Actual::PhaseDescription(&mode,true),"Waiting for a conscious defender"));
mode.phase=ERR_Phase::EXTRACTION;assert(Actual::PhaseDescription(&mode,true)=="extraction");
mode.phase=ERR_Phase::VICTORY;assert(Actual::PhaseDescription(&mode,true).empty());
mode.phase=ERR_Phase::DEFEAT;assert(Actual::PhaseDescription(&mode,true).empty());
}
''')
print('PASS: actual initial WAITING task/guide copy, configured duration, unchanged interruption and rounded progress; native task assertion pending')
+2 -1
View File
@@ -10,7 +10,7 @@ s = s.replace('foreach (ERR_LocationComponent location : locations)', 'for (ERR_
s = s.replace('location.', 'location->').replace('null', 'nullptr')
s = s.replace('bool roughRadio;', 'bool roughRadio = false;') # Enforce default initialization
s = s.replace('.IsEmpty()', '.empty()').replace('string.Format(', 'Format(')
for scope in ('ERR_Phase', 'ERR_LocationKind', 'Math'):
for scope in ('ERR_Phase', 'ERR_LocationKind', 'ERR_RadioFeedback', 'Math'):
s = s.replace(scope + '.', scope + '::')
coordinate = body(ROOT / 'addons/core/Scripts/Game/ERR/ERR_DiscoveryMap.c', 'static int Coordinate(').replace('Math.', 'Math::')
run(r'''
@@ -19,6 +19,7 @@ run(r'''
#include <vector>
#include <cmath>
using string = std::string;
namespace ERR_RadioFeedback {enum {INACTIVE,WAITING,TRANSMITTING,INTERRUPTED};}
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT};}
namespace Math {float Ceil(float v){return std::ceil(v);}float Floor(float v){return std::floor(v);}float Round(float v){return std::round(v);}}