Files
Shiip-of-Hakinian-Espanol/soh/soh/Enhancements/mod_menu.cpp

391 lines
14 KiB
C++

#include <map>
#include <ranges>
#include <vector>
#include <libultraship/classes.h>
#include <ship/utils/StringHelper.h>
#include "mod_menu.h"
#include "soh/OTRGlobals.h"
#include "soh/resource/type/Skeleton.h"
#include "soh/SohGui/MenuTypes.h"
#include "soh/SohGui/SohMenu.h"
#include "soh/SohGui/SohGui.hpp"
std::vector<std::string> enabledModFiles;
std::vector<std::string> disabledModFiles;
std::vector<std::string> unsupportedFiles;
std::map<std::string, std::filesystem::path> filePaths;
static int dragSourceIndex = -1;
static int dragTargetIndex = -1;
namespace SohGui {
extern std::shared_ptr<SohMenu> mSohMenu;
}
static WidgetInfo enableModsWidget;
static WidgetInfo tabHotkeyWidget;
#define CVAR_ENABLED_MODS_NAME CVAR_SETTING("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::GetInstance()->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<std::string>& 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<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();
}
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<std::string, std::string> tempMods;
if (modsPath.length() > 0 && std::filesystem::exists(modsPath)) {
std::vector<std::filesystem::path> 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<std::string> 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<std::string>& selectedModFiles = GetModFiles(enabled);
if (selectedModFiles.empty()) {
return;
}
bool madeAnyChange = false;
int switchFromIndex = -1;
int switchToIndex = -1;
uint32_t index = 0;
for (int i = static_cast<int>(selectedModFiles.size()) - 1; i >= 0; 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() {
SohGui::mSohMenu->MenuDrawItem(enableModsWidget, 200, THEME_COLOR);
ImGui::SameLine();
SohGui::mSohMenu->MenuDrawItem(tabHotkeyWidget, 200, THEME_COLOR);
ImGui::TextColored(
UIWidgets::ColorValues.at(UIWidgets::Colors::Yellow),
"Mods are currently not reloaded at runtime. Close and re-open Ship 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("Apply & Close",
UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline).Color(THEME_COLOR))) {
SohGui::RegisterPopup("Apply & Close",
"Application currently requires a restart. Save the mod info and close SoH?", "Close",
"Cancel", [&]() {
// TODO: runtime changes
SetEnabledModsCVarValue();
// TODO: runtime changes
/*
gfx_texture_cache_clear();
SOH::SkeletonPatcher::ClearSkeletons();
*/
Ship::Context::GetInstance()->GetConsoleVariables()->Save();
Ship::Context::GetInstance()->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(CVAR_SETTING("AltAssets"))
.RaceDisable(false)
.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<UIWidgets::CheckboxOptions>(info.options);
options->disabled = editing;
});
SohGui::mSohMenu->AddSearchWidget({ enableModsWidget, "Settings", "Mod Menu", "Top", "alternate assets" });
tabHotkeyWidget = { .name = "Mods Tab Hotkey", .type = WidgetType::WIDGET_CVAR_CHECKBOX };
tabHotkeyWidget.CVar(CVAR_SETTING("Mods.AlternateAssetsHotkey"))
.RaceDisable(false)
.Options(UIWidgets::CheckboxOptions()
.Color(THEME_COLOR)
.Tooltip("Allows pressing the Tab key to toggle mods")
.DefaultValue(true));
SohGui::mSohMenu->AddSearchWidget(
{ tabHotkeyWidget, "Settings", "Mod Menu", "Top", "alternate assets tab hotkey" });
}
static RegisterMenuInitFunc menuInitFunc(RegisterModMenuWidgets);