63 lines
7.3 KiB
Python
63 lines
7.3 KiB
Python
"""Execute failure-menu lifecycle and native-exit routing with UI adapters."""
|
|
from policy_harness import ROOT, body, run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PreparationFailureMenu.c'
|
|
code='\n'.join(body(p,key) for key in ('static bool Failed(', 'static bool Show(', 'override void OnMenuOpen(', 'protected void Refresh(', 'override void OnMenuUpdate(', 'protected void OpenPause(', 'protected void OnBack(', 'protected void OnPause(', 'protected void OnReturn(', 'override void OnMenuClose('))
|
|
for a,b in [('protected ',''),('override ',''),('ERR_GameMode mode','Mode* mode'),('ERR_GameMode.Get()','Mode::current'),('ERR_Phase.PREPARING','PREPARING'),('MenuManager manager','Manager* manager'),('ChimeraWorld world','World* world'),('SCR_ButtonBaseComponent button','Button* button'),('SCR_ButtonTextComponent.GetButtonText','Button::GetButtonText'),('TextWidget.Cast(','CastText('),('System.IsConsoleApp()','System::console'),('UIConstants.MENU_ACTION_BACK','"MenuBack"'),('EActionTrigger.PRESSED','1'),('ERR_ObjectiveText.Description','Description'),('GameStateTransitions.RequestGameplayEndTransition()','Transitions::End()'),('ChimeraMenuPreset.ERR_PreparationFailure','FAILURE'),('ChimeraMenuPreset.PauseMenu','PAUSE'),('GetGame().','GetGame()->'),('GetRootWidget().','GetRootWidget()->'),('GetInputManager().','GetInputManager()->'),('super.','Native::'),('null','nullptr'),('.IsEmpty()','.empty()')]:code=code.replace(a,b)
|
|
for name in ('mode','m_Mode','manager','world','m_Description','m_ReturnButton','m_PauseButton'):code=code.replace(name+'.',name+'->')
|
|
for name in ('OnReturn','OnPause','OnBack'):
|
|
code=code.replace('Insert('+name+')','Insert("'+name+'")').replace('Remove('+name+')','Remove("'+name+'")').replace(', '+name+');', ', "'+name+'");')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <set>
|
|
using string=std::string;constexpr int PREPARING=0,ACTIVE=1,FAILURE=2,PAUSE=3;
|
|
namespace System{bool console=false;}namespace Transitions{int ends=0;void End(){ends++;}}
|
|
struct Mode{inline static Mode* current=nullptr;int phase=PREPARING;string error="No safe prison: water=10";int Phase(){return phase;}string PreparationError(){return error;}};
|
|
string Description(Mode*m,bool){return "Reason: "+m->error;}
|
|
struct Invoker{std::set<string> callbacks;void Insert(string s){assert(callbacks.insert(s).second);}void Remove(string s){callbacks.erase(s);}};
|
|
struct Text{string text;int writes=0;void SetText(string s){text=s;writes++;}};
|
|
struct Root;
|
|
struct Button{Invoker m_OnClicked;bool enabled=true;void SetEnabled(bool b){enabled=b;}static Button*GetButtonText(string,Root*);};
|
|
struct Root{Text description;Button back,pause;bool buttons=true;Text*FindAnyWidget(string){return &description;}};
|
|
Button*Button::GetButtonText(string n,Root*r){return r->buttons?(n=="ReturnToMainMenu"?&r->back:&r->pause):nullptr;}Text*CastText(Text*t){return t;}
|
|
struct Manager{bool any=false,dialog=false,customOK=true,pauseOK=true,pause=false;int opens=0;bool IsAnyMenuOpen(){return any;}bool IsAnyDialogOpen(){return dialog;}void*OpenMenu(int n){assert(n==FAILURE);opens++;if(!customOK)return nullptr;any=true;return this;}void*FindMenuByPreset(int n){assert(n==PAUSE);return pause?this:nullptr;}};
|
|
struct Input{std::set<string> callbacks;void AddActionListener(string,int,string s){assert(callbacks.insert(s).second);}void RemoveActionListener(string,int,string s){callbacks.erase(s);}};
|
|
struct World{int unpauses=0;void PauseGameTime(bool pause){assert(!pause);unpauses++;}};
|
|
struct Game{Manager manager;Input input;World world;int pauses=0;Manager*GetMenuManager(){return &manager;}Input*GetInputManager(){return &input;}World*GetWorld(){return &world;}void OpenPauseMenu(bool hide,bool fade){assert(!hide&&fade);pauses++;if(manager.pauseOK){manager.pause=true;manager.any=true;}};}game;
|
|
Game*GetGame(){return &game;}
|
|
struct Native{Root root;bool focused=true;int closed=0;Root*GetRootWidget(){return &root;}bool IsFocused(){return focused;}void Close(){closed++;}void OnMenuOpen(){}void OnMenuClose(){}void OnMenuUpdate(float){}};
|
|
struct Menu:Native{Mode*m_Mode=nullptr;Button*m_ReturnButton=nullptr,*m_PauseButton=nullptr;Text*m_Description=nullptr;string m_LastError;bool m_Bound=false,m_Leaving=false;
|
|
'''+code+r'''
|
|
};
|
|
void Reset(Mode&m){game={};System::console=false;Transitions::ends=0;Mode::current=&m;}
|
|
int main(){
|
|
Mode m;Reset(m);assert(!Menu::Show(nullptr));Mode other;assert(!Menu::Show(&other));
|
|
m.error.clear();assert(!Menu::Show(&m));m.error="bad";m.phase=ACTIVE;assert(!Menu::Show(&m));m.phase=PREPARING;
|
|
System::console=true;assert(!Menu::Show(&m)&&game.manager.opens==0);System::console=false;
|
|
assert(Menu::Show(&m)&&game.manager.opens==1);assert(Menu::Show(&m)&&game.manager.opens==1);
|
|
Reset(m);game.manager.dialog=true;assert(Menu::Show(&m)&&game.manager.opens==0); // Don't steal confirmation focus.
|
|
Reset(m);game.manager.customOK=false;assert(Menu::Show(&m)&&game.pauses==1&&game.manager.pause);
|
|
Reset(m);game.manager.customOK=false;game.manager.pauseOK=false;assert(!Menu::Show(&m));
|
|
Reset(m);Menu menu;menu.OnMenuOpen();assert(menu.m_Bound&&game.input.callbacks.size()==1);
|
|
assert(menu.root.back.m_OnClicked.callbacks.count("OnReturn")&&menu.root.pause.m_OnClicked.callbacks.count("OnPause"));
|
|
assert(menu.root.description.text=="Reason: bad");menu.OnMenuUpdate(.1);assert(menu.root.description.writes==1);
|
|
m.error="changed detail";menu.OnMenuUpdate(.1);assert(menu.root.description.text=="Reason: changed detail");
|
|
menu.OnBack();assert(game.pauses==1);menu.focused=false;menu.OnBack();menu.OnReturn(nullptr);assert(game.pauses==1&&Transitions::ends==0);
|
|
menu.focused=true;menu.OnReturn(nullptr);assert(menu.m_Leaving&&Transitions::ends==1&&game.world.unpauses==1);
|
|
assert(!menu.root.back.enabled&&!menu.root.pause.enabled);menu.OnReturn(nullptr);menu.OnBack();assert(Transitions::ends==1&&game.pauses==1);
|
|
menu.OnMenuClose();assert(!menu.m_Bound&&!menu.m_Mode&&game.input.callbacks.empty()&&menu.root.back.m_OnClicked.callbacks.empty()&&menu.root.pause.m_OnClicked.callbacks.empty());
|
|
Reset(m);Menu stale;stale.OnMenuOpen();Mode::current=&other;stale.OnReturn(nullptr);stale.OnBack();assert(Transitions::ends==0&&game.pauses==0);stale.OnMenuUpdate(.1);assert(stale.closed==1);stale.OnMenuClose();
|
|
Reset(m);Menu recovered;recovered.OnMenuOpen();m.error.clear();recovered.OnMenuUpdate(.1);assert(recovered.closed==1);recovered.OnMenuClose();
|
|
m.error="bad";Reset(m);Menu missing;missing.root.buttons=false;missing.OnMenuOpen();missing.OnBack();assert(game.pauses==1);missing.OnMenuClose();
|
|
}
|
|
''')
|
|
status=(p.parent/'ERR_PreparationStatus.c').read_text()
|
|
assert 'if (failed && ERR_PreparationFailureMenu.Show(mode)) m_wLoadingPlaceholder.SetVisible(false);' in status
|
|
config=(ROOT/'addons/core/Configs/ERR/AdminMenus.conf').read_text()
|
|
assert 'MenuPreset ERR_PreparationFailure' in config and 'Class "ERR_PreparationFailureMenu"' in config
|
|
layout=(ROOT/'addons/core/UI/layouts/ERR/PreparationFailure.layout').read_text()
|
|
for name in ('FailureDescription','ReturnToMainMenu','OpenPauseMenu'):assert 'Name "'+name+'"' in layout
|
|
for forbidden in ('Rpc(', 'Transition(ERR_Phase', 'Observe(', 'SetControlledEntity(', 'SpawnEntity'):
|
|
assert forbidden not in p.read_text()
|
|
print('PASS: failure-only menu, repeated-frame/dialog focus, pause fallback, exact error, Esc routing, single local exit, stale/recovered mode guards and listener cleanup; native rendering unverified')
|