652 lines
34 KiB
C++
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, §ion)) 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, §ionName)) {
|
|
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
|