Config system v1

Roughly inspired by what I've learned from my work on Space Station 14, without some of the unnecessary cruft and complexity.

Implementation is relatively simple once I figured out all the template order shenanigans.
This commit is contained in:
PJB3005
2026-04-04 22:47:48 +02:00
parent 97ab7313d4
commit 824263fa6e
23 changed files with 984 additions and 43 deletions
+5 -3
View File
@@ -54,6 +54,8 @@
#include "res/Object/Alink.h"
#include <cstring>
#include "dusk/settings.hpp"
static int daAlink_Create(fopAc_ac_c* i_this);
static int daAlink_Delete(daAlink_c* i_this);
static int daAlink_Execute(daAlink_c* i_this);
@@ -7510,7 +7512,7 @@ void daAlink_c::setBlendMoveAnime(f32 i_morf) {
BOOL sp24 = checkEventRun();
BOOL sp20 = checkBootsMoveAnime(1);
#if TARGET_PC
if (dusk::ImGuiMenuEnhancements::m_enhancements.fastIronBoots) {
if (dusk::settings::enhancements::FastIronBoots.getValue()) {
sp20 = FALSE;
}
#endif
@@ -9475,7 +9477,7 @@ void daAlink_c::setStickData() {
mHeavySpeedMultiplier = mpHIO->mItem.mIronBoots.m.mInputFactor;
}
#if TARGET_PC
if (dusk::ImGuiMenuEnhancements::m_enhancements.fastIronBoots) {
if (dusk::settings::enhancements::FastIronBoots.getValue()) {
mHeavySpeedMultiplier = 1.0f;
}
#endif
@@ -9487,7 +9489,7 @@ void daAlink_c::setStickData() {
mHeavySpeedMultiplier = mpHIO->mItem.mIronBoots.m.mWaterInputFactor;
}
#if TARGET_PC
if (dusk::ImGuiMenuEnhancements::m_enhancements.fastIronBoots) {
if (dusk::settings::enhancements::FastIronBoots.getValue()) {
mHeavySpeedMultiplier = 1.0f;
}
#endif
+2 -2
View File
@@ -4301,7 +4301,7 @@ bool daAlink_c::checkAcceptWarp() {
*/
if (mLinkAcch.ChkGroundHit() && !checkModeFlg(MODE_PLAYER_FLY)
#if TARGET_PC
&& (dusk::ImGuiMenuEnhancements::m_enhancements.restoreWiiGlitches || !checkNoResetFlg0(FLG0_WATER_IN_MOVE))
&& (dusk::settings::enhancements::RestoreWiiGlitches.getValue() || !checkNoResetFlg0(FLG0_WATER_IN_MOVE))
#elif VERSION != VERSION_WII_USA_R0
&& !checkNoResetFlg0(FLG0_WATER_IN_MOVE)
#endif
@@ -4312,7 +4312,7 @@ bool daAlink_c::checkAcceptWarp() {
*/
if (
#if TARGET_PC
(dusk::ImGuiMenuEnhancements::m_enhancements.restoreWiiGlitches || !getSlidePolygon(&plane)) &&
(dusk::settings::enhancements::RestoreWiiGlitches.getValue() || !getSlidePolygon(&plane)) &&
#elif VERSION != VERSION_WII_USA_R0
!getSlidePolygon(&plane) &&
#endif
+1 -1
View File
@@ -6,7 +6,7 @@
#include "dusk/imgui/ImGuiMenuEnhancements.hpp"
void daAlink_c::handleQuickTransform() {
if (!dusk::ImGuiMenuEnhancements::m_enhancements.quickTransform) {
if (!dusk::settings::enhancements::QuickTransform.getValue()) {
return;
}
+1 -1
View File
@@ -351,7 +351,7 @@ int daAlink_c::procMagneBootsFly() {
*/
if (dComIfG_Bgsp().ChkPolySafe(mPolyInfo2)
#if TARGET_PC
&& (dusk::ImGuiMenuEnhancements::m_enhancements.restoreWiiGlitches || checkEquipHeavyBoots())
&& (dusk::settings::enhancements::RestoreWiiGlitches.getValue() || checkEquipHeavyBoots())
#elif PLATFORM_GCN || VERSION == VERSION_WII_KOR
&& checkEquipHeavyBoots()
#endif
+1 -1
View File
@@ -766,7 +766,7 @@ void dCamera_c::updatePad() {
var_f31 = mDoCPd_c::getSubStickX3D(mPadID);
#if TARGET_PC
if (dusk::ImGuiMenuEnhancements::m_enhancements.invertCameraXAxis) {
if (dusk::settings::enhancements::InvertCameraXAxis.getValue()) {
var_f31 *= -1.0f;
}
#endif
+1 -1
View File
@@ -11381,7 +11381,7 @@ void dKy_bg_MAxx_proc(void* bg_model_p) {
C_MTXLightPerspective(sp1D8, dComIfGd_getView()->fovy,
camera_p->view.aspect, 1.0f, 1.0f,
#if TARGET_PC
dusk::ImGuiMenuEnhancements::m_enhancements.useWaterProjectionOffset ? -0.01f : 0.0f, 0.0f);
dusk::settings::enhancements::UseWaterProjectionOffset.getValue() ? -0.01f : 0.0f, 0.0f);
#else
-0.01f, 0.0f);
#endif
+230
View File
@@ -0,0 +1,230 @@
#include "dusk/config.hpp"
#include "SDL3/SDL_filesystem.h"
#include "SDL3/SDL_iostream.h"
#include "dusk/appname.hpp"
#include "dusk/scope.hpp"
#include "fmt/format.h"
#include "nlohmann/json.hpp"
#include "absl/container/flat_hash_map.h"
#include "aurora/lib/logging.hpp"
#include "dusk/io.hpp"
#include <limits>
#include <string>
using namespace dusk::config;
constexpr auto ConfigFileName = "config.json";
using json = nlohmann::json;
aurora::Module DuskConfigLog("dusk::config");
static absl::flat_hash_map<std::string_view, ConfigVarBase*> RegisteredConfigVars;
static bool RegistrationDone;
static std::string GetConfigJsonPath() {
dusk::ScopeValue folder(SDL_GetPrefPath(dusk::OrgName, dusk::AppName), SDL_free);
if (folder.value == nullptr) {
DuskConfigLog.error("Failed to get user preference path: %s", SDL_GetError());
return "";
}
return fmt::format("{}{}", folder.value, ConfigFileName);
}
ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) {
}
const char* ConfigVarBase::getName() const noexcept {
return name;
}
const ConfigImplBase* ConfigVarBase::getImpl() const noexcept {
return impl;
}
template<ConfigValue T>
void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
cVar.setValue(jsonValue.get<T>(), false);
}
template<ConfigValue T>
nlohmann::json ConfigImpl<T>::dumpToJson(const ConfigVar<T>& cVar) {
return cVar.getValue();
}
template<ConfigValue T> requires std::is_integral_v<T> && std::is_signed_v<T>
static void loadFromArgImpl(ConfigVar<T>& cVar, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stoll(str);
if (result >= std::numeric_limits<T>::min() && result <= std::numeric_limits<T>::max()) {
cVar.setOverrideValue(result);
} else {
throw std::out_of_range("Value is too large");
}
}
template<ConfigValue T> requires std::is_integral_v<T> && std::is_unsigned_v<T>
static void loadFromArgImpl(ConfigVar<T>& cVar, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stoull(str);
if (result <= std::numeric_limits<T>::max()) {
cVar.setOverrideValue(result);
} else {
throw std::out_of_range("Value is too large");
}
}
static void loadFromArgImpl(ConfigVar<f32>& cVar, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stof(str);
cVar.setOverrideValue(result);
}
static void loadFromArgImpl(ConfigVar<f64>& cVar, const std::string_view stringValue) {
const std::string str(stringValue);
const auto result = std::stod(str);
cVar.setOverrideValue(result);
}
static void loadFromArgImpl(ConfigVar<std::string>& cVar, const std::string_view stringValue) {
cVar.setOverrideValue(std::string(stringValue));
}
template<ConfigValue T>
void ConfigImpl<T>::loadFromArg(ConfigVar<T>& cVar, const std::string_view stringValue) {
loadFromArgImpl(cVar, stringValue);
}
template<>
void ConfigImpl<bool>::loadFromArg(ConfigVar<bool>& cVar, const std::string_view stringValue) {
if (stringValue == "1" || stringValue == "TRUE" || stringValue == "true" || stringValue == "True") {
cVar.setOverrideValue(true);
} else if (stringValue == "0" || stringValue == "FALSE" || stringValue == "false" || stringValue == "False") {
cVar.setOverrideValue(false);
} else {
throw InvalidConfigError("Value cannot be parsed as boolean");
}
}
// My IDE is convinced this namespace is necessary. It shouldn't be AFAICT?
namespace dusk::config {
template class ConfigImpl<bool>;
template class ConfigImpl<s8>;
template class ConfigImpl<u8>;
template class ConfigImpl<s16>;
template class ConfigImpl<u16>;
template class ConfigImpl<s32>;
template class ConfigImpl<u32>;
template class ConfigImpl<s64>;
template class ConfigImpl<u64>;
template class ConfigImpl<f32>;
template class ConfigImpl<f64>;
template class ConfigImpl<std::string>;
}
void dusk::config::Register(ConfigVarBase& configVar) {
const auto& name = configVar.getName();
if (RegistrationDone) {
DuskConfigLog.fatal("Tried to register CVar {} after registrations closed!", name);
}
if (RegisteredConfigVars.contains(name)) {
DuskConfigLog.fatal("Tried to register CVar {} twice!", name);
}
RegisteredConfigVars[name] = &configVar;
configVar.markRegistered();
}
void ConfigVarBase::markRegistered() {
if (registered)
abort();
registered = true;
}
void dusk::config::FinishRegistration() {
RegistrationDone = true;
}
void dusk::config::LoadFromUserPreferences() {
const auto configPath = GetConfigJsonPath();
if (configPath.empty()) {
return;
}
LoadFromFileName(configPath.c_str());
}
static void LoadFromPath(const char* path) {
auto data = dusk::io::FileStream::ReadAllBytes(path);
json j = json::parse(data);
if (!j.is_object()) {
DuskConfigLog.error("Config JSON is not an object!");
return;
}
for (const auto& el : j.items()) {
const auto& key = el.key();
auto configVar = RegisteredConfigVars.find(key);
if (configVar == RegisteredConfigVars.end()) {
DuskConfigLog.error("Unknown key '{}' found in config!", key);
continue;
}
try {
configVar->second->getImpl()->loadFromJson(*configVar->second, el.value());
} catch (std::exception& e) {
DuskConfigLog.error("Failed to load key '{}' from config: {}", key, e.what());
}
}
}
void dusk::config::LoadFromFileName(const char* path) {
if (!RegistrationDone) {
DuskConfigLog.fatal("Registration not finished yet!");
}
DuskConfigLog.info("Loading config from '{}'", path);
try {
LoadFromPath(path);
} catch (const std::system_error& e) {
if (e.code() == std::errc::no_such_file_or_directory) {
DuskConfigLog.info("Config file did not exist, staying with defaults");
} else {
DuskConfigLog.error("Failed to load from config! {}", e.what());
}
}
}
void dusk::config::Save() {
const auto configPath = GetConfigJsonPath();
if (configPath.empty()) {
return;
}
DuskConfigLog.info("Saving config to '{}'", configPath);
json j;
for (const auto& pair : RegisteredConfigVars) {
if (pair.second->getLayer() == ConfigVarLayer::Value) {
j[pair.first] = pair.second->getImpl()->dumpToJson(*pair.second);
}
}
io::FileStream::WriteAllText(configPath.c_str(), j.dump(4));
}
ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
const auto configVar = RegisteredConfigVars.find(name);
if (configVar != RegisteredConfigVars.end()) {
return configVar->second;
}
return nullptr;
}
+17
View File
@@ -0,0 +1,17 @@
#ifndef DUSK_IMGUICONFIG_HPP
#define DUSK_IMGUICONFIG_HPP
#include "dusk/config.hpp"
#include "imgui.h"
namespace dusk::config {
inline void ImguiCheckbox(const char* title, ConfigVar<bool>& var) {
bool copy = var.getValue();
if (ImGui::Checkbox(title, &copy)) {
var.setValue(copy);
Save();
}
}
}
#endif // DUSK_IMGUICONFIG_HPP
+9 -16
View File
@@ -4,43 +4,36 @@
#include "ImGuiConsole.hpp"
#include "ImGuiMenuEnhancements.hpp"
#include "ImGuiConfig.hpp"
#include "dusk/settings.hpp"
#include <imgui_internal.h>
namespace dusk {
EnhancementsSettings ImGuiMenuEnhancements::m_enhancements = {
.fastIronBoots = false,
.invertCameraXAxis = false,
.restoreWiiGlitches = false,
.enableBloom = true,
.useWaterProjectionOffset = false,
.mirrorMode = false,
};
ImGuiMenuEnhancements::ImGuiMenuEnhancements() {}
void ImGuiMenuEnhancements::draw() {
if (ImGui::BeginMenu("Enhancements")) {
if (ImGui::BeginMenu("Quality of Life")) {
ImGui::Checkbox("Fast Iron Boots", &m_enhancements.fastIronBoots);
ImGui::Checkbox("Invert Camera X Axis", &m_enhancements.invertCameraXAxis);
ImGui::Checkbox("Quick Transform (R+Y)", &m_enhancements.quickTransform);
config::ImguiCheckbox("Fast Iron Boots", settings::enhancements::FastIronBoots);
config::ImguiCheckbox("Invert Camera X Axis", settings::enhancements::InvertCameraXAxis);
config::ImguiCheckbox("Quick Transform (R+Y)", settings::enhancements::QuickTransform);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Graphics")) {
ImGui::Checkbox("Native Bloom", &m_enhancements.enableBloom);
ImGui::Checkbox("Water Projection Offset", &m_enhancements.useWaterProjectionOffset);
config::ImguiCheckbox("Native Bloom", settings::enhancements::EnableBloom);
config::ImguiCheckbox("Water Projection Offset", settings::enhancements::UseWaterProjectionOffset);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Adds GC-specific -0.01 transS offset\n"
"that causes ~6px ghost artifacts in water reflections");
}
ImGui::Checkbox("Mirror Mode", &m_enhancements.mirrorMode);
config::ImguiCheckbox("Mirror Mode", settings::enhancements::MirrorMode);
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Restorations")) {
ImGui::Checkbox("Restore Wii 1.0 Glitches", &m_enhancements.restoreWiiGlitches);
config::ImguiCheckbox("Restore Wii 1.0 Glitches", settings::enhancements::RestoreWiiGlitches);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Restores patched glitches from Wii USA 1.0, the first released version");
}
-12
View File
@@ -8,22 +8,10 @@
#include "imgui.h"
namespace dusk {
struct EnhancementsSettings {
bool fastIronBoots;
bool invertCameraXAxis;
bool quickTransform;
bool restoreWiiGlitches;
bool enableBloom;
bool useWaterProjectionOffset;
bool mirrorMode;
};
class ImGuiMenuEnhancements {
public:
ImGuiMenuEnhancements();
void draw();
static EnhancementsSettings m_enhancements;
};
}
+143
View File
@@ -0,0 +1,143 @@
#include "dusk/io.hpp"
#include <cstdio>
#include <filesystem>
using namespace dusk::io;
#if _WIN32
#define MODE_TYPE wchar_t
#define MODE(val) L##val
#define ftell(file) _ftelli64(file)
#define fseek(a, b, c) _fseeki64(a, b, c)
#else
#define MODE_TYPE char
#define MODE(val) ##val
#endif
static FILE* ThrowIfNotOpen(const FileStream& file) {
if (!file.GetFileHandle()) {
throw std::runtime_error("Invalid file handle!");
}
return static_cast<FILE*>(file.GetFileHandle());
}
[[noreturn]] static void ThrowForError(int code) {
throw std::system_error(std::make_error_code(static_cast<std::errc>(code)));
}
static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode) {
FILE* file;
int err;
#if _WIN32
static_assert(std::is_same_v<std::filesystem::path::value_type, wchar_t>);
err = _wfopen_s(&file, path.c_str(), mode);
#else
errno = 0;
static_assert(std::is_same_v<std::filesystem::path::value_type, char>);
file = fopen(path.c_str(), mode);
err = errno;
#endif
if (!file) {
ThrowForError(err);
}
return file;
}
static FILE* OpenCore(const char* path, const MODE_TYPE* mode) {
return OpenCore(reinterpret_cast<const char8_t*>(path), mode);
}
FileStream::FileStream() noexcept : file(nullptr) {
}
FileStream::FileStream(void* file) : file(file) {
if (!file) {
CRASH("Invalid file handle");
}
}
FileStream::FileStream(FileStream&& other) noexcept {
file = other.file;
other.file = nullptr;
}
FileStream::~FileStream() {
if (file)
fclose(static_cast<FILE*>(file));
}
FileStream FileStream::OpenRead(const char* utf8Path) {
return FileStream(OpenCore(utf8Path, MODE("rb")));
}
FileStream FileStream::Create(const char* utf8Path) {
return FileStream(OpenCore(utf8Path, MODE("wb")));
}
std::vector<u8> FileStream::ReadFull() {
const auto fileHandle = ThrowIfNotOpen(*this);
std::vector<u8> result;
const auto startPos = ftell(fileHandle);
if (startPos == -1) {
ThrowForError(errno);
}
auto seekRes = fseek(fileHandle, 0, SEEK_END);
if (seekRes != 0) {
ThrowForError(errno);
}
const auto endPos = ftell(fileHandle);
if (endPos == -1) {
ThrowForError(errno);
}
seekRes = fseek(fileHandle, startPos, SEEK_SET);
if (seekRes != 0) {
ThrowForError(errno);
}
result.resize(endPos - startPos);
if (result.empty()) {
return result;
}
auto ret = fread(result.data(), 1, result.size(), fileHandle);
if (ret < result.size()) {
int err = errno;
// Error or EOF
if (feof(fileHandle)) {
result.resize(ret);
} else {
ThrowForError(err);
}
}
return result;
}
std::vector<u8> FileStream::ReadAllBytes(const char* utf8Path) {
auto handle = OpenRead(utf8Path);
return std::move(handle.ReadFull());
}
void FileStream::Write(const char* data, size_t dataLen) {
FILE* fileHandle = ThrowIfNotOpen(*this);
const auto ret = fwrite(data, 1, dataLen, fileHandle);
if (ret < dataLen) {
ThrowForError(errno);
}
}
void FileStream::WriteAllText(const char* utf8Path, const std::string_view text) {
auto handle = Create(utf8Path);
handle.Write(text.data(), text.size());
}
+28
View File
@@ -0,0 +1,28 @@
#include "dusk/settings.hpp"
#include "dusk/config.hpp"
namespace dusk::settings::enhancements {
ConfigVar<bool> FastIronBoots("enhancements.fast_iron_boots", false);
ConfigVar<bool> InvertCameraXAxis("enhancements.invert_camera_x_axis", false);
ConfigVar<bool> QuickTransform("enhancements.quick_transform", false);
ConfigVar<bool> RestoreWiiGlitches("enhancements.restore_wii_glitches", false);
ConfigVar<bool> EnableBloom("enhancements.enable_bloom", true);
ConfigVar<bool> UseWaterProjectionOffset("enhancements.use_water_projection_offset", false);
ConfigVar<bool> MirrorMode("enhancements.mirror_mode", false);
void Register() {
Register(FastIronBoots);
Register(InvertCameraXAxis);
Register(QuickTransform);
Register(RestoreWiiGlitches);
Register(EnableBloom);
Register(UseWaterProjectionOffset);
Register(MirrorMode);
}
}
namespace dusk::settings {
void Register() {
enhancements::Register();
}
}
+2 -2
View File
@@ -1163,7 +1163,7 @@ void mDoGph_gInf_c::bloom_c::remove() {
void mDoGph_gInf_c::bloom_c::draw() {
#if TARGET_PC
if (!dusk::ImGuiMenuEnhancements::m_enhancements.enableBloom) {
if (!dusk::settings::enhancements::EnableBloom.getValue()) {
return;
}
#endif
@@ -2113,7 +2113,7 @@ int mDoGph_Painter() {
#endif
#if TARGET_PC
if (dusk::ImGuiMenuEnhancements::m_enhancements.mirrorMode)
if (dusk::settings::enhancements::MirrorMode.getValue())
#elif PLATFORM_WII
if (data_8053a730)
#endif
+38 -1
View File
@@ -56,6 +56,8 @@
#include <dolphin/dvd.h>
#include "cxxopts.hpp"
#include "dusk/config.hpp"
#include "dusk/settings.hpp"
// --- GLOBALS ---
s8 mDoMain::developmentMode = -1;
@@ -225,10 +227,41 @@ static AuroraBackend ParseAuroraBackend(const std::string& value) {
exit(1);
}
static void ApplyCVarOverrides(const cxxopts::OptionValue& option) {
if (option.count() == 0) {
return;
}
const auto& cVars = option.as<std::vector<std::string>>();
for (const auto& cvarArg : cVars) {
const auto sep = cvarArg.find('=');
if (sep == std::string::npos) {
DuskLog.fatal("--cvar argument has no '=': '{}'", cvarArg);
continue;
}
const auto name = std::string_view(cvarArg).substr(0, sep);
const auto value = std::string_view(cvarArg).substr(sep + 1);
const auto cVar = dusk::config::GetConfigVar(name);
if (!cVar) {
DuskLog.fatal("Unknown --cvar name: '{}'", name);
}
try {
cVar->getImpl()->loadFromArg(*cVar, value);
} catch (const std::exception& e) {
DuskLog.fatal("Unable to parse: '{}': {}", value, e.what());
}
}
}
// =========================================================================
// PC ENTRY POINT
// =========================================================================
int game_main(int argc, char* argv[]) {
dusk::settings::Register();
cxxopts::ParseResult parsed_arg_options;
try {
@@ -238,7 +271,8 @@ int game_main(int argc, char* argv[]) {
("l,log-level", "Log level from " + std::to_string(AuroraLogLevel::LOG_DEBUG) + " to " + std::to_string(AuroraLogLevel::LOG_FATAL), cxxopts::value<uint8_t>()->default_value("0"))
("h,help", "Print usage")
("dvd", "Path to DVD image file", cxxopts::value<std::string>()->default_value("game.iso"))
("backend", "Graphics API backend to use (auto, d3d11, d3d12, metal, vulkan, opengl, opengles, webgpu, null)", cxxopts::value<std::string>()->default_value("auto"));
("backend", "Graphics API backend to use (auto, d3d11, d3d12, metal, vulkan, opengl, opengles, webgpu, null)", cxxopts::value<std::string>()->default_value("auto"))
("cvar", "Override configuration variables without modifying config", cxxopts::value<std::vector<std::string>>());
arg_options.parse_positional({"dvd"});
arg_options.positional_help("<dvd-image>");
@@ -257,6 +291,9 @@ int game_main(int argc, char* argv[]) {
exit(1);
}
dusk::config::LoadFromUserPreferences();
ApplyCVarOverrides(parsed_arg_options["cvar"]);
AuroraConfig config{};
config.appName = "Dusk";
config.windowPosX = -1;