Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
"""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')
|
||||
Reference in New Issue
Block a user