diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp index b53b1ee40d..d9c11e213a 100644 --- a/mm/2s2h/BenGui/BenMenu.cpp +++ b/mm/2s2h/BenGui/BenMenu.cpp @@ -9,6 +9,7 @@ #include "DeveloperTools/CollisionViewer.h" #include "DeveloperTools/EventLog.h" #include "2s2h/Enhancements/GfxPatcher/AuthenticGfxPatches.h" +#include "2s2h/PresetManager/PresetManager.h" #include "HudEditor.h" #include "Notification.h" #include @@ -382,6 +383,10 @@ void BenMenu::AddSettings() { }); }) .Options(ButtonOptions().Tooltip("Displays a test notification.")); + + path.sidebarName = "Presets"; + AddSidebarEntry("Settings", "Presets", 1); + AddWidget(path, "Presets", WIDGET_CUSTOM).CustomFunction([](WidgetInfo& info) { PresetManager_Draw(); }); } int32_t motionBlurStrength; diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp index 198040c3f3..dd1363f967 100644 --- a/mm/2s2h/BenPort.cpp +++ b/mm/2s2h/BenPort.cpp @@ -58,6 +58,7 @@ CrowdControl* CrowdControl::Instance; #include "2s2h/BenGui/Notification.h" #include "2s2h/ShipUtils.h" #include "2s2h/ShipInit.hpp" +#include "2s2h/PresetManager/PresetManager.h" // Resource Types/Factories #include "resource/type/Blob.h" @@ -451,9 +452,9 @@ void Ben_ProcessDroppedFiles(std::string filePath) { // handled = Randomizer_HandleFileDropped(filePath); // } - // if (!handled) { - // handled = Presets_HandleFileDropped(filePath); - // } + if (!handled) { + handled = PresetManager_HandleFileDropped(filePath); + } if (!handled) { auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); diff --git a/mm/2s2h/PresetManager/PresetManager.cpp b/mm/2s2h/PresetManager/PresetManager.cpp new file mode 100644 index 0000000000..0d98e4ff1e --- /dev/null +++ b/mm/2s2h/PresetManager/PresetManager.cpp @@ -0,0 +1,516 @@ +#include "PresetManager.h" +#include +#include +#include +#include +#include "2s2h/BenPort.h" +#include "2s2h/BenGui/UIWidgets.hpp" +#include "2s2h/BenGui/Notification.h" + +std::unordered_map tagMap = { + { "gEventLog", "Developer Tools" }, + { "gDeveloperTools", "Developer Tools" }, + { "gCollisionViewer", "Developer Tools" }, + { "gCheats", "Enhancements" }, + { "gEnhancements", "Enhancements" }, + { "gFixes", "Enhancements" }, + { "gModes", "Enhancements" }, + { "gHudEditor", "HUD" }, + { "ItemTracker", "HUD" }, + { "gRando", "Rando" }, +}; + +nlohmann::json defaultsPresetJ = R"( +{ + "ClearCVars": [ + "gCheats", + "gCollisionViewer", + "gDeveloperTools", + "gDisplayOverlay", + "gEnhancements", + "gEventLog", + "gFixes", + "gHudEditor", + "gModes", + "gNetwork", + "gNotifications", + "gRando", + "gWindows", + "ItemTracker" + ], + "CVars": {}, + "type": "2S2H_PRESET", + "version": 1 +} +)"_json; + +nlohmann::json curatedPresetJ = R"( +{ + "CVars": { + "gCheats": { + "EasyFrameAdvance": 1 + }, + "gDeveloperTools": { + "BetterMapSelect": { + "Enabled": 1 + }, + "DebugEnabled": 1, + "LogLevel": 2 + }, + "gEnhancements": { + "Cutscenes": { + "HideTitleCards": 1, + "SkipEntranceCutscenes": 1, + "SkipFirstCycle": 1, + "SkipIntroSequence": 1, + "SkipMiscInteractions": 1, + "SkipStoryCutscenes": 1, + "SkipToFileSelect": 1 + }, + "Cycle": { + "DoNotResetBottleContent": 1, + "DoNotResetConsumables": 1, + "DoNotResetRazorSword": 1, + "DoNotResetRupees": 1, + "DoNotResetTimeSpeed": 1, + "KeepExpressMail": 1 + }, + "Dialogue": { + "FastBankSelection": 1, + "FastText": 1 + }, + "Dpad": { + "DpadEquips": 1 + }, + "Equipment": { + "MagicArrowEquipSpeed": 1, + "TwoHandedSwordSpinAttack": 1 + }, + "Fixes": { + "CompletedHeartContainerAudio": 1, + "ControlCharacters": 1, + "FierceDeityZTargetMovement": 1 + }, + "Graphics": { + "3DItemDrops": 1, + "ActorCullingAccountsForWidescreen": 1, + "AuthenticLogo": 1, + "BowReticle": 1, + "ClockType": 1, + "DisableSceneGeometryDistanceCheck": 1, + "IncreaseActorDrawDistance": 5 + }, + "Masks": { + "FastTransformation": 1, + "FierceDeitysAnywhere": 1, + "GoronRollingFastSpikes": 1, + "GoronRollingIgnoresMagic": 1, + "NoBlastMaskCooldown": 1, + "PersistentBunnyHood": { + "Enabled": 1 + } + }, + "Minigames": { + "AlwaysWinDoggyRace": 1, + "BeaverRaceRingsCollected": 2, + "BombersHideAndSeek": 2, + "CuccoShackCuccoCount": 2, + "HoneyAndDarlingDay1": 4, + "HoneyAndDarlingDay2": 4, + "HoneyAndDarlingDay3": 8, + "SwampArcheryScore": 1580, + "SwordsmanSchoolScore": 6, + "TownArcheryScore": 25 + }, + "Mods": { + "AlternateAssetsHotkey": 0 + }, + "Playback": { + "DpadOcarina": 1, + "NoDropOcarinaInput": 1, + "SkipScarecrowSong": 1 + }, + "Player": { + "ClimbSpeed": 5, + "FastFlowerLaunch": 1, + "FierceDeityPutaway": 1, + "InfiniteDekuHopping": 1, + "InstantPutaway": 1, + "PreventDiveOverWater": 1 + }, + "PlayerActions": { + "ArrowCycle": 1, + "InstantRecall": 1 + }, + "Restorations": { + "ConstantFlipsHops": 1, + "OoTFasterSwim": 1, + "PowerCrouchStab": 1, + "SideRoll": 1, + "TatlISG": 1 + }, + "Saving": { + "Autosave": 1, + "DisableSaveDelay": 1, + "PauseSave": 1, + "PersistentOwlSaves": 1 + }, + "Songs": { + "BetterSongOfDoubleTime": 1, + "EnableSunsSong": 1, + "FasterSongPlayback": 1, + "PauseOwlWarp": 1, + "SkipSoTCutscenes": 1 + }, + "Timesavers": { + "DampeDiggingSkip": 1, + "MarineLabHP": 1, + "SwampBoatSpeed": 1 + } + }, + "gFixes": { + "FixAmmoCountEnvColor": 1, + "FixIkanaGreatFairyFountainColor": 1 + }, + "gHudEditor": { + "A": { + "Mode": 4 + }, + "B": { + "Mode": 4 + }, + "CDown": { + "Mode": 4 + }, + "CLeft": { + "Mode": 4 + }, + "CRight": { + "Mode": 4 + }, + "CUp": { + "Mode": 4 + }, + "Carrots": { + "Mode": 2 + }, + "Clock": { + "Mode": 2 + }, + "DPad": { + "Mode": 4 + }, + "Hearts": { + "Mode": 3 + }, + "Keys": { + "Mode": 3 + }, + "Magic": { + "Mode": 3 + }, + "Minigames": { + "Mode": 3 + }, + "Minimap": { + "Mode": 4 + }, + "Rupees": { + "Mode": 3 + }, + "SkullKidTimer": { + "Mode": 2 + }, + "Skulltulas": { + "Mode": 3 + }, + "Start": { + "Mode": 4 + }, + "Timers": { + "Mode": 3 + } + }, + "gModes": { + "PlayAsKafei": 1 + } + }, + "type": "2S2H_PRESET", + "version": 1 +} +)"_json; + +std::unordered_map>> presets = {}; +const std::filesystem::path presetsFolderPath(Ship::Context::GetPathRelativeToAppDirectory("presets", appShortName)); + +void PresetManager_RefreshPresets() { + presets.clear(); + presets.insert( + { "Defaults (Everything Off)", { defaultsPresetJ, { "Developer Tools", "Enhancements", "HUD", "Rando" } } }); + presets.insert({ "Curated", { curatedPresetJ, { "Developer Tools", "Enhancements", "HUD" } } }); + + // ensure the presets folder exists + if (!std::filesystem::exists(presetsFolderPath)) { + std::filesystem::create_directory(presetsFolderPath); + } + + // Add all files in the presets folder to the list of presets + for (const auto& entry : std::filesystem::directory_iterator(presetsFolderPath)) { + if (entry.is_regular_file()) { + std::string fileName = entry.path().filename().string(); + fileName.erase(fileName.find_last_of('.')); + + try { + // Read the file + nlohmann::json j; + std::ifstream file(entry.path()); + file >> j; + + // Ensure the file is a valid preset + if (!j.contains("type") || j["type"] != "2S2H_PRESET") { + continue; + } + + // TODO: Migrate preset if it's an old version + + presets[fileName] = { j, {} }; + + if (j.contains("ClearCVars")) { + std::vector clearCVars = j["ClearCVars"].get>(); + for (const auto& cvar : clearCVars) { + if (tagMap.contains(cvar)) { + presets[fileName].second.insert(tagMap[cvar]); + } + } + } + + if (j.contains("CVars")) { + for (const auto& [key, value] : j["CVars"].items()) { + if (tagMap.contains(key)) { + presets[fileName].second.insert(tagMap[key]); + } + } + } + // Add the file to the list of presets + } catch (...) {} + } + } +} + +void PresetManager_ApplyPreset(nlohmann::json j) { + if (!j.contains("type") || j["type"] != "2S2H_PRESET") { + throw std::runtime_error("Invalid preset"); + } + + if (j.contains("ClearCVars")) { + auto clearCVars = j["ClearCVars"].get>(); + + for (const auto& cvar : clearCVars) { + // Replace slashes with dots in key, and remove leading dot + std::string path = cvar; + std::replace(path.begin(), path.end(), '/', '.'); + if (path[0] == '.') { + path.erase(0, 1); + } + CVarClearBlock(path.c_str()); + CVarClear(path.c_str()); + } + } + + if (j.contains("CVars")) { + auto cvars = j["CVars"].flatten(); + + for (auto& [key, value] : cvars.items()) { + // Replace slashes with dots in key, and remove leading dot + std::string path = key; + std::replace(path.begin(), path.end(), '/', '.'); + if (path[0] == '.') { + path.erase(0, 1); + } + if (value.is_string()) { + CVarSetString(path.c_str(), value.get().c_str()); + } else if (value.is_number_integer()) { + CVarSetInteger(path.c_str(), value.get()); + } else if (value.is_number_float()) { + CVarSetFloat(path.c_str(), value.get()); + } + } + } + + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + ShipInit::Init("*"); + Notification::Emit({ .message = "Preset Loaded" }); +} + +// Copies 2ship2harkinian.json to the presets folder, then removes everything except the CVars block +void PresetManager_CreatePreset(std::string presetName) { + try { + std::ifstream existingFileStream(Ship::Context::GetPathRelativeToAppDirectory("2ship2harkinian.json")); + + nlohmann::json existingJson; + existingFileStream >> existingJson; + + nlohmann::json newJson; + newJson["type"] = "2S2H_PRESET"; + newJson["version"] = 1; + newJson["CVars"] = existingJson["CVars"]; + // We may handle this differently in the future, but for now we're just not going to include gSettings and + // gWindows. Users can still manually add them if they want. + newJson["CVars"].erase("gSettings"); + newJson["CVars"].erase("gWindows"); + + std::string presetFileName = presetName + ".json"; + const std::filesystem::path newPresetFilePath = presetsFolderPath / presetFileName; + std::ofstream newFileStream(newPresetFilePath); + newFileStream << newJson.dump(4); + + newFileStream.close(); + + PresetManager_RefreshPresets(); + } catch (...) { Notification::Emit({ .suffix = "Failed to create preset" }); } +} + +bool PresetManager_HandleFileDropped(std::string filePath) { + try { + std::ifstream fileStream(filePath); + + if (!fileStream.is_open()) { + return false; + } + + // Check if first byte is "{" + if (fileStream.peek() != '{') { + return false; + } + + nlohmann::json j; + + // Attempt to parse the file + try { + fileStream >> j; + } catch (nlohmann::json::exception& e) { return false; } + + // Check if the file is a spoiler file + if (!j.contains("type") || j["type"] != "2S2H_PRESET") { + return false; + } + + // Save the spoiler file to the presets folder + std::string presetFileName = std::filesystem::path(filePath).filename().string(); + const std::filesystem::path newPresetFilePath = presetsFolderPath / presetFileName; + std::filesystem::copy_file(filePath, newPresetFilePath, std::filesystem::copy_options::overwrite_existing); + + PresetManager_RefreshPresets(); + PresetManager_ApplyPreset(j); + + return true; + } catch (std::exception& e) { return false; } catch (...) { + return false; + } +} + +void PresetManager_Draw() { + ImGui::BeginChild("PresetManager", ImVec2(500, 0)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 0.5f)); + ImGui::TextWrapped("Drag and drop a preset file into the window to load it, or drop it into the presets folder and " + "refresh the list."); + ImGui::PopStyleColor(); + if (UIWidgets::Button("Open Presets Folder", { .size = ImVec2(ImGui::GetContentRegionAvail().x - 42, 0) })) { + std::string path = "file:///" + std::filesystem::absolute(presetsFolderPath).string(); + SDL_OpenURL(path.c_str()); + } + ImGui::SameLine(); + if (UIWidgets::Button(ICON_FA_REFRESH)) { + PresetManager_RefreshPresets(); + } + ImGui::PushStyleVar(ImGuiStyleVar_SeparatorTextPadding, ImVec2(20, 0)); + ImGui::SeparatorText("Available Presets"); + + std::string clickedPreset; + + for (const auto& [preset, pair] : presets) { + const auto& [j, categories] = pair; + ImGui::PushID(preset.c_str()); + + ImGui::BeginGroup(); + ImGui::TextWrapped("%s", preset.c_str()); + int index = 0; + ImGui::PushStyleColor(ImGuiCol_Button, UIWidgets::ColorValues.at(UIWidgets::Colors::DarkGray)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, UIWidgets::ColorValues.at(UIWidgets::Colors::DarkGray)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, UIWidgets::ColorValues.at(UIWidgets::Colors::DarkGray)); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(6, 4)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(5, 6)); + for (const auto& category : categories) { + if (index++ > 0) { + ImGui::SameLine(); + } + ImGui::Button(category.c_str()); + } + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(3); + ImGui::EndGroup(); + auto lastGroupSize = ImGui::GetItemRectSize(); + auto lastGroupPos = ImGui::GetCursorPosY(); + // Vertically align to center, horizontally align to the right + ImGui::BeginGroup(); + ImGui::SetCursorPos({ ImGui::GetContentRegionAvail().x - 80, lastGroupPos - (lastGroupSize.y / 2) - 22 }); + if (UIWidgets::Button("Apply", { .color = UIWidgets::Colors::Orange })) { + clickedPreset = preset; + } + ImGui::EndGroup(); + // ImGui::SeparatorText(""); + ImGui::PopID(); + } + + if (!clickedPreset.empty()) { + PresetManager_ApplyPreset(presets[clickedPreset].first); + } + + ImGui::PopStyleVar(1); + + static bool showAddPresetForm = false; + static bool focusPresetName = false; + + if (showAddPresetForm) { + ImGui::Text("New Preset"); + + static char presetName[256] = ""; + + if (focusPresetName) { + ImGui::SetKeyboardFocusHere(); + focusPresetName = false; + } + + UIWidgets::PushStyleSlider(UIWidgets::Colors::Gray); + ImGui::InputText("##Name", presetName, sizeof(presetName)); + UIWidgets::PopStyleSlider(); + + ImGui::SameLine(); + + if (UIWidgets::Button(ICON_FA_FLOPPY_O, { .size = ImVec2(0, 0), .color = UIWidgets::Colors::Green })) { + PresetManager_CreatePreset(presetName); + presetName[0] = '\0'; + showAddPresetForm = false; + } + + ImGui::SameLine(); + if (UIWidgets::Button(ICON_FA_TIMES, { .size = ImVec2(0, 0), .color = UIWidgets::Colors::Red })) { + presetName[0] = '\0'; + showAddPresetForm = false; + } + } else { + if (UIWidgets::Button("Create Preset from Current Config", { .color = UIWidgets::Colors::Green })) { + showAddPresetForm = true; + focusPresetName = true; + } + } + + ImGui::EndChild(); +} + +void PresetManager_RegisterHooks() { + PresetManager_RefreshPresets(); +} + +static RegisterShipInitFunc initFunc(PresetManager_RegisterHooks, {}); diff --git a/mm/2s2h/PresetManager/PresetManager.h b/mm/2s2h/PresetManager/PresetManager.h new file mode 100644 index 0000000000..043ccce1b0 --- /dev/null +++ b/mm/2s2h/PresetManager/PresetManager.h @@ -0,0 +1,10 @@ + +#ifndef PRESET_MANAGER_H +#define PRESET_MANAGER_H + +#include + +bool PresetManager_HandleFileDropped(std::string filePath); +void PresetManager_Draw(); + +#endif // PRESET_MANAGER_H