Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
200 lines
12 KiB
Python
200 lines
12 KiB
Python
"""Execute the saved-terrain write/recovery rules with failing filesystem operations.
|
|
Native discovery, transitions and multiplayer are separate engine checks.
|
|
"""
|
|
from policy_harness import ROOT, body, run
|
|
core = ROOT/'addons/core/Scripts/Game/ERR/00_Core'
|
|
key = body(core/'ERR_TerrainCatalog.c', 'static string Key(')
|
|
key = key.replace('"0123456789abcdef".IndexOf', 'string("0123456789abcdef").IndexOf')
|
|
pref = core/'ERR_TerrainPreference.c'
|
|
code = '\n'.join(body(pref, s) for s in ['void ERR_TerrainPreference(', 'ResourceName Scenario(', 'protected bool Read(', 'static bool Parse(', 'bool Save('])
|
|
for a,b in [('void ERR_TerrainPreference(', 'Preference('), ('protected ', ''), ('out ResourceName ', 'ResourceName& '), ('Replication.', 'Replication::'), ('ERR_TerrainCatalog.', 'Catalog::'), ('FileIO.', 'FileIO::'), ('LogLevel.', 'LogLevel::'), ('JsonLoadContext json = new JsonLoadContext();', 'JsonLoadContext json;'), ('JsonSaveContext json = new JsonSaveContext();', 'JsonSaveContext json;')]:
|
|
code = code.replace(a,b)
|
|
code = code.replace('ERR_ProfileConfig.', 'ERR_ProfileConfig::').replace('m_Path == PATH', 'm_Path == \"$profile:EscapistsTerrain.json\"')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <map>
|
|
#include <algorithm>
|
|
struct string:std::string {
|
|
using std::string::string; string(std::string s):std::string(s){}
|
|
bool IsEmpty()const{return empty();} int Length()const{return size();}
|
|
string Substring(int a,int b)const{return substr(a,b);}
|
|
int IndexOf(string s)const{auto p=find(s);return p==npos?-1:int(p);}
|
|
void ToLower(){std::transform(begin(),end(),begin(),[](unsigned char c){return std::tolower(c);});}
|
|
};
|
|
using ResourceName=string;
|
|
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
|
namespace LogLevel {int ERROR=1;}
|
|
void Print(string,int){}
|
|
std::map<string,string> files;
|
|
string failWrite,failCopy,corruptCopy;
|
|
struct JsonLoadContext {
|
|
string path;
|
|
bool LoadFromFile(string p){path=p;return files.count(p)&&files[p]!="CORRUPT";}
|
|
bool ReadValue(string,ResourceName& v){v=files[path];return true;}
|
|
};
|
|
struct JsonSaveContext {
|
|
string value;
|
|
void EnableTypeDiscriminator(bool){} void WriteValue(string,string v){value=v;}
|
|
bool SaveToFile(string p){if(p==failWrite)return false;files[p]=value;return true;}
|
|
};
|
|
namespace FileIO {
|
|
bool FileExists(string p){return files.count(p);}
|
|
bool CopyFile(string a,string b){if(b==failCopy)return false;if(!files.count(a))return false;files[b]=b==corruptCopy?"CORRUPT":files[a];return true;}
|
|
void DeleteFile(string p){files.erase(p);}
|
|
}
|
|
// Isolated custom paths retain standalone transactions; unified default paths
|
|
// are covered by the central store regression and native fixture.
|
|
namespace ERR_ProfileConfig {
|
|
bool Load(string path,JsonLoadContext& json){return json.LoadFromFile(path);}
|
|
bool Exists(string path){return FileIO::FileExists(path);}
|
|
bool Save(string,JsonSaveContext&){assert(false);return false;}
|
|
}
|
|
struct Catalog {''' + key + r'''};
|
|
const string PATH="terrain.json";
|
|
struct Preference {
|
|
string m_Path;ResourceName m_Scenario;bool m_ReadOnly=false;
|
|
''' + code + r'''
|
|
};
|
|
int main(){
|
|
const string everon="{C1E20667DCE324ED}Missions/ERR_Everon.conf";
|
|
const string nogova="{469880DFE1B48F95}Missions/Nogova_TerrainTest.conf";
|
|
assert(Catalog::Key(everon)=="c1e20667dce324ed");
|
|
for(string bad:{"", "../outside.conf", "{ZZZZZZZZZZZZZZZZ}Missions/x.conf", "{C1E20667DCE324E}x"})assert(Catalog::Key(bad).empty());
|
|
Preference fresh;assert(fresh.Scenario().empty()&&files.empty());
|
|
assert(fresh.Save(everon));Preference restart;assert(restart.Scenario()==everon);
|
|
assert(!restart.Save("../outside.conf")&&restart.Scenario()==everon);
|
|
assert(restart.Save(nogova));assert(files[PATH+".bak"]==everon);
|
|
Preference again;assert(again.Scenario()==nogova);
|
|
// Temp-write, backup-copy and final-copy failure must not change the selected map.
|
|
failWrite=PATH+".tmp";assert(!again.Save(everon)&&again.Scenario()==nogova&&files[PATH]==nogova);failWrite="";
|
|
failCopy=PATH+".bak";assert(!again.Save(everon)&&again.Scenario()==nogova&&files[PATH]==nogova);failCopy="";
|
|
failCopy=PATH;assert(!again.Save(everon)&&again.Scenario()==nogova&&files[PATH]==nogova);failCopy="";
|
|
// Readback detects a damaged final write; a retained backup permits recovery.
|
|
corruptCopy=PATH;assert(!again.Save(everon)&&again.Scenario()==nogova);corruptCopy="";
|
|
Preference recovered;assert(recovered.Scenario()==nogova);
|
|
assert(recovered.Save(""));Preference cleared;assert(cleared.Scenario().empty());
|
|
files[PATH]="CORRUPT";files[PATH+".bak"]="CORRUPT";Preference damaged;
|
|
assert(damaged.Scenario().empty()&&!damaged.Save(everon)&&files[PATH]=="CORRUPT");
|
|
files.clear();failCopy=PATH;Preference firstFailure;assert(!firstFailure.Save(everon)&&!files.count(PATH));failCopy="";
|
|
Replication::server=false;Preference client;assert(!client.Save(everon)&&!files.count(PATH));
|
|
}
|
|
''')
|
|
print('PASS: saved terrain GUIDs, defaults, restart persistence, clearing, backup recovery, corrupt-file preservation, write failures, and client write denial')
|
|
|
|
prepare = body(core/'ERR_TerrainSelection.c', 'bool ERR_PrepareTerrainSelection(')
|
|
for a,b in [('World world', 'World* world'), ('SCR_MissionHeader header', 'SCR_MissionHeader* header'), ('SCR_MissionHeader.Cast', 'SCR_MissionHeader::Cast'), ('header.', 'header->'), ('Replication.', 'Replication::'), ('SCR_Global.', 'SCR_Global::'), ('ERR_TerrainCatalog.', 'Catalog::'), ('LogLevel.', 'LogLevel::'), ('ERR_Terrains().', 'ERR_Terrains()->')]:
|
|
prepare=prepare.replace(a,b)
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
struct string:std::string {using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}};
|
|
using ResourceName=string;
|
|
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
|
namespace SCR_Global{bool editor=false;bool IsEditMode(){return editor;}}
|
|
namespace LogLevel{int ERROR=1;}
|
|
void Print(string,int){}
|
|
struct World{};
|
|
struct SCR_MissionHeader {
|
|
ResourceName resource="nogova";
|
|
ResourceName GetHeaderResourceName(){return resource;}
|
|
static SCR_MissionHeader* Cast(SCR_MissionHeader* p){return p;}
|
|
};
|
|
struct Catalog{static string Key(ResourceName r){return r;}};
|
|
struct Selection{ResourceName destination;ResourceName Destination(){return destination;}};
|
|
struct Game{
|
|
World* m_ERRTerrainSelectionWorld=nullptr;
|
|
ResourceName m_ERRTerrainDestination,m_ERRLastTerrainRedirect;
|
|
float m_ERRTerrainSelectionWait=0;
|
|
Selection selection;bool mode=true;SCR_MissionHeader* header=nullptr;
|
|
Selection* ERR_Terrains(){return &selection;}
|
|
bool GetGameMode(){return mode;}SCR_MissionHeader* GetMissionHeader(){return header;}
|
|
''' + prepare + r'''
|
|
};
|
|
int main(){
|
|
World world;SCR_MissionHeader h;Game g;g.header=&h;
|
|
// Empty or unavailable saved destination preserves the server-selected scenario.
|
|
assert(!g.ERR_PrepareTerrainSelection(&world));
|
|
g.selection.destination="everon";
|
|
Replication::server=false;assert(!g.ERR_PrepareTerrainSelection(&world));Replication::server=true;
|
|
SCR_Global::editor=true;assert(!g.ERR_PrepareTerrainSelection(&world));SCR_Global::editor=false;
|
|
g.mode=false;assert(!g.ERR_PrepareTerrainSelection(&world));g.mode=true;
|
|
g.header=nullptr;assert(!g.ERR_PrepareTerrainSelection(&world));g.header=&h;
|
|
assert(g.ERR_PrepareTerrainSelection(&world));
|
|
assert(g.m_ERRTerrainDestination=="everon"&&g.m_ERRTerrainSelectionWorld==&world&&g.m_ERRTerrainSelectionWait==0);
|
|
// Failing to reach a requested destination cannot cause an infinite reload.
|
|
assert(!g.ERR_PrepareTerrainSelection(&world));
|
|
h.resource="everon";assert(!g.ERR_PrepareTerrainSelection(&world)&&g.m_ERRLastTerrainRedirect.empty());
|
|
// Saving in a running world does not mutate the captured startup destination.
|
|
g.selection.destination="nogova";assert(g.m_ERRTerrainDestination=="everon");
|
|
assert(g.ERR_PrepareTerrainSelection(&world)&&g.m_ERRTerrainDestination=="nogova");
|
|
}
|
|
''')
|
|
print('PASS: startup terrain capture protects client/editor/menu worlds, default behavior, same-map loads and reload-loop recovery')
|
|
|
|
update = body(core/'ERR_TerrainSelection.c', 'override void OnUpdate(')
|
|
for a,b in [('override ', ''), ('BaseWorld world', 'World* world'), ('super.', 'Base::'), ('Replication.', 'Replication::'), ('GameStateTransitions.', 'Transitions::'), ('GameProject.', 'Projects::'), ('PlayerManagerKickReason.', 'KickReason::'), ('LogLevel.', 'LogLevel::'), ('PlayerManager players', 'PlayerManager* players'), ('players.', 'players->'), ('array<string>', 'Array<string>'), ('array<int>', 'Array<int>'), ('foreach (', 'for (')]:
|
|
update=update.replace(a,b)
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <vector>
|
|
struct string:std::string {using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}};
|
|
using ResourceName=string;
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
|
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
|
namespace LogLevel{int ERROR=1;}
|
|
namespace KickReason{int KICK=1;}
|
|
void Print(string,int){}template<class... T>void PrintFormat(T...){}
|
|
struct World{};
|
|
namespace Projects{
|
|
Array<string> loaded={"vanilla","escape","nogova","equipment"};
|
|
void GetLoadedAddons(Array<string>& out){out=loaded;}
|
|
bool IsVanillaAddon(string id){return id=="vanilla";}
|
|
}
|
|
namespace Transitions{
|
|
bool busy=false;int requests=0;string target,addons;
|
|
bool IsTransitionRequestedOrInProgress(){return busy;}
|
|
bool RequestScenarioChangeTransition(string d,string,string mods){requests++;target=d;addons=mods;return true;}
|
|
}
|
|
struct PlayerManager{
|
|
Array<int> connected;int kicks=0;
|
|
int GetPlayerCount(){return connected.size();}
|
|
void GetPlayers(Array<int>& out){out=connected;}
|
|
void KickPlayer(int,int reason,int timeout){assert(reason==KickReason::KICK&&timeout==0);kicks++;connected.clear();}
|
|
};
|
|
struct Base{void OnUpdate(World*,float){}};
|
|
struct Game:Base{
|
|
string m_ERRTerrainDestination="everon";
|
|
World* m_ERRTerrainSelectionWorld=nullptr;World* current=nullptr;
|
|
float m_ERRTerrainSelectionWait=0,m_ERRTerrainDisconnectWait=0,m_ERRTerrainEmptyWait=0;
|
|
PlayerManager players;
|
|
World* GetWorld(){return current;}PlayerManager* GetPlayerManager(){return &players;}
|
|
''' + update + r'''
|
|
};
|
|
int main(){
|
|
World w,other;
|
|
Game g;g.current=&w;g.m_ERRTerrainSelectionWorld=&w;g.players.connected={1,2};
|
|
for(int i=0;i<12;i++)g.OnUpdate(&w,.25f);
|
|
assert(!Transitions::requests&&!g.players.kicks);
|
|
// A terrain change must never carry connected clients into the new world.
|
|
for(int i=0;i<4;i++)g.OnUpdate(&w,.25f);
|
|
assert(g.players.kicks==2&&!Transitions::requests);
|
|
for(int i=0;i<4;i++)g.OnUpdate(&w,.25f);
|
|
assert(Transitions::requests==1&&Transitions::target=="everon");
|
|
assert(Transitions::addons=="escape,nogova,equipment");
|
|
for(int i=0;i<100;i++)g.OnUpdate(&w,.25f);
|
|
assert(Transitions::requests==1); // Consumed once, no live polling of menu saves.
|
|
Game busy;busy.current=&w;busy.m_ERRTerrainSelectionWorld=&w;Transitions::busy=true;
|
|
for(int i=0;i<64;i++)busy.OnUpdate(&w,.25f);
|
|
assert(busy.m_ERRTerrainDestination.empty()&&Transitions::requests==1);Transitions::busy=false;
|
|
Game stale;stale.current=&other;stale.m_ERRTerrainSelectionWorld=&w;stale.OnUpdate(&w,4);
|
|
assert(stale.m_ERRTerrainDestination.empty()&&Transitions::requests==1);
|
|
Game client;client.current=&w;client.m_ERRTerrainSelectionWorld=&w;Replication::server=false;client.OnUpdate(&w,4);
|
|
assert(client.m_ERRTerrainDestination.empty()&&Transitions::requests==1);Replication::server=true;
|
|
Projects::loaded={"vanilla"};Game empty;empty.current=&w;empty.m_ERRTerrainSelectionWorld=&w;empty.OnUpdate(&w,4);
|
|
assert(empty.m_ERRTerrainDestination.empty()&&Transitions::requests==1);
|
|
}
|
|
''')
|
|
print('PASS: actual transition waits for native startup, disconnects without bans, requires an empty session, retains every loaded mod, bounds waiting and consumes requests once')
|