From 91f976e524758ab8b43e4e4039f5c4057a1a16a2 Mon Sep 17 00:00:00 2001 From: Adrian <78108584+AdrianCassar@users.noreply.github.com> Date: Sun, 16 Jul 2023 18:25:50 +0100 Subject: [PATCH] [Patcher] Plugin Loader Added a plugin loader which can be enabled with allow_plugins in the config. --- src/xenia/app/emulator_window.cc | 18 ++- src/xenia/base/string_util.h | 15 ++ src/xenia/cpu/xex_module.cc | 7 + src/xenia/emulator.cc | 27 +++- src/xenia/emulator.h | 5 + src/xenia/kernel/xthread.cc | 2 +- src/xenia/patcher/plugin_loader.cc | 235 +++++++++++++++++++++++++++++ src/xenia/patcher/plugin_loader.h | 57 +++++++ 8 files changed, 355 insertions(+), 11 deletions(-) create mode 100644 src/xenia/patcher/plugin_loader.cc create mode 100644 src/xenia/patcher/plugin_loader.h diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 0cadca940..1ee263176 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -1100,6 +1100,12 @@ void EmulatorWindow::UpdateTitle() { if (patcher && patcher->IsAnyPatchApplied()) { sb.Append(u8" [Patches Applied]"); } + + patcher::PluginLoader* pluginloader = emulator()->plugin_loader(); + if (pluginloader && pluginloader->IsAnyPluginLoaded()) { + sb.Append(u8" [Plugins Loaded]"); + } + window_->SetTitle(sb.to_string_view()); } @@ -1429,10 +1435,6 @@ bool EmulatorWindow::IsUseNexusForGameBarEnabled() { #endif } -std::string EmulatorWindow::BoolToString(bool value) { - return std::string(value ? "true" : "false"); -} - void EmulatorWindow::DisplayHotKeysConfig() { std::string msg = ""; std::string msg_passthru = ""; @@ -1472,14 +1474,16 @@ void EmulatorWindow::DisplayHotKeysConfig() { msg.insert(0, msg_passthru); msg += "\n"; - msg += "Readback Resolve: " + BoolToString(cvars::d3d12_readback_resolve); + msg += "Readback Resolve: " + + xe::string_util::BoolToString(cvars::d3d12_readback_resolve); msg += "\n"; msg += "Clear Memory Page State: " + - BoolToString(cvars::d3d12_clear_memory_page_state); + xe::string_util::BoolToString(cvars::d3d12_clear_memory_page_state); msg += "\n"; - msg += "Controller Hotkeys: " + BoolToString(cvars::controller_hotkeys); + msg += "Controller Hotkeys: " + + xe::string_util::BoolToString(cvars::controller_hotkeys); imgui_drawer_.get()->ClearDialogs(); xe::ui::ImGuiDialog::ShowMessageBox(imgui_drawer_.get(), "Controller Hotkeys", diff --git a/src/xenia/base/string_util.h b/src/xenia/base/string_util.h index 1c0d8c522..2bd14faa0 100644 --- a/src/xenia/base/string_util.h +++ b/src/xenia/base/string_util.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include "third_party/fmt/include/fmt/format.h" @@ -141,6 +142,20 @@ inline bool hex_string_to_array(std::vector& output_array, return true; } +inline std::string BoolToString(bool value) { return value ? "true" : "false"; } + +inline std::string ltrim(const std::string& value) { + return std::regex_replace(value, std::regex("^\\s+"), std::string("")); +} + +inline std::string rtrim(const std::string& value) { + return std::regex_replace(value, std::regex("\\s+$"), std::string("")); +} + +inline std::string trim(const std::string& value) { + return ltrim(rtrim(value)); +} + inline std::string to_hex_string(uint32_t value) { return fmt::format("{:08X}", value); } diff --git a/src/xenia/cpu/xex_module.cc b/src/xenia/cpu/xex_module.cc index de9ddf26d..ad3b6ea41 100644 --- a/src/xenia/cpu/xex_module.cc +++ b/src/xenia/cpu/xex_module.cc @@ -48,6 +48,8 @@ DEFINE_bool( "finding/stress testing with the JIT", "CPU"); +DECLARE_bool(allow_plugins); + static const uint8_t xe_xex2_retail_key[16] = { 0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3, 0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91}; @@ -1073,6 +1075,11 @@ bool XexModule::LoadContinue() { } } + // Disable write protection if plugins are enabled + if (cvars::allow_plugins && !cvars::writable_code_segments) { + OVERRIDE_bool(writable_code_segments, true); + } + // Setup memory protection. for (uint32_t i = 0, page = 0; i < sec_header->page_descriptor_count; i++) { // Byteswap the bitfield manually. diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index 5308eaad1..f929d8d4a 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -31,6 +31,7 @@ #include "xenia/cpu/backend/null_backend.h" #include "xenia/cpu/cpu_flags.h" #include "xenia/cpu/thread_state.h" +#include "xenia/gpu/command_processor.h" #include "xenia/gpu/graphics_system.h" #include "xenia/hid/input_driver.h" #include "xenia/hid/input_system.h" @@ -56,23 +57,27 @@ #include "xenia/cpu/backend/x64/x64_backend.h" #endif // XE_ARCH -DECLARE_int32(user_language); - DEFINE_double(time_scalar, 1.0, "Scalar used to speed or slow time (1x, 2x, 1/2x, etc).", "General"); + DEFINE_string( launch_module, "", "Executable to launch from the .iso or the package instead of default.xex " "or the module specified by the game. Leave blank to launch the default " "module.", "General"); + DEFINE_bool(allow_game_relative_writes, false, "Not useful to non-developers. Allows code to write to paths " "relative to game://. Used for " "generating test data to compare with original hardware. ", "General"); +DECLARE_int32(user_language); + +DECLARE_bool(allow_plugins); + namespace xe { using namespace xe::literals; @@ -261,6 +266,9 @@ X_STATUS Emulator::Setup( // Shared kernel state. kernel_state_ = std::make_unique(this); + plugin_loader_ = std::make_unique( + kernel_state_.get(), storage_root() / "plugins"); + // Setup the core components. result = graphics_system_->Setup( processor_.get(), kernel_state_.get(), @@ -388,9 +396,12 @@ X_STATUS Emulator::MountPath(const std::filesystem::path& path, file_system_->UnregisterSymbolicLink("d:"); file_system_->UnregisterSymbolicLink("game:"); + file_system_->UnregisterSymbolicLink("plugins:"); + // Create symlinks to the device. file_system_->RegisterSymbolicLink("game:", mount_path); file_system_->RegisterSymbolicLink("d:", mount_path); + return X_STATUS_SUCCESS; } @@ -932,7 +943,7 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, title_version_ = format_version(title_version); } } - + // Try and load the resource database (xex only). if (module->title_id()) { auto title_id = fmt::format("{:08X}", module->title_id()); @@ -993,6 +1004,16 @@ X_STATUS Emulator::CompleteLaunch(const std::filesystem::path& path, main_thread_ = main_thread; on_launch(title_id_.value(), title_name_); + // Plugins must be loaded after calling LaunchModule() and + // FinishLoadingUserModule() which will apply TUs and patching to the main + // xex. + if (cvars::allow_plugins) { + if (plugin_loader_->IsAnyPluginForTitleAvailable(title_id_.value(), + module->hash().value())) { + plugin_loader_->LoadTitlePlugins(title_id_.value()); + } + } + return X_STATUS_SUCCESS; } diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index 64a7f09f1..0e0893fb8 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -22,6 +22,7 @@ #include "xenia/kernel/kernel_state.h" #include "xenia/memory.h" #include "xenia/patcher/patcher.h" +#include "xenia/patcher/plugin_loader.h" #include "xenia/vfs/device.h" #include "xenia/vfs/virtual_file_system.h" #include "xenia/xbox.h" @@ -158,6 +159,8 @@ class Emulator { patcher::Patcher* patcher() const { return patcher_.get(); } + patcher::PluginLoader* plugin_loader() const { return plugin_loader_.get(); } + // Initializes the emulator and configures all components. // The given window is used for display and the provided functions are used // to create subsystems as required. @@ -259,6 +262,7 @@ class Emulator { std::unique_ptr export_resolver_; std::unique_ptr file_system_; std::unique_ptr patcher_; + std::unique_ptr plugin_loader_; std::unique_ptr kernel_state_; @@ -272,6 +276,7 @@ class Emulator { size_t game_config_load_callback_loop_next_index_ = SIZE_MAX; kernel::object_ref main_thread_; + kernel::object_ref plugin_loader_thread_; std::optional title_id_; // Currently running title ID bool paused_; diff --git a/src/xenia/kernel/xthread.cc b/src/xenia/kernel/xthread.cc index f49645710..0afbaa8ba 100644 --- a/src/xenia/kernel/xthread.cc +++ b/src/xenia/kernel/xthread.cc @@ -117,7 +117,7 @@ bool XThread::IsInThread(XThread* other) { XThread* XThread::GetCurrentThread() { XThread* thread = reinterpret_cast(current_xthread_tls_); if (!thread) { - assert_always("Attempting to use kernel stuff from a non-kernel thread"); + assert_always("Attempting to use guest stuff from a non-guest thread."); } return thread; } diff --git a/src/xenia/patcher/plugin_loader.cc b/src/xenia/patcher/plugin_loader.cc new file mode 100644 index 000000000..35c0c3e59 --- /dev/null +++ b/src/xenia/patcher/plugin_loader.cc @@ -0,0 +1,235 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2023 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ +#include + +#include "xenia/base/logging.h" +#include "xenia/config.h" +#include "xenia/kernel/user_module.h" +#include "xenia/kernel/xthread.h" +#include "xenia/patcher/plugin_loader.h" +#include "xenia/vfs/devices/host_path_device.h" + +DEFINE_bool( + allow_plugins, false, + "Allows loading of plugins/trainers from plugins\\title_id\\plugin.xex." + "Plugin are homebrew xex modules which can be used for making mods " + "This feature is experimental.", + "General"); + +namespace xe { +namespace patcher { + +PluginLoader::PluginLoader(kernel::KernelState* kernel_state, + const std::filesystem::path plugins_root) { + kernel_state_ = kernel_state; + plugins_root_ = plugins_root; + + if (!cvars::allow_plugins) { + return; + } + + plugin_configs_.clear(); + LoadConfigs(); +} + +void PluginLoader::LoadConfigs() { + // Iterate through directory and check if .toml file exists + std::vector dir_files = + xe::filesystem::ListDirectories(plugins_root_); + + dir_files = + xe::filesystem::FilterByName(dir_files, std::regex("[A-Fa-f0-9]{8}")); + + for (const auto& entry : dir_files) { + const uint32_t title_id = + std::stoi(entry.name.filename().string(), nullptr, 16); + + LoadTitleConfig(title_id); + } + + XELOGI("Plugins: Loaded plugins for {} titles", dir_files.size()); +} + +void PluginLoader::LoadTitleConfig(const uint32_t title_id) { + const std::filesystem::path title_plugins_config = + plugins_root_ / fmt::format("{:08X}\\plugins.toml", title_id); + + if (!std::filesystem::exists(title_plugins_config)) { + return; + } + + std::shared_ptr plugins_config; + + try { + plugins_config = ParseFile(title_plugins_config); + } catch (...) { + XELOGE("Plugins: Cannot load plugin file {}", + path_to_utf8(title_plugins_config)); + return; + } + + const std::string title_name = + *plugins_config->get_as("title_name"); + + const std::string patch_title_id = + *plugins_config->get_as("title_id"); + + const auto plugin_tabels = plugins_config->get_table_array("plugin"); + + if (!plugin_tabels) { + XELOGE("Plugins: Cannot find [[plugin]] table in {}", + path_to_utf8(title_plugins_config)); + return; + } + + for (auto& plugin : *plugin_tabels) { + PluginInfoEntry entry; + + const std::string name = *plugin->get_as("name"); + const std::string file = *plugin->get_as("file"); + const std::string desc = *plugin->get_as("desc"); + const bool is_enabled = *plugin->get_as("is_enabled"); + + if (!plugin->contains("hash")) { + XELOGE("Hash error! skipping plugin {} in: {}", name, + path_to_utf8(title_plugins_config)); + continue; + } + + entry.hashes = GetHashes(plugin->get("hash")); + entry.name = name; + entry.file = xe::string_util::trim(file); + entry.desc = desc; + entry.title_id = title_id; + entry.is_enabled = is_enabled; + plugin_configs_.push_back(entry); + } +} + +std::vector PluginLoader::GetHashes( + const std::shared_ptr toml_entry) { + std::vector hashes; + + if (!toml_entry) { + return hashes; + } + + if (toml_entry->is_array()) { + const auto arr = toml_entry->as_array(); + + for (cpptoml::array::const_iterator itr = arr->begin(); itr != arr->end(); + itr++) { + const std::string hash_entry = itr->get()->as()->get(); + hashes.push_back(strtoull(hash_entry.c_str(), NULL, 16)); + } + } + + if (toml_entry->is_value()) { + const std::string hash = toml_entry->as()->get(); + hashes.push_back(strtoull(hash.c_str(), NULL, 16)); + } + return hashes; +} + +bool PluginLoader::IsAnyPluginForTitleAvailable( + const uint32_t title_id, const uint64_t module_hash) const { + const auto result = + std::find_if(plugin_configs_.cbegin(), plugin_configs_.cend(), + [title_id, module_hash](const PluginInfoEntry& entry) { + const auto hash_exists = + std::find(entry.hashes.cbegin(), entry.hashes.cend(), + module_hash) != entry.hashes.cend(); + + return entry.title_id == title_id && hash_exists; + }); + + return result != plugin_configs_.cend(); +} + +void PluginLoader::LoadTitlePlugins(const uint32_t title_id) { + std::vector title_plugins; + + std::copy_if(plugin_configs_.cbegin(), plugin_configs_.cend(), + std::back_inserter(title_plugins), + [title_id](const PluginInfoEntry& entry) { + return entry.title_id == title_id; + }); + + if (title_plugins.empty()) { + return; + } + + CreatePluginDevice(title_id); + + for (const auto& entry : title_plugins) { + kernel::object_ref plugin_thread = + kernel::object_ref(new xe::kernel::XHostThread( + kernel_state_, 128 * 1024, 0, [this, entry]() { + LoadTitlePlugin(entry); + return 0; + })); + + plugin_thread->set_name( + fmt::format("Plugin Loader: {} Thread", entry.name)); + plugin_thread->Create(); + + xe::threading::Wait(plugin_thread->thread(), false); + + is_any_plugin_loaded_ = true; + } +} + +void PluginLoader::LoadTitlePlugin(const PluginInfoEntry& entry) { + auto user_module = + kernel_state_->LoadUserModule(fmt::format("plugins:\\{}", entry.file)); + + if (!user_module) { + return; + } + + kernel_state_->FinishLoadingUserModule(user_module); + const uint32_t hmodule = user_module->hmodule_ptr(); + + if (hmodule == 0) { + return; + } + + kernel_state_->memory() + ->TranslateVirtual(hmodule) + ->load_count++; + + XELOGI("Plugin Loader: {} Plugin successfully loaded!", entry.name); +} + +void PluginLoader::CreatePluginDevice(const uint32_t title_id) { + const std::string mount_plugins = "\\Device\\Plugins"; + + const std::filesystem::path plugins_host_path = + plugins_root_ / fmt::format("{:08X}", title_id); + + kernel_state_->file_system()->RegisterSymbolicLink("plugins:", mount_plugins); + auto device_plugins = std::make_unique( + mount_plugins, plugins_host_path, false); + + if (!device_plugins->Initialize()) { + xe::FatalError("Unable to mount {}; file not found or corrupt."); + return; + } + + if (!kernel_state_->file_system()->RegisterDevice( + std::move(device_plugins))) { + xe::FatalError("Unable to register {}."); + return; + } + + return; +} + +} // namespace patcher +} // namespace xe \ No newline at end of file diff --git a/src/xenia/patcher/plugin_loader.h b/src/xenia/patcher/plugin_loader.h new file mode 100644 index 000000000..2ca1b150c --- /dev/null +++ b/src/xenia/patcher/plugin_loader.h @@ -0,0 +1,57 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2023 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#ifndef XENIA_PLUGIN_LOADER_H_ +#define XENIA_PLUGIN_LOADER_H_ + +#include "xenia/kernel/kernel_state.h" +#include "xenia/memory.h" + +namespace xe { +namespace patcher { + +struct PluginInfoEntry { + std::string name; + std::string file; + std::vector hashes; + uint32_t title_id; + std::string desc; + bool is_enabled; +}; + +class PluginLoader { + public: + PluginLoader(kernel::KernelState* kernel_state, + const std::filesystem::path plugins_root); + + void LoadTitlePlugins(const uint32_t title_id); + bool IsAnyPluginForTitleAvailable(const uint32_t title_id, + const uint64_t module_hash) const; + bool IsAnyPluginLoaded() { return is_any_plugin_loaded_; } + + private: + void LoadConfigs(); + void LoadTitleConfig(const uint32_t title_id); + void CreatePluginDevice(const uint32_t title_id); + void LoadTitlePlugin(const PluginInfoEntry& entry); + + std::vector GetHashes( + const std::shared_ptr toml_entry); + + kernel::KernelState* kernel_state_; + std::filesystem::path plugins_root_; + + std::vector plugin_configs_; + + bool is_any_plugin_loaded_; +}; + +} // namespace patcher +} // namespace xe +#endif