#include #include #include #include #include "ModMenu.h" #include "2s2h/BenPort.h" #include "2s2h/resource/type/Skeleton.h" #include "2s2h/BenGui/MenuTypes.h" #include "2s2h/BenGui/BenMenu.h" #include "2s2h/BenGui/BenGui.hpp" std::vector enabledModFiles; std::vector disabledModFiles; std::vector unsupportedFiles; std::map filePaths; static int dragSourceIndex = -1; static int dragTargetIndex = -1; namespace BenGui { extern std::shared_ptr mBenMenu; } static WidgetInfo enableModsWidget; static WidgetInfo tabHotkeyWidget; #define CVAR_ENABLED_MODS_NAME "gSettings.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()); Ship::Context::GetRawInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); } void AfterModChange() { // 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 ModsPostDragAndDrop() { if (dragTargetIndex != -1) { std::string file = enabledModFiles[dragSourceIndex]; enabledModFiles.erase(enabledModFiles.begin() + dragSourceIndex); enabledModFiles.insert(enabledModFiles.begin() + dragTargetIndex, file); dragTargetIndex = dragSourceIndex = -1; AfterModChange(); } } void ModsHandleDragAndDrop(std::vector& objectList, int targetIndex, const std::string& itemName, ImGuiDragDropFlags flags = ImGuiDragDropFlags_SourceAllowNullID) { if (ImGui::BeginDragDropSource(flags)) { ImGui::SetDragDropPayload("DragMove", &targetIndex, sizeof(uint32_t)); ImGui::Text("Move %s", itemName.c_str()); ImGui::EndDragDropSource(); } if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload("DragMove")) { IM_ASSERT(payload->DataSize == sizeof(uint32_t)); dragSourceIndex = *(const int*)payload->Data; dragTargetIndex = targetIndex; } ImGui::EndDragDropTarget(); } } std::vector GetEnabledModsFromCVar() { std::string enabledModsCVarValue = CVAR_ENABLED_MODS_VALUE; if (enabledModsCVarValue.empty()) return {}; return StringHelper::Split(enabledModsCVarValue, SEPARATOR); } std::vector& GetModFiles(bool enabled) { return enabled ? enabledModFiles : disabledModFiles; } std::shared_ptr GetArchiveManager() { return Ship::Context::GetRawInstance()->GetResourceManager()->GetArchiveManager(); } bool IsValidExtension(std::string extension) { if ( #ifdef INCLUDE_MPQ_SUPPORT // .mpq doesn't make sense to support because all tools to make such mods output OTR StringHelper::IEquals(extension, ".otr") /*|| StringHelper::IEquals(extension, ".mpq")*/ || #endif // .zip needs to be excluded because mods are most often distributed in zip archives // and thus could contain .otr/o2r files StringHelper::IEquals(extension, ".o2r") /*|| StringHelper::IEquals(extension, ".zip")*/) { return true; } return false; } void UpdateModFiles(bool init = false, bool reset = false) { if (init || reset) { enabledModFiles.clear(); enabledModFiles = GetEnabledModsFromCVar(); } disabledModFiles.clear(); unsupportedFiles.clear(); filePaths.clear(); bool changed = false; std::string modsPath = Ship::Context::LocateFileAcrossAppDirs("mods", appShortName); std::map tempMods; if (modsPath.length() > 0 && std::filesystem::exists(modsPath)) { std::vector enabledFiles; 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)) { if (p.is_directory()) { continue; } std::string filename = p.path().filename().generic_string().substr(0, p.path().filename().generic_string().rfind(".")); std::string extension = p.path().extension().generic_string(); if (!IsValidExtension(extension)) { continue; } bool enabled = std::find(enabledModFiles.begin(), enabledModFiles.end(), filename) != enabledModFiles.end(); if (!enabled) { tempMods.emplace(p.path().lexically_normal().generic_string(), filename); } filePaths.emplace(filename, p.path()); } if (tempMods.size() > 0) { changed = true; for (auto [path, name] : tempMods) { enabledModFiles.push_back(name); } tempMods.clear(); } if (init) { std::vector enabledTemp(enabledModFiles); for (std::string mod : enabledTemp) { if (filePaths.contains(mod)) { GetArchiveManager()->AddArchive(filePaths.at(mod).generic_string()); } else { enabledModFiles.erase(std::find(enabledModFiles.begin(), enabledModFiles.end(), mod)); changed = true; } } } } if (changed) { SetEnabledModsCVarValue(); } } } extern "C" void gfx_texture_cache_clear(); 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& selectedModFiles = GetModFiles(enabled); if (selectedModFiles.empty()) { return; } bool madeAnyChange = false; int switchFromIndex = -1; int switchToIndex = -1; uint32_t index = 0; for (size_t i = selectedModFiles.size() - 1; i != SIZE_MAX; i--) { std::string file = selectedModFiles[i]; if (enabled) { ImGui::BeginGroup(); } // if (UIWidgets::StateButton((file + "_left_right").c_str(), enabled ? ICON_FA_ARROW_RIGHT : // ICON_FA_ARROW_LEFT, // 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 == selectedModFiles.size() - 1) { 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 == selectedModFiles.size() - 1) { ImGui::EndDisabled(); } ImGui::SameLine(); if (i == 0) { 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 == 0) { ImGui::EndDisabled(); } } DrawModInfo(filePaths.at(file).filename().generic_string()); if (enabled) { ImGui::EndGroup(); ModsHandleDragAndDrop(selectedModFiles, i, file); } } if (enabled) { ModsPostDragAndDrop(); } if (madeAnyChange) { std::iter_swap(selectedModFiles.begin() + switchFromIndex, selectedModFiles.begin() + switchToIndex); AfterModChange(); } } bool editing = false; void ModMenuWindow::DrawElement() { BenGui::mBenMenu->MenuDrawItem(enableModsWidget, 200, THEME_COLOR); ImGui::SameLine(); BenGui::mBenMenu->MenuDrawItem(tabHotkeyWidget, 200, THEME_COLOR); ImGui::TextColored( UIWidgets::ColorValues.at(UIWidgets::Colors::Yellow), "Mods are currently not reloaded at runtime. Close and re-open 2Ship for the changes to take effect.\n" "Drag ordering for the enabled list is available.\nMod priority is top to bottom. They override mods listed " "below them."); // if (UIWidgets::Button( // "Update", UIWidgets::ButtonOptions({ { .disabled = editing, .disabledTooltip = "Currently editing..." } // }) // .Size(UIWidgets::Sizes::Inline) // .Color(THEME_COLOR))) { // UpdateModFiles(); // } // ImGui::SameLine(); if (UIWidgets::Button("Edit", UIWidgets::ButtonOptions({ { .disabled = editing, .disabledTooltip = "Already editing..." } }) .Size(UIWidgets::Sizes::Inline) .Color(THEME_COLOR))) { editing = true; } if (editing) { ImGui::SameLine(); if (UIWidgets::Button("Cancel", UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { editing = false; UpdateModFiles(false, true); } ImGui::SameLine(); if (UIWidgets::Button("Clear List", UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { BenGui::RegisterPopup("Clear List", "Clear the current mod list and force a rebuild on next boot.\nClick Apply & Close " "to save this change.", "Clear", "Cancel", [&]() { enabledModFiles.clear(); AfterModChange(); }); } ImGui::SameLine(); if (UIWidgets::Button("Apply & Close", UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline).Color(THEME_COLOR))) { BenGui::RegisterPopup("Apply & Close", "Application currently requires a restart. Save the mod info and close 2S2H?", "Close", "Cancel", [&]() { // TODO: runtime changes SetEnabledModsCVarValue(); // TODO: runtime changes /* gfx_texture_cache_clear(); SOH::SkeletonPatcher::ClearSkeletons(); */ Ship::Context::GetRawInstance()->GetConsoleVariables()->Save(); Ship::Context::GetRawInstance()->GetWindow()->Close(); }); } } ImGui::BeginDisabled(!editing); if (ImGui::BeginTable("tableMods", 2, ImGuiTableFlags_BordersH | ImGuiTableFlags_BordersV)) { ImGui::TableSetupColumn("Enabled Mods", ImGuiTableColumnFlags_WidthStretch, 200.0f); // ImGui::TableSetupColumn("Disabled Mods", ImGuiTableColumnFlags_WidthStretch, 200.0f); ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); ImGui::TableHeadersRow(); ImGui::PopItemFlag(); ImGui::TableNextRow(); ImGui::TableNextColumn(); if (ImGui::BeginChild("Enabled Mods", ImVec2(0, -8))) { DrawMods(true); } ImGui::EndChild(); /*ImGui::TableNextColumn(); if (ImGui::BeginChild("Disabled Mods", ImVec2(0, -8))) { DrawMods(false); ImGui::EndChild(); }*/ ImGui::EndTable(); } ImGui::EndDisabled(); } void ModMenuWindow::InitElement() { UpdateModFiles(true); } void RegisterModMenuWidgets() { enableModsWidget = { .name = "Enable Mods", .type = WidgetType::WIDGET_CVAR_CHECKBOX }; enableModsWidget.CVar("gEnhancements.Mods.AlternateAssets") .Options(UIWidgets::CheckboxOptions({ { .disabledTooltip = "Temporarily disabled while editing mods list." } }) .Color(THEME_COLOR) .Tooltip("Toggle mods. For graphics mods, this means toggling between default and mod graphics.") .DefaultValue(true)) .PreFunc([](WidgetInfo& info) { auto options = std::static_pointer_cast(info.options); options->disabled = editing; }); BenGui::mBenMenu->AddSearchWidget({ enableModsWidget, "Settings", "Mod Menu", "Top", "alternate assets" }); tabHotkeyWidget = { .name = "Mods Tab Hotkey", .type = WidgetType::WIDGET_CVAR_CHECKBOX }; tabHotkeyWidget.CVar("gEnhancements.Mods.AlternateAssetsHotkey") .Options(UIWidgets::CheckboxOptions() .Color(THEME_COLOR) .Tooltip("Allows pressing the Tab key to toggle mods") .DefaultValue(true)); BenGui::mBenMenu->AddSearchWidget( { tabHotkeyWidget, "Settings", "Mod Menu", "Top", "alternate assets tab hotkey" }); } static RegisterMenuInitFunc menuInitFunc(RegisterModMenuWidgets);