DnD prototype.

This commit is contained in:
Dario 2025-03-30 18:07:22 -03:00
parent cdde3d47bb
commit efb3fe4fbc
6 changed files with 354 additions and 0 deletions

View File

@ -175,6 +175,7 @@ set (SOURCES
${CMAKE_SOURCE_DIR}/src/ui/ui_rml_hacks.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_elements.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_details_panel.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_installer.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_mod_menu.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_api.cpp
${CMAKE_SOURCE_DIR}/src/ui/ui_api_events.cpp

View File

@ -114,6 +114,8 @@ namespace recompui {
void queue_image_from_bytes_rgba32(const std::string &src, const std::vector<char> &bytes, uint32_t width, uint32_t height);
void queue_image_from_bytes_file(const std::string &src, const std::vector<char> &bytes);
void release_image(const std::string &src);
void drop_files(const std::list<std::filesystem::path> &file_list);
}
#endif

View File

@ -42,6 +42,10 @@ static struct {
bool rumble_active;
} InputState;
static struct {
std::list<std::filesystem::path> files_dropped;
} DropState;
std::atomic<recomp::InputDevice> scanning_device = recomp::InputDevice::COUNT;
std::atomic<recomp::InputField> scanned_input;
@ -276,6 +280,18 @@ bool sdl_event_filter(void* userdata, SDL_Event* event) {
InputState.pending_mouse_delta[0] += motion_event->xrel;
InputState.pending_mouse_delta[1] += motion_event->yrel;
}
queue_if_enabled(event);
break;
case SDL_EventType::SDL_DROPBEGIN:
DropState.files_dropped.clear();
break;
case SDL_EventType::SDL_DROPFILE:
DropState.files_dropped.emplace_back(std::filesystem::path(std::u8string_view((const char8_t *)(event->drop.file))));
SDL_free(event->drop.file);
break;
case SDL_EventType::SDL_DROPCOMPLETE:
recompui::drop_files(DropState.files_dropped);
break;
default:
queue_if_enabled(event);
break;

299
src/ui/ui_mod_installer.cpp Normal file
View File

@ -0,0 +1,299 @@
#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_files.emplace_back(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();
installation.mod_files.emplace_back(target_path);
}
}
std::error_code ec;
if (exists) {
std::filesystem::copy(file_path, target_path, ec);
if (ec) {
result.error_messages.emplace_back(std::format("Unable to copy to {}.", target_write_path.string()));
return;
}
}
else {
result.error_messages.emplace_back(std::format("Unable to install {} as it does not seem to be a mod.", file_path.string()));
std::filesystem::remove(target_write_path, ec);
return;
}
for (const std::filesystem::path &path : installation.mod_files) {
if (std::filesystem::exists(path, ec)) {
installation.needs_overwrite_confirmation = true;
break;
}
}
result.pending_installations.emplace_back(installation);
}
void start_package_mod_installation(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();
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")) {
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(std::format("Unable to open {} for writing.", target_write_path.string()));
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(std::format("Unable to extract to {}.", target_write_path.string()));
continue;
}
output_stream.close();
if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back(std::format("Unable to write to {}.", target_write_path.string()));
continue;
}
// Try to load the extracted file as a mod file handle.
recomp::mods::ModOpenError open_error;
recomp::mods::ZipModFileHandle extracted_file_handle(target_write_path, open_error);
if (open_error != recomp::mods::ModOpenError::Good) {
result.error_messages.emplace_back(std::format("Unable to open {}.", target_write_path.string()));
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_files.emplace_back(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_files.emplace_back(target_path);
}
}
if (!exists) {
result.error_messages.emplace_back(std::format("Unable to install {} as it does not seem to be a mod.", target_path.filename().string()));
std::filesystem::remove(target_write_path, ec);
continue;
}
for (const std::filesystem::path &path : installation.mod_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(std::format("Unable to open {} for writing.", target_write_path.string()));
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(std::format("Unable to extract to {}.", target_write_path.string()));
continue;
}
output_stream.close();
if (output_stream.bad()) {
std::filesystem::remove(target_write_path, ec);
result.error_messages.emplace_back(std::format("Unable to write to {}.", target_write_path.string()));
continue;
}
dynamic_lib_files.emplace_back(target_write_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->mod_files.emplace_back(path);
// Run verification against for overwrite confirmations.
for (const std::filesystem::path &path : first_nrm_iterator->mod_files) {
if (std::filesystem::exists(path, ec)) {
first_nrm_iterator->needs_overwrite_confirmation = true;
break;
}
}
}
}
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);
}
}
}
}
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 = { std::format("File %s is not a valid container.", path.string()) };
continue;
}
// First we verify if the container itself isn't a mod already.
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(file_handle, progress_callback, result);
}
}
}
void ModInstaller::finish_mod_installation(const std::unordered_set<std::string> &confirmed_overwrites, Result &result) {
result.error_messages.clear();
std::error_code ec;
for (const Installation &installation : result.pending_installations) {
if (installation.needs_overwrite_confirmation && !confirmed_overwrites.contains(installation.mod_id)) {
// If the user hasn't confirmed this overwrite, simply delete all the files that were extracted for this mod.
for (const std::filesystem::path &path : installation.mod_files) {
std::filesystem::path new_path(path.u8string() + NewExtension);
std::filesystem::remove(new_path, ec);
}
continue;
}
for (const std::filesystem::path &path : installation.mod_files) {
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);
result.error_messages.emplace_back(std::format("Unable to rename {}.", path.string()));
continue;
}
}
// 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);
result.error_messages.emplace_back(std::format("Unable to rename {}.", path.string()));
continue;
}
// If nothing failed, just remove the temporary old path.
std::filesystem::remove(old_path, ec);
}
}
}
};

28
src/ui/ui_mod_installer.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef RECOMPUI_MOD_INSTALLER_H
#define RECOMPUI_MOD_INSTALLER_H
#include <librecomp/game.hpp>
#include <unordered_set>
namespace recompui {
struct ModInstaller {
struct Installation {
std::string mod_id;
std::string display_name;
recomp::Version mod_version;
std::list<std::filesystem::path> mod_files;
bool needs_overwrite_confirmation = false;
};
struct Result {
std::list<std::string> error_messages;
std::list<Installation> pending_installations;
};
static void 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);
static void finish_mod_installation(const std::unordered_set<std::string> &confirmed_overwrites, Result &result);
};
};
#endif

View File

@ -23,6 +23,7 @@
#include "ui_rml_hacks.hpp"
#include "ui_elements.h"
#include "ui_mod_menu.h"
#include "ui_mod_installer.h"
#include "ui_renderer.h"
bool can_focus(Rml::Element* element) {
@ -833,3 +834,10 @@ void recompui::queue_image_from_bytes_rgba32(const std::string &src, const std::
void recompui::release_image(const std::string &src) {
Rml::ReleaseTexture(src);
}
void recompui::drop_files(const std::list<std::filesystem::path> &file_list) {
// TODO: Needs a progress callback and a prompt for every mod that needs to be confirmed to be overwritten.
ModInstaller::Result result;
ModInstaller::start_mod_installation(file_list, nullptr, result);
ModInstaller::finish_mod_installation({}, result);
}