"""Actual result callbacks/UI lifecycle with engine boundaries doubled, not native replication.""" import os,re,sys from pathlib import Path REPO=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced')) sys.path.insert(0,str(REPO/'tests')) from policy_harness import body,run SOURCE=Path(os.environ.get('ERR_RESULT_SOURCE',REPO/'addons/core/Scripts/Game/ERR/00_Core/ERR_EscapeGameOverScreenInfo.c')) MODE=Path(os.environ.get('ERR_MODE_SOURCE',REPO/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c')) def adapt(s): s=s.replace('protected ','').replace('override ','').replace('null','nullptr') s=s.replace('ERR_GameMode.Get()','Mode::Get()').replace('ERR_GameMode mode','Mode* mode') s=s.replace('ERR_FailureGameOverScreenInfo.Cast(', 'FailureInfo::Cast(').replace('ERR_EscapeGameOverScreenInfo.Cast(', 'EscapeInfo::Cast(') s=s.replace('ERR_FailureGameOverScreenInfo info = new ERR_FailureGameOverScreenInfo();','FailureInfo info;') s=s.replace('TextWidget.Cast(', 'TextWidget::Cast(').replace('TextWidget debrief','TextWidget* debrief') s=s.replace('SCR_PlayerController.ERR_OnEscapeResultChanged()','escapeEvent') s=s.replace('SCR_GameOverScreenUIContentData endScreenUIContent','Data* endScreenUIContent').replace('Widget w','int w') s=s.replace('array otherPlayerIds','void* otherPlayerIds').replace('string ','std::string ') s=s.replace('super.InitContent(endScreenUIContent);','nativeInit++;').replace('super.HandlerDeattached(w);','nativeDetach++;') for name in ('m_ERRFailureMode','m_wRoot','debrief','mode','endScreenUIContent'):s=s.replace(name+'.',name+'->') s=s.replace('OnEndReasonChanged().','OnEndReasonChanged()->').replace('EndReason().IsEmpty()','EndReason().empty()') s=s.replace('Insert(ERR_RefreshFailureContent)','Insert(1)').replace('Remove(ERR_RefreshFailureContent)','Remove(1)') s=s.replace('Insert(ERR_RefreshEscapeContent)','Insert(2)').replace('Remove(ERR_RefreshEscapeContent)','Remove(2)') return s failure=body(SOURCE,'class ERR_FailureGameOverScreenInfo') failure=failure[failure.index(' override string'):failure.rindex('}')] callbacks='\n'.join(body(SOURCE,x) for x in ('override void InitContent(','protected void ERR_RefreshFailureContent(','override void HandlerDeattached(')) notify=body(MODE,'protected void ERR_OnEndReasonReplicated(') assert '[RplProp(onRplName: "ERR_OnEndReasonReplicated")]' in MODE.read_text() run(r''' #include #include #include struct Event{int count=0;std::function handler;void Insert(int){count=1;}void Remove(int){count=0;}void Invoke(){if(count&&handler)handler();}}; struct Mode{static inline Mode* current=nullptr;Event m_OnEndReasonChanged;std::string reason;static Mode* Get(){return current;}std::string EndReason(){return reason;}Event* OnEndReasonChanged(){return &m_OnEndReasonChanged;} NOTIFY }; struct FailureInfo{static bool Cast(int kind){return kind==1;} FAILURE }; struct EscapeInfo{static EscapeInfo* Cast(int kind){static EscapeInfo value;return kind==2?&value:nullptr;}}; struct TextWidget{std::string text;int writes=0;static TextWidget* Cast(TextWidget* w){return w;}void SetText(std::string value){text=value;writes++;}}; struct Root{TextWidget debrief;bool exists=true;TextWidget* FindAnyWidget(std::string){return exists?&debrief:nullptr;}}; struct Data{int m_GameOverInfo;}; struct Content{Event escapeEvent;bool m_ERREscapeContent=false;Mode* m_ERRFailureMode=nullptr;Root* m_wRoot=nullptr;std::string m_sDebriefingName="debrief";int nativeInit=0,nativeDetach=0,escapeRefresh=0;void ERR_RefreshEscapeContent(){escapeRefresh++;} CALLBACKS }; int main(){Mode mode;Mode::current=&mode;Root root;Content content;content.m_wRoot=&root;mode.m_OnEndReasonChanged.handler=[&](){content.ERR_RefreshFailureContent();};Data failure{1},escape{2},other{3}; content.InitContent(&failure);assert(mode.m_OnEndReasonChanged.count==1&&root.debrief.text.find("No surviving")!=std::string::npos); mode.reason="An administrator abandoned this round.";mode.ERR_OnEndReasonReplicated();assert(root.debrief.text==mode.reason&&root.debrief.writes==2); content.HandlerDeattached(0);assert(mode.m_OnEndReasonChanged.count==0&&content.m_ERRFailureMode==nullptr);mode.reason="late";mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==2); content.InitContent(&failure);assert(root.debrief.writes==3);content.InitContent(&escape);assert(mode.m_OnEndReasonChanged.count==0&&content.escapeEvent.count==1&&content.escapeRefresh==1);mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==3); content.InitContent(&failure);assert(content.escapeEvent.count==0);Mode replacement;Mode::current=&replacement;mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==4); Mode::current=&mode;content.m_wRoot=nullptr;mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==4);content.m_wRoot=&root;root.exists=false;mode.ERR_OnEndReasonReplicated();assert(root.debrief.writes==4); content.InitContent(&other);assert(content.m_ERRFailureMode==nullptr&&mode.m_OnEndReasonChanged.count==0&&content.escapeEvent.count==0); } '''.replace('NOTIFY',adapt(notify)).replace('FAILURE',adapt(failure)).replace('CALLBACKS',adapt(callbacks))) print('PASS: late authoritative failure reason refreshes open result; detach, mode change and success subscriptions preserved')