DS2Revival/mods/common/settings_file.h

652 lines
34 KiB
C++

// The shared settings file DS2Revival.ini, in the game folder next to DungeonSiege2.exe: created on the first start,
// completed on later starts. (Broken World started from the Steam game's "Broken World" folder uses the main game folder's
// file: one file for both games; a Broken World folder on its own keeps its own.) Versions 1.2.0 and 1.2.1 kept it in the
// scripts folder: the first start of a newer plugin moves it up (when the game folder has none; when both exist the game
// folder's wins and the scripts one is renamed DS2Revival.ini.old).
//
// The release zip does not contain DS2Revival.ini (dragging a new zip over the game folder must never overwrite
// the player's settings). Whichever plugin starts first writes it from the commented default text compiled into
// every plugin (mods/common/DS2Revival.ini is the one source; mods/build.py generates revival_ini_text.h from it).
// * created: the text is written to a temp file and moved into place, under a named mutex, so several plugins
// (or the Steam game and Broken World) starting at once write it exactly once;
// * one-time migration while creating it: values the player changed in the old per-plugin files
// (DS2Multiplayer.ini, DS2Widescreen.ini, DS2Fixes.ini, DS2BrokenWorld.ini, DS2ModLoader.ini - also in Broken
// World's scripts folder - and the very old ds2fix.ini / server.txt) are copied into it (only values that differ
// from the defaults; the GPU options of [Display] go to [Graphics]); those files are renamed to <name>.old;
// * later starts and updates: settings the file lacks are added with their comment lines under the right section.
// Existing values, the player's own comments and the order of their lines are never touched, and a file that
// already has every setting is not rewritten. (An old DS2Revival.ini that still kept the GPU options in [Display]
// has them moved to [Graphics] with their values, so that the new [Graphics] default does not hide them.)
// * changed defaults, once: [Settings] Version (absent = a 1.2.x file; the default text has kSettingsVersion) says which
// defaults the file has been brought up to. When a release changes the default of a setting (kChangedDefaults: section,
// key, the old default(s), the new one, the version), a value that is still an old default becomes the new default at the
// first start of that release, a value the player set is left alone; then the version line is raised, so it is done once.
// The same for the first-start migration: a value of an old per-plugin file that equals an old default is not carried over.
// Not done for a UTF-16 file (it is left exactly as it is) or in a test instance of the ds2dev harness (DS2DEV_DOCS
// is set: developer tools write per-plugin inis there between runs; DS2_SETTINGS_AUTOCREATE=1 switches it on).
#pragma once
#include "patch.h"
#include "revival_ini_text.h" // kRevivalIniText: generated by mods/build.py from mods/common/DS2Revival.ini
#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace ds2 {
// The settings file all release plugins share (in the game folder, next to DungeonSiege2.exe).
constexpr char kSharedIni[] = "DS2Revival.ini";
inline bool FileExists(const char* path) { return GetFileAttributesA(path) != INVALID_FILE_ATTRIBUTES; }
namespace settings {
using Lines = std::vector<std::string>;
inline std::string Trim(const std::string& s) {
auto blank = [](char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; };
size_t a = 0, b = s.size();
while (a < b && blank(s[a])) ++a;
while (b > a && blank(s[b - 1])) --b;
return s.substr(a, b - a);
}
inline std::string Lower(std::string s) {
for (char& c : s) if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + 32);
return s;
}
// "[name]"
inline bool IsSection(const std::string& line, std::string* name = nullptr) {
std::string t = Trim(line);
if (t.size() < 2 || t.front() != '[' || t.back() != ']') return false;
if (name) *name = Trim(t.substr(1, t.size() - 2));
return true;
}
// "key=value" (not a comment)
inline bool IsKey(const std::string& line, std::string* key = nullptr, std::string* value = nullptr) {
std::string t = Trim(line);
if (t.empty() || t[0] == ';') return false;
size_t eq = t.find('=');
if (eq == std::string::npos) return false;
if (key) *key = Trim(t.substr(0, eq));
if (value) *value = Trim(t.substr(eq + 1));
return true;
}
// An ini as lines. Anything but UTF-16 can be edited: the lines are kept byte for byte (UTF-8 or the ANSI code
// page), a BOM and the line ending style (CRLF or LF) are kept.
struct Doc {
Lines lines;
bool crlf = true;
bool bom = false;
};
inline bool Parse(const std::string& bytes, Doc& d) {
d = Doc();
auto u = [&](size_t i) { return static_cast<unsigned char>(bytes[i]); };
if (bytes.size() >= 2 && ((u(0) == 0xFF && u(1) == 0xFE) || (u(0) == 0xFE && u(1) == 0xFF))) return false; // UTF-16
if (bytes.find('\0') != std::string::npos) return false; // UTF-16 without BOM
size_t pos = 0;
if (bytes.size() >= 3 && u(0) == 0xEF && u(1) == 0xBB && u(2) == 0xBF) d.bom = true, pos = 3;
d.crlf = bytes.find("\r\n") != std::string::npos || bytes.find('\n') == std::string::npos;
while (pos < bytes.size()) {
size_t nl = bytes.find('\n', pos);
size_t end = nl == std::string::npos ? bytes.size() : nl;
size_t stop = end > pos && bytes[end - 1] == '\r' ? end - 1 : end;
d.lines.push_back(bytes.substr(pos, stop - pos));
pos = nl == std::string::npos ? bytes.size() : nl + 1;
}
return true;
}
inline std::string Serialize(const Doc& d) {
std::string out = d.bom ? "\xEF\xBB\xBF" : "";
for (const std::string& l : d.lines) out += l, out += d.crlf ? "\r\n" : "\n";
return out;
}
// ---- the defaults (the compiled-in text) ----------------------------------------------------------------------------
struct Setting {
std::string section, key, value;
};
using SettingMap = std::map<std::string, Setting>; // "section\nkey" (lower case)
inline std::string MapKey(const std::string& section, const std::string& key) { return Lower(section) + '\n' + Lower(key); }
inline Doc DefaultDoc() {
Doc d;
Parse(kRevivalIniText, d);
d.crlf = true;
return d;
}
inline SettingMap Settings(const Doc& d) {
SettingMap out;
std::string section, k, v;
for (const std::string& l : d.lines) {
if (IsSection(l, &section)) continue;
if (!section.empty() && IsKey(l, &k, &v)) out.emplace(MapKey(section, k), Setting{section, k, v});
}
return out;
}
// Equal as text (any case) or as numbers (0.20 = 0.2).
inline bool SameValue(const std::string& a, const std::string& b) {
std::string x = Trim(a), y = Trim(b);
if (Lower(x) == Lower(y)) return true;
auto number = [](const std::string& s, double& n) {
if (s.empty() || s.find_first_not_of("0123456789+-.eE") != std::string::npos) return false;
char* end = nullptr;
n = std::strtod(s.c_str(), &end);
return end && !*end;
};
double p = 0, q = 0;
return number(x, p) && number(y, q) && p == q;
}
// The GPU options that moved from [Display] to [Graphics] (a graphics plugin is split off from the display one).
// The plugin reads [Graphics] first and falls back to [Display].
constexpr const char* kGpuKeys[] = {"antialiasing", "menuantialiasing", "sharpen", "ssao", "ssaoradius", "ssaointensity", "anisotropy",
"texturelodbias", "shaders", "shadowsize", "portraitfix", "finerglow", "renderer", "remixsky",
"remixskipoffscreen", "remixcursor", "remixmenulight"};
inline bool IsGpuKey(const std::string& lowerKey) {
for (const char* k : kGpuKeys) if (lowerKey == k) return true;
return false;
}
// ---- defaults that changed between releases ---------------------------------------------------------------------------
// MergeMissing only adds keys, and a file made by an earlier version has every default spelled out, so a new default would
// never reach a player who never touched the setting. Each row: the value(s) the setting had as its default before (several
// separated by '|'), the new default, and the settings version that introduced the change. A file's version is
// [Settings] Version (absent = 1, a file from 1.2.x). On the first start with a newer plugin a value equal to an old default
// becomes the new default (once; the version line is then raised), any other value is the player's and stays.
// kSettingsVersion must equal Version= in mods/common/DS2Revival.ini (tools/test_settings_ini.py checks it).
constexpr int kSettingsVersion = 2;
struct ChangedDefault {
const char *section, *key, *oldValues, *newValue;
int version; // the settings version that introduced this default
const char* release; // for the log: the plugin release that did
};
constexpr ChangedDefault kChangedDefaults[] = {
{"Graphics", "AntiAliasing", "smaa", "msaa4+smaa", 2, "v1.3"}, // MSAA 4x + SMAA (1.2.x: SMAA only)
{"BrokenWorld", "AcceptContentCrcs", "D4EE6791,75112728|D4EE6791", "", 2, "v1.3"}, // the known-editions table has both now
};
// ---- editing --------------------------------------------------------------------------------------------------------
// The value of key in [section] (the first one), false when there is none.
inline bool GetValue(const Doc& d, const std::string& section, const std::string& key, std::string& value) {
std::string wantSection = Lower(section), wantKey = Lower(key), cur, name, k, v;
for (const std::string& l : d.lines) {
if (IsSection(l, &name)) cur = Lower(name);
else if (cur == wantSection && IsKey(l, &k, &v) && Lower(k) == wantKey) return value = v, true;
}
return false;
}
// `value` is an old default of [section] key (any row of the table, whatever its version).
inline bool IsOldDefault(const std::string& section, const std::string& key, const std::string& value, const ChangedDefault** row = nullptr) {
for (const ChangedDefault& c : kChangedDefaults) {
if (Lower(c.section) != Lower(section) || Lower(c.key) != Lower(key)) continue;
std::string olds = c.oldValues;
for (size_t a = 0; a <= olds.size();) {
size_t b = olds.find('|', a);
if (b == std::string::npos) b = olds.size();
if (SameValue(olds.substr(a, b - a), value)) {
if (row) *row = &c;
return true;
}
a = b + 1;
}
}
return false;
}
// key=value in [section], other lines and comments untouched; the key is added at the end of the section (the
// section at the end of the file) when missing.
inline void SetValue(Doc& d, const std::string& section, const std::string& key, const std::string& value) {
std::string wantSection = Lower(section), wantKey = Lower(key), cur, name, k;
long end = -1;
bool done = false;
for (size_t i = 0; i < d.lines.size(); ++i) {
std::string& l = d.lines[i];
if (IsSection(l, &name)) {
cur = Lower(name);
if (cur == wantSection) end = static_cast<long>(i);
} else if (cur == wantSection) {
if (!Trim(l).empty()) end = static_cast<long>(i);
if (IsKey(l, &k) && Lower(k) == wantKey) l = key + "=" + value, done = true;
}
}
if (done) return;
if (end < 0) {
if (!d.lines.empty()) d.lines.push_back("");
d.lines.push_back("[" + section + "]");
end = static_cast<long>(d.lines.size()) - 1;
}
d.lines.insert(d.lines.begin() + end + 1, key + "=" + value);
}
// Adds the settings of `tmpl` that `d` lacks, each with its comment lines, at the end of the matching section (a
// section that is missing is appended). Returns "section.key" of what was added; `moved` gets the legacy [Display]
// GPU settings that were carried over to [Graphics] with their values.
inline std::vector<std::string> MergeMissing(Doc& d, const Doc& tmpl, std::vector<std::string>& moved) {
struct Sec {
size_t end = 0;
std::set<std::string> keys;
};
std::map<std::string, Sec> have;
std::string section, name, k, v;
for (size_t i = 0; i < d.lines.size(); ++i) {
const std::string& l = d.lines[i];
if (IsSection(l, &name)) {
section = Lower(name);
have[section].end = i;
} else if (!section.empty() && IsKey(l, &k)) {
have[section].keys.insert(Lower(k));
have[section].end = i; // new settings go after the last setting; comments below it (the player's own notes,
} // often meant for the next section) stay where they are
}
// GPU options an older file still has in [Display] and [Graphics] lacks: their values go to [Graphics]
std::map<std::string, std::string> legacy;
const auto graphics = have.find("graphics"); // (not operator[]: that would add the section)
section.clear();
for (const std::string& l : d.lines) {
if (IsSection(l, &name)) section = Lower(name);
else if (section == "display" && IsKey(l, &k, &v) && IsGpuKey(Lower(k)) &&
(graphics == have.end() || !graphics->second.keys.count(Lower(k))))
legacy.emplace(Lower(k), v);
}
std::vector<std::string> added;
std::map<size_t, Lines> inserts; // line index -> lines to insert after it
Lines comments;
section.clear();
std::string sectionName;
for (const std::string& l : tmpl.lines) {
if (IsSection(l, &sectionName)) {
section = Lower(sectionName);
comments.clear();
} else if (!Trim(l).empty() && Trim(l)[0] == ';') {
comments.push_back(l);
} else if (!section.empty() && IsKey(l, &k, &v)) {
if (!have.count(section)) {
d.lines.push_back("");
d.lines.push_back("[" + sectionName + "]");
have[section].end = d.lines.size() - 1;
}
Sec& s = have[section];
if (!s.keys.count(Lower(k))) {
std::string line = l;
auto lg = section == "graphics" ? legacy.find(Lower(k)) : legacy.end();
if (lg != legacy.end()) line = k + "=" + lg->second, moved.push_back(k);
Lines& at = inserts[s.end];
at.insert(at.end(), comments.begin(), comments.end());
at.push_back(line);
s.keys.insert(Lower(k));
added.push_back(section + "." + Lower(k));
}
comments.clear();
} else {
comments.clear();
}
}
for (auto it = inserts.rbegin(); it != inserts.rend(); ++it)
d.lines.insert(d.lines.begin() + it->first + 1, it->second.begin(), it->second.end());
if (!moved.empty()) { // the carried-over options leave [Display]
Lines keep;
section.clear();
for (const std::string& l : d.lines) {
if (IsSection(l, &name)) section = Lower(name);
else if (section == "display" && IsKey(l, &k) && IsGpuKey(Lower(k)) && legacy.count(Lower(k))) continue;
keep.push_back(l);
}
d.lines.swap(keep);
}
return added;
}
// The settings version the file says it has (1 when there is no [Settings] Version: written by 1.2.x). A value that is not a
// number counts as the current version: such a file is left alone.
inline int FileVersion(const Doc& d) {
std::string v;
if (!GetValue(d, "Settings", "Version", v)) return 1;
char* end = nullptr;
long n = std::strtol(v.c_str(), &end, 10);
return !v.empty() && end && !*end && n >= 1 ? static_cast<int>(n) : kSettingsVersion;
}
// The one-time upgrade: for every row of kChangedDefaults newer than the file's version, a value that is still the old
// default becomes the new one (a value the player changed is left alone), then [Settings] Version is raised. Run it BEFORE
// MergeMissing: a GPU option an old file still has in [Display] (while [Graphics] lacks it) is changed there, and MergeMissing
// then carries the new value over to [Graphics]. Returns what it did, as log text.
inline std::vector<std::string> UpgradeDefaults(Doc& d) {
std::vector<std::string> done;
const int have = FileVersion(d);
if (have >= kSettingsVersion) return done;
for (const ChangedDefault& c : kChangedDefaults) {
if (c.version <= have) continue;
std::string section = c.section, value;
std::string where = std::string("[") + c.section + "] " + c.key;
if (!GetValue(d, section, c.key, value)) {
std::string legacy; // the GPU options lived in [Display] before the [Graphics] section
if (Lower(section) != "graphics" || !IsGpuKey(Lower(c.key)) || !GetValue(d, "Display", c.key, legacy)) continue;
section = "Display", value = legacy, where = std::string("[Display] ") + c.key; // (MergeMissing moves it on)
}
if (!IsOldDefault(c.section, c.key, value)) continue;
SetValue(d, section, c.key, c.newValue);
done.push_back(where + "=" + value + " was the old default, now " + (*c.newValue ? c.newValue : "(empty)") + " (the " + c.release +
" default; set it back if you prefer)");
}
// the marker: raised when it is there but older; a file without it gets it (with its comment) from MergeMissing
std::string v;
if (GetValue(d, "Settings", "Version", v)) SetValue(d, "Settings", "Version", std::to_string(kSettingsVersion));
return done;
}
// ---- files ----------------------------------------------------------------------------------------------------------
inline bool ReadAll(const std::string& path, std::string& out) {
HANDLE h = CreateFileA(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (h == INVALID_HANDLE_VALUE) return false;
out.clear();
char buf[8192];
DWORD n = 0;
bool ok = true;
while (out.size() < (4u << 20)) {
if (!ReadFile(h, buf, sizeof(buf), &n, nullptr)) { ok = false; break; }
if (!n) break;
out.append(buf, n);
}
CloseHandle(h);
return ok;
}
// Writes `bytes` to a temp file in the same folder, then moves it over `path` (replace = false: only when `path`
// does not exist, so a file that appeared meanwhile is never overwritten).
inline bool WriteFileAtomic(const std::string& path, const std::string& bytes, bool replace) {
std::string tmp = path + "." + std::to_string(GetCurrentProcessId()) + ".tmp";
HANDLE h = CreateFileA(tmp.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (h == INVALID_HANDLE_VALUE) return false;
DWORD n = 0;
bool ok = WriteFile(h, bytes.data(), static_cast<DWORD>(bytes.size()), &n, nullptr) && n == bytes.size();
FlushFileBuffers(h);
CloseHandle(h);
if (ok) ok = MoveFileExA(tmp.c_str(), path.c_str(), replace ? MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH : 0) != 0;
if (!ok) DeleteFileA(tmp.c_str());
return ok;
}
// Every plugin and every game process that wants to write the file takes this first (named mutex per file).
class Lock {
public:
explicit Lock(const std::string& path) {
unsigned h = 2166136261u;
for (char c : Lower(path)) h = (h ^ static_cast<unsigned char>(c)) * 16777619u;
char name[64];
sprintf_s(name, "Local\\DS2RevivalSettings%08X", h);
mutex_ = CreateMutexA(nullptr, FALSE, name);
if (mutex_) held_ = WaitForSingleObject(mutex_, 5000) != WAIT_TIMEOUT;
}
~Lock() {
if (mutex_) {
if (held_) ReleaseMutex(mutex_);
CloseHandle(mutex_);
}
}
bool held() const { return mutex_ && held_; }
private:
HANDLE mutex_ = nullptr;
bool held_ = false;
};
// Every [section] key=value of an ini file in any of the encodings Windows reads (UTF-16 included), via the
// profile API; comments are skipped by it. Lower-case section and key.
inline std::vector<Setting> ReadItems(const std::string& path) {
std::vector<Setting> out;
std::vector<char> names(4096);
DWORD n;
while ((n = GetPrivateProfileSectionNamesA(names.data(), static_cast<DWORD>(names.size()), path.c_str())) + 2 >= names.size())
names.resize(names.size() * 4);
for (const char* sec = names.data(); *sec; sec += std::strlen(sec) + 1) {
std::vector<char> body(4096);
while (GetPrivateProfileSectionA(sec, body.data(), static_cast<DWORD>(body.size()), path.c_str()) + 2 >= body.size())
body.resize(body.size() * 4);
for (const char* e = body.data(); *e; e += std::strlen(e) + 1) {
std::string line = e, k, v;
if (IsKey(line, &k, &v)) out.push_back(Setting{Lower(sec), Lower(k), v});
}
}
return out;
}
// ---- the old files --------------------------------------------------------------------------------------------------
// The per-plugin files of earlier versions and the sections each plugin read from its own file (a section it never
// read does not count as its setting).
struct OldIni {
const char* file; // <file>.ini
const char* sections; // lower case, space separated
};
constexpr OldIni kOldInis[] = {
{"DS2Multiplayer", "network multiplayer"},
{"DS2Widescreen", "display graphics camera photo"},
{"DS2Graphics", "graphics"},
{"DS2Fixes", "fixes"},
{"DS2BrokenWorld", "brokenworld display"}, // [Display] Mode / ExclusiveCursor: it leaves the pointer edits to DS2Widescreen
{"DS2ModLoader", "modloader"},
};
// The very old all-in-one ds2fix.ini: its (section, key) -> the same setting in DS2Revival.ini and the default it had
// (a value equal to it is not moved).
struct OldFixKey {
const char *oldSection, *oldKey, *section, *key, *oldDefault;
};
constexpr OldFixKey kOldFixKeys[] = {
{"multiplayer", "enablemenubutton", "Multiplayer", "EnableMenuButton", "1"},
{"multiplayer", "fixstartguard", "Multiplayer", "FixStartGuard", "1"},
{"multiplayer", "skipfirewallprompt", "Multiplayer", "SkipFirewallPrompt", "1"},
{"multiplayer", "directconnecttimeout", "Multiplayer", "DirectConnectTimeout", "60"},
{"multiplayer", "lobbyserver", "Multiplayer", "LobbyServer", "1"},
{"network", "relay", "Network", "Relay", "auto"},
{"network", "singlelanaddress", "Network", "SingleLanAddress", "1"},
{"network", "virtualcdkey", "Network", "VirtualCdKey", "1"},
{"network", "cdkeyhash", "Network", "CdKeyHash", ""},
{"network", "productid", "Network", "ProductId", "00000-000-0000000-00000"},
{"network", "distroid", "Network", "DistroId", "0"},
{"display", "mode", "Display", "Mode", "borderless"},
{"display", "resolution", "Display", "Resolution", "desktop"},
{"display", "retrydevicereset", "Display", "RetryDeviceReset", "1"},
{"display", "noglobalkeyboardhook", "Fixes", "NoGlobalKeyboardHook", "1"},
{"brokenworld", "launch", "BrokenWorld", "Launch", "auto"},
{"brokenworld", "acceptsteamparties", "BrokenWorld", "AcceptSteamParties", "1"},
{"brokenworld", "acceptcontentcrcs", "BrokenWorld", "AcceptContentCrcs", "D4EE6791"},
{"brokenworld", "acceptanysavesingleplayer", "BrokenWorld", "AcceptAnySaveSinglePlayer", "1"},
{"brokenworld", "basegame", "BrokenWorld", "BaseGame", ""},
};
inline bool Allowed(const OldIni& o, const std::string& section) {
return (std::string(" ") + o.sections + " ").find(" " + section + " ") != std::string::npos;
}
struct Change {
std::string section, key, value, from; // section and key as the default text spells them
};
// What the old files say, as changes of the new file: only values that differ from the defaults. `files` = every old
// file that exists (to retire them afterwards). Folders in priority order (the first folder's value wins).
inline void CollectOld(const std::vector<std::string>& folders, const std::string& gameDir, const SettingMap& defaults,
std::vector<Change>& changes, std::vector<std::string>& files, Lines& notes) {
auto find = [&](const std::string& section, const std::string& key) -> const Setting* {
auto it = defaults.find(MapKey(section, key));
return it == defaults.end() ? nullptr : &it->second;
};
auto setdefault = [&](const Setting* d, const std::string& value, const std::string& from) {
for (const Change& c : changes)
if (Lower(c.section) == Lower(d->section) && Lower(c.key) == Lower(d->key)) return;
changes.push_back(Change{d->section, d->key, value, from});
};
// 1. the very old ds2fix.ini (lowest priority: the per-plugin files below overwrite its values)
std::vector<Change> fix;
std::string server;
const std::string fixIni = folders[0] + "\\ds2fix.ini";
if (FileExists(fixIni.c_str())) {
files.push_back(fixIni);
for (const Setting& s : ReadItems(fixIni)) {
if (s.section == "network" && s.key == "server") { server = s.value; continue; }
for (const OldFixKey& m : kOldFixKeys)
if (s.section == m.oldSection && s.key == m.oldKey && s.value != m.oldDefault)
if (const Setting* d = find(m.section, m.key)) fix.push_back(Change{d->section, d->key, s.value, "ds2fix.ini"});
}
}
const std::string txt = gameDir + "\\server.txt";
std::string txtServer;
if (FileExists(txt.c_str())) {
files.push_back(txt);
std::string text;
if (ReadAll(txt, text)) {
size_t pos = 0;
while (pos < text.size() && txtServer.empty()) {
size_t nl = text.find('\n', pos);
std::string l = Trim(text.substr(pos, nl == std::string::npos ? std::string::npos : nl - pos));
pos = nl == std::string::npos ? text.size() : nl + 1;
if (!l.empty() && l[0] != '#' && l[0] != ';') txtServer = l;
}
}
}
// 2. the per-plugin files, main game's scripts folder first
// A Broken World folder's own copy only counts when the main folder has no file of that name: the old plugins in Broken
// World read the main game's <name>.ini whenever it existed, so values in the Broken World copy were never in effect
// (moving them would quietly switch settings a player never had, e.g. a leftover Mode=off or HudScale=1).
std::vector<Change> plugin;
for (size_t fi = 0; fi < folders.size(); ++fi) {
const std::string& folder = folders[fi];
for (const OldIni& o : kOldInis) {
const std::string path = folder + "\\" + o.file + ".ini";
if (!FileExists(path.c_str())) continue;
files.push_back(path); // retired to .old either way
if (fi > 0 && FileExists((folders[0] + "\\" + o.file + ".ini").c_str())) {
notes.push_back(path + ": not used (the main game's " + o.file + ".ini was the one in effect)");
continue;
}
for (Setting s : ReadItems(path)) {
if (s.section == "display" && IsGpuKey(s.key)) s.section = "graphics";
const Setting* d = find(s.section, s.key);
if (!d || !Allowed(o, s.section)) {
notes.push_back(std::string(o.file) + ".ini: [" + s.section + "] " + s.key + " is not a setting of that plugin - it stays in the .old file");
} else if (!SameValue(s.value, d->value)) {
bool dup = false;
for (const Change& c : plugin) dup = dup || (Lower(c.section) == s.section && Lower(c.key) == s.key);
if (!dup) plugin.push_back(Change{d->section, d->key, s.value, std::string(o.file) + ".ini"});
}
}
}
}
for (const Change& c : plugin) setdefault(find(c.section, c.key), c.value, c.from);
for (const Change& c : fix) setdefault(find(c.section, c.key), c.value, c.from);
const Setting* srv = find("Network", "Server");
if (srv) {
if (!server.empty() && !SameValue(server, srv->value)) setdefault(srv, server, "ds2fix.ini");
else if (server.empty() && !txtServer.empty() && !SameValue(txtServer, srv->value)) setdefault(srv, txtServer, "server.txt");
}
}
inline void Retire(const std::string& path) {
if (MoveFileExA(path.c_str(), (path + ".old").c_str(), MOVEFILE_REPLACE_EXISTING))
log().Write("settings: kept the older settings file %s as %s.old", path.c_str(), path.c_str());
else
log().Write("settings: could not rename %s (error %lu) - it is ignored anyway while %s exists", path.c_str(), GetLastError(), kSharedIni);
}
// Makes sure <gameDir>\DS2Revival.ini exists and has every setting. `gameDir` = the folder of DungeonSiege2.exe (for Broken World
// run from the Steam game's subfolder: the main game folder); `scriptsDir` = the folder of the plugins (where versions 1.2.0 and
// 1.2.1 kept the file, and where old per-plugin files are); `otherDirs` = more folders to look for old per-plugin files in
// (Broken World's scripts). Never throws, never fails the start: problems are logged and the plugins use their built-in defaults.
inline void Ensure(const std::string& gameDir, const std::string& scriptsDir, const std::vector<std::string>& otherDirs) {
if (GetEnvironmentVariableA("DS2DEV_DOCS", nullptr, 0) && !GetEnvironmentVariableA("DS2_SETTINGS_AUTOCREATE", nullptr, 0)) {
log().Write("settings: test instance (DS2DEV_DOCS) - %s is not created or completed here", kSharedIni);
return;
}
const std::string path = gameDir + "\\" + kSharedIni;
const std::string oldPath = scriptsDir + "\\" + kSharedIni; // where 1.2.0 / 1.2.1 kept it
const bool oldElsewhere = _stricmp(path.c_str(), oldPath.c_str()) != 0;
Lock lock(path);
if (!lock.held()) {
log().Write("settings: another process holds the lock on %s - not touching it now", path.c_str());
return;
}
if (oldElsewhere && FileExists(oldPath.c_str())) {
if (!FileExists(path.c_str())) {
if (MoveFileExA(oldPath.c_str(), path.c_str(), MOVEFILE_WRITE_THROUGH))
log().Write("settings: moved %s to %s (the settings file now lives next to DungeonSiege2.exe)", oldPath.c_str(), path.c_str());
else {
log().Write("settings: could not move %s to %s (error %lu) - not touching it", oldPath.c_str(), path.c_str(), GetLastError());
return;
}
} else if (MoveFileExA(oldPath.c_str(), (oldPath + ".old").c_str(), MOVEFILE_REPLACE_EXISTING)) {
log().Write("settings: %s exists, so the older %s is not used: renamed to %s.old", path.c_str(), oldPath.c_str(), oldPath.c_str());
} else {
log().Write("settings: could not rename the older %s (error %lu) - it is ignored anyway while %s exists", oldPath.c_str(), GetLastError(), path.c_str());
}
}
const Doc tmpl = DefaultDoc();
std::string bytes;
if (!FileExists(path.c_str())) {
// ---- first start: create it from the default text, with what the old files said
Doc doc = tmpl;
std::vector<std::string> folders{scriptsDir};
folders.insert(folders.end(), otherDirs.begin(), otherDirs.end());
std::vector<Change> changes;
std::vector<std::string> oldFiles;
Lines notes;
CollectOld(folders, gameDir, Settings(tmpl), changes, oldFiles, notes);
for (const Change& c : changes) {
const ChangedDefault* row = nullptr;
if (IsOldDefault(c.section, c.key, c.value, &row)) { // an older file's untouched default: the new default applies
log().Write("settings: %s: [%s] %s=%s was the old default - the %s default %s is used", c.from.c_str(), c.section.c_str(),
c.key.c_str(), c.value.c_str(), row->release, *row->newValue ? row->newValue : "(empty)");
continue;
}
SetValue(doc, c.section, c.key, c.value);
log().Write("settings: moved [%s] %s=%s from %s", c.section.c_str(), c.key.c_str(), c.value.c_str(), c.from.c_str());
}
for (const std::string& n : notes) log().Write("settings: %s", n.c_str());
if (!WriteFileAtomic(path, Serialize(doc), false)) {
log().Write("settings: could not create %s (error %lu) - built-in defaults are used", path.c_str(), GetLastError());
return;
}
log().Write("settings: created %s with the default settings%s", path.c_str(), oldFiles.empty() ? "" : " and the values of the older settings files");
for (const std::string& f : oldFiles) Retire(f);
return;
}
// ---- later starts and updates: add what is missing
if (!ReadAll(path, bytes)) {
log().Write("settings: cannot read %s (error %lu) - not completed", path.c_str(), GetLastError());
return;
}
Doc doc;
if (!Parse(bytes, doc)) {
log().Write("settings: %s is UTF-16 - left exactly as it is (new settings are not added to it)", path.c_str());
return;
}
const int version = FileVersion(doc);
std::vector<std::string> upgraded = UpgradeDefaults(doc); // before MergeMissing (see there)
std::vector<std::string> moved;
std::vector<std::string> added = MergeMissing(doc, tmpl, moved);
if (added.empty() && upgraded.empty() && version >= kSettingsVersion) return; // has every setting, up to date: not rewritten
if (!WriteFileAtomic(path, Serialize(doc), true)) {
log().Write("settings: could not update %s (error %lu)", path.c_str(), GetLastError());
return;
}
for (const std::string& u : upgraded) log().Write("settings: %s", u.c_str());
if (version < kSettingsVersion) log().Write("settings: %s is now at settings version %d", path.c_str(), kSettingsVersion);
if (added.empty()) return;
std::string list;
for (const std::string& a : added) list += (list.empty() ? "" : ", ") + a;
log().Write("settings: added %u new setting(s) to %s: %s", static_cast<unsigned>(added.size()), path.c_str(), list.c_str());
for (const std::string& m : moved) log().Write("settings: [Display] %s moved to [Graphics] with its value", m.c_str());
}
} // namespace settings
} // namespace ds2