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:
+242
@@ -0,0 +1,242 @@
|
||||
// One operator-owned document. Legacy names identify sections internally only;
|
||||
// custom paths used by isolated probes keep their original standalone behavior.
|
||||
class ERR_ProfileConfig
|
||||
{
|
||||
static const string PATH = "$profile:Escapists.json";
|
||||
static const string VOICE = "$profile:EscapistsWelcomeVoice.json";
|
||||
static array<string> LegacyPaths()
|
||||
{
|
||||
return {ERR_Settings.PATH, ERR_ContentSettings.PATH, ERR_FactionSelection.PATH, ERR_TerrainPreference.PATH, ERR_ItemPreferences.PATH, VOICE};
|
||||
}
|
||||
static string Section(string path)
|
||||
{
|
||||
if (path == ERR_Settings.PATH) return "mission";
|
||||
if (path == ERR_ContentSettings.PATH) return "mods";
|
||||
if (path == ERR_FactionSelection.PATH) return "factions";
|
||||
if (path == ERR_TerrainPreference.PATH) return "terrain";
|
||||
if (path == ERR_ItemPreferences.PATH) return "items";
|
||||
if (path == VOICE) return "voice";
|
||||
return "";
|
||||
}
|
||||
static bool Text(string path, out string text)
|
||||
{
|
||||
FileHandle file = FileIO.OpenFile(path, FileMode.READ);
|
||||
if (!file) return false;
|
||||
int length = file.GetLength();
|
||||
if (length < 2 || length > 8388608) { file.Close(); return false; }
|
||||
file.Read(text, length); file.Close();
|
||||
return text.Length() == length;
|
||||
}
|
||||
// The engine validates JSON syntax. This scan only separates root members,
|
||||
// retaining unknown fields and dormant addon entries without reinterpreting them.
|
||||
static bool Split(string text, map<string, string> members)
|
||||
{
|
||||
members.Clear(); text.TrimInPlace();
|
||||
JsonLoadContext valid = new JsonLoadContext();
|
||||
if (text.Length() < 2 || text.Substring(0, 1) != "{" || !valid.LoadFromString(text)) return false;
|
||||
int depth, start = 1, colon = -1;
|
||||
bool quoted, escaped;
|
||||
string block;
|
||||
for (int i = 0; i < text.Length(); i++)
|
||||
{
|
||||
if (i % 1024 == 0) block = text.Substring(i, Math.Min(1024, text.Length() - i));
|
||||
string ch = block.Substring(i % 1024, 1);
|
||||
if (quoted)
|
||||
{
|
||||
if (escaped) escaped = false;
|
||||
else if (ch == "\\") escaped = true;
|
||||
else if (ch == "\"") quoted = false;
|
||||
continue;
|
||||
}
|
||||
if (ch == "\"") { quoted = true; continue; }
|
||||
if (ch == "{" || ch == "[") depth++;
|
||||
if (ch == "}" || ch == "]") depth--;
|
||||
if (depth == 1 && ch == ":" && colon < 0) colon = i;
|
||||
if ((depth == 1 && ch == ",") || (depth == 0 && ch == "}"))
|
||||
{
|
||||
if (colon < start)
|
||||
{
|
||||
string empty = text.Substring(start, i - start); empty.TrimInPlace();
|
||||
return members.Count() == 0 && empty.IsEmpty() && i == text.Length() - 1;
|
||||
}
|
||||
JsonLoadContext keyJson = new JsonLoadContext();
|
||||
string key;
|
||||
if (!keyJson.LoadFromString("{\"key\":" + text.Substring(start, colon - start) + "}") || !keyJson.ReadValue("key", key) || members.Contains(key)) return false;
|
||||
string value = text.Substring(colon + 1, i - colon - 1); value.TrimInPlace();
|
||||
members.Set(key, value); start = i + 1; colon = -1;
|
||||
}
|
||||
}
|
||||
return depth == 0 && !quoted;
|
||||
}
|
||||
static bool Document(string path, out map<string, string> members)
|
||||
{
|
||||
string text; members = new map<string, string>();
|
||||
if (!Text(path, text) || !Split(text, members)) return false;
|
||||
// Never downgrade or rewrite a configuration created by an unknown version.
|
||||
return members.Contains("configVersion") && members.Get("configVersion") == "1";
|
||||
}
|
||||
static bool Valid(string section, string text)
|
||||
{
|
||||
JsonLoadContext json = new JsonLoadContext();
|
||||
map<string, string> fields = new map<string, string>();
|
||||
if (!Split(text, fields) || !json.LoadFromString(text)) return false;
|
||||
map<string, int> numbers; map<string, string> roles; ResourceName scenario; array<string> records;
|
||||
if (section == "mission") return ERR_Settings.Parse(json, numbers);
|
||||
if (section == "mods") return ERR_ContentSettings.Parse(json, numbers);
|
||||
if (section == "factions") return ERR_FactionSelection.Parse(json, roles);
|
||||
if (section == "terrain") return ERR_TerrainPreference.Parse(json, scenario);
|
||||
if (section == "items") return ERR_ItemPreferences.Parse(json, records);
|
||||
string url;
|
||||
return section == "voice" && json.ReadValue("welcomeUrl", url) && (url.IsEmpty() || ERR_WelcomeVoiceRules.URL(url));
|
||||
}
|
||||
static string Defaults(string section)
|
||||
{
|
||||
if (section == "mods") return "{}";
|
||||
if (section == "factions") return "{\"escapists\":\"us\",\"enemy\":\"ussr\",\"independent\":\"fia\"}";
|
||||
if (section == "terrain") return "{\"scenario\":\"\"}";
|
||||
if (section == "items") return "{\"version\":1,\"records\":[\"active\"]}";
|
||||
if (section == "voice") return "{\"welcomeUrl\":\"\"}";
|
||||
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
||||
foreach (string key : ERR_Settings.Keys())
|
||||
{
|
||||
int value = ERR_Settings.AddedDefault(key);
|
||||
if (key == "reportIntervalSeconds") value = 10;
|
||||
if (key == "supportSquadLimit") value = 4;
|
||||
if (key == "responseRadiusMetres") value = 2000;
|
||||
if (key == "aiIntelChancePercent") value = 60;
|
||||
if (key == "intelRevealChancePercent") value = 15;
|
||||
if (key == "radioDefenceSeconds") value = 180;
|
||||
json.WriteValue(key, value);
|
||||
}
|
||||
return json.SaveToString();
|
||||
}
|
||||
static string Encode(map<string, string> members)
|
||||
{
|
||||
string result = "{\n";
|
||||
int index;
|
||||
foreach (string key, string raw : members)
|
||||
{
|
||||
// Quote arbitrary root keys through the engine, including escaped characters.
|
||||
JsonSaveContext name = new JsonSaveContext(); name.EnableTypeDiscriminator(false); name.WriteValue("key", key);
|
||||
string pair = name.SaveToString(); int colon = pair.IndexOf(":");
|
||||
string quotedKey = pair.Substring(colon + 1, pair.Length() - colon - 2); quotedKey.TrimInPlace();
|
||||
if (index > 0) result += ",\n";
|
||||
result += " " + quotedKey + ": " + raw; index++;
|
||||
}
|
||||
return result + "\n}\n";
|
||||
}
|
||||
static bool Same(map<string, string> a, map<string, string> b)
|
||||
{
|
||||
if (!a || !b || a.Count() != b.Count()) return false;
|
||||
foreach (string key, string value : a) if (!b.Contains(key) || b.Get(key) != value) return false;
|
||||
return true;
|
||||
}
|
||||
// A section may have loaded from the backup while the primary remains valid
|
||||
// JSON. Never promote that damaged document or overwrite its recovery copy.
|
||||
static bool ContentsValid(map<string, string> members)
|
||||
{
|
||||
foreach (string path : LegacyPaths())
|
||||
{
|
||||
string section = Section(path);
|
||||
if (members.Contains(section) && !Valid(section, members.Get(section))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool Commit(map<string, string> members)
|
||||
{
|
||||
if (!Replication.IsServer() || !ContentsValid(members)) return false;
|
||||
FileHandle file = FileIO.OpenFile(PATH + ".tmp", FileMode.WRITE);
|
||||
if (!file) return false;
|
||||
file.Write(Encode(members)); file.Close();
|
||||
map<string, string> checked, previous;
|
||||
if (!Document(PATH + ".tmp", checked) || !Same(members, checked)) return false;
|
||||
bool hadPrevious = Document(PATH, previous) && ContentsValid(previous);
|
||||
if (hadPrevious)
|
||||
{
|
||||
if (!FileIO.CopyFile(PATH, PATH + ".bak") || !Document(PATH + ".bak", checked) || !Same(previous, checked)) return false;
|
||||
}
|
||||
if (!FileIO.CopyFile(PATH + ".tmp", PATH) || !Document(PATH, checked) || !Same(members, checked))
|
||||
{
|
||||
if (FileIO.FileExists(PATH + ".bak")) FileIO.CopyFile(PATH + ".bak", PATH);
|
||||
else if (!hadPrevious) FileIO.DeleteFile(PATH);
|
||||
return false;
|
||||
}
|
||||
FileIO.DeleteFile(PATH + ".tmp");
|
||||
return true;
|
||||
}
|
||||
static bool Ensure()
|
||||
{
|
||||
if (!Replication.IsServer()) return false;
|
||||
// Existing unified files are authoritative; never reimport stale legacy data.
|
||||
if (FileIO.FileExists(PATH) || FileIO.FileExists(PATH + ".bak"))
|
||||
{
|
||||
string existing; map<string, string> root = new map<string, string>();
|
||||
if (Text(PATH, existing) && Split(existing, root) && (!root.Contains("configVersion") || root.Get("configVersion") != "1"))
|
||||
{
|
||||
Print("Reintroduced [config]: unsupported Escapists.json configVersion; file preserved, writes disabled", LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
map<string, string> members = new map<string, string>(); members.Set("configVersion", "1");
|
||||
foreach (string path : LegacyPaths())
|
||||
{
|
||||
string section = Section(path), raw;
|
||||
if (!Text(path, raw) || !Valid(section, raw))
|
||||
{
|
||||
if (!Text(path + ".bak", raw) || !Valid(section, raw))
|
||||
{
|
||||
if (FileIO.FileExists(path) || FileIO.FileExists(path + ".bak"))
|
||||
{
|
||||
Print("Reintroduced [config]: cannot import " + path + "; repair it or its backup. Existing files preserved.", LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
raw = Defaults(section);
|
||||
}
|
||||
}
|
||||
raw.TrimInPlace(); members.Set(section, raw);
|
||||
}
|
||||
if (!Commit(members)) return false;
|
||||
Print("Reintroduced [config]: using $profile:Escapists.json; legacy files retained but no longer used.");
|
||||
return true;
|
||||
}
|
||||
static bool Load(string path, JsonLoadContext json)
|
||||
{
|
||||
string section = Section(path); bool backup;
|
||||
if (section.IsEmpty() && path.EndsWith(".bak")) { section = Section(path.Substring(0, path.Length() - 4)); backup = true; }
|
||||
if (section.IsEmpty()) return json.LoadFromFile(path);
|
||||
if (!Ensure()) return false;
|
||||
string source = PATH; if (backup) source += ".bak";
|
||||
map<string, string> members;
|
||||
if (!Document(source, members)) return false;
|
||||
if (!members.Contains(section)) return json.LoadFromString(Defaults(section));
|
||||
return json.LoadFromString(members.Get(section));
|
||||
}
|
||||
static bool Exists(string path)
|
||||
{
|
||||
string section = Section(path);
|
||||
if (section.IsEmpty() && path.EndsWith(".bak")) section = Section(path.Substring(0, path.Length() - 4));
|
||||
if (section.IsEmpty()) return FileIO.FileExists(path);
|
||||
// A failed migration must make all owners read-only, not just the bad section.
|
||||
if (!Ensure()) return true;
|
||||
return FileIO.FileExists(PATH) || FileIO.FileExists(PATH + ".bak");
|
||||
}
|
||||
static bool Save(string path, JsonSaveContext json)
|
||||
{
|
||||
if (!Replication.IsServer() || !Ensure()) return false;
|
||||
string section = Section(path), raw = json.SaveToString(); raw.TrimInPlace();
|
||||
if (section.IsEmpty() || !Valid(section, raw)) return false;
|
||||
map<string, string> members;
|
||||
if (!Document(PATH, members) && !Document(PATH + ".bak", members)) return false;
|
||||
// Keep unrecognized fields within the edited section too (for returning mods
|
||||
// and newer optional settings). Explicitly written values replace old values.
|
||||
map<string, string> fields = new map<string, string>();
|
||||
map<string, string> changed = new map<string, string>();
|
||||
if (members.Contains(section) && !Split(members.Get(section), fields)) return false;
|
||||
if (!Split(raw, changed)) return false;
|
||||
foreach (string key, string value : changed) fields.Set(key, value);
|
||||
raw = Encode(fields); raw.TrimInPlace();
|
||||
members.Set(section, raw);
|
||||
return Commit(members);
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
"""Execute the actual shared-file transaction with injected disk failures.
|
||||
Engine JSON/migration/menu checks are recorded separately in native evidence.
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT, body, run
|
||||
p = Path(os.environ.get('ERR_PROFILE_CONFIG_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ProfileConfig.c'))
|
||||
method = body(p, 'static bool Commit(')
|
||||
for before, after in [
|
||||
('map<string, string>', 'Map'), ('FileHandle file', 'FileHandle* file'),
|
||||
('file.', 'file->'), ('FileIO.', 'FileIO::'), ('FileMode.', 'FileMode::'),
|
||||
('Replication.', 'Replication::')]:
|
||||
method = method.replace(before, after)
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <string>
|
||||
using string=std::string;
|
||||
using Map=std::map<string,string>;
|
||||
const string PATH="Escapists.json";
|
||||
std::map<string,string> files;
|
||||
std::map<string,Map> documents;
|
||||
int fault=0,serial=0;
|
||||
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
||||
namespace FileMode {int WRITE=1;}
|
||||
struct FileHandle {
|
||||
string path;
|
||||
void Write(string data){files[path]=fault==2?"CORRUPT":data;}
|
||||
void Close(){}
|
||||
};
|
||||
namespace FileIO {
|
||||
FileHandle* OpenFile(string path,int){static FileHandle h;h.path=path;return fault==1?nullptr:&h;}
|
||||
bool FileExists(string path){return files.count(path);}
|
||||
bool DeleteFile(string path){return files.erase(path);}
|
||||
bool CopyFile(string from,string to){
|
||||
if(!files.count(from))return false;
|
||||
if(to==PATH+".bak"&&fault==3)return false;
|
||||
files[to]=files[from];
|
||||
if(to==PATH+".bak"&&fault==4)files[to]="CORRUPT";
|
||||
if(from==PATH+".tmp"){
|
||||
if(fault==5){files[to]="PARTIAL";return false;}
|
||||
if(fault==6)files[to]="CORRUPT";
|
||||
if(fault==7){files[to]="WRONG";documents["WRONG"]={{"mission","wrong"}};}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
struct Store {
|
||||
static bool ContentsValid(Map){return true;}
|
||||
static string Encode(Map values){string id="doc"+std::to_string(++serial);documents[id]=values;return id;}
|
||||
static bool Document(string path,Map& values){
|
||||
if(!files.count(path)||!documents.count(files[path]))return false;
|
||||
values=documents[files[path]];return true;
|
||||
}
|
||||
static bool Same(Map a,Map b){return a==b;}
|
||||
''' + method + r'''
|
||||
};
|
||||
int main(){
|
||||
Map old={{"mission","old"},{"items","presets"},{"voice","url"}}, next=old,read;
|
||||
next["mission"]="new";
|
||||
for(int injected:{1,2,5,6,7}){
|
||||
files.clear();fault=injected;
|
||||
assert(!Store::Commit(next)&&!files.count(PATH)); // failed first save stays absent
|
||||
}
|
||||
for(int injected=1;injected<=7;injected++){
|
||||
files.clear();fault=0;assert(Store::Commit(old));
|
||||
fault=injected;assert(!Store::Commit(next));
|
||||
assert(Store::Document(PATH,read)&&read==old); // current valid config remains
|
||||
fault=0;assert(Store::Commit(next));
|
||||
assert(Store::Document(PATH,read)&&read==next);
|
||||
}
|
||||
for(int injected:{1,2,5,6,7}){
|
||||
files.clear();files[PATH]="CORRUPT";files[PATH+".bak"]=Store::Encode(old);
|
||||
fault=injected;assert(!Store::Commit(next));
|
||||
assert(Store::Document(PATH+".bak",read)&&read==old); // only good backup not replaced
|
||||
fault=0;assert(Store::Commit(next));
|
||||
assert(Store::Document(PATH,read)&&read==next);
|
||||
}
|
||||
files.clear();fault=0;assert(Store::Commit(old));assert(Store::Commit(next));
|
||||
assert(Store::Document(PATH+".bak",read)&&read==old);
|
||||
assert(!files.count(PATH+".tmp"));
|
||||
Replication::server=false;assert(!Store::Commit(old));
|
||||
assert(Store::Document(PATH,read)&&read==next);
|
||||
}
|
||||
''')
|
||||
print('PASS: shared config transaction rejects client writes, preserves first-save/prior/fallback state across open, write, backup, partial-copy, readback and mismatch failures')
|
||||
|
||||
# Execute actual shared Save and Commit, with a small token document store.
|
||||
# JSON lexing and non-terrain section parsers are explicit doubles. The terrain
|
||||
# rejection runs the actual TerrainPreference.Parse method with a catalog double.
|
||||
# No native filesystem atomicity or full JsonLoadContext implementation claim.
|
||||
def translate(method):
|
||||
for before, after in [
|
||||
('map<string, string>', 'Map'), ('new Map()', 'Map()'),
|
||||
('FileHandle file','FileHandle* file'), ('file.','file->'),
|
||||
('FileIO.','FileIO::'), ('FileMode.','FileMode::'), ('Replication.','Replication::'),
|
||||
('JsonSaveContext json','JsonSaveContext& json')]:
|
||||
method=method.replace(before,after)
|
||||
method=method.replace('foreach (string key, string value : changed)', 'for (auto [key,value] : changed)')
|
||||
method=method.replace('foreach (string path : LegacyPaths())', 'for (string path : LegacyPaths())')
|
||||
return method
|
||||
helper='static bool ContentsValid(Map) {return true;}'
|
||||
if 'static bool ContentsValid(' in p.read_text(): helper=translate(body(p,'static bool ContentsValid('))
|
||||
terrain = body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_TerrainPreference.c', 'static bool Parse(').replace('JsonLoadContext json', 'JsonLoadContext& json').replace('out ResourceName scenario', 'string& scenario').replace('ERR_TerrainCatalog.', 'ERR_TerrainCatalog::')
|
||||
run(r"""
|
||||
#include <cassert>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}int Length(){return size();}void TrimInPlace(){}};
|
||||
struct Map:std::map<string,string>{using std::map<string,string>::map;bool Contains(string key){return count(key);}string Get(string key){return at(key);}void Set(string key,string value){(*this)[key]=value;}};
|
||||
const string PATH="Escapists.json";
|
||||
std::map<string,string> files;
|
||||
std::map<string,Map> documents;
|
||||
int serial=0,copies=0,writes=0;
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace FileMode{int WRITE=1;}
|
||||
struct FileHandle{string path;void Write(string data){writes++;files[path]=data;}void Close(){}};
|
||||
namespace FileIO{
|
||||
FileHandle* OpenFile(string path,int){static FileHandle h;h.path=path;return &h;}
|
||||
bool FileExists(string path){return files.count(path);}
|
||||
bool DeleteFile(string path){return files.erase(path);}
|
||||
bool CopyFile(string from,string to){if(!files.count(from))return false;copies++;files[to]=files[from];return true;}
|
||||
}
|
||||
struct JsonSaveContext{string token;string SaveToString(){return token;}};
|
||||
struct JsonLoadContext{Map fields;bool ReadValue(string key,string&value){if(!fields.Contains(key))return false;value=fields.Get(key);return true;}};
|
||||
namespace ERR_TerrainCatalog{string Key(string scenario){return scenario=="valid-saved-terrain"?"known":"";}}
|
||||
struct Terrain{
|
||||
""" + terrain + r"""
|
||||
};
|
||||
struct Store{
|
||||
static std::vector<string> LegacyPaths(){return {"mission","mods","factions","terrain","items","voice"};}
|
||||
static string Section(string path){return path;}
|
||||
static bool Ensure(){return true;}
|
||||
static string Encode(Map values){string id="doc"+std::to_string(++serial);documents[id]=values;return id;}
|
||||
static bool Split(string text,Map& values){if(!documents.count(text))return false;values=documents[text];return true;}
|
||||
static bool Valid(string section,string text){Map fields;if(!Split(text,fields))return false;if(section!="terrain")return true;JsonLoadContext json{fields};string scenario;return Terrain::Parse(json,scenario);}
|
||||
static bool Document(string path,Map& values){return files.count(path)&&Split(files[path],values)&&values.Contains("configVersion")&&values.Get("configVersion")=="1";}
|
||||
static bool Same(Map a,Map b){return a==b;}
|
||||
""" + helper + translate(body(p,'static bool Commit(')) + translate(body(p,'static bool Save(')) + r"""
|
||||
};
|
||||
int main(){
|
||||
auto validTerrain=Store::Encode({{"scenario","valid-saved-terrain"},{"futureTerrainKey","keep"}});
|
||||
auto invalidTerrain=Store::Encode({{"scenario","malformed"},{"futureTerrainKey","keep"}});
|
||||
auto mission=Store::Encode({{"timer","10"},{"unknownMissionKey","keep"}});
|
||||
auto changedMission=Store::Encode({{"timer","20"}});
|
||||
Map good={{"configVersion","1"},{"mission",mission},{"terrain",validTerrain},{"unknownRoot","keep"}},current=good,read;
|
||||
current.Set("terrain",invalidTerrain);string originalGood=Store::Encode(good),originalBad=Store::Encode(current);
|
||||
files[PATH]=originalBad;files[PATH+".bak"]=originalGood;
|
||||
// Owner would reject current terrain and accept backup. An unrelated save must
|
||||
// not destroy the only recovery document or report success with invalid data.
|
||||
assert(!Store::Valid("terrain",current.Get("terrain"))&&Store::Valid("terrain",good.Get("terrain")));
|
||||
JsonSaveContext change{changedMission};
|
||||
assert(!Store::Save("mission",change));
|
||||
assert(files[PATH]==originalBad&&files[PATH+".bak"]==originalGood&&copies==0&&writes==0&&files.size()==2);
|
||||
// Repair the section through its recovered menu value. Preserve the only good
|
||||
// backup; merge unknown fields and allow subsequent ordinary menu persistence.
|
||||
JsonSaveContext repair{validTerrain};assert(Store::Save("terrain",repair));
|
||||
assert(files[PATH+".bak"]==originalGood&&Store::Document(PATH,read));
|
||||
assert(read.Get("unknownRoot")=="keep");Map fields;assert(Store::Split(read.Get("terrain"),fields)&&fields.Get("futureTerrainKey")=="keep");
|
||||
string repaired=files[PATH];assert(Store::Save("mission",change));assert(files[PATH+".bak"]==repaired);
|
||||
assert(Store::Document(PATH,read)&&Store::Split(read.Get("mission"),fields));assert(fields.Get("timer")=="20"&&fields.Get("unknownMissionKey")=="keep");
|
||||
// Missing optional sections continue to use their owner defaults; unknown roots
|
||||
// are not subject to this version's validators.
|
||||
Map sparse={{"configVersion","1"},{"unrecognized","opaque"}};assert(Store::Commit(sparse));
|
||||
// Existing defaults and each known section are checked; no invalid first write.
|
||||
files.clear();copies=0;writes=0;assert(!Store::Commit(current)&&files.empty()&&copies==0&&writes==0);
|
||||
// Updating an already healthy document retains ordinary last-valid backup.
|
||||
files[PATH]=originalGood;assert(Store::Save("mission",change));assert(files[PATH+".bak"]==originalGood);
|
||||
}
|
||||
""")
|
||||
print('PASS: actual Save/Commit preserves semantically valid backup across unrelated menu edits and repairs; unknown fields/default sections retained; actual terrain parser; other parsers, catalog and disk doubled')
|
||||
Reference in New Issue
Block a user