--- a/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeover.c +++ b/addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeover.c @@ -173,7 +173,7 @@ protected override void OnWorldPostProcess(World world) { // Do not opt an existing authored Escape mission into replacement. - if (ERR_TerrainTakeoverSettings.Enabled() && !ERR_GameMode.Get() && Replication.IsServer()) + if (ERR_TerrainTakeoverSettings.Enabled() && !ERR_GameMode.Get() && Replication.IsServer() && GetGameMode()) { ERR_ResetTerrainTakeover(); m_ERRTerrainWorld = world; --- a/tests/test_terrain_takeover.py +++ b/tests/test_terrain_takeover.py @@ -318,3 +318,64 @@ } ''') print('PASS: standalone old faction delegates collected once, with null/deleted owners ignored') + + +# Main-menu world regression: actual startup body and enable policy. +"""Actual world-postprocess and enable policy: menu worlds never enter terrain preflight.""" +from policy_harness import ROOT,body,run +from pathlib import Path +import os +P=Path(os.environ.get('ERR_TAKEOVER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeover.c')) +S=Path(os.environ.get('ERR_TAKEOVER_SETTINGS_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainTakeoverSettings.c')) +def cpp(s): + for a,b in [('protected ',''),('override ',''),('super.','Base::'),('World world','World* world'),('SaveGameManager saves','SaveGameManager* saves'),('SCR_MissionHeader header','SCR_MissionHeader* header'),('GetGame().','GetGame()->'),('header.','header->'),('saves.','saves->'),('null','nullptr')]:s=s.replace(a,b) + for name in ['ERR_TerrainTakeoverSettings','ERR_GameMode','Replication','SCR_BaseGameMode','SCR_MissionHeader','SCR_Global','System','ERR_TerrainTakeoverStage']:s=s.replace(name+'.',name+'::') + return s +run(r''' +#include +#include +#include +#include +using string=std::string; +struct World{}; +struct Mode {virtual ~Mode()=default;}; +struct SCR_BaseGameMode:Mode {static SCR_BaseGameMode*Cast(Mode*m){return dynamic_cast(m);}}; +struct ERR_GameMode:SCR_BaseGameMode{inline static ERR_GameMode*current=nullptr;static ERR_GameMode*Get(){return current;}}; +struct SCR_MissionHeader{bool m_bERR_EnableEscape=true;static SCR_MissionHeader*Cast(SCR_MissionHeader*h){return h;}}; +struct Game{SCR_MissionHeader*header=nullptr;SCR_MissionHeader*GetMissionHeader(){return header;}}game; +Game*currentGame=&game;Game*GetGame(){return currentGame;} +namespace SCR_Global{bool edit=false;bool IsEditMode(){return edit;}} +namespace System{std::mapcli;bool GetCLIParam(string key,string&value){auto i=cli.find(key);if(i==cli.end())return false;value=i->second;return true;}} +namespace Replication{bool server=true;bool IsServer(){return server;}} +struct ERR_TerrainTakeoverSettings{ +'''+cpp(body(S,'static bool Enabled('))+r''' +}; +namespace ERR_TerrainTakeoverStage{enum Stage{INACTIVE,SETTLING,FAILED};} +struct SaveGameManager{int writes=0;bool allowed=true;void SetSavingAllowed(bool b){writes++;allowed=b;}}; +struct Array{int Count(){return 0;}}; +struct Base{int superCalls=0;World*superWorld=nullptr;void OnWorldPostProcess(World*w){superCalls++;superWorld=w;}}; +struct App:Base{ + Mode*mode=nullptr;World*m_ERRTerrainWorld=nullptr;SCR_BaseGameMode*m_ERRTerrainOriginal=nullptr;ERR_TerrainTakeoverStage::Stage m_ERRTerrainStage=ERR_TerrainTakeoverStage::INACTIVE;Array m_ERRTerrainAirfields;SaveGameManager saves;int resets=0,preflights=0,failures=0,prints=0;bool nativePrerequisites=true; + Mode*GetGameMode(){return mode;}SaveGameManager*GetSaveGameManager(){return &saves;} + void ERR_ResetTerrainTakeover(){resets++;m_ERRTerrainStage=ERR_TerrainTakeoverStage::INACTIVE;m_ERRTerrainWorld=nullptr;m_ERRTerrainOriginal=nullptr;} + bool ERR_PreflightTerrainTakeover(){preflights++;if(!m_ERRTerrainOriginal||!nativePrerequisites){failures++;m_ERRTerrainStage=ERR_TerrainTakeoverStage::FAILED;return false;}return true;} + void PrintFormat(const char*,int){prints++;} +'''+cpp(body(P,'protected override void OnWorldPostProcess('))+r''' +}; +int main(){ + World menu,world;SCR_BaseGameMode native;ERR_GameMode own;Mode unsupported; + // Actual Enabled defaults true without a header. A menu has no mode, not an unsupported scenario. + {App a;assert(ERR_TerrainTakeoverSettings::Enabled());a.OnWorldPostProcess(&menu);assert(a.superCalls==1&&a.superWorld==&menu);assert(a.resets==0&&a.preflights==0&&a.failures==0&&a.saves.writes==0&&!a.m_ERRTerrainWorld);} + // A mode-less callback must not erase an already retained operation either. + {App a;a.m_ERRTerrainStage=ERR_TerrainTakeoverStage::SETTLING;a.m_ERRTerrainWorld=&world;a.m_ERRTerrainOriginal=&native;a.OnWorldPostProcess(&menu);assert(a.superCalls==1&&a.resets==0&&a.preflights==0&&a.m_ERRTerrainStage==ERR_TerrainTakeoverStage::SETTLING&&a.m_ERRTerrainWorld==&world&&a.m_ERRTerrainOriginal==&native);} + {App a;a.mode=&native;a.OnWorldPostProcess(&world);assert(a.superCalls==1&&a.resets==1&&a.preflights==1&&!a.failures&&a.m_ERRTerrainOriginal==&native&&a.m_ERRTerrainWorld==&world&&a.m_ERRTerrainStage==ERR_TerrainTakeoverStage::SETTLING&&a.saves.writes==1&&!a.saves.allowed);} + // Actual unsupported game modes still reach the diagnostic, preserving original scenario and saving. + {App a;a.mode=&unsupported;a.OnWorldPostProcess(&world);assert(a.superCalls==1&&a.resets==1&&a.preflights==1&&a.failures==1&&a.m_ERRTerrainStage==ERR_TerrainTakeoverStage::FAILED&&a.saves.writes==0&&a.saves.allowed);} + {App a;a.mode=&native;a.nativePrerequisites=false;a.OnWorldPostProcess(&world);assert(a.superCalls==1&&a.failures==1&&a.saves.writes==0);} + // Authored successor, client, editor and explicit disabled configurations keep original behavior. + for(int i=0;i<6;i++){App a;a.mode=&native;SCR_MissionHeader header;game.header=&header;if(i==0){ERR_GameMode::current=&own;a.mode=&own;}if(i==1)Replication::server=false;if(i==2)SCR_Global::edit=true;if(i==3)header.m_bERR_EnableEscape=false;if(i==4)System::cli["errTerrainTakeover"]="0";if(i==5)currentGame=nullptr;a.OnWorldPostProcess(&world);assert(a.superCalls==1&&a.resets==0&&a.preflights==0&&a.saves.writes==0);ERR_GameMode::current=nullptr;Replication::server=true;SCR_Global::edit=false;System::cli.clear();game.header=nullptr;currentGame=&game;} + // Explicit enabling never creates a mode for a menu; same app can later enter an ordinary scenario. + {System::cli["errTerrainTakeover"]="1";App a;a.OnWorldPostProcess(&menu);assert(!a.resets);a.mode=&native;a.OnWorldPostProcess(&world);assert(a.superCalls==2&&a.resets==1&&a.preflights==1&&a.saves.writes==1);System::cli.clear();} +} +''') +print('PASS: actual terrain takeover startup skips mode-less/menu worlds, always forwards super, retains eligible/unsupported/authored/client/editor semantics')