Files
Zelda64Recomp/src/ui/ui_mod_installer.cpp
T
Wiseguy d766cf328f Modding menu and UI and additional mod exports (#535)
* init config opt system w/ 3 types and description support

* Move config registry/option to librecomp + added Color conf opt type

* Updated color option type styling

* Added dropdown option type

* Added TextField option type

* Button config type + callback wip

* init mod menu + bem class + button presets

* WIP mod menu, fix some warnings

* Rewrite mod details under new UI system.

* Refactored mods menu entirely.

* Remove ModMenu.scss.

* Take ownership of created pointers on Element class.

* Add styles.

* Multi-style state and disabled state propagation.

* Switch to string views.

* Convert to spaces, hook up mod enabled to toggle.

* Mod menu progress.

* Layout for mod details panel, add gap property setters

* Update RmlUi for gap property in flexbox

* Add slot_map and begin ui context

* Implement context and resource storage slotmaps

* Config submenu.

* Refactored to account for context changes.

* Turn off tab searching when submenu is open.

* Revert accidental RmlUi downgrade

* Upgrade RmlUi to 6.0 release

* Text input.

* Radio option.

* Cleanup.

* Refactor Rml document handling to use new ContextId system (prompts currently unimplemented)

* Add support for config schema.

* Split config sub menu into separate context and fix configure button, prevent infinite loop when looking for autofocus element

* Reimplement mechanism to open the config menu to a specific tab

* Begin implementing mod UI API

* Link storage to mod menu.

* Proper enum parsing.

* Enable mod reordering.

* Draggable improvements to mod menu and runtime update.

* Adjust styling of submenu.

* Mods folder button.

* Linux build fixes.

* Hook up new manifest fields to mod UI

* Add basic thumbnail parsing functionality.

* More style changes.

* Implement update event for elements

* Use RT64's texture laoding instead.

* Restore spacer animations.

* Animation API begone.

* Auto-enabled mods.

* Update runtime submodule and N64Recomp commit in CI for mod config API, remove unnecessary extern C

* Sub menu display name, assert on text input.

* Clamp delta time to fix UI disappearing on OS with timestamps that don't always increase.

* Add a state for when no mods are installed.

* Unify API function naming scheme and export relevant API functions

* Add actor update/init events and save init event (#536)

* Expose remaining property setters to mod UI API

* Implemented mod UI callbacks

* Implement actor extension data and use it for transform tagging

* Zero the memory allocated to hold extended actor data

* Implement label and textinput in mod UI API

* Patch virtual address translation to support entire extended RAM address space (#533)

* Download full target build of llvm in CI Windows runners to fix missing MIPS support and update N64Recomp CI commit

* Enable triple buffering in RT64 (#546)

* Implement controlling input capturing for mod UI contexts

* Created mod UI API functions for setting visibility, setting text, and destroying elements

* Fix errant RML tag in mod menu and insert breaks for newlines when setting element text

* Fix compilation after rebase

* Fixes for macOS

* Set the blender description manually for the UI renderer

* Created mod UI API functions for imageview elements

* Switch to designated initializers to work around missing aggregate initialization compiler support

* Update RT64 for driver bug workarounds and misc fixes

* Update RT64 to fix native sampler issues with tile copies

* Update RT64 for depth clear optimization and more native sampler changes

* Update RT64 and allow it to choose the graphics API when set to Auto

* Update runtime to allow renderers to choose the graphics API

* Update RT64 to enable early Z test shader optimization

* Implement data structure mod APIs

* Update lunasvg to increase its minimum cmake version

* Switch to runtime concatenation of function name in data API error reporting to fix Linux compilation issue

* Add missing typename to fix compilation on some compilers

* Update RT64 to fix failed assert with MSAA off

* Reimplement prompts as a separate UI context and make it so the quit game prompt doesn't bring up the config menu

* DnD prototype.

* Fix to dynamic lib path and runtime commit.

* Finish drag and drop mod installation, disable mod refresh button and code mod toggle when game starts

* Remove std::format usage and add missing <list> includes to fix Linux/MacOS compilation

* Switch to aggregate initialization for Version to work around missing implicit constructor on some compilers

* Replace use of std::bind with lambdas

* Add mod install button, put mod description in scroll container, minor mod menu tweaks

* Update runtime to fix renderer shutdown race condition

* Implement texture pack reordering

* Add mod UI API exports for slider, password input, and label radio and expose RmlUi debugger on F8

* Update runtime for mod version export

* Update runtime for save swapping mod API

* Apply recomp.rcss to mod UI contexts (fixes scrolls)

* Updated mod list styling (#561)

* Updated mod list styling

* mod entry max height

* Update RT64 for v5 texture hash

* Update runtime for mod API to get save file path

* Add special config option id to control texture pack state for code mods

* Update runtime for mod default enabled state

* Add exports for stars' display lists (#563)

* Update runtime to fix default value of enabled_by_default

* Update runtime to allow NULL recomp_free

* Implement navigation and focus styling for new UI framework (no manual overrides yet)

* Fix the previous scissor state bleeding when drawing the RmlUi output onto the swapchain buffer

* Use a multiple file select dialog for mod install button

* Add mod export for loading UI image from bytes (png/dds)

* Manual navigation in UI framework and WIP mod menu navigation

* Repeat key events when holding down controller inputs for UI navigation

* Patch AnimationContext_SetLoadFrame to allow custom animations (#564)

* Close context when showing or hiding a context and reopen afterwards to prevent deadlocks

* Add quotes around xdg-open and open commands to support paths with spaces

* Update RT64 for high precision texture coordinates when using texture replacements

* Add support for built-in mods and convert D-Pad to a built-in mod (#567)

* Add embedded mod (using mm_recomp_draw_distance as an example).

* Update runtime after merge

* Experiment with removing the D-Pad.

* Add event needed for dpad as mod, revert remaining changes in built-in patches for dpad

* Add built-in dpad mod, add remaining event calls to input.c

* Add built-in mods readme

---------

Co-authored-by: Dario <dariosamo@gmail.com>

* Fixing navigation of mods menu.

* Focused state for mod entry.

* Prevent hover styling and focus on input elements when disabled

* Fix up/down navigation on text input elements

* Set mod tab to navigate down to first mod, fix redundant mod scanning

* Remove more redundant mod scanning and fix mods being scanned during gameplay

* Update runtime for mod folder export

* Improve radio navigation and setup mod config submenu navigation setup

* Restore fd anywhere export functionality (#570)

* fix fd

* add comment back in

* Make config tabset navigate down to first mod entry when mod menu is open, make mod configure screen focus on configure button after closing

* Add navigation exports to mod UI API

* Fix opening the config menu via keyboard/controller causing a double animation warning in RmlUi

---------

Co-authored-by: Dario <dariosamo@gmail.com>
Co-authored-by: thecozies <79979276+thecozies@users.noreply.github.com>
Co-authored-by: Garrett Cox <garrettjcox@gmail.com>
Co-authored-by: David Chavez <david@dcvz.io>
Co-authored-by: danielryb <59661841+danielryb@users.noreply.github.com>
Co-authored-by: Reonu <danileon95@gmail.com>
Co-authored-by: LittleCube <littlecubehax@gmail.com>
2025-04-28 03:01:36 -04:00

331 lines
16 KiB
C++

#include "ui_mod_installer.h"
#include "librecomp/mods.hpp"
namespace recompui {
static const std::string ManifestFilename = "mod.json";
static const char *TextureDatabaseFilename = "rt64.json";
static const std::u8string OldExtension = u8".old";
static const std::u8string NewExtension = u8".new";
size_t zip_write_func(void *opaque, mz_uint64 offset, const void *bytes, size_t count) {
std::ofstream &stream = *(std::ofstream *)(opaque);
stream.seekp(offset, std::ios::beg);
stream.write((const char *)(bytes), count);
return stream.bad() ? 0 : count;
}
void start_single_mod_installation(const std::filesystem::path &file_path, recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) {
// Check for the existence of the manifest file.
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
std::filesystem::path target_path = mods_directory / file_path.filename();
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
ModInstaller::Installation installation;
bool exists = false;
std::vector<char> manifest_bytes = file_handle.read_file(ManifestFilename, exists);
if (exists) {
// Parse the manifest file to check for its validity.
std::string error;
recomp::mods::ModManifest manifest;
recomp::mods::ModOpenError open_error = parse_manifest(manifest, manifest_bytes, error);
exists = (open_error == recomp::mods::ModOpenError::Good);
if (exists) {
installation.mod_id = manifest.mod_id;
installation.display_name = manifest.display_name;
installation.mod_version = manifest.version;
installation.mod_file = target_path;
}
}
else if (file_path.extension() == ".rtz") {
// When it's an rtz file, check if the texture database file exists.
exists = mz_zip_reader_locate_file(file_handle.archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0;
if (exists) {
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
installation.display_name = installation.mod_id;
installation.mod_version = recomp::Version{0, 0, 0, ""};
installation.mod_file = target_path;
}
}
std::error_code ec;
if (exists) {
std::filesystem::copy(file_path, target_write_path, ec);
if (ec) {
result.error_messages.emplace_back("Unable to install " + file_path.filename().string() + " to mod directory.");
return;
}
}
else {
result.error_messages.emplace_back(file_path.string() + " is not a mod.");
std::filesystem::remove(target_write_path, ec);
return;
}
if (std::filesystem::exists(installation.mod_file, ec)) {
installation.needs_overwrite_confirmation = true;
}
if (!installation.needs_overwrite_confirmation) {
// This check isn't really needed as additional_files will be empty for a single mod installation,
// but it's good to have in case this logic ever changes.
for (const std::filesystem::path &path : installation.additional_files) {
if (std::filesystem::exists(path, ec)) {
installation.needs_overwrite_confirmation = true;
break;
}
}
}
result.pending_installations.emplace_back(installation);
}
void start_package_mod_installation(const std::filesystem::path &path, recomp::mods::ZipModFileHandle &file_handle, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, ModInstaller::Result &result) {
std::error_code ec;
char filename[1024];
std::filesystem::path mods_directory = recomp::mods::get_mods_directory();
mz_zip_archive *zip_archive = file_handle.archive.get();
mz_uint num_files = mz_zip_reader_get_num_files(file_handle.archive.get());
std::list<std::filesystem::path> dynamic_lib_files;
std::list<ModInstaller::Installation>::iterator first_nrm_iterator = result.pending_installations.end();
bool found_mod = false;
for (mz_uint i = 0; i < num_files; i++) {
mz_uint filename_length = mz_zip_reader_get_filename(zip_archive, i, filename, sizeof(filename));
if (filename_length == 0) {
continue;
}
std::filesystem::path target_path = mods_directory / std::u8string_view((const char8_t *)(filename));
if ((target_path.extension() == ".rtz") || (target_path.extension() == ".nrm")) {
found_mod = true;
ModInstaller::Installation installation;
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
std::ofstream output_stream(target_write_path, std::ios::binary);
if (!output_stream.is_open()) {
result.error_messages.emplace_back("Unable to write to mod directory.");
continue;
}
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
output_stream.close();
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
output_stream.close();
if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
// Try to load the extracted file as a mod file handle.
recomp::mods::ModOpenError open_error;
std::unique_ptr<recomp::mods::ZipModFileHandle> extracted_file_handle = std::make_unique<recomp::mods::ZipModFileHandle>(target_write_path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) {
result.error_messages.emplace_back("Invalid mod (" + target_path.filename().string() + ") in " + path.filename().string() + ".");
extracted_file_handle.reset();
std::filesystem::remove(target_write_path, ec);
continue;
}
// Check for the existence of the manifest file.
bool exists = false;
std::vector<char> manifest_bytes = extracted_file_handle->read_file(ManifestFilename, exists);
if (exists) {
// Parse the manifest file to check for its validity.
std::string error;
recomp::mods::ModManifest manifest;
open_error = parse_manifest(manifest, manifest_bytes, error);
exists = (open_error == recomp::mods::ModOpenError::Good);
if (exists) {
installation.mod_id = manifest.mod_id;
installation.display_name = manifest.display_name;
installation.mod_version = manifest.version;
installation.mod_file = target_path;
}
}
else if (target_path.extension() == ".rtz") {
// When it's an rtz file, check if the texture database file exists.
exists = mz_zip_reader_locate_file(extracted_file_handle->archive.get(), TextureDatabaseFilename, nullptr, 0) >= 0;
if (exists) {
installation.mod_id = std::string((const char *)(target_path.stem().u8string().c_str()));
installation.display_name = installation.mod_id;
installation.mod_version = recomp::Version();
installation.mod_file = target_path;
}
}
if (!exists) {
result.error_messages.emplace_back("Invalid mod (" + target_path.filename().string() + ") in " + path.filename().string() + ".");
extracted_file_handle.reset();
std::filesystem::remove(target_write_path, ec);
continue;
}
if (std::filesystem::exists(installation.mod_file, ec)) {
installation.needs_overwrite_confirmation = true;
}
if (!installation.needs_overwrite_confirmation) {
// This check isn't really needed as additional_files will be empty at this point,
// but it's good to have in case this logic ever changes.
for (const std::filesystem::path &path : installation.additional_files) {
if (std::filesystem::exists(path, ec)) {
installation.needs_overwrite_confirmation = true;
break;
}
}
}
result.pending_installations.emplace_back(installation);
// Store the first nrm found for any dynamic libraries that might be found.
if ((first_nrm_iterator == result.pending_installations.end()) && (target_path.extension() == ".nrm")) {
first_nrm_iterator = std::prev(result.pending_installations.end());
}
}
#if defined(_WIN32)
else if (target_path.extension() == ".dll") {
#elif defined(__linux__)
else if (target_path.extension() == ".so") {
#elif defined(__APPLE__)
else if (target_path.extension() == ".dylib") {
#else
static_assert(false, "Unimplemented for this platform."); {
#endif
std::filesystem::path target_write_path = target_path.u8string() + NewExtension;
std::ofstream output_stream(target_write_path, std::ios::binary);
if (!output_stream.is_open()) {
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
if (!mz_zip_reader_extract_to_callback(zip_archive, i, &zip_write_func, &output_stream, 0)) {
output_stream.close();
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
output_stream.close();
if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back("Failed to install " + path.filename().string() + " to mod directory.");
continue;
}
dynamic_lib_files.emplace_back(target_path);
}
}
if (!dynamic_lib_files.empty()) {
if (first_nrm_iterator != result.pending_installations.end()) {
// Associate all these files to the first mod that is found.
for (const std::filesystem::path &path : dynamic_lib_files) {
first_nrm_iterator->additional_files.emplace_back(path);
// Run verification against for overwrite confirmations.
if (std::filesystem::exists(path, ec)) {
first_nrm_iterator->needs_overwrite_confirmation = true;
}
}
}
else {
// These library files were not required by any mod, just delete them.
for (const std::filesystem::path &path : dynamic_lib_files) {
std::filesystem::path new_path(path.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
}
}
}
if (!found_mod) {
result.error_messages.emplace_back("No mods found in " + path.filename().string() + ".");
}
}
void remove_and_rename(std::vector<std::string>& error_messages, const std::filesystem::path& path) {
std::error_code ec;
std::filesystem::path old_path(path.u8string() + OldExtension);
std::filesystem::path new_path(path.u8string() + NewExtension);
// Rename the current path to a temporary old path, but only if the current path already exists.
if (std::filesystem::exists(path, ec)) {
std::filesystem::remove(old_path, ec);
std::filesystem::rename(path, old_path, ec);
if (ec) {
// If it fails, remove the new path.
std::filesystem::remove(new_path, ec);
error_messages.emplace_back("Unable to rename " + path.filename().string() + ".");
return;
}
}
// Rename the new path to the current path.
std::filesystem::rename(new_path, path, ec);
if (ec) {
// If it fails, remove the new path and also restore the temporary old path to the current path.
std::filesystem::remove(new_path, ec);
std::filesystem::rename(old_path, path, ec);
error_messages.emplace_back("Unable to rename " + path.filename().string() + ".");
return;
}
// If nothing failed, just remove the temporary old path.
std::filesystem::remove(old_path, ec);
};
void ModInstaller::start_mod_installation(const std::list<std::filesystem::path> &file_paths, std::function<void(std::filesystem::path, size_t, size_t)> progress_callback, Result &result) {
result = Result();
for (const std::filesystem::path &path : file_paths) {
recomp::mods::ModOpenError open_error;
recomp::mods::ZipModFileHandle file_handle(path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) {
result.error_messages.emplace_back(path.filename().string() + " is not a valid zip or mod.");
continue;
}
// First we verify if the container itself isn't a mod already.
// TODO hook into the runtime's container registration to check the extension instead of using hardcoded values.
if ((path.extension() == ".rtz") || (path.extension() == ".nrm")) {
start_single_mod_installation(path, file_handle, progress_callback, result);
}
else {
// Scan the container for compatible mods instead. This is the case for packages made by users or how they're tipically uploaded to Thunderstore.
start_package_mod_installation(path, file_handle, progress_callback, result);
}
}
}
void ModInstaller::cancel_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
error_messages.clear();
std::error_code ec;
// Delete all the files that were extracted for all mods.
for (const Installation &installation : result.pending_installations) {
std::filesystem::path new_path(installation.mod_file.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
for (const std::filesystem::path &path : installation.additional_files) {
std::filesystem::path new_path(path.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
}
}
}
void ModInstaller::finish_mod_installation(const Result &result, std::vector<std::string>& error_messages) {
error_messages.clear();
std::error_code ec;
for (const Installation &installation : result.pending_installations) {
// Overwrite the mod files.
remove_and_rename(error_messages, installation.mod_file);
for (const std::filesystem::path &path : installation.additional_files) {
remove_and_rename(error_messages, path);
}
}
}
};