Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
384 lines
24 KiB
Python
384 lines
24 KiB
Python
"""Execute takeover parsing and admission/readiness policy from production bodies.
|
|
Native startup and multiplayer replication require separate engine tests.
|
|
"""
|
|
from policy_harness import ROOT, body, run
|
|
|
|
core = ROOT/'addons/core/Scripts/Game/ERR/00_Core'
|
|
settings = core/'ERR_TerrainTakeoverSettings.c'
|
|
takeover = core/'ERR_TerrainTakeover.c'
|
|
|
|
def cpp(text):
|
|
return (text.replace('out float ', 'float& ')
|
|
.replace('array<vector> centres', 'Array<Vec>& centres')
|
|
.replace('array<string>', 'Array<string>')
|
|
.replace('foreach (string entry : entries)', 'for (string entry : entries)')
|
|
.replace('bool digit, dot;', 'bool digit=false, dot=false;')
|
|
.replace('"0123456789".IndexOf(', 'string("0123456789").IndexOf('))
|
|
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <vector>
|
|
template<class T> struct Array:std::vector<T>{
|
|
using std::vector<T>::vector;
|
|
void Clear(){this->clear();} int Count(){return this->size();}
|
|
bool IsEmpty(){return this->empty();} void Insert(T v){this->push_back(v);}
|
|
};
|
|
struct string:std::string {
|
|
using std::string::string; string(std::string s):std::string(s){}
|
|
bool IsEmpty(){return empty();} int Length(){return size();}
|
|
string Substring(int a,int b){return substr(a,b);}
|
|
int IndexOf(string s){auto p=find(s);return p==npos?-1:int(p);}
|
|
float ToFloat(){return std::stof(*this);}
|
|
void TrimInPlace(){auto a=find_first_not_of(" \t\r\n"),b=find_last_not_of(" \t\r\n");*this=a==npos?"":substr(a,b-a+1);}
|
|
void Split(string delimiter,Array<string>& out,bool skipEmpty){
|
|
size_t start=0;while(true){auto end=find(delimiter,start);string part=substr(start,end==npos?npos:end-start);
|
|
if(!skipEmpty||!part.empty())out.Insert(part);
|
|
if(end==npos)break;
|
|
start=end+delimiter.size();
|
|
}
|
|
}
|
|
};
|
|
struct Vec{float x,y,z;}; Vec Vector(float x,float y,float z){return {x,y,z};}
|
|
''' + cpp(body(settings, 'static bool Coordinate(')) + cpp(body(settings, 'static bool ParseAirfields(')) +
|
|
body(takeover, 'static bool TimedOut(') + body(takeover, 'static bool MayDrain(') +
|
|
body(takeover, 'static bool MayCreate(') + body(takeover, 'static bool SameAdmission(') + r'''
|
|
int main(){
|
|
float value=0;
|
|
assert(Coordinate("-1000000",value)&&value==-1000000);
|
|
assert(Coordinate("42.75",value)&&value==42.75f);
|
|
for(string bad:{"", "-", ".", "-.", "nan", "inf", "1e4", "12x", "--1", "1.2.3", "1000001", "1 2", "12345678901234567"})assert(!Coordinate(bad,value));
|
|
Array<Vec> centres;
|
|
assert(ParseAirfields("1525 5075; -20.5 200",centres));
|
|
assert(centres.Count()==2&¢res[0].x==1525&¢res[0].z==5075&¢res[1].x==-20.5f);
|
|
assert(ParseAirfields(" 1 2 ",centres)&¢res.Count()==1);
|
|
assert(ParseAirfields("\t \n",centres)&¢res.IsEmpty());
|
|
for(string bad:{"1", "1 2 3", "1 2;", ";1 2", "1 2;;3 4", "1 2;bad 4", "1 2;nan inf", "1 2;3 4;5"}){
|
|
centres.Insert({9,0,9});assert(!ParseAirfields(bad,centres));assert(centres.IsEmpty());
|
|
}
|
|
string many="1 2";for(int i=1;i<16;i++)many+=";1 2";
|
|
assert(ParseAirfields(many,centres)&¢res.Count()==16);
|
|
many+=";1 2";assert(!ParseAirfields(many,centres)&¢res.IsEmpty());
|
|
assert(!TimedOut(9.999f)&&TimedOut(10)&&TimedOut(30));
|
|
assert(!MayDrain(.49f,true)&&MayDrain(.5f,true)&&!MayDrain(1,false));
|
|
assert(MayCreate(0,true)&&MayCreate(.49f,true)&&MayCreate(.5f,true)&&!MayCreate(1,false)&&!MayCreate(10,true));
|
|
assert(SameAdmission(true,true,true,true));
|
|
assert(!SameAdmission(false,true,true,true));
|
|
assert(!SameAdmission(true,false,true,true)); // Reused connection id, different controller.
|
|
assert(!SameAdmission(true,true,true,false)); // Account changed after audit.
|
|
assert(SameAdmission(true,true,false,false)); // No identity is manufactured: actual spawn logic still decides.
|
|
}
|
|
''')
|
|
|
|
# Lifecycle wiring is checked here; the world/manager lifecycle itself is native.
|
|
cleanup = body(takeover, 'override protected void OnBeforeWorldCleanup(')
|
|
assert cleanup.index('ERR_ResetTerrainTakeover()') < cleanup.index('super.OnBeforeWorldCleanup()')
|
|
reset = body(takeover, 'protected void ERR_ResetTerrainTakeover(')
|
|
for cleared in ('m_ERRTerrainAdmissions.Clear()', 'm_ERRTerrainAirfields.Clear()',
|
|
'm_ERRTerrainMarkerSlots.Clear()', 'm_ERRTerrainWorld = null',
|
|
'm_ERRTerrainOriginal = null', 'm_ERRTerrainElapsed = 0'):
|
|
assert cleared in reset
|
|
print('PASS: configured airport parsing, bounded readiness, stale admission rejection and cleanup wiring')
|
|
|
|
def settings_cpp(text):
|
|
return (text.replace('SCR_MissionHeader header', 'SCR_MissionHeader* header')
|
|
.replace('SCR_MissionHeader.Cast', 'SCR_MissionHeader::Cast')
|
|
.replace('GetGame().', 'GetGame()->').replace('header.', 'header->')
|
|
.replace('SCR_Global.', 'SCR_Global::').replace('System.', 'System::'))
|
|
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <map>
|
|
using string=std::string;
|
|
struct SCR_MissionHeader {
|
|
bool m_bERR_EnableEscape=true; string m_sERR_Airfields;
|
|
static SCR_MissionHeader* Cast(SCR_MissionHeader* header){return header;}
|
|
} header;
|
|
struct Game {SCR_MissionHeader* current=&header;SCR_MissionHeader* GetMissionHeader(){return current;}} game;
|
|
Game* currentGame=&game;Game* GetGame(){return currentGame;}
|
|
namespace SCR_Global {bool edit=false;bool IsEditMode(){return edit;}}
|
|
namespace System {
|
|
std::map<string,string> cli;
|
|
bool GetCLIParam(string key,string& value){auto it=cli.find(key);if(it==cli.end())return false;value=it->second;return true;}
|
|
}
|
|
''' + settings_cpp(body(settings, 'static bool Enabled(')) +
|
|
settings_cpp(body(settings, 'static string Airfields(')) + r'''
|
|
int main(){
|
|
assert(Enabled()&&Airfields().empty()); // Installing Escape requires no header or CLI flags.
|
|
header.m_bERR_EnableEscape=false;assert(!Enabled());
|
|
for(string bad:{"0","true","yes","01",""}){System::cli["errTerrainTakeover"]=bad;assert(!Enabled());}
|
|
System::cli["errTerrainTakeover"]="1";System::cli["errAirfields"]="1525 5075";
|
|
assert(Enabled()&&Airfields()=="1525 5075");
|
|
SCR_Global::edit=true;assert(!Enabled());SCR_Global::edit=false;
|
|
currentGame=nullptr;assert(!Enabled());currentGame=&game;
|
|
game.current=nullptr;assert(Enabled());game.current=&header;
|
|
header.m_bERR_EnableEscape=true;header.m_sERR_Airfields="10 20";
|
|
System::cli["errTerrainTakeover"]="0";assert(!Enabled()); // Explicit opt-out wins over the default header.
|
|
System::cli.erase("errTerrainTakeover");assert(Enabled());
|
|
assert(Airfields()=="1525 5075"); // Explicit CLI coordinates win.
|
|
System::cli.erase("errAirfields");assert(Airfields()=="10 20");
|
|
header.m_sERR_Airfields="";assert(Airfields().empty());
|
|
game.current=nullptr;System::cli.clear();assert(Enabled()&&Airfields().empty());
|
|
}
|
|
''')
|
|
assert '[Attribute("1", desc: "Automatically start Escape' in settings.read_text()
|
|
startup = body(takeover, 'protected override void OnWorldPostProcess(')
|
|
assert '!ERR_GameMode.Get() && Replication.IsServer()' in startup
|
|
assert startup.index('ERR_PreflightTerrainTakeover()') < startup.index('saves.SetSavingAllowed(false)')
|
|
print('PASS: automatic default, explicit header/CLI opt-out, editor/authored-mode guards, optional airport override precedence and fresh-round saving guard')
|
|
|
|
client = core/'ERR_TerrainTakeoverClient.c'
|
|
managers = core/'ERR_TerrainTakeoverManagers.c'
|
|
# Execute singleton release bodies: delayed cleanup must not clear a new owner.
|
|
components = (
|
|
('SCR_EntityCatalogManagerComponent', 's_Instance', 'ERR_ReleaseClientCatalog'),
|
|
('SCR_AdditionalGameModeSettingsComponent', 's_Instance', 'ERR_ReleaseTerrainSingleton'),
|
|
('SCR_PerceivedFactionManagerComponent', 's_Instance', 'ERR_ReleaseTerrainSingleton'),
|
|
('SCR_PlayerListedAdminManagerComponent', 'm_Instance', 'ERR_ReleaseTerrainSingleton'),
|
|
('SCR_IdentityManagerComponent', 'm_sInstance', 'ERR_ReleaseTerrainSingleton'),
|
|
('SCR_ArsenalManagerComponent', 's_Instance', 'ERR_ReleaseTerrainSingleton'),
|
|
('SCR_GroupsManagerComponent', 's_Instance', 'ERR_ReleaseTerrainSingleton'),
|
|
('SCR_SupportStationManagerComponent', 's_Instance', 'ERR_ReleaseTerrainSingleton'),
|
|
('SCR_TaskFinishHistoryManagerComponent', 's_Instance', 'ERR_ReleaseTerrainSingleton'),
|
|
('SCR_TaskManagerUIComponent', 's_Instance', 'ERR_ReleaseTerrainSingleton'),
|
|
('SCR_TextsTaskManagerComponent', 's_Instance', 'ERR_ReleaseTerrainSingleton'),
|
|
)
|
|
release_cpp = '#include <cassert>\n'
|
|
release_cases = ''
|
|
for name, field, method in components:
|
|
owner_file = client if f'static void {method}({name} expected)' in client.read_text() else managers
|
|
method_body = body(owner_file, f'static void {method}({name} expected)')
|
|
method_body = method_body.replace(f'{name} expected', f'{name}* expected').replace('null', 'nullptr')
|
|
release_cpp += f'struct {name} {{inline static {name}* {field}=nullptr; {method_body}}};\n'
|
|
release_cases += f'''{{
|
|
{name} original, replacement;
|
|
{name}::{field}=&original;
|
|
{name}::{method}(&original); assert(!{name}::{field});
|
|
{name}::{field}=&replacement;
|
|
{name}::{method}(&original); assert({name}::{field}==&replacement);
|
|
{name}::{method}(nullptr); assert({name}::{field}==&replacement);
|
|
}}\n'''
|
|
run(release_cpp + 'int main(){' + release_cases + '}')
|
|
|
|
# Execute both ends of the prefix protocol and verify faction binding occurs
|
|
# before the base deserializer touches it. Outside takeover, leave it unchanged.
|
|
save = body(client, 'override bool RplSave(')
|
|
load = body(client, 'override bool RplLoad(')
|
|
def wire_cpp(text):
|
|
return (text.replace('override ', '').replace('ScriptBitWriter writer', 'ScriptBitWriter& writer')
|
|
.replace('ScriptBitReader reader', 'ScriptBitReader& reader')
|
|
.replace('super.', 'Base::').replace('GetGame().', 'GetGame()->'))
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
struct Game {bool takeover=false;bool ERR_IsClientTerrainTakeover(){return takeover;}} game;
|
|
Game* GetGame(){return &game;}
|
|
struct ScriptBitWriter {std::vector<int> data;void WriteInt(int v){data.push_back(v);}};
|
|
struct ScriptBitReader {
|
|
std::vector<int> data;unsigned pos=0;
|
|
bool ReadInt(int& v){if(pos>=data.size())return false;v=data[pos++];return true;}
|
|
};
|
|
struct Base {
|
|
int m_iFactionIndex=3;bool m_ScrFaction=false;int bindCalls=0;bool baseCalled=false;
|
|
void SetFactionIndexBroadcast(int index){++bindCalls;m_ScrFaction=index>=0&&index<5;}
|
|
bool RplSave(ScriptBitWriter& writer){writer.WriteInt(12345);return true;}
|
|
bool RplLoad(ScriptBitReader& reader){baseCalled=true;assert(m_ScrFaction);int payload;return reader.ReadInt(payload)&&payload==12345;}
|
|
};
|
|
struct Delegate:Base {
|
|
''' + wire_cpp(save) + wire_cpp(load) + r'''
|
|
};
|
|
int main(){
|
|
Delegate server;ScriptBitWriter writer;assert(server.RplSave(writer));
|
|
assert((writer.data==std::vector<int>{3,12345}));
|
|
game.takeover=true;Delegate joining;ScriptBitReader reader{writer.data};
|
|
assert(joining.RplLoad(reader)&&joining.baseCalled&&joining.bindCalls==1&&reader.pos==2);
|
|
Delegate existing;existing.m_ScrFaction=true;ScriptBitReader existingReader{writer.data};
|
|
assert(existing.RplLoad(existingReader)&&existing.bindCalls==0);
|
|
game.takeover=false;Delegate normal;normal.m_ScrFaction=true;ScriptBitReader normalReader{writer.data};
|
|
assert(normal.RplLoad(normalReader)&&normal.bindCalls==0&&normalReader.pos==2);
|
|
game.takeover=true;Delegate truncated;ScriptBitReader empty;
|
|
assert(!truncated.RplLoad(empty)&&!truncated.baseCalled&&truncated.bindCalls==0);
|
|
Delegate invalid;ScriptBitReader bad{{999,12345}};
|
|
assert(!invalid.RplLoad(bad)&&!invalid.baseCalled);
|
|
}
|
|
''')
|
|
client_reset = body(client, 'override protected void OnBeforeWorldCleanup(')
|
|
for cleared in ('m_ERRClientTerrainTakeover = false', 'm_ERRClientOldManagers.Clear()',
|
|
'm_ERROldCatalog = null', 'm_ERROldSettings = null',
|
|
'm_ERROldPerceivedFaction = null', 'm_ERROldAdmins = null',
|
|
'm_ERROldIdentity = null', 'm_ERROldLoadoutId = EntityID.INVALID'):
|
|
assert client_reset.index(cleared) < client_reset.index('super.OnBeforeWorldCleanup()')
|
|
assert 'm_ERRTerrainRadio = null' in reset
|
|
assert 'm_ERRTerrainOldLoadoutId = EntityID.INVALID' in reset
|
|
for component in ('Arsenal', 'Groups', 'Support', 'TaskHistory', 'TaskUI', 'TaskTexts'):
|
|
assert f'm_ERROld{component} = null' in client_reset
|
|
print('PASS: singleton identity guards, paired faction deserialization and client world-cleanup wiring')
|
|
|
|
# Late native destructors can null the original caches after the new component
|
|
# exists. Each affected getter resolves only takeover owners directly, and keeps
|
|
# the original behavior for authored modes and during world cleanup.
|
|
for component in ('SCR_TaskFinishHistoryManagerComponent', 'SCR_TaskManagerUIComponent',
|
|
'SCR_TextsTaskManagerComponent', 'SCR_MapMarkerManagerComponent',
|
|
'SCR_RespawnSystemComponent'):
|
|
getter = body(managers, f'override static {component} GetInstance(')
|
|
getter = (getter.replace('override static ', 'static ')
|
|
.replace(f'{component} GetInstance', f'{component}* GetInstance')
|
|
.replace('ERR_TerrainTakeoverMode mode;', 'ERR_TerrainTakeoverMode* mode=nullptr;')
|
|
.replace('ERR_TerrainTakeoverMode.Cast', 'ERR_TerrainTakeoverMode::Cast')
|
|
.replace(f'{component}.Cast', f'{component}::Cast')
|
|
.replace(f'FindComponent({component})', 'FindComponent(0)')
|
|
.replace('GetGame().', 'GetGame()->').replace('mode.', 'mode->')
|
|
.replace('super.', 'Base::'))
|
|
run(r'''
|
|
#include <cassert>
|
|
struct Component {static Component* Cast(Component* v){return v;}};
|
|
''' + f'using {component}=Component;\n' + r'''
|
|
struct ERR_TerrainTakeoverMode {
|
|
bool takeover=false;Component* current=nullptr;
|
|
static ERR_TerrainTakeoverMode* Cast(ERR_TerrainTakeoverMode* m){return m&&m->takeover?m:nullptr;}
|
|
Component* FindComponent(int){return current;}
|
|
};
|
|
struct Game {ERR_TerrainTakeoverMode* mode=nullptr;ERR_TerrainTakeoverMode* GetGameMode(){return mode;}} game;
|
|
Game* activeGame=&game;Game* GetGame(){return activeGame;}
|
|
struct Base {inline static Component* cached=nullptr;static Component* GetInstance(){return cached;}};
|
|
struct Patched:Base {
|
|
''' + getter + r'''
|
|
};
|
|
int main(){
|
|
Component old, replacement;Base::cached=&old;
|
|
ERR_TerrainTakeoverMode mode;mode.current=&replacement;game.mode=&mode;
|
|
assert(Patched::GetInstance()==&old); // Ordinary mode preserves base behavior.
|
|
mode.takeover=true;assert(Patched::GetInstance()==&replacement);
|
|
Base::cached=nullptr;assert(Patched::GetInstance()==&replacement); // Old destructor ran.
|
|
mode.current=nullptr;assert(Patched::GetInstance()==nullptr); // Never return old components.
|
|
game.mode=nullptr;assert(Patched::GetInstance()==nullptr);
|
|
activeGame=nullptr;assert(Patched::GetInstance()==nullptr);
|
|
}
|
|
''')
|
|
print('PASS: task/map/respawn getters survive delayed old destruction and preserve ordinary-mode behavior')
|
|
|
|
# Native delivery dependencies must put managers below the mode and faction
|
|
# delegates below the faction manager. Test the actual node-link primitive with
|
|
# absent/aliased nodes and idempotent reuse of an existing correct relationship.
|
|
parent = body(managers, 'protected static bool Parent(')
|
|
parent = (parent.replace('protected static ', '').replace('IEntity entity', 'IEntity* entity')
|
|
.replace('IEntity parent', 'IEntity* parent').replace('RplComponent childRpl', 'RplComponent* childRpl')
|
|
.replace('RplComponent parentRpl', 'RplComponent* parentRpl')
|
|
.replace('RplNode childNode', 'RplNode* childNode').replace('RplNode parentNode', 'RplNode* parentNode')
|
|
.replace('SCR_EntityHelper.', 'SCR_EntityHelper::').replace('childRpl.', 'childRpl->')
|
|
.replace('parentRpl.', 'parentRpl->').replace('childNode.', 'childNode->'))
|
|
run(r'''
|
|
#include <cassert>
|
|
struct RplNode {RplNode* parent=nullptr;int changes=0;RplNode* GetParent(){return parent;}void SetParent(RplNode* p){parent=p;++changes;}};
|
|
struct RplComponent {RplNode* node=nullptr;RplNode* GetNode(){return node;}};
|
|
struct IEntity {RplComponent* rpl=nullptr;};
|
|
namespace SCR_EntityHelper {RplComponent* GetEntityRplComponent(IEntity* e){return e?e->rpl:nullptr;}}
|
|
''' + parent + r'''
|
|
int main(){
|
|
RplNode root, child, unrelated;RplComponent rootRpl{&root},childRpl{&child};
|
|
IEntity mode{&rootRpl}, manager{&childRpl}, empty;
|
|
assert(!Parent(nullptr,&mode)&&!Parent(&manager,nullptr)&&!Parent(&empty,&mode));
|
|
assert(!Parent(&mode,&mode));
|
|
assert(Parent(&manager,&mode)&&child.parent==&root&&child.changes==1);
|
|
assert(Parent(&manager,&mode)&&child.changes==1);
|
|
child.parent=&unrelated;assert(Parent(&manager,&mode)&&child.changes==2);
|
|
childRpl.node=nullptr;assert(!Parent(&manager,&mode));
|
|
}
|
|
''')
|
|
print('PASS: replication hierarchy refuses absent/aliased nodes and establishes delivery dependencies idempotently')
|
|
|
|
collect = body(managers, 'static void CollectDelegates(')
|
|
collect = (collect.replace('SCR_DelegateFactionManagerComponent manager', 'SCR_DelegateFactionManagerComponent* manager')
|
|
.replace('array<IEntity> entities', 'Entities& entities')
|
|
.replace('map<Faction, SCR_EditableFactionComponent> delegates = new map<Faction, SCR_EditableFactionComponent>();', 'Delegates delegates;')
|
|
.replace('SCR_EditableFactionComponent delegate', 'SCR_EditableFactionComponent* delegate')
|
|
.replace('manager.', 'manager->').replace('delegate.', 'delegate->'))
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
struct IEntity {};
|
|
struct SCR_EditableFactionComponent {IEntity* owner;IEntity* GetOwner(){return owner;}};
|
|
struct Delegates:std::vector<SCR_EditableFactionComponent*> {
|
|
int Count(){return size();}SCR_EditableFactionComponent* GetElement(int i){return at(i);}
|
|
};
|
|
struct Entities:std::vector<IEntity*> {
|
|
bool Contains(IEntity* p){return std::find(begin(),end(),p)!=end();}void Insert(IEntity* p){push_back(p);}
|
|
};
|
|
struct SCR_DelegateFactionManagerComponent {Delegates data;void GetFactionDelegates(Delegates& out){out=data;}};
|
|
''' + collect + r'''
|
|
int main(){
|
|
IEntity a,b;SCR_EditableFactionComponent da{&a},duplicate{&a},db{&b},deleted{nullptr};
|
|
SCR_DelegateFactionManagerComponent manager;manager.data={};
|
|
manager.data.insert(manager.data.end(),{&da,nullptr,&duplicate,&deleted,&db});
|
|
Entities entities;entities.push_back(&a);CollectDelegates(nullptr,entities);assert(entities.size()==1);
|
|
CollectDelegates(&manager,entities);assert(entities.size()==2&&entities[1]==&b);
|
|
CollectDelegates(&manager,entities);assert(entities.size()==2);
|
|
}
|
|
''')
|
|
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'),('bool changingTerrain;','bool changingTerrain=false;'),('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 <cassert>
|
|
#include <string>
|
|
#include <map>
|
|
#include <vector>
|
|
using string=std::string;
|
|
struct World{};
|
|
struct Mode {virtual ~Mode()=default;};
|
|
struct SCR_BaseGameMode:Mode {static SCR_BaseGameMode*Cast(Mode*m){return dynamic_cast<SCR_BaseGameMode*>(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{bool changingTerrain=false;bool ERR_PrepareTerrainSelection(World*){return changingTerrain;}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::map<string,string>cli;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);}
|
|
// A pending terrain change must not race native manager teardown/replacement.
|
|
{game.changingTerrain=true;App a;a.mode=&native;a.OnWorldPostProcess(&world);assert(a.superCalls==1&&a.resets==0&&a.preflights==0&&a.saves.writes==0);game.changingTerrain=false;}
|
|
// 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')
|