Files
EscapistsReintroduced/tests/test_preparation_status.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

96 lines
12 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 mode,','ERR_GameMode* mode,')
s=s.replace('ERR_GameMode mode)', 'ERR_GameMode* mode)')
for name in ['System','ERR_GameMode','ERR_Phase','ERR_ObjectiveText']: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};}
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_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_GameMode::current=&mode;p.m_pGameMode=&mode;p.m_wLoadingPlaceholder=&parent;parent.children["Spinner"]=&spinner;spinner.parent=&parent;}};
int main(){
{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);
}
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=ERR_Phase::VICTORY;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')