[Patcher] Plugin Loader

Added a plugin loader which can be enabled with allow_plugins in the config.
This commit is contained in:
Adrian 2023-07-16 18:25:50 +01:00 committed by Radosław Gliński
parent 6e86eacf5a
commit 91f976e524
8 changed files with 355 additions and 11 deletions

View File

@ -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",

View File

@ -14,6 +14,7 @@
#include <charconv>
#include <cstddef>
#include <cstring>
#include <regex>
#include <string>
#include "third_party/fmt/include/fmt/format.h"
@ -141,6 +142,20 @@ inline bool hex_string_to_array(std::vector<uint8_t>& 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);
}

View File

@ -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.

View File

@ -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<xe::kernel::KernelState>(this);
plugin_loader_ = std::make_unique<xe::patcher::PluginLoader>(
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;
}

View File

@ -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<cpu::ExportResolver> export_resolver_;
std::unique_ptr<vfs::VirtualFileSystem> file_system_;
std::unique_ptr<patcher::Patcher> patcher_;
std::unique_ptr<patcher::PluginLoader> plugin_loader_;
std::unique_ptr<kernel::KernelState> kernel_state_;
@ -272,6 +276,7 @@ class Emulator {
size_t game_config_load_callback_loop_next_index_ = SIZE_MAX;
kernel::object_ref<kernel::XThread> main_thread_;
kernel::object_ref<kernel::XHostThread> plugin_loader_thread_;
std::optional<uint32_t> title_id_; // Currently running title ID
bool paused_;

View File

@ -117,7 +117,7 @@ bool XThread::IsInThread(XThread* other) {
XThread* XThread::GetCurrentThread() {
XThread* thread = reinterpret_cast<XThread*>(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;
}

View File

@ -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 <string>
#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<xe::filesystem::FileInfo> 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<cpptoml::table> 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<std::string>("title_name");
const std::string patch_title_id =
*plugins_config->get_as<std::string>("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<std::string>("name");
const std::string file = *plugin->get_as<std::string>("file");
const std::string desc = *plugin->get_as<std::string>("desc");
const bool is_enabled = *plugin->get_as<bool>("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<uint64_t> PluginLoader::GetHashes(
const std::shared_ptr<cpptoml::base> toml_entry) {
std::vector<uint64_t> 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<std::string>()->get();
hashes.push_back(strtoull(hash_entry.c_str(), NULL, 16));
}
}
if (toml_entry->is_value()) {
const std::string hash = toml_entry->as<std::string>()->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<PluginInfoEntry> 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<kernel::XHostThread> plugin_thread =
kernel::object_ref<kernel::XHostThread>(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<xe::kernel::X_LDR_DATA_TABLE_ENTRY*>(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<xe::vfs::HostPathDevice>(
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

View File

@ -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<uint64_t> 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<uint64_t> GetHashes(
const std::shared_ptr<cpptoml::base> toml_entry);
kernel::KernelState* kernel_state_;
std::filesystem::path plugins_root_;
std::vector<PluginInfoEntry> plugin_configs_;
bool is_any_plugin_loaded_;
};
} // namespace patcher
} // namespace xe
#endif