Files
EscapistsReintroduced/tests/test_preparation_status.py
T
jakub 9233320fd1
continuous-integration/drone/push Build is passing
Allow leaving failed-start screens; record Workshop 0.0.122
2026-10-25 14:55:36 +01:00

128 lines
16 KiB
Python

"""Execute loading presentation/cleanup and native observation bodies with UI adapters.
No native layout rendering, multiplayer, or real startup-failure claim.
"""
from policy_harness import ROOT, body, run
SOURCE=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PreparationStatus.c'
def cpp(s):
s=s.replace('protected ','').replace('override ','').replace('null','nullptr')
s=s.replace('ERR_GameMode mode = ERR_GameMode.Get();','ERR_GameMode* mode = ERR_GameMode::Get();')
s=s.replace('ERR_GameMode current = ERR_GameMode::Get();','ERR_GameMode* current = ERR_GameMode::Get();')
s=s.replace('ERR_GameMode current = ERR_GameMode.Get();','ERR_GameMode* current = ERR_GameMode::Get();')
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','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+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorkspace().','GetWorkspace()->').replace('.IsEmpty()','.empty()')
return s.replace('super.','Native::')
methods='\n'.join(cpp(body(SOURCE,key)) for key in ('void ERR_KeepSpawnSceneLoading(', 'void ERR_ShowSpawnSceneLoading(', 'protected string ERR_PreparationDescription(', 'protected void ERR_ClearPreparationStatus(', 'override void UpdateLoadingPlaceholder(', 'override void DestroyLoadingPlaceholder(', 'protected bool ERR_PreparationWidgetVisible(', 'bool ERR_ProbePreparationVisible('))
run(r'''
#include <cassert>
#include <string>
#include <map>
using string=std::string;
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;}
Widget* FindAnyWidget(string name){return children.count(name)?children[name]:nullptr;}void SetText(string value){text=value;textWrites++;}string GetText(){return text;}};
Widget* CastText(Widget* w){return w&&w->textWidget?w:nullptr;}
struct Workspace{int creates=0;bool fail=false,missingText=false;Widget* CreateWidgets(const char*,Widget* parent){creates++;if(fail)return nullptr;auto root=new Widget;root->parent=parent;for(string name:{"ERR_PreparationTitle","ERR_PreparationDescription"}){auto child=new Widget;child->parent=root;child->textWidget=true;root->children[name]=child;}if(missingText)root->children.erase("ERR_PreparationTitle");return root;}};
struct Game{Workspace workspace;Workspace* GetWorkspace(){return &workspace;}};Game game;Game* GetGame(){return &game;}
struct Native{Widget* m_wLoadingPlaceholder=nullptr;ERR_GameMode* m_pGameMode=nullptr;int nativeUpdates=0,destroys=0;void CreateLoadingPlaceholder(){m_wLoadingPlaceholder=new Widget;}void UpdateLoadingPlaceholder(float){nativeUpdates++;}void DestroyLoadingPlaceholder(){destroys++;if(m_wLoadingPlaceholder)m_wLoadingPlaceholder->RemoveFromHierarchy();m_wLoadingPlaceholder=nullptr;}};
struct Probe:Native{
Widget *m_ERRPreparationParent=nullptr,*m_ERRPreparationRoot=nullptr,*m_ERRPreparationTitle=nullptr,*m_ERRPreparationDescription=nullptr,*m_ERRPreparationSpinner=nullptr;
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_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;
for(int i=0;i<200;i++){f.p.UpdateLoadingPlaceholder(.1f);}assert(game.workspace.creates==1&&title->textWrites==1&&description->textWrites==1);
f.mode.error="invalid catalog";f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.ERR_ProbePreparationVisible(&f.mode,true)&&!f.spinner.visible);
for(int i=0;i<200;i++){f.p.UpdateLoadingPlaceholder(.1f);}assert(f.p.m_ERRPreparationRoot==root&&title->textWrites==2&&description->textWrites==2&&f.spinner.visibilityWrites==1);
title->width=0;assert(!f.p.ERR_ProbePreparationVisible(&f.mode,true));title->width=900;f.mode.error.clear();f.p.UpdateLoadingPlaceholder(.1f);assert(f.spinner.visible&&f.p.ERR_ProbePreparationVisible(&f.mode,false));
f.mode.phase=ERR_Phase::PRISON;f.p.UpdateLoadingPlaceholder(.1f);assert(!root->removed&&!f.parent.removed&&f.spinner.visible&&f.p.ERR_ProbePreparationVisible(&f.mode,false));
assert(description->text=="Please wait while your spawn area and equipment finish loading. You will take control when preparation is complete.");
for(int phase=ERR_Phase::PRISON;phase<=ERR_Phase::EXTRACTION;phase++){f.mode.phase=phase;for(int i=0;i<200;i++)f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.m_ERRPreparationRoot==root&&f.p.ERR_ProbePreparationVisible(&f.mode,false));}
f.mode.phase=ERR_Phase::VICTORY;f.p.UpdateLoadingPlaceholder(.1f);assert(root->removed&&f.parent.removed&&f.spinner.visible&&!f.p.m_ERRPreparationRoot&&f.p.destroys==1);
}
for(int phase : {ERR_Phase::VICTORY,ERR_Phase::DEFEAT}){Fixture f;f.p.UpdateLoadingPlaceholder(.1f);f.p.ERR_KeepSpawnSceneLoading(true);f.mode.phase=phase;f.p.UpdateLoadingPlaceholder(.1f);assert(f.parent.removed&&!f.p.m_ERRKeepSpawnSceneLoading&&f.p.destroys==1);}
{Fixture f;f.p.ERR_KeepSpawnSceneLoading(true);ERR_GameMode replacement;ERR_GameMode::current=&replacement;f.p.DestroyLoadingPlaceholder();assert(f.parent.removed&&f.p.destroys==1);}
{Fixture f;f.p.ERR_KeepSpawnSceneLoading(true);ERR_GameMode::current=nullptr;f.p.DestroyLoadingPlaceholder();assert(f.parent.removed&&f.p.destroys==1);}
for(int mismatch=0;mismatch<6;mismatch++){Fixture f;ERR_GameMode other;if(mismatch==0)ERR_GameMode::current=nullptr;if(mismatch==1)f.p.m_pGameMode=&other;if(mismatch==2)f.mode.phase=-1;if(mismatch==3)System::console=true;if(mismatch==4)f.p.m_wLoadingPlaceholder=nullptr;if(mismatch==5)f.p.m_pGameMode=nullptr;f.p.UpdateLoadingPlaceholder(.1f);assert(game.workspace.creates==0&&f.p.nativeUpdates==1&&!f.parent.removed&&f.spinner.visibilityWrites==0);}
{Fixture f;f.mode.phase=ERR_Phase::PRISON;f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.ERR_ProbePreparationVisible(&f.mode,false));Widget* root=f.p.m_ERRPreparationRoot;f.p.DestroyLoadingPlaceholder();assert(root->removed&&f.parent.removed&&!f.p.m_ERRPreparationRoot&&f.p.destroys==1);f.p.UpdateLoadingPlaceholder(.1f);assert(game.workspace.creates==1);}
{Fixture f;f.mode.error="bad";f.spinner.visible=false;f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.ERR_ProbePreparationVisible(&f.mode,true));f.p.ERR_ClearPreparationStatus();assert(!f.spinner.visible);}
{Fixture f;f.mode.error="bad";f.p.UpdateLoadingPlaceholder(.1f);Widget* old=f.p.m_ERRPreparationRoot;Widget newParent,newSpinner;newParent.children["Spinner"]=&newSpinner;newSpinner.parent=&newParent;f.p.m_wLoadingPlaceholder=&newParent;f.p.UpdateLoadingPlaceholder(.1f);assert(old->removed&&f.spinner.visible&&!newSpinner.visible&&f.p.m_ERRPreparationParent==&newParent&&game.workspace.creates==2);f.p.DestroyLoadingPlaceholder();assert(newParent.removed&&!f.parent.removed&&newSpinner.visible&&!f.p.m_ERRPreparationRoot&&f.p.destroys==1);}
{Fixture f;f.mode.error="bad";f.p.UpdateLoadingPlaceholder(.1f);f.p.m_ERRPreparationParent=nullptr;f.p.m_ERRPreparationRoot=nullptr;f.p.m_ERRPreparationTitle=nullptr;f.p.m_ERRPreparationDescription=nullptr;f.p.m_ERRPreparationSpinner=nullptr;f.p.m_wLoadingPlaceholder=nullptr;f.p.UpdateLoadingPlaceholder(.1f);assert(!f.p.m_ERRPreparationSpinnerHidden);}
{Fixture f;game.workspace.fail=true;f.p.UpdateLoadingPlaceholder(.1f);assert(!f.p.m_ERRPreparationRoot&&f.spinner.visibilityWrites==0);game.workspace.fail=false;f.p.UpdateLoadingPlaceholder(.1f);assert(f.p.ERR_ProbePreparationVisible(&f.mode,false));}
{Fixture f;game.workspace.missingText=true;f.p.UpdateLoadingPlaceholder(.1f);assert(!f.p.m_ERRPreparationRoot&&f.spinner.visibilityWrites==0);}
{Fixture f;f.p.UpdateLoadingPlaceholder(.1f);Widget other;Widget* moved=f.p.m_ERRPreparationRoot;moved->parent=&other;f.p.UpdateLoadingPlaceholder(.1f);assert(moved->removed&&f.p.m_ERRPreparationRoot!=moved&&f.p.m_ERRPreparationParent==&f.parent);}
}
''')
probe=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativePreparationStatusProbe.c'
s=body(probe,'static int Tick(').replace('ERR_GameMode mode,','ERR_GameMode* mode,').replace('ERR_GameMode.Get()','ERR_GameMode::Get()').replace('ERR_Phase.PREPARING','PREPARING').replace('mode.','mode->').replace('.IsEmpty()','.empty()').replace('SCR_RespawnSystemComponent respawn','SCR_RespawnSystemComponent* respawn').replace('SCR_RespawnSystemComponent.GetInstance()','SCR_RespawnSystemComponent::GetInstance()').replace('respawn.','respawn->').replace('System.MakeScreenshot','System::MakeScreenshot')
run(r'''
#include <cassert>
#include <string>
using string=std::string;const int PREPARING=0;
struct ERR_GameMode{static ERR_GameMode* current;int phase=0;string error="bad";static ERR_GameMode* Get(){return current;}int Phase(){return phase;}string PreparationError(){return error;}};ERR_GameMode* ERR_GameMode::current=nullptr;
struct SCR_RespawnSystemComponent{bool visible=true;static SCR_RespawnSystemComponent instance;static SCR_RespawnSystemComponent* GetInstance(){return &instance;}bool ERR_ProbePreparationVisible(ERR_GameMode*,bool failed){assert(failed);return visible;}};SCR_RespawnSystemComponent SCR_RespawnSystemComponent::instance;
namespace System{int captures=0;void MakeScreenshot(const char*){captures++;}}void Print(const char*){}
struct Probe{static ERR_GameMode* s_Mode;static float s_Deadline,s_Since,s_Last,s_CaptureAt;
'''+s+r'''
};ERR_GameMode* Probe::s_Mode=nullptr;float Probe::s_Deadline=0,Probe::s_Since=-1,Probe::s_Last=-1,Probe::s_CaptureAt=-1;
int main(){ERR_GameMode mode;ERR_GameMode::current=&mode;
for(int i=0;i<=33;i++){assert(Probe::Tick(&mode,i*500)==0);}assert(System::captures==0);assert(Probe::Tick(&mode,17000)==0&&System::captures==1);assert(Probe::Tick(&mode,17500)==1&&System::captures==1);
ERR_GameMode next;ERR_GameMode::current=&next;assert(Probe::Tick(&next,20000)==0);assert(Probe::Tick(&next,22000)==0&&Probe::s_Since==22000);SCR_RespawnSystemComponent::instance.visible=false;assert(Probe::Tick(&next,22500)==0&&Probe::s_Since==-1);assert(Probe::Tick(&next,50000)==-1);
}
''')
text=SOURCE.read_text()
prefab=(ROOT/'addons/core/Prefabs/ERR/GameMode.et').read_text()
assert 'm_bUseSpawnPreload 1' in prefab and 'm_bUseSpawnPreload 0' not in prefab
assert 'GetOwner(' not in text
assert text.count('m_pGameMode != mode') == 3
# m_pGameMode is the installed vanilla protected owner field, assigned by
# SCR_RespawnSystemComponent.OnInit(IEntity owner). The adapter deliberately
# exposes that field rather than inventing a ScriptComponent.GetOwner API.
for forbidden in ('CallLater(', 'SetCamera(', 'SpawnEntity', 'SetControlledEntity(', 'Transition(', 'PreparationError() ='):
assert forbidden not in text,forbidden
native=probe.read_text()
for forbidden in ('CreateWidgets(', 'UpdateLoadingPlaceholder(', 'SetText(', 'SetVisible('):assert forbidden not in native
base=(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_NativeBaseSmoke.c').read_text()
assert 'ERR_NativePreparationStatusProbe.Tick(this, now)' in base
assert 'native-end-active' in base and 'native-end-preparing' in base
print('PASS: actual preparation status scope, persistent updates, spinner restoration, placeholder replacement/deletion/destruction, native observation persistence/timeout; rendering remains native-pending')
# Execute the production world-system rebind that restores frame delivery.
source=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverManagers.c'
bind=body(source,'void ERR_BindEscapeRespawn(').replace('SCR_RespawnSystemComponent respawn','Respawn* respawn').replace('ERR_GameMode mode = ERR_GameMode.Get();','Mode* mode = Mode::current;').replace('SCR_RespawnSystemComponent.Cast(mode.FindComponent(SCR_RespawnSystemComponent))','mode->respawn')
run(r"""
#include <cassert>
struct Respawn{int frames=0;};struct Mode{inline static Mode* current=nullptr;Respawn* respawn=nullptr;};void Print(const char*){}
struct System{Respawn* m_RespawnSystem=nullptr;bool m_bReady=true,enabled=false;int enables=0;void Enable(bool value){enabled=value;enables++;}
"""+bind+r"""
void Frame(){if(enabled&&m_RespawnSystem)m_RespawnSystem->frames++;}
};
int main(){System system;Mode mode;Respawn original,replacement,unrelated;Mode::current=&mode;mode.respawn=&replacement;system.m_RespawnSystem=&original;
system.ERR_BindEscapeRespawn(&unrelated);assert(system.enables==0&&system.m_RespawnSystem==&original);
system.ERR_BindEscapeRespawn(nullptr);assert(system.enables==0);
system.ERR_BindEscapeRespawn(&replacement);assert(system.enabled&&!system.m_bReady&&system.m_RespawnSystem==&replacement);system.Frame();assert(replacement.frames==1&&original.frames==0);
system.m_bReady=true;system.ERR_BindEscapeRespawn(&replacement);assert(system.m_bReady); // Idempotent: keep current deploy readiness.
Mode::current=nullptr;system.ERR_BindEscapeRespawn(&original);assert(system.m_RespawnSystem==&replacement);
}
""")
create=body(SOURCE,'protected override void CreateLoadingPlaceholder(')
assert create.index('super.CreateLoadingPlaceholder()') < create.index('ERR_BindEscapeRespawn(this)')
assert 'm_fAutoReloadTime 10' in prefab
print('PASS: replaced respawn system receives frame updates, disabled updater wakes, stale/non-Escape binding refused; terminal and stale preload leases release black overlay')