Files
EscapistsReintroduced/tests/test_escape_result_wording.py
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

106 lines
5.6 KiB
Python

"""Run actual personal-result title/debrief and refresh methods with UI adapters.
No native replication, end-screen rendering or multiplayer claim.
"""
import re
from policy_harness import ROOT, body, run
SOURCE=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_EscapeGameOverScreenInfo.c'
methods='\n'.join(body(SOURCE, signature) for signature in (
'protected ERR_PersonalEscapeResult PersonalResult(',
'override string GetTitle(',
'override string GetDebriefing('))
refresh=body(SOURCE,'protected void ERR_RefreshEscapeContent(')
def adapt(text):
text=re.sub(r' #ifdef WORKBENCH\n.*? #endif\n','',text,flags=re.S)
text=text.replace('protected ','').replace('override ','')
text=text.replace('ERR_PersonalEscapeResult.','Result::').replace('ERR_PersonalEscapeResult','Result::Value')
text=text.replace('array<int> otherPlayerIds','std::vector<int>* otherPlayerIds')
text=re.sub(r'\bstring\b','std::string',text)
text=text.replace('GetGame().GetPlayerController()', 'GetGame()->GetPlayerController()')
text=text.replace('SCR_PlayerController.Cast(', 'Controller::Cast(').replace('SCR_PlayerController controller', 'Controller* controller')
text=text.replace('TextWidget.Cast(', 'TextWidget::Cast(')
text=text.replace('TextWidget title', 'TextWidget* title').replace('TextWidget debrief', 'TextWidget* debrief')
# Enforce manages this local object; the C++ adapter uses scoped ownership.
text=text.replace('ERR_EscapeGameOverScreenInfo info = new ERR_EscapeGameOverScreenInfo();', 'auto info = std::make_unique<Info>();')
for variable in ('controller','info','title','debrief','m_wRoot'):
text=text.replace(variable+'.',variable+'->')
text=re.sub(r'\bnull\b','nullptr',text)
return text
run(r'''
#include <cassert>
#include <memory>
#include <string>
#include <vector>
namespace Result {enum Value {UNKNOWN,ESCAPED,MISSED};}
struct Controller {
int id=7;Result::Value result=Result::UNKNOWN;
static Controller* Cast(Controller* value){return value;}
int GetPlayerId(){return id;}Result::Value ERR_EscapeResult(){return result;}
};
struct Game {Controller* local=nullptr;Controller* GetPlayerController(){return local;}} game;
Game* GetGame(){return &game;}
struct Info {
'''+adapt(methods)+r'''
};
struct TextWidget {std::string value;int writes=0;
static TextWidget* Cast(TextWidget* widget){return widget;}
void SetText(std::string text){value=text;writes++;}
};
struct Root {TextWidget title,debrief;bool hasTitle=true,hasDebrief=true;
TextWidget* FindAnyWidget(std::string name){if(name=="title")return hasTitle?&title:nullptr;return hasDebrief?&debrief:nullptr;}
};
struct Content {
bool m_ERREscapeContent=true;Root* m_wRoot=nullptr;
std::string m_sTileName="title",m_sDebriefingName="debrief";
'''+adapt(refresh)+r'''
};
int main(){
Controller controller;game.local=&controller;Info info;
std::vector<int> staleWinners={7};std::vector<int> noWinners;
const char* titles[]={"Round ended","You escaped","The helicopter escaped"};
const char* descriptions[]={"Your personal escape result is not available.","You crossed the extraction boundary aboard the helicopter.","You did not escape with the helicopter."};
for(int state=0;state<3;state++){
controller.result=static_cast<Result::Value>(state);
for(auto* winners:{&staleWinners,&noWinners,static_cast<std::vector<int>*>(nullptr)}){
assert(info.GetTitle(7,winners)==titles[state]);
assert(info.GetDebriefing(7,winners)==descriptions[state]);
// A different connection ID cannot use this local controller's result.
assert(info.GetTitle(8,winners)==titles[0]);
assert(info.GetDebriefing(8,winners)==descriptions[0]);
}
}
game.local=nullptr;
assert(info.GetTitle(7,&staleWinners)==titles[0]);
assert(info.GetDebriefing(7,&staleWinners)==descriptions[0]);
// The unchanged actual refresh method pulls only the current personal result;
// late arrival, account reset, and missing local controller refresh both texts.
game.local=&controller;Root root;Content content;content.m_wRoot=&root;
for(int state:{0,1,2,0}){
controller.result=static_cast<Result::Value>(state);content.ERR_RefreshEscapeContent();
assert(root.title.value==titles[state]&&root.debrief.value==descriptions[state]);
}
game.local=nullptr;content.ERR_RefreshEscapeContent();
assert(root.title.value==titles[0]&&root.debrief.value==descriptions[0]);
assert(root.title.writes==5&&root.debrief.writes==5);
content.m_ERREscapeContent=false;content.ERR_RefreshEscapeContent();
assert(root.title.writes==5&&root.debrief.writes==5);
content.m_ERREscapeContent=true;content.m_wRoot=nullptr;content.ERR_RefreshEscapeContent();
assert(root.title.writes==5&&root.debrief.writes==5);
content.m_wRoot=&root;root.hasTitle=false;content.ERR_RefreshEscapeContent();
assert(root.title.writes==5&&root.debrief.writes==6);
root.hasTitle=true;root.hasDebrief=false;content.ERR_RefreshEscapeContent();
assert(root.title.writes==6&&root.debrief.writes==6);
}
''')
# Keep native subscription/detach wiring intact; actual text refresh is executed above.
init=body(SOURCE,'override void InitContent(')
detach=body(SOURCE,'override void HandlerDeattached(')
assert 'SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);' in init
assert 'SCR_PlayerController.ERR_OnEscapeResultChanged().Insert(ERR_RefreshEscapeContent);' in init
assert 'ERR_RefreshEscapeContent();' in init
assert 'SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);' in detach
assert 'super.HandlerDeattached(w);' in detach
print('PASS: actual three-state result wording and UI refresh; stale winner IDs never manufacture escape')