Files
EscapistsReintroduced/tests/test_failure_result_refresh.py
jakub c6ee764c6c
continuous-integration/drone/push Build is passing
Add after-mission statistics; record Workshop 0.0.125
2026-10-30 12:59:32 +01:00

62 lines
6.1 KiB
Python

"""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').replace('TextWidget text','TextWidget* text')
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<int> 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_ERRStatisticsMode','m_wRoot','debrief','text','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)')
s=s.replace('ERR_OnStatisticsChanged().','ERR_OnStatisticsChanged()->').replace('summary.IsEmpty()','summary.empty()')
s=s.replace('Insert(ERR_RefreshStatistics)','Insert(3)').replace('Remove(ERR_RefreshStatistics)','Remove(3)')
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(','protected void ERR_RefreshStatistics(','override void HandlerDeattached('))
notify=body(MODE,'protected void ERR_OnEndReasonReplicated(')
assert '[RplProp(onRplName: "ERR_OnEndReasonReplicated")]' in MODE.read_text()
run(r'''
#include <cassert>
#include <string>
#include <functional>
struct Event{int count=0;std::function<void()> 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,statsChanged;std::string reason,summary;Event* ERR_OnStatisticsChanged(){return &statsChanged;}std::string ERR_RoundSummary(){return summary;}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,statistics;bool exists=true;TextWidget* FindAnyWidget(std::string name){return exists?(name=="RoundStatistics"?&statistics:&debrief):nullptr;}};
struct Data{int m_GameOverInfo;};
struct Content{Event escapeEvent;bool m_ERREscapeContent=false;Mode* m_ERRFailureMode=nullptr;Mode* m_ERRStatisticsMode=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};mode.statsChanged.handler=[&](){content.ERR_RefreshStatistics();};
content.InitContent(&failure);assert(mode.m_OnEndReasonChanged.count==1&&root.debrief.text.find("No surviving")!=std::string::npos);
assert(mode.statsChanged.count==1&&root.statistics.text=="Waiting for mission statistics...");
mode.summary="Player: kills 2, escaped";mode.statsChanged.Invoke();assert(root.statistics.text==mode.summary);
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.statsChanged.count==0&&content.m_ERRStatisticsMode==nullptr);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')