Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
192 lines
14 KiB
Python
192 lines
14 KiB
Python
"""Execute the native name callback and reversible runtime header presentation."""
|
|
from policy_harness import ROOT, body, run
|
|
p = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverPresentation.c'
|
|
s = '\n'.join(body(p, signature) for signature in [
|
|
'static string Path(', 'string ERR_TerrainMissionTitle(', 'override string GetMissionName(',
|
|
'void ERR_PresentTerrainTakeover(', 'void ERR_RefreshTerrainPresentation(', 'string ERR_TerrainLoadingTitle(', 'override void OnUpdate(', 'protected void ERR_RestoreTerrainPresentation(',
|
|
'override protected void OnBeforeWorldCleanup('])
|
|
for a, b in [('override ', ''), ('protected ', ''),
|
|
('SCR_MissionHeader header', 'SCR_MissionHeader* header'), ('SCR_MissionHeader requested','SCR_MissionHeader* requested'), ('BaseWorld world','World* world'), ('requested.','requested->'),
|
|
('SCR_MissionHeader.Cast', 'SCR_MissionHeader::Cast'),
|
|
('ResourceName ', 'string '), ('GetWorld().', 'GetWorld()->'), ('ERR_TerrainDisplayName.Resolve', 'Resolve'), ('ERR_TerrainDisplayName.Path', 'Path'), ('header.', 'header->'), ('m_ERRPresentedTerrainHeader.', 'm_ERRPresentedTerrainHeader->'),
|
|
('ERR_TerrainTakeoverStage.', 'Stage::'), ('ERR_TerrainTakeoverSettings.Enabled', 'Enabled'),
|
|
('SCR_Global.IsEditMode', 'EditMode'), ('Replication.IsServer', 'Server'), ('super.', 'Base::'), ('null', 'nullptr')]:
|
|
s = s.replace(a, b)
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
struct string:std::string {using std::string::string;string(std::string v):std::string(v){}bool IsEmpty(){return empty();}int Length(){return size();}string Substring(int a,int n){return substr(a,n);}string GetPath(){assert(false && "native resource lookup in title hot path");return *this;}};
|
|
int resolutions=0;string Resolve(string world){resolutions++;return world;}
|
|
namespace Stage {enum {INACTIVE,RUNNING,FAILED};}
|
|
bool enabled=true,editor=false,server=true;
|
|
bool Server(){return server;}
|
|
bool Enabled(){return enabled&&!editor;}bool EditMode(){return editor;}
|
|
template<class... T>void PrintFormat(T...){}
|
|
struct SCR_MissionHeader {
|
|
string m_sName="Nogova — Exploration Alpha",m_sGameMode="Terrain exploration";
|
|
string world="Nogova";string GetWorldResourceName(){return world;}
|
|
string resource="{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf";
|
|
static SCR_MissionHeader* Cast(SCR_MissionHeader* p){return p;}
|
|
string GetHeaderResourceName(){return resource;}
|
|
};
|
|
struct World {string root="Armenhof";int count=1;int GetNumSubScenes(){return count;}string GetSubSceneName(int){return root;}};
|
|
struct Base {World* world=nullptr;string loaded;World* GetWorld(){return world;}string GetWorldFile(){return loaded;}
|
|
SCR_MissionHeader* header=nullptr;int cleanup=0;
|
|
SCR_MissionHeader* GetMissionHeader(){return header;}
|
|
string GetMissionName(){return header?header->m_sName:"";}
|
|
int updates=0;void OnUpdate(World*,float){updates++;}
|
|
void OnBeforeWorldCleanup(){cleanup++;}
|
|
};
|
|
struct Game:Base {
|
|
SCR_MissionHeader* m_ERRPresentedTerrainHeader=nullptr;
|
|
string m_ERROriginalTerrainTitle,m_ERROriginalTerrainGameMode,m_ERRAppliedTerrainTitle,m_ERRTitleWorld,m_ERRTerrainLabel;
|
|
World* m_ERRPresentedTerrainWorld=nullptr;bool m_ERRTitleLoaded=false;float m_ERRTerrainPresentationWait=0;
|
|
int m_ERRTerrainStage=Stage::INACTIVE;
|
|
''' + s + r'''
|
|
};
|
|
int main(){
|
|
Game g;SCR_MissionHeader h;g.header=&h;
|
|
// Native DS request occurs before the original mode has been replaced.
|
|
assert(g.GetMissionName()=="Co10 Escape Reintroduced - Nogova");
|
|
assert(h.m_sName=="Nogova — Exploration Alpha");
|
|
assert(g.GetMissionName()=="Co10 Escape Reintroduced - Nogova"&&resolutions==1);
|
|
h.world="Armenhof";assert(g.GetMissionName()=="Co10 Escape Reintroduced - Armenhof");
|
|
h.world="Nogova";
|
|
World loaded;g.world=&loaded;g.loaded="different";assert(g.GetMissionName()=="Co10 Escape Reintroduced - Nogova");
|
|
g.loaded=h.world;assert(g.GetMissionName()=="Co10 Escape Reintroduced - Armenhof");g.world=nullptr;
|
|
server=false;assert(g.GetMissionName()==h.m_sName);server=true;
|
|
enabled=false;assert(g.GetMissionName()==h.m_sName);enabled=true;
|
|
editor=true;assert(g.GetMissionName()==h.m_sName);g.ERR_PresentTerrainTakeover();assert(!g.m_ERRPresentedTerrainHeader);editor=false;
|
|
g.m_ERRTerrainStage=Stage::FAILED;assert(g.GetMissionName()==h.m_sName);g.m_ERRTerrainStage=Stage::RUNNING;
|
|
// Confirmed takeover changes display metadata, never the terrain selection.
|
|
g.ERR_PresentTerrainTakeover();g.ERR_PresentTerrainTakeover();
|
|
assert(h.m_sName=="Co10 Escape Reintroduced - Nogova"&&h.m_sGameMode=="Escape");
|
|
assert(h.resource=="{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf");
|
|
g.OnBeforeWorldCleanup();assert(g.cleanup==1&&!g.m_ERRPresentedTerrainHeader);
|
|
assert(h.m_sName=="Nogova — Exploration Alpha"&&h.m_sGameMode=="Terrain exploration");
|
|
// A reused header cannot keep Escape's name after an opt-out/restart.
|
|
enabled=false;assert(g.GetMissionName()==h.m_sName);enabled=true;
|
|
SCR_MissionHeader authored;authored.m_sName="Escape — Authored island";authored.m_sGameMode="Escape";
|
|
g.header=&authored;assert(g.GetMissionName()==authored.m_sName);
|
|
g.header=&h;g.ERR_PresentTerrainTakeover();h.m_sName="Another system's title";
|
|
g.ERR_RestoreTerrainPresentation();assert(h.m_sName=="Another system's title"&&h.m_sGameMode=="Terrain exploration");
|
|
h.world="";g.ERR_RestoreTerrainPresentation();assert(g.GetMissionName()=="Co10 Escape Reintroduced");
|
|
// A confirmed running terrain wins over a stale source header, on server and client.
|
|
g.OnBeforeWorldCleanup();h.m_sName="Original Everon scenario";h.world="Everon";g.header=&h;
|
|
World actual;actual.root="Nogova";actual.count=0;g.world=&actual;g.loaded="Nogova";server=false;
|
|
assert(g.ERR_TerrainLoadingTitle(&h)=="Original Everon scenario");
|
|
g.ERR_PresentTerrainTakeover();assert(h.m_sName=="Co10 Escape Reintroduced - Nogova");
|
|
// A late root resolution must refresh the SAME header even after GameMode became Escape.
|
|
actual.count=1;actual.root="Armenhof";g.OnUpdate(&actual,.6);
|
|
assert(h.m_sName=="Co10 Escape Reintroduced - Armenhof"&&g.updates==1);
|
|
assert(g.GetMissionName()==h.m_sName);server=true;assert(g.GetMissionName()==h.m_sName);
|
|
// Raw addon-qualified paths must never reach the native GetPath resolver.
|
|
g.loaded="$ChernarusMinus:Worlds/GM_Cherno/GM_Cherno.ent";actual.root="$ChernarusMinus:Worlds/ChernarusMinusBeta.ent";
|
|
g.ERR_RefreshTerrainPresentation();int warm=resolutions;
|
|
for(int i=0;i<120;i++){g.OnUpdate(&actual,.5);g.GetMissionName();g.ERR_TerrainLoadingTitle(&h);}
|
|
assert(resolutions==warm&&h.m_sName=="Co10 Escape Reintroduced - $ChernarusMinus:Worlds/ChernarusMinusBeta.ent");
|
|
assert(g.Path("{0123456789ABCDEF}Worlds/Island.ent")=="Worlds/Island.ent");
|
|
assert(g.Path(g.loaded)==g.loaded);
|
|
|
|
SCR_MissionHeader copy=h;copy.m_sName="Original Everon scenario";
|
|
assert(g.ERR_TerrainLoadingTitle(©)==h.m_sName);
|
|
copy.resource="different incoming scenario";copy.m_sName="Next map";
|
|
assert(g.ERR_TerrainLoadingTitle(©)=="Next map");
|
|
// A different current header cannot inherit the outgoing world's confirmed title.
|
|
SCR_MissionHeader incoming;incoming.world="New island";g.header=&incoming;
|
|
assert(g.GetMissionName()=="Co10 Escape Reintroduced - New island");
|
|
g.header=&h;h.m_sName="Later mod title";actual.root="Other";g.ERR_PresentTerrainTakeover();
|
|
assert(h.m_sName=="Later mod title");g.ERR_RestoreTerrainPresentation();assert(h.m_sName=="Later mod title");
|
|
h.m_sName="Original Everon scenario";g.ERR_PresentTerrainTakeover();g.OnBeforeWorldCleanup();
|
|
assert(h.m_sName=="Original Everon scenario"&&!g.m_ERRPresentedTerrainWorld);
|
|
g.header=nullptr;assert(g.GetMissionName().empty());g.ERR_PresentTerrainTakeover();g.OnBeforeWorldCleanup();
|
|
}
|
|
''')
|
|
print('PASS: native name callback preserves authored modes, opt-outs and failures; terrain suffix follows world changes, cached resolution and confirmed takeover metadata restores on cleanup without changing scenario resource or overwriting later titles')
|
|
|
|
# Resolve actual parent chains without requiring a loaded game world.
|
|
resolver='\n'.join(body(p,signature) for signature in ('static string Path(', 'static string Label(', 'static string Resolve('))
|
|
for a,b in [('foreach (','for ('),('ResourceName','string'),('array<string>','Array<string>'),('Resource resource','Resource* resource'),('BaseContainer container','Container* container'),('BaseContainerTools.LoadContainer','Load'),('resource.','resource->'),('container.','container->'),('GetResource().','GetResource()->'),('FilePath.StripPath','StripPath'),('container->Get("Parent", parent)','container->Get(parent)')]:resolver=resolver.replace(a,b)
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <algorithm>
|
|
struct string:std::string {
|
|
using std::string::string;string(std::string v):std::string(v){}bool IsEmpty(){return empty();}int Length(){return size();}
|
|
int IndexOf(string x){auto n=find(x);return n==npos?-1:int(n);}string Substring(int a,int n){return substr(a,n);}
|
|
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
|
|
void ToUpper(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::toupper(c);});}
|
|
void Replace(string a,string b){size_t n=0;while((n=find(a,n))!=npos){replace(n,a.size(),b);n+=b.size();}}
|
|
string Trim(){auto a=find_first_not_of(" \r\n");if(a==npos)return "";return substr(a,find_last_not_of(" \r\n")-a+1);}
|
|
string GetPath(){auto n=find('}');return n==npos?*this:string(substr(n+1));}
|
|
};
|
|
string StripPath(string s){auto n=s.find_last_of('/');return n==s.npos?s:string(s.substr(n+1));}
|
|
template<class T>struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
|
struct Container{string parent;bool Get(string& p){p=parent;return !p.IsEmpty();}};
|
|
struct Resource{Container container;bool valid=true;bool IsValid(){return valid;}Resource* GetResource(){return this;}Container* ToBaseContainer(){return &container;}};
|
|
std::map<string,Resource> resources;int calls=0;
|
|
Resource* Load(string world){calls++;auto it=resources.find(world);return it==resources.end()?nullptr:&it->second;}
|
|
struct Names {
|
|
'''+resolver+r'''
|
|
};
|
|
int main(){
|
|
resources["Worlds/GM/Conflict.ent"].container.parent="Worlds/Armenhof/Armenhof.ent";
|
|
resources["Worlds/Armenhof/Armenhof.ent"];
|
|
assert(Names::Resolve("Worlds/GM/Conflict.ent")=="Armenhof");
|
|
resources["Worlds/Nogova/Nogova_Test.ent"].container.parent="Worlds/Nogova/Nogova.ent";
|
|
resources["Worlds/Nogova/Nogova.ent"];
|
|
assert(Names::Resolve("Worlds/Nogova/Nogova_Test.ent")=="Nogova");
|
|
assert(Names::Label("{853E92315D1D9EFE}worlds/Eden/Eden.ent")=="Everon");
|
|
assert(Names::Resolve("Worlds/New_Island.ent")=="New Island");
|
|
assert(Names::Label("Worlds/GameMaster/GM_Armenhof.ent")=="Armenhof");
|
|
assert(Names::Label("Worlds/Nogova/Nogova_Test.ent")=="Nogova");
|
|
assert(Names::Label("Worlds/ERR_Everon.ent")=="Everon");
|
|
assert(Names::Label("Worlds/armenhof.ent")=="Armenhof");
|
|
assert(Names::Resolve("")=="");
|
|
assert(Names::Path("$ChernarusMinus:Worlds/GM_Cherno/GM_Cherno.ent")=="$ChernarusMinus:Worlds/GM_Cherno/GM_Cherno.ent");
|
|
assert(Names::Path("{0123456789ABCDEF}Worlds/Island.ent")=="Worlds/Island.ent");
|
|
assert(Names::Path("{bad}Worlds/Island.ent")=="{bad}Worlds/Island.ent");
|
|
assert(Names::Label("$NogovaReforger:Worlds/Nogova/Nogova_Test.ent")=="Nogova");
|
|
|
|
resources["a.ent"].container.parent="b.ent";resources["b.ent"].container.parent="a.ent";
|
|
calls=0;assert(Names::Resolve("a.ent")=="B"&&calls==2);
|
|
for(int i=0;i<30;i++)resources[std::to_string(i)+".ent"].container.parent=std::to_string(i+1)+".ent";
|
|
calls=0;assert(Names::Resolve("0.ent")=="16"&&calls==16);
|
|
}
|
|
''')
|
|
print('PASS: actual terrain parent traversal, Armenhof/Nogova names, Everon alias, missing-resource fallback, empty names and bounded cycles/depth')
|
|
|
|
# Run the real loading component hooks with widget/native-super boundaries.
|
|
loading='\n'.join(body(p,x) for x in ('override void OnMissionDataRetrieved(', 'protected void ERR_RefreshLoadingTerrainTitle(', 'override void Update(float timeSlice'))
|
|
for a,b in [('override ',''),('protected ',''),('SCR_MissionHeader header','SCR_MissionHeader* header'),('super.','Base::'),('m_Widgets.','m_Widgets->'),('m_wTitle.','m_wTitle->'),('GetGame().','GetGame()->')]:loading=loading.replace(a,b)
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
using string=std::string;
|
|
struct SCR_MissionHeader{string title;};
|
|
struct Text{string text;int writes=0;void SetText(string t){text=t;writes++;}};
|
|
struct Widgets{Text*m_wTitle=nullptr;};
|
|
struct Game{bool confirmed=false;string actual="Nogova";string ERR_TerrainLoadingTitle(SCR_MissionHeader*h){return confirmed?"Co10 Escape Reintroduced - "+actual:h->title;}};
|
|
Game game;Game*current=&game;Game*GetGame(){return current;}
|
|
struct Base{Widgets*m_Widgets=nullptr;int inits=0,updates=0;void OnMissionDataRetrieved(SCR_MissionHeader*h){inits++;if(h&&m_Widgets&&m_Widgets->m_wTitle)m_Widgets->m_wTitle->SetText(h->title);}void Update(float,float,float){updates++;}};
|
|
struct Loading:Base{SCR_MissionHeader*m_ERRLoadingTerrainHeader=nullptr;string m_ERRLoadingTerrainTitle;float m_ERRLoadingTerrainWait=0;
|
|
'''+loading+r'''
|
|
};int main(){
|
|
Loading screen;Widgets widgets;Text text;widgets.m_wTitle=&text;screen.m_Widgets=&widgets;
|
|
SCR_MissionHeader source{"Original Everon scenario"};screen.OnMissionDataRetrieved(&source);
|
|
assert(text.text==source.title&&screen.inits==1);
|
|
game.confirmed=true;screen.Update(.25);assert(text.text==source.title);
|
|
screen.Update(.25);assert(text.text=="Co10 Escape Reintroduced - Nogova"&&screen.inits==1&&screen.updates==2);
|
|
int writes=text.writes;screen.Update(.6);assert(text.writes==writes); // unchanged titles don't churn widgets
|
|
game.actual="Armenhof";screen.Update(.6);assert(text.text=="Co10 Escape Reintroduced - Armenhof");
|
|
game.confirmed=false;screen.Update(.6);assert(text.text==source.title);
|
|
screen.OnMissionDataRetrieved(nullptr);screen.Update(.6);assert(screen.inits==2);
|
|
screen.m_Widgets=nullptr;screen.OnMissionDataRetrieved(&source);screen.Update(.6); // layout not attached yet
|
|
screen.m_Widgets=&widgets;current=nullptr;screen.Update(.6); // game teardown
|
|
assert(screen.updates==8);
|
|
}
|
|
''')
|
|
print('PASS: actual loading title hooks follow late takeover/terrain changes, keep native initialization/progress updates, bound refreshes and tolerate missing headers/widgets/game')
|