summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPepe20129 <72659707+Pepe20129@users.noreply.github.com>2025-09-30 17:35:37 +0200
committerGitHub <noreply@github.com>2025-09-30 08:35:37 -0700
commit3cf1d654660ef7bb0ce9ae253877bf0ab86615d1 (patch)
treed9ffa49b588b1cee2530cddae92e77d731a300e1
parentb7de53bf8d7237c4c8ad4cd5329f3b0d62a7b0a6 (diff)
Mod menu V1 (#4962)
* Proof of Concept * Fixed saving/loading * Switch sides & add arrow buttons * Update mod_menu.cpp * Remove old mod loading code * Small cleanup * Post-merge fixes * Update mod_menu.cpp * Lots of changes * clang format * Address review * Update SohMenuEnhancements.cpp
-rw-r--r--soh/soh/Enhancements/mod_menu.cpp239
-rw-r--r--soh/soh/Enhancements/mod_menu.h14
-rw-r--r--soh/soh/OTRGlobals.cpp21
-rw-r--r--soh/soh/SohGui/SohGui.cpp5
-rw-r--r--soh/soh/SohGui/SohMenuEnhancements.cpp9
5 files changed, 268 insertions, 20 deletions
diff --git a/soh/soh/Enhancements/mod_menu.cpp b/soh/soh/Enhancements/mod_menu.cpp
new file mode 100644
index 000000000..427f5b2b4
--- /dev/null
+++ b/soh/soh/Enhancements/mod_menu.cpp
@@ -0,0 +1,239 @@
+#include "mod_menu.h"
+#include "utils/StringHelper.h"
+#include <libultraship/classes.h>
+#include "soh/SohGui/SohGui.hpp"
+#include "soh/OTRGlobals.h"
+#include "soh/resource/type/Skeleton.h"
+#include <map>
+#include <ranges>
+#include <vector>
+
+std::vector<std::string> enabledModFiles;
+std::vector<std::string> disabledModFiles;
+
+#define CVAR_ENABLED_MODS_NAME CVAR_GENERAL("EnabledMods")
+#define CVAR_ENABLED_MODS_DEFAULT ""
+#define CVAR_ENABLED_MODS_VALUE CVarGetString(CVAR_ENABLED_MODS_NAME, CVAR_ENABLED_MODS_DEFAULT)
+
+// "|" was chosen as the separator due to
+// it being an invalid character in NTFS
+// and being rarely used in ext4
+// it is also an ASCII character
+// improving portability
+
+// if being an ASCII character is not a requirement,
+// other possible candidates include:
+// - U+FFFF: non-character
+// - any private use character
+#define SEPARATOR "|"
+
+void SetEnabledModsCVarValue() {
+ std::string s = "";
+
+ for (auto& modPath : enabledModFiles) {
+ s += modPath + SEPARATOR;
+ }
+
+ // remove trailing separator if present
+ if (s.length() != 0) {
+ s.pop_back();
+ }
+
+ CVarSetString(CVAR_ENABLED_MODS_NAME, s.c_str());
+}
+
+std::vector<std::string> GetEnabledModsFromCVar() {
+ std::string enabledModsCVarValue = CVAR_ENABLED_MODS_VALUE;
+ return StringHelper::Split(enabledModsCVarValue, SEPARATOR);
+}
+
+std::vector<std::string>& GetModFiles(bool enabled) {
+ return enabled ? enabledModFiles : disabledModFiles;
+}
+
+std::shared_ptr<Ship::ArchiveManager> GetArchiveManager() {
+ return Ship::Context::GetInstance()->GetResourceManager()->GetArchiveManager();
+}
+
+void UpdateModFiles(bool init = false) {
+ if (init) {
+ enabledModFiles.clear();
+ }
+ disabledModFiles.clear();
+ std::vector<std::string> enabledMods = GetEnabledModsFromCVar();
+ std::string modsPath = Ship::Context::LocateFileAcrossAppDirs("mods", appShortName);
+ if (modsPath.length() > 0 && std::filesystem::exists(modsPath)) {
+ if (std::filesystem::is_directory(modsPath)) {
+ for (const std::filesystem::directory_entry& p : std::filesystem::recursive_directory_iterator(
+ modsPath, std::filesystem::directory_options::follow_directory_symlink)) {
+ std::string extension = p.path().extension().string();
+ if (
+#ifndef EXCLUDE_MPQ_SUPPORT
+ StringHelper::IEquals(extension, ".otr") || StringHelper::IEquals(extension, ".mpq") ||
+#endif
+ StringHelper::IEquals(extension, ".o2r") || StringHelper::IEquals(extension, ".zip")) {
+ std::string path = p.path().generic_string();
+ bool shouldBeEnabled = std::find(enabledMods.begin(), enabledMods.end(), path) != enabledMods.end();
+
+ if (shouldBeEnabled) {
+ if (init) {
+ enabledModFiles.push_back(path);
+ GetArchiveManager()->AddArchive(path);
+ }
+ } else {
+ disabledModFiles.push_back(path);
+ }
+ }
+ }
+ }
+ }
+}
+
+extern "C" void gfx_texture_cache_clear();
+
+void AfterModChange() {
+ SetEnabledModsCVarValue();
+ // TODO: runtime changes
+ /*
+ gfx_texture_cache_clear();
+ SOH::SkeletonPatcher::ClearSkeletons();
+ */
+ Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame();
+
+ // disabled mods are always sorted
+ std::sort(disabledModFiles.begin(), disabledModFiles.end(), [](const std::string& a, const std::string& b) {
+ return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(),
+ [](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); });
+ });
+}
+
+void EnableMod(std::string file) {
+ disabledModFiles.erase(std::find(disabledModFiles.begin(), disabledModFiles.end(), file));
+ enabledModFiles.insert(enabledModFiles.begin(), file);
+
+ // TODO: runtime changes
+ // GetArchiveManager()->AddArchive(file);
+ AfterModChange();
+}
+
+void DisableMod(std::string file) {
+ enabledModFiles.erase(std::find(enabledModFiles.begin(), enabledModFiles.end(), file));
+ disabledModFiles.insert(disabledModFiles.begin(), file);
+
+ // TODO: runtime changes
+ // GetArchiveManager()->RemoveArchive(file);
+ AfterModChange();
+}
+
+void DrawModInfo(std::string file) {
+ ImGui::SameLine();
+ ImGui::Text("%s", file.c_str());
+}
+
+void DrawMods(bool enabled) {
+ std::vector<std::string>& selectedModFiles = GetModFiles(enabled);
+ if (selectedModFiles.empty()) {
+ return;
+ }
+
+ bool madeAnyChange = false;
+ int switchFromIndex = -1;
+ int switchToIndex = -1;
+
+ for (int i = 0; i < selectedModFiles.size(); i += 1) {
+ std::string file = selectedModFiles[i];
+ if (UIWidgets::StateButton((file + "_left_right").c_str(), enabled ? ICON_FA_ARROW_LEFT : ICON_FA_ARROW_RIGHT,
+ ImVec2(25, 25), UIWidgets::ButtonOptions().Color(THEME_COLOR))) {
+ if (enabled) {
+ DisableMod(file);
+ } else {
+ EnableMod(file);
+ }
+ }
+
+ // it's not relevant to reorder disabled mods
+ if (enabled) {
+ ImGui::SameLine();
+ if (i == 0) {
+ ImGui::BeginDisabled();
+ }
+ if (UIWidgets::StateButton((file + "_up").c_str(), ICON_FA_ARROW_UP, ImVec2(25, 25),
+ UIWidgets::ButtonOptions().Color(THEME_COLOR))) {
+ madeAnyChange = true;
+ switchFromIndex = i;
+ switchToIndex = i - 1;
+ }
+ if (i == 0) {
+ ImGui::EndDisabled();
+ }
+
+ ImGui::SameLine();
+ if (i == selectedModFiles.size() - 1) {
+ ImGui::BeginDisabled();
+ }
+ if (UIWidgets::StateButton((file + "_down").c_str(), ICON_FA_ARROW_DOWN, ImVec2(25, 25),
+ UIWidgets::ButtonOptions().Color(THEME_COLOR))) {
+ madeAnyChange = true;
+ switchFromIndex = i;
+ switchToIndex = i + 1;
+ }
+ if (i == selectedModFiles.size() - 1) {
+ ImGui::EndDisabled();
+ }
+ }
+
+ DrawModInfo(file);
+ }
+
+ if (madeAnyChange) {
+ std::iter_swap(selectedModFiles.begin() + switchFromIndex, selectedModFiles.begin() + switchToIndex);
+ AfterModChange();
+ }
+}
+
+void ModMenuWindow::DrawElement() {
+ ImGui::BeginDisabled(CVarGetInteger(CVAR_SETTING("DisableChanges"), 0));
+
+ const ImVec4 yellow = ImVec4(1, 1, 0, 1);
+
+ ImGui::TextColored(
+ yellow, "Mods are currently not reloaded at runtime.\nClose and re-open Ship for the changes to take effect.");
+
+ if (UIWidgets::Button("Update", UIWidgets::ButtonOptions().Size(ImVec2(250.0f, 0.0f)).Color(THEME_COLOR))) {
+ UpdateModFiles();
+ }
+ UIWidgets::Tooltip("Re-check the mods folder for new files");
+
+ if (ImGui::BeginTable("tableMods", 2, ImGuiTableFlags_BordersH | ImGuiTableFlags_BordersV)) {
+ ImGui::TableSetupColumn("Disabled Mods", ImGuiTableColumnFlags_WidthStretch, 200.0f);
+ ImGui::TableSetupColumn("Enabled Mods", ImGuiTableColumnFlags_WidthStretch, 200.0f);
+ ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
+ ImGui::TableHeadersRow();
+ ImGui::PopItemFlag();
+ ImGui::TableNextRow();
+
+ ImGui::TableNextColumn();
+
+ if (ImGui::BeginChild("Disabled Mods", ImVec2(0, -8))) {
+ DrawMods(false);
+
+ ImGui::EndChild();
+ }
+
+ ImGui::TableNextColumn();
+
+ if (ImGui::BeginChild("Enabled Mods", ImVec2(0, -8))) {
+ DrawMods(true);
+
+ ImGui::EndChild();
+ }
+
+ ImGui::EndTable();
+ }
+
+ ImGui::EndDisabled();
+}
+
+void ModMenuWindow::InitElement() {
+ UpdateModFiles(true);
+} \ No newline at end of file
diff --git a/soh/soh/Enhancements/mod_menu.h b/soh/soh/Enhancements/mod_menu.h
new file mode 100644
index 000000000..cb29b4001
--- /dev/null
+++ b/soh/soh/Enhancements/mod_menu.h
@@ -0,0 +1,14 @@
+#pragma once
+
+#include <libultraship/libultraship.h>
+
+#ifdef __cplusplus
+class ModMenuWindow : public Ship::GuiWindow {
+ public:
+ using GuiWindow::GuiWindow;
+
+ void InitElement() override;
+ void DrawElement() override;
+ void UpdateElement() override{};
+};
+#endif \ No newline at end of file
diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp
index d306ce228..11dcc220e 100644
--- a/soh/soh/OTRGlobals.cpp
+++ b/soh/soh/OTRGlobals.cpp
@@ -282,26 +282,7 @@ void OTRGlobals::Initialize() {
if (std::filesystem::exists(sohOtrPath)) {
OTRFiles.push_back(sohOtrPath);
}
- std::string patchesPath = Ship::Context::LocateFileAcrossAppDirs("mods", appShortName);
- std::vector<std::string> patchOTRs = {};
- if (patchesPath.length() > 0 && std::filesystem::exists(patchesPath)) {
- if (std::filesystem::is_directory(patchesPath)) {
- for (const auto& p : std::filesystem::recursive_directory_iterator(
- patchesPath, std::filesystem::directory_options::follow_directory_symlink)) {
- if (StringHelper::IEquals(p.path().extension().string(), ".otr") ||
- StringHelper::IEquals(p.path().extension().string(), ".mpq") ||
- StringHelper::IEquals(p.path().extension().string(), ".o2r") ||
- StringHelper::IEquals(p.path().extension().string(), ".zip")) {
- patchOTRs.push_back(p.path().generic_string());
- }
- }
- }
- }
- std::sort(patchOTRs.begin(), patchOTRs.end(), [](const std::string& a, const std::string& b) {
- return std::lexicographical_compare(a.begin(), a.end(), b.begin(), b.end(),
- [](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); });
- });
- OTRFiles.insert(OTRFiles.end(), patchOTRs.begin(), patchOTRs.end());
+
std::unordered_set<uint32_t> ValidHashes = {
OOT_PAL_MQ, OOT_NTSC_JP_MQ, OOT_NTSC_US_MQ, OOT_PAL_GC_MQ_DBG, OOT_NTSC_US_10,
OOT_NTSC_US_11, OOT_NTSC_US_12, OOT_PAL_10, OOT_PAL_11, OOT_NTSC_JP_GC_CE,
diff --git a/soh/soh/SohGui/SohGui.cpp b/soh/soh/SohGui/SohGui.cpp
index 8de5478f1..cf9182a62 100644
--- a/soh/soh/SohGui/SohGui.cpp
+++ b/soh/soh/SohGui/SohGui.cpp
@@ -33,6 +33,7 @@
#include "soh/Enhancements/debugger/MessageViewer.h"
#include "soh/Notification/Notification.h"
#include "soh/Enhancements/TimeDisplay/TimeDisplay.h"
+#include "soh/Enhancements/mod_menu.h"
namespace SohGui {
@@ -72,6 +73,7 @@ std::shared_ptr<SohStatsWindow> mStatsWindow;
std::shared_ptr<Ship::GuiWindow> mGfxDebuggerWindow;
std::shared_ptr<SohMenu> mSohMenu;
+std::shared_ptr<ModMenuWindow> mModMenuWindow;
std::shared_ptr<AudioEditor> mAudioEditorWindow;
std::shared_ptr<InputViewer> mInputViewer;
std::shared_ptr<InputViewerSettingsWindow> mInputViewerSettings;
@@ -134,6 +136,8 @@ void SetupGuiElements() {
SPDLOG_ERROR("Could not find input editor window");
}*/
+ mModMenuWindow = std::make_shared<ModMenuWindow>(CVAR_WINDOW("ModMenu"), "Mod Menu", ImVec2(820, 630));
+ gui->AddGuiWindow(mModMenuWindow);
mAudioEditorWindow = std::make_shared<AudioEditor>(CVAR_WINDOW("AudioEditor"), "Audio Editor", ImVec2(820, 630));
gui->AddGuiWindow(mAudioEditorWindow);
mInputViewer = std::make_shared<InputViewer>(CVAR_WINDOW("InputViewer"), "Input Viewer");
@@ -225,6 +229,7 @@ void Destroy() {
mColViewerWindow = nullptr;
mActorViewerWindow = nullptr;
mCosmeticsEditorWindow = nullptr;
+ mModMenuWindow = nullptr;
mAudioEditorWindow = nullptr;
mStatsWindow = nullptr;
mConsoleWindow = nullptr;
diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp
index 87eb881cb..6e967951d 100644
--- a/soh/soh/SohGui/SohMenuEnhancements.cpp
+++ b/soh/soh/SohGui/SohMenuEnhancements.cpp
@@ -1900,6 +1900,15 @@ void SohMenu::AddMenuEnhancements() {
.CVar(timer.timeEnable)
.Callback([](WidgetInfo& info) { TimeDisplayUpdateDisplayOptions(); });
}
+
+ // Mod Menu
+ path.sidebarName = "Mod Menu";
+ AddSidebarEntry("Enhancements", path.sidebarName, 1);
+ AddWidget(path, "Popout Mod Menu Window", WIDGET_WINDOW_BUTTON)
+ .CVar(CVAR_WINDOW("ModMenu"))
+ .WindowName("Mod Menu")
+ .HideInSearch(true)
+ .Options(WindowButtonOptions().Tooltip("Enables the separate Mod Menu Window."));
}
} // namespace SohGui