Merge remote-tracking branch 'origin/main' into feature/dynamic-aspect-ratio

This commit is contained in:
MelonSpeedruns
2026-04-06 13:16:38 -04:00
45 changed files with 1278 additions and 124 deletions
+20
View File
@@ -0,0 +1,20 @@
#ifndef DUSK_APPNAME_HPP
#define DUSK_APPNAME_HPP
namespace dusk {
/**
* \brief The internal application name for the game.
*
* This gets used for file paths and such, and cannot be changed!
*/
constexpr auto AppName = "Dusk";
/**
* \brief The internal organization name for the game.
*
* This gets used for file paths and such, and cannot be changed!
*/
constexpr auto OrgName = "TwilitRealm";
}
#endif // DUSK_APPNAME_HPP
+122
View File
@@ -0,0 +1,122 @@
#ifndef DUSK_CONFIG_HPP
#define DUSK_CONFIG_HPP
#include <stdexcept>
#include "nlohmann/json.hpp"
#include "config_var.hpp"
namespace dusk::config {
/*
* config.hpp is a heavier "full" header for the configuration system.
* For a basic overview and the basic types (sufficient for accessing settings),
* look at config_var.hpp.
*
* Avoid including this header in the entire game, it's heavier than I'd like!
*/
/**
* \brief Base class containing virtual functions used for save/load of CVars.
*/
class ConfigImplBase {
protected:
virtual ~ConfigImplBase() = default;
public:
/**
* \brief Load a JSON value into a CVar at the Value layer.
*/
virtual void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const = 0;
/**
* \brief Load a simple launch argument into the CVar at the Override layer.
*/
virtual void loadFromArg(ConfigVarBase& cVar, std::string_view stringValue) const = 0;
/**
* \brief Dump the value contained in the CVar to JSON.
*/
[[nodiscard]] virtual nlohmann::json dumpToJson(const ConfigVarBase& cVar) const = 0;
};
template<ConfigValue T>
class ConfigImpl : public ConfigImplBase {
// Just downcasting the references...
void loadFromJson(ConfigVarBase& cVar, const nlohmann::json& jsonValue) const final {
assert(typeid(cVar) == typeid(ConfigVar<T>));
loadFromJson(dynamic_cast<ConfigVar<T>&>(cVar), jsonValue);
}
void loadFromArg(ConfigVarBase& cVar, std::string_view stringValue) const final {
assert(typeid(cVar) == typeid(ConfigVar<T>));
loadFromArg(dynamic_cast<ConfigVar<T>&>(cVar), stringValue);
}
[[nodiscard]] nlohmann::json dumpToJson(const ConfigVarBase& cVar) const final {
assert(typeid(cVar) == typeid(ConfigVar<T>));
return dumpToJson(dynamic_cast<const ConfigVar<T>&>(cVar));
}
/**
* \brief Load a JSON value into a CVar at the Value layer.
*/
static void loadFromJson(ConfigVar<T>& cVar, const nlohmann::json& jsonValue);
/**
* \brief Load a simple launch argument into the CVar at the Override layer.
*/
static void loadFromArg(ConfigVar<T>& cVar, std::string_view stringValue);
/**
* \brief Dump the value contained in the CVar to JSON.
*/
[[nodiscard]] static nlohmann::json dumpToJson(const ConfigVar<T>& cVar);
};
/**
* \brief Thrown by config loading functions if the value provided is invalid for the CVar.
*/
class InvalidConfigError : public std::runtime_error {
public:
explicit InvalidConfigError(const char* what) : runtime_error(what) {}
};
/**
* \brief Register a CVar to make the config system aware of it.
*
* This must be done on startup *before* config has been loaded.
*/
void Register(ConfigVarBase& configVar);
/**
* \brief Indicate that all registrations have happened and everything should lock in.
*/
void FinishRegistration();
/**
* \brief Load config from the standard user preferences location.
*/
void LoadFromUserPreferences();
void LoadFromFileName(const char* path);
/**
* \brief Save the config to file.
*/
void Save();
/**
* \brief Get a registered CVar by name.
*
* @return null if the CVar does not exist.
*/
ConfigVarBase* GetConfigVar(std::string_view name);
template <ConfigValue T>
const ConfigImplBase* GetConfigImpl() {
static ConfigImpl<T> config;
return &config;
}
} // namespace dusk::config
#endif
+235
View File
@@ -0,0 +1,235 @@
#ifndef DUSK_CONFIG_VAR_HPP
#define DUSK_CONFIG_VAR_HPP
#include "dolphin/types.h"
#include <type_traits>
#include <cstdlib>
#include <string>
/**
* The configuration system.
*
* Configuration works via "configuration variables" aka "CVars". Each stores a single value that
* may be individually written to/from a configuration file.
*
* CVars, like ogres, have layers. Higher layers (e.g. a set value) override lower layers
* (e.g. the default value).
*
* To define a CVar, simply make a global variable of type ConfigVar<T>,
* and make sure Register() is called on it during program startup.
*
* config_var.hpp contains the simplest "just the configuration vars themselves".
* This should be safe to include for files that need to *access* configuration,
* without blowing up compile times on implementation details.
*
* config.hpp on the other hand contains far more calls for mutating, loading, and defining CVars.
*/
namespace dusk::config {
/**
* \brief Layers that a configuration variable can currently be at.
*
* A configuration variable can be on one of multiple *layers*, which determines where
* the current value is coming from.
*/
enum class ConfigVarLayer : u8 {
/**
* The CVar is at the default value defined by the application code.
*/
Default,
/**
* The CVar has been modified by the user and may be saved to config.
*/
Value,
/**
* The CVar is modified by launch argument, overruling the normal config value.
* Will not get saved to config.
*/
Override,
};
class ConfigImplBase;
/**
* Base class that all CVars inherit from.
* You want the templated ConfigVar instead for actual usage.
*/
class ConfigVarBase {
protected:
/**
* The name of this CVar, used in the configuration file.
*/
const char* name;
/**
* Whether this CVar has been registered with the global managing logic.
* If this is not done, it is not functional.
*/
bool registered;
/**
* The layer this CVar is at.
*/
ConfigVarLayer layer;
/**
* Pointer to an implementation struct for various load/save calls.
*/
const ConfigImplBase* impl;
ConfigVarBase(const char* name, const ConfigImplBase* impl);
virtual ~ConfigVarBase() = default;
/**
* Check that the CVar is registered, aborting if this is not the case.
*/
void checkRegistered() const {
if (!registered)
abort();
}
public:
/**
* Get the name of this CVar, used in the configuration file.
*/
[[nodiscard]] const char* getName() const noexcept;
/**
* Get the pointer to the implementation struct.
*/
[[nodiscard]] const ConfigImplBase* getImpl() const noexcept;
/**
* Get the layer this CVar is currently at.
*/
[[nodiscard]] constexpr ConfigVarLayer getLayer() const noexcept {
return layer;
}
/**
* Mark this CVar as being registered with the central save/load logic.
* This is necessary to make it legal to access.
*/
void markRegistered();
};
template <typename T>
concept ConfigValueInteger =
std::is_same_v<T, s8>
|| std::is_same_v<T, u8>
|| std::is_same_v<T, s16>
|| std::is_same_v<T, u16>
|| std::is_same_v<T, s32>
|| std::is_same_v<T, u32>
|| std::is_same_v<T, s64>
|| std::is_same_v<T, u64>;
/**
* \brief Concept that defines the legal set of types that can be used for CVar values.
*
* Valid types cannot be cv-qualified and must be basic primitive types (int, float, bool),
* strings, or enums of the basic primitives.
*/
template <typename T>
concept ConfigValue =
!std::is_const_v<T>
&& !std::is_volatile_v<T>
&& (std::is_same_v<T, bool>
|| ConfigValueInteger<T>
|| std::is_same_v<T, f32>
|| std::is_same_v<T, f64>
|| std::is_same_v<T, std::string>
|| (std::is_enum_v<T> && ConfigValueInteger<std::underlying_type_t<T>>));
template <ConfigValue T>
const ConfigImplBase* GetConfigImpl();
/**
* \brief A CVar storing values.
*
* @tparam T The type of value stored in the CVar.
*/
template <ConfigValue T>
class ConfigVar : public ConfigVarBase {
T defaultValue;
T value;
T overrideValue;
public:
/**
* \brief Construct a CVar.
*
* @param name The name of this CVar. Must be unique.
* @param arg Arguments to forward to construct the default value.
*/
template <typename... Args>
explicit ConfigVar(const char* name, Args&&... arg)
: ConfigVarBase(name, GetConfigImpl<T>()), defaultValue(std::forward<Args>(arg)...),
value(), overrideValue() {}
/**
* \brief Get the current value of the CVar.
*
* This reference is not guaranteed to remain up-to-date after modification of the CVar.
* It will, however, remain sound to access.
*/
[[nodiscard]] constexpr const T& getValue() const noexcept {
checkRegistered();
switch (layer) {
case ConfigVarLayer::Default:
return defaultValue;
case ConfigVarLayer::Value:
return value;
case ConfigVarLayer::Override:
return overrideValue;
default:
abort();
}
}
/**
* \brief Change the value of a CVar.
*
* The new value is always stored at the Value layer.
*
* @param newValue The new value the CVar will get.
* @param replaceOverride If true, clear an existing override layer if there is one.
* If this is false and there is an override layer,
* the result of getValue() will not change immediately.
*/
void setValue(T newValue, bool replaceOverride = true) {
checkRegistered();
value = std::move(newValue);
if (replaceOverride) {
overrideValue = {};
layer = ConfigVarLayer::Value;
} else if (layer != ConfigVarLayer::Override) {
layer = ConfigVarLayer::Value;
}
}
operator const T&() {
return getValue();
}
/**
* \brief Give a CVar an override value.
*
* This overrides (but does not replace) the apparent set value of this CVar.
* The overriden value will not get saved to config.
*
* @param newValue The new value the CVar will get.
*/
void setOverrideValue(T newValue) {
checkRegistered();
overrideValue = std::move(newValue);
layer = ConfigVarLayer::Override;
}
};
}
#endif // DUSK_CONFIG_VAR_HPP
+1
View File
@@ -4,5 +4,6 @@
#include <aurora/aurora.h>
extern AuroraInfo auroraInfo;
extern const char* configPath;
#endif // DUSK_DUSK_H
+75
View File
@@ -0,0 +1,75 @@
#ifndef DUSK_IO_HPP
#define DUSK_IO_HPP
#include <vector>
// I can't believe it's 2026 and neither SDL (no error codes) nor
// C++ (no error codes) have a file system API functional enough for me to use.
// Here you go, this one's inspired by C#. I only wrote the functions I need.
namespace dusk::io {
/**
* \brief A simple file stream wrapping cstdio FILE*.
*
* Methods on this class throw appropriate C++ exceptions when an error occurs.
*/
class FileStream {
void* file;
public:
FileStream() noexcept;
/**
* \brief Take ownership of a FILE* handle.
*/
explicit FileStream(void* file);
FileStream(const FileStream& other) = delete;
FileStream(FileStream&& other) noexcept;
~FileStream();
/**
* \brief Open a file for reading at the given path.
*/
static FileStream OpenRead(const char* utf8Path);
/**
* \brief Create a file for writing.
*
* If there is an existing file, its contents are demolished.
*/
static FileStream Create(const char* utf8Path);
/**
* \brief Read the byte contents of a file directly into a vector.
*/
static std::vector<u8> ReadAllBytes(const char* utf8Path);
/**
* \brief Read the byte contents of a file directly into a vector.
*/
static void WriteAllText(const char* utf8Path, std::string_view text);
/**
* \brief Read the remaining contents of the file directly into a vector.
*/
std::vector<u8> ReadFull();
/**
* Get direct access to the underlying FILE* handle.
*/
[[nodiscard]] void* GetFileHandle() const noexcept {
return file;
}
/**
* Write data to the file.
*/
void Write(const char* data, size_t dataLen);
};
}
#endif // DUSK_IO_HPP
+34 -28
View File
@@ -1,8 +1,12 @@
#ifndef DUSK_CONFIG_H
#define DUSK_CONFIG_H
#include "dusk/config_var.hpp"
namespace dusk {
using namespace config;
// Persistent user settings
struct UserSettings {
@@ -10,61 +14,63 @@ struct UserSettings {
struct {
// Video
bool enableFullscreen;
ConfigVar<bool> enableFullscreen;
} video;
struct {
// Audio
int masterVolume;
int mainMusicVolume;
int subMusicVolume;
int soundEffectsVolume;
int fanfareVolume;
bool enableReverb;
ConfigVar<int> masterVolume;
ConfigVar<int> mainMusicVolume;
ConfigVar<int> subMusicVolume;
ConfigVar<int> soundEffectsVolume;
ConfigVar<int> fanfareVolume;
ConfigVar<bool> enableReverb;
} audio;
// Game settings
struct {
// QoL
bool enableQuickTransform;
bool hideTvSettingsScreen;
bool biggerWallets;
bool noReturnRupees;
bool disableRupeeCutscenes;
bool noSwordRecoil;
int damageMultiplier;
bool instantDeath;
bool fastClimbing;
bool noMissClimbing;
bool fastTears;
ConfigVar<bool> enableQuickTransform;
ConfigVar<bool> hideTvSettingsScreen;
ConfigVar<bool> biggerWallets;
ConfigVar<bool> noReturnRupees;
ConfigVar<bool> disableRupeeCutscenes;
ConfigVar<bool> noSwordRecoil;
ConfigVar<int> damageMultiplier;
ConfigVar<bool> instantDeath;
ConfigVar<bool> fastClimbing;
ConfigVar<bool> noMissClimbing;
ConfigVar<bool> fastTears;
// Preferences
bool enableMirrorMode;
bool invertCameraXAxis;
ConfigVar<bool> enableMirrorMode;
ConfigVar<bool> invertCameraXAxis;
// Graphics
bool enableBloom;
bool useWaterProjectionOffset;
ConfigVar<bool> enableBloom;
ConfigVar<bool> useWaterProjectionOffset;
// Audio
bool noLowHpSound;
bool midnasLamentNonStop;
ConfigVar<bool> noLowHpSound;
ConfigVar<bool> midnasLamentNonStop;
// Cheats
bool enableFastIronBoots;
bool canTransformAnywhere;
ConfigVar<bool> enableFastIronBoots;
ConfigVar<bool> canTransformAnywhere;
// Technical
bool restoreWiiGlitches;
ConfigVar<bool> restoreWiiGlitches;
// Controls
bool enableTurboKeybind;
ConfigVar<bool> enableTurboKeybind;
} game;
};
UserSettings& getSettings();
void registerSettings();
// Transient settings
struct CollisionViewSettings {