Allow leaving failed-start screens; record Workshop 0.0.122
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""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')
|
||||
@@ -11,7 +11,7 @@ def cpp(s):
|
||||
s=s.replace('current.Phase()', 'current->Phase()')
|
||||
s=s.replace('ERR_GameMode mode,','ERR_GameMode* mode,')
|
||||
s=s.replace('ERR_GameMode mode)', 'ERR_GameMode* mode)')
|
||||
for name in ['System','ERR_GameMode','ERR_Phase','ERR_ObjectiveText','ERR_MissionRules']:s=s.replace(name+'.',name+'::')
|
||||
for name in ['System','ERR_GameMode','ERR_Phase','ERR_ObjectiveText','ERR_MissionRules','ERR_PreparationFailureMenu']:s=s.replace(name+'.',name+'::')
|
||||
s=s.replace('TextWidget.Cast(','CastText(').replace('Widget widget)', 'Widget* widget)').replace('widget.', 'widget->')
|
||||
for name in ['mode','m_wLoadingPlaceholder','m_ERRPreparationSpinner','m_ERRPreparationRoot','m_ERRPreparationTitle','m_ERRPreparationDescription']:
|
||||
s=s.replace(name+'.',name+'->')
|
||||
@@ -27,6 +27,7 @@ namespace System{bool console=false;bool IsConsoleApp(){return console;}}
|
||||
namespace ERR_Phase{enum{PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION,VICTORY,DEFEAT};}
|
||||
namespace ERR_MissionRules{bool Terminal(int p){return p==ERR_Phase::VICTORY||p==ERR_Phase::DEFEAT;}}
|
||||
struct ERR_GameMode{static ERR_GameMode* current;int phase=ERR_Phase::PREPARING;string error;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}bool Active(){return phase>=ERR_Phase::PRISON&&phase<=ERR_Phase::EXTRACTION;}string PreparationError(){return error;}};ERR_GameMode* ERR_GameMode::current=nullptr;
|
||||
namespace ERR_PreparationFailureMenu{bool shown=false;bool Show(ERR_GameMode*){return shown;}bool Visible(ERR_GameMode*){return shown;}};
|
||||
namespace ERR_ObjectiveText{string Description(ERR_GameMode* mode,bool){return mode->error.empty()?"Preparing sites":"Mission failed; ask an administrator to correct and restart";}}
|
||||
struct Widget{Widget* parent=nullptr;bool visible=true,removed=false,textWidget=false;string text;int textWrites=0,visibilityWrites=0;float width=900,height=170;std::map<string,Widget*> children;
|
||||
void GetScreenSize(float& w,float& h){w=width;h=height;}Widget* GetParent(){return parent;}void RemoveFromHierarchy(){removed=true;parent=nullptr;}void SetVisible(bool value){visible=value;visibilityWrites++;}bool IsVisible(){return visible;}
|
||||
@@ -40,8 +41,9 @@ struct Probe:Native{
|
||||
bool m_ERRKeepSpawnSceneLoading=false,m_ERRPreparationSpinnerHidden=false,m_ERRPreparationSpinnerWasVisible=false;string m_ERRPreparationLastTitle,m_ERRPreparationLastDescription;
|
||||
'''+methods+r'''
|
||||
};
|
||||
struct Fixture{ERR_GameMode mode;Widget parent,spinner;Probe p;Fixture(){game.workspace={};System::console=false;ERR_GameMode::current=&mode;p.m_pGameMode=&mode;p.m_wLoadingPlaceholder=&parent;parent.children["Spinner"]=&spinner;spinner.parent=&parent;}};
|
||||
struct Fixture{ERR_GameMode mode;Widget parent,spinner;Probe p;Fixture(){game.workspace={};System::console=false;ERR_PreparationFailureMenu::shown=false;ERR_GameMode::current=&mode;p.m_pGameMode=&mode;p.m_wLoadingPlaceholder=&parent;parent.children["Spinner"]=&spinner;spinner.parent=&parent;}};
|
||||
int main(){
|
||||
{Fixture f;f.mode.error="bad";ERR_PreparationFailureMenu::shown=true;f.p.UpdateLoadingPlaceholder(.1f);assert(!f.parent.visible&&f.p.ERR_ProbePreparationVisible(&f.mode,true));}
|
||||
{Fixture f;f.mode.phase=ERR_Phase::PRISON;f.p.ERR_ShowSpawnSceneLoading();auto root=f.p.m_ERRPreparationRoot;f.p.ERR_KeepSpawnSceneLoading(true);f.p.DestroyLoadingPlaceholder();assert(!root->removed&&!f.parent.removed&&f.p.destroys==0);f.p.ERR_KeepSpawnSceneLoading(false);f.p.DestroyLoadingPlaceholder();assert(root->removed&&f.p.destroys==1);f.p.ERR_ShowSpawnSceneLoading();assert(f.p.m_wLoadingPlaceholder&&f.p.ERR_ProbePreparationVisible(&f.mode,false));}
|
||||
{Fixture f;f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.nativeUpdates==1&&game.workspace.creates==1&&f.p.ERR_ProbePreparationVisible(&f.mode,false));
|
||||
Widget* root=f.p.m_ERRPreparationRoot;auto title=f.p.m_ERRPreparationTitle;auto description=f.p.m_ERRPreparationDescription;
|
||||
|
||||
Reference in New Issue
Block a user