Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add gamesettings functions for features #533

Merged
merged 1 commit into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 279 additions & 1 deletion src/Feature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,282 @@ const std::vector<Feature*>& Feature::GetFeatureList()
return !a->SupportsVR();
});
return (REL::Module::IsVR() && !State::GetSingleton()->IsDeveloperMode()) ? featuresVR : features;
}
}

void Feature::EnableBooleanSettings(const std::map<std::string, Feature::gameSetting>& settingsMap, const std::string& featureName)
{
// Extract first letter from each word in featureName
std::string logTag;
bool capitalizeNext = true;
for (char ch : featureName) {
if (std::isalpha(ch) && capitalizeNext) {
logTag += static_cast<char>(std::toupper(static_cast<unsigned char>(ch)));
capitalizeNext = false;
}
if (std::isspace(ch)) {
capitalizeNext = true;
}
}

// Handle INI settings
for (const auto& [settingName, settingData] : settingsMap) {
if (settingData.offset == 0) {
// This is an INI setting
if (auto setting = RE::INISettingCollection::GetSingleton()->GetSetting(settingName); setting) {
if (!setting->data.b) {
logger::info("[{}] Changing {} from {} to {} to support {}", logTag, settingName, setting->data.b, true, featureName);
setting->data.b = true;
}
}
} else {
// This is a setting with an offset
const auto address = REL::Offset{ settingData.offset }.address();
bool* setting = reinterpret_cast<bool*>(address);
if (!*setting) {
logger::info("[{}] Changing {} from {} to {} to support {}", logTag, settingName, *setting, true, featureName);
*setting = true;
}
}
}
}

void Feature::ResetGameSettingsToDefaults(std::map<std::string, gameSetting>& settingsMap)
{
for (auto& [settingName, settingData] : settingsMap) {
char inputTypeChar = settingName[0];

if (settingData.offset == 0) {
// Handle INI settings
if (auto setting = RE::INISettingCollection::GetSingleton()->GetSetting(settingName); setting) {
switch (inputTypeChar) {
case 'b':
{
bool currentValue = setting->data.b;
bool defaultValue = std::get<bool>(settingData.defaultValue);
if (currentValue != defaultValue) {
setting->data.b = defaultValue;
logger::debug("Setting {}: changed from {} to default boolean value {}.", settingName, currentValue, defaultValue);
}
}
break;
case 'f':
{
float currentValue = setting->data.f;
float defaultValue = std::get<float>(settingData.defaultValue);
if (currentValue != defaultValue) {
setting->data.f = defaultValue;
logger::debug("Setting {}: changed from {} to default float value {}.", settingName, currentValue, defaultValue);
}
}
break;
case 'i':
case 'u':
{
int32_t currentValue = setting->data.i;
int32_t defaultValue = std::get<int32_t>(settingData.defaultValue);
if (currentValue != defaultValue) {
setting->data.i = defaultValue;
logger::debug("Setting {}: changed from {} to default integer value {}.", settingName, currentValue, defaultValue);
}
}
break;
default:
logger::debug("Unknown type for setting {}.", settingName);
break;
}
}
} else {
// Handle settings with offsets (raw memory)
auto address = REL::Offset{ settingData.offset }.address();
switch (inputTypeChar) {
case 'b':
{
bool* ptr = reinterpret_cast<bool*>(address);
bool currentValue = *ptr;
bool defaultValue = std::get<bool>(settingData.defaultValue);
if (currentValue != defaultValue) {
*ptr = defaultValue;
logger::debug("Setting {}: changed from {} to default boolean value {}.", settingName, currentValue, defaultValue);
}
}
break;
case 'f':
{
float* ptr = reinterpret_cast<float*>(address);
float currentValue = *ptr;
float defaultValue = std::get<float>(settingData.defaultValue);
if (currentValue != defaultValue) {
*ptr = defaultValue;
logger::debug("Setting {}: changed from {} to default float value {}.", settingName, currentValue, defaultValue);
}
}
break;
case 'i':
case 'u':
{
int32_t* ptr = reinterpret_cast<int32_t*>(address);
int32_t currentValue = *ptr;
int32_t defaultValue = std::get<int32_t>(settingData.defaultValue);
if (currentValue != defaultValue) {
*ptr = defaultValue;
logger::debug("Setting {}: changed from {} to default integer value {}.", settingName, currentValue, defaultValue);
}
}
break;
default:
logger::debug("Unknown type for setting {}.", settingName);
break;
}
}
}
}

void Feature::RenderImGuiSettingsTree(const std::map<std::string, gameSetting>& settingsMap, const std::string& treeName)
{
if (ImGui::TreeNode(treeName.c_str())) { // Create a tree node
for (const auto& [settingName, settingData] : settingsMap) {
if (settingData.offset == 0) {
// Handle INI settings
if (auto setting = RE::INISettingCollection::GetSingleton()->GetSetting(settingName); setting) {
RenderImGuiElement(settingName, settingData, setting);
}
} else {
// Handle settings with offsets (raw memory)
std::visit([&](auto&& defaultValue) {
using ValueType = std::decay_t<decltype(defaultValue)>;
if constexpr (std::is_same_v<ValueType, bool>) {
bool* ptr = reinterpret_cast<bool*>(REL::Offset{ settingData.offset }.address());
RenderImGuiElement(settingName, settingData, ptr);
} else if constexpr (std::is_same_v<ValueType, float>) {
float* ptr = reinterpret_cast<float*>(REL::Offset{ settingData.offset }.address());
RenderImGuiElement(settingName, settingData, ptr);
} else if constexpr (std::is_same_v<ValueType, int32_t>) {
int32_t* ptr = reinterpret_cast<int32_t*>(REL::Offset{ settingData.offset }.address());
RenderImGuiElement(settingName, settingData, ptr);
} else if constexpr (std::is_same_v<ValueType, uint32_t>) {
uint32_t* ptr = reinterpret_cast<uint32_t*>(REL::Offset{ settingData.offset }.address());
RenderImGuiElement(settingName, settingData, ptr);
} else {
logger::warn("Unsupported type for setting {}", settingName);
}
},
settingData.defaultValue);
}
}
ImGui::TreePop(); // Close the tree node
}
}

template <typename T>
void Feature::RenderImGuiElement(const std::string& settingName, const gameSetting& settingData, T* valuePtr)
{
// Unique ID for each element
ImGui::PushID(settingName.c_str()); // Use settingName as unique ID

if constexpr (std::is_same_v<T, bool>) {
ImGui::Checkbox(settingData.friendlyName.c_str(), valuePtr);
} else if constexpr (std::is_same_v<T, float>) {
try {
auto minFloat = std::get<float>(settingData.minValue);
auto maxFloat = std::get<float>(settingData.maxValue);
ImGui::SliderFloat(settingData.friendlyName.c_str(), valuePtr, minFloat, maxFloat);
} catch (const std::bad_variant_access&) {
logger::warn("Type mismatch for {}: expected float for minValue or maxValue but received other type", settingName);
}
} else if constexpr (std::is_same_v<T, int> || std::is_same_v<T, unsigned int>) {
try {
auto minInt = std::get<int32_t>(settingData.minValue);
auto maxInt = std::get<int32_t>(settingData.maxValue);
ImGui::SliderInt(settingData.friendlyName.c_str(), reinterpret_cast<int*>(valuePtr), minInt, maxInt);
} catch (const std::bad_variant_access&) {
logger::warn("Type mismatch for {}: expected int for minValue or maxValue but received other type", settingName);
}
} else {
logger::warn("Unsupported type for {}: {}", settingName, typeid(T).name());
}

// Tooltip handling
if (auto _tt = Util::HoverTooltipWrapper()) {
std::string hover = "";
if (settingData.offset != 0)
hover = std::format("{}\n\nNOTE: CS cannot save this game setting directly. Game setting '{}' might be able to be saved manually in the ini. Use the Copy button to export to clipboard.", settingData.description, settingName);
else
hover = settingData.description;
ImGui::Text(hover.c_str());
}
if (settingData.offset != 0) {
ImGui::SameLine();
if (ImGui::Button("Copy")) {
ImGui::SetClipboardText(settingName.c_str());
}
if (auto _tt = Util::HoverTooltipWrapper()) {
ImGui::Text(std::format("Copy '{}' to clipboard.", settingName).c_str());
}
}
ImGui::PopID(); // End unique ID scope
}

void Feature::RenderImGuiElement(const std::string& settingName, const gameSetting& settingData, RE::Setting* setting)
{
// Determine the type of the setting
switch (setting->GetType()) {
case RE::Setting::Type::kBool:
RenderImGuiElement(settingName, settingData, &setting->data.b);
break;
case RE::Setting::Type::kFloat:
RenderImGuiElement(settingName, settingData, &setting->data.f);
break;
case RE::Setting::Type::kSignedInteger:
RenderImGuiElement(settingName, settingData, &setting->data.i);
break;
case RE::Setting::Type::kUnsignedInteger:
RenderImGuiElement(settingName, settingData, &setting->data.u);
break;
case RE::Setting::Type::kColor:
case RE::Setting::Type::kString:
case RE::Setting::Type::kUnknown:
default:
alandtse marked this conversation as resolved.
Show resolved Hide resolved
logger::warn("Unsupported type for setting '{}'", settingName);
break;
}
}

void Feature::SaveGameSettings(const std::map<std::string, gameSetting>& settingsMap)
{
auto iniSettings = RE::INISettingCollection::GetSingleton();

for (const auto& [settingName, settingData] : settingsMap) {
// Only process settings without an offset (INI-based settings)
if (settingData.offset == 0) {
if (auto setting = iniSettings->GetSetting(settingName); setting) {
if (iniSettings->WriteSetting(setting)) {
logger::debug("Saved INI setting '{}'", settingName);
} else {
logger::warn("Failed to save INI setting '{}'", settingName);
}
} else {
logger::warn("INI setting '{}' not found.", settingName);
}
}
}
}

void Feature::LoadGameSettings(const std::map<std::string, gameSetting>& settingsMap)
{
auto iniSettings = RE::INISettingCollection::GetSingleton();

for (const auto& [settingName, settingData] : settingsMap) {
// Only process settings without an offset (INI-based settings)
if (settingData.offset == 0) {
if (auto setting = iniSettings->GetSetting(settingName); setting) {
if (iniSettings->ReadSetting(setting)) {
logger::debug("Loaded INI setting '{}'", settingName);
} else {
logger::warn("Failed to load INI setting '{}'", settingName);
}
} else {
logger::warn("INI setting '{}' not found.", settingName);
}
}
}
}
92 changes: 92 additions & 0 deletions src/Feature.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,96 @@ struct Feature
virtual void ClearShaderCache() {}

static const std::vector<Feature*>& GetFeatureList();

struct gameSetting
{
std::string friendlyName;
std::string description;
std::uintptr_t offset;
std::variant<bool, std::int32_t, std::uint32_t, float> defaultValue;
std::variant<bool, std::int32_t, std::uint32_t, float> minValue;
std::variant<bool, std::int32_t, std::uint32_t, float> maxValue;
};

/**
* @brief Updates boolean settings in the provided map, setting them to true.
* This is intended to be used when features are disabled in Skyrim by default.
* Logs the changes and supports customizing the feature name in the log output.
*
* @param settingsMap The map containing settings to update.
* @param featureName The name of the feature being enabled, used for logging purposes.
*/
void EnableBooleanSettings(const std::map<std::string, Feature::gameSetting>& settingsMap, const std::string& featureName);

/**
* @brief Sets each game setting to its default value if the current value does not match the default.
*
* This function iterates through all game settings in the provided map and checks whether each setting's
* current value is equal to its default value. If a setting's current value differs from its default,
* the function updates the setting to the default value.
*
* The function assumes that the `gameSetting` structure contains the following fields:
* - `defaultValue`: The default value for the setting.
* - `currentValue`: The current value of the setting.
*
* The function performs the following steps:
* - Iterates over each setting in the `settingsMap`.
* - Compares the `currentValue` of the setting to its `defaultValue`.
* - If they are not equal, updates `currentValue` to `defaultValue`.
*
* This function is useful for resetting settings to their defaults, typically in scenarios where
* a reset operation is required, or to ensure consistency of default values across different parts of the application.
*
* @param settingsMap A map of setting names to `gameSetting` objects, where each object contains
* the information for a specific game setting. The map is modified in place, with settings
* updated to their default values if necessary.
*/
void ResetGameSettingsToDefaults(std::map<std::string, gameSetting>& settingsMap);

/**
* @brief Helper function to render ImGui elements based on variable type inferred from the setting name.
* The first letter of the variable name is used to determine the type (b = bool, f = float, i = int, etc.).
*
* @param settingsMap The map of settings to be rendered.
* @param tableName A unique identifier for the ImGui tree.
*/
void RenderImGuiSettingsTree(const std::map<std::string, gameSetting>& settingsMap, const std::string& tableName);

/**
* @brief Templated function to render an appropriate ImGui element (e.g., checkbox, slider) based on the type.
*
* @param settingName The name of the setting.
* @param valuePtr Pointer to the value (either from an RE::Setting or direct memory access).
*/
template <typename T>
void RenderImGuiElement(const std::string& settingName, const gameSetting& settingData, T* valuePtr);

/**
* @brief Renders an appropriate ImGui element for RE::Setting data.
*
* @param settingName The name of the setting.
* @param inputTypeChar Character determining the variable type (b for bool, f for float, i for int).
* @param setting Pointer to the RE::Setting.
*/
void RenderImGuiElement(const std::string& settingName, const gameSetting& settingData, RE::Setting* setting);

/**
* @brief Saves the provided game settings to the INI file.
*
* This function iterates through the settings map, identifies settings that are managed via
* the INISettingCollection (i.e., those without an offset), and saves them by calling `WriteSetting`.
*
* @param settingsMap The map of game settings.
*/
void SaveGameSettings(const std::map<std::string, gameSetting>& settingsMap);

/**
* @brief Loads the provided game settings from the INI file.
*
* This function iterates through the settings map, identifies settings that are managed via
* the INISettingCollection (i.e., those without an offset), and loads them by calling `ReadSetting`.
*
* @param settingsMap The map of game settings.
*/
void LoadGameSettings(const std::map<std::string, gameSetting>& settingsMap);
};
Loading
Loading