diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 2ab77fd5e..b046d892b 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include "third_party/imgui/imgui.h" #include "third_party/stb/stb_image_write.h" @@ -24,7 +25,6 @@ #include "xenia/app/config_dialog_qt.h" #include "xenia/app/context_menu_widget_qt.h" #include "xenia/app/game_list_dialog_qt.h" -#include "xenia/app/launch_data_restart_dialog_qt.h" #include "xenia/app/notification_widget_qt.h" #include "xenia/app/postprocessing_dialog_qt.h" #include "xenia/app/profile_dialog_qt.h" @@ -395,11 +395,22 @@ void EmulatorWindow::OnEmulatorInitialized() { auto* qt_window = dynamic_cast(window_.get()); if (qt_window) { emulator_->set_on_launch_data_restart([this, qt_window]() { - // Show dialog in UI thread + // Show notification in UI thread window_->app_context().CallInUIThread([this, qt_window]() { - auto* dialog = new app::LaunchDataRestartDialogQt( - qt_window->qwindow(), window_.get(), emulator_->kernel_state()); - dialog->show(); + auto* notification = new NotificationWidgetQt( + qt_window->qwindow(), "Title Restart Required", + "Title is restarting with new launch data.\n" + "Game will be loaded automatically.", + 5000); // 5 second duration + notification->Show(); + + // Schedule terminate and exit after notification duration + QTimer::singleShot(5000, [this]() { + if (emulator_->kernel_state()) { + emulator_->kernel_state()->TerminateTitle(); + } + std::quick_exit(0); + }); }); }); } @@ -2123,9 +2134,46 @@ void EmulatorWindow::LaunchTitleInNewProcess( // Get the path to the current executable std::filesystem::path executable_path = xe::filesystem::GetExecutablePath(); - // Verify the file exists (unless launching for launch_data) - if (!for_launch_data && !std::filesystem::exists(path_to_file)) { - XELOGE("Cannot launch title - file not found: {}", path_to_file.string()); + // Handle launch_data.bin if present + std::filesystem::path actual_path = path_to_file; + std::string launch_module_arg; + + if (for_launch_data) { + // Read launch_data.bin to get the host_path and launch_path + FILE* file = xe::filesystem::OpenFile( + kernel::xam::kXamModuleLoaderDataFileName, "rb"); + if (!file) { + XELOGE("launch_data.bin not found"); + return; + } + + // Read host_path (the disc/package path) + uint16_t host_path_length = 0; + fread(&host_path_length, sizeof(host_path_length), 1, file); + std::string host_path; + host_path.resize(host_path_length); + fread(host_path.data(), host_path_length, 1, file); + + // Read launch_path (the XEX to launch) + uint16_t launch_path_length = 0; + fread(&launch_path_length, sizeof(launch_path_length), 1, file); + std::string launch_path; + launch_path.resize(launch_path_length); + fread(launch_path.data(), launch_path_length, 1, file); + + fclose(file); + + // Delete launch_data.bin so new process doesn't try to process it + std::filesystem::remove(kernel::xam::kXamModuleLoaderDataFileName); + + // Use the host_path as the target and launch_path as --launch_module + actual_path = host_path; + launch_module_arg = launch_path; + } + + // Verify the file exists + if (!actual_path.empty() && !std::filesystem::exists(actual_path)) { + XELOGE("Cannot launch title - file not found: {}", actual_path.string()); return; } @@ -2141,9 +2189,15 @@ void EmulatorWindow::LaunchTitleInNewProcess( cmd_line += u" --config=\"" + xe::to_utf16(cvars::config) + u"\""; } - // Add the target game file (unless launching for launch_data) - if (!for_launch_data) { - auto game_path_u16 = xe::path_to_utf16(path_to_file); + // Add --launch_module if specified + if (!launch_module_arg.empty()) { + cmd_line += + u" --launch_module=\"" + xe::to_utf16(launch_module_arg) + u"\""; + } + + // Add the target game file + if (!actual_path.empty()) { + auto game_path_u16 = xe::path_to_utf16(actual_path); cmd_line += u" \"" + game_path_u16 + u"\""; } @@ -2187,10 +2241,17 @@ void EmulatorWindow::LaunchTitleInNewProcess( argv.push_back(config_arg.c_str()); } - // Add the target game file (unless launching for launch_data) + // Add --launch_module if specified + std::string launch_module_arg_str; + if (!launch_module_arg.empty()) { + launch_module_arg_str = "--launch_module=" + launch_module_arg; + argv.push_back(launch_module_arg_str.c_str()); + } + + // Add the target game file std::string target_arg; - if (!for_launch_data) { - target_arg = path_to_file.string(); + if (!actual_path.empty()) { + target_arg = actual_path.string(); argv.push_back(target_arg.c_str()); } argv.push_back(nullptr); diff --git a/src/xenia/app/launch_data_restart_dialog_qt.cc b/src/xenia/app/launch_data_restart_dialog_qt.cc deleted file mode 100644 index ca25bf59f..000000000 --- a/src/xenia/app/launch_data_restart_dialog_qt.cc +++ /dev/null @@ -1,79 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Xenia Canary. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#include "xenia/app/launch_data_restart_dialog_qt.h" - -#include -#include -#include - -#include "xenia/kernel/kernel_state.h" -#include "xenia/ui/window.h" - -namespace xe { -namespace app { - -LaunchDataRestartDialogQt::LaunchDataRestartDialogQt( - QWidget* parent, ui::Window* display_window, - kernel::KernelState* kernel_state) - : QDialog(parent), - display_window_(display_window), - kernel_state_(kernel_state) { - SetupUI(); -} - -LaunchDataRestartDialogQt::~LaunchDataRestartDialogQt() { - // Cleanup handled by Qt -} - -void LaunchDataRestartDialogQt::SetupUI() { - setWindowTitle("Title Restart Required"); - setModal(true); - setAttribute(Qt::WA_DeleteOnClose); - setMinimumWidth(400); - - auto* main_layout = new QVBoxLayout(this); - - // Message text - auto* message_label = new QLabel( - "Title is restarting with new launch data.\n" - "Click OK to continue. Game will be loaded automatically.", - this); - message_label->setWordWrap(true); - main_layout->addWidget(message_label); - - main_layout->addSpacing(20); - - // OK button - auto* button_layout = new QHBoxLayout(); - button_layout->addStretch(); - - ok_button_ = new QPushButton("OK", this); - ok_button_->setMinimumWidth(120); - connect(ok_button_, &QPushButton::clicked, this, - &LaunchDataRestartDialogQt::OnOKClicked); - button_layout->addWidget(ok_button_); - - button_layout->addStretch(); - main_layout->addLayout(button_layout); -} - -void LaunchDataRestartDialogQt::OnOKClicked() { - // Terminate the title and quit after user clicks OK - if (kernel_state_) { - kernel_state_->TerminateTitle(); - } - accept(); - - // Use quick_exit to avoid cleanup issues - std::quick_exit(0); -} - -} // namespace app -} // namespace xe diff --git a/src/xenia/app/launch_data_restart_dialog_qt.h b/src/xenia/app/launch_data_restart_dialog_qt.h deleted file mode 100644 index 3a7e035fe..000000000 --- a/src/xenia/app/launch_data_restart_dialog_qt.h +++ /dev/null @@ -1,49 +0,0 @@ -/** - ****************************************************************************** - * Xenia : Xbox 360 Emulator Research Project * - ****************************************************************************** - * Copyright 2025 Xenia Canary. All rights reserved. * - * Released under the BSD license - see LICENSE in the root for more details. * - ****************************************************************************** - */ - -#ifndef XENIA_APP_LAUNCH_DATA_RESTART_DIALOG_QT_H_ -#define XENIA_APP_LAUNCH_DATA_RESTART_DIALOG_QT_H_ - -#include -#include - -namespace xe { -namespace ui { -class Window; -} // namespace ui - -namespace kernel { -class KernelState; -} // namespace kernel - -namespace app { - -class LaunchDataRestartDialogQt : public QDialog { - Q_OBJECT - - public: - LaunchDataRestartDialogQt(QWidget* parent, ui::Window* display_window, - kernel::KernelState* kernel_state); - ~LaunchDataRestartDialogQt() override; - - private slots: - void OnOKClicked(); - - private: - void SetupUI(); - - ui::Window* display_window_; - kernel::KernelState* kernel_state_; - QPushButton* ok_button_; -}; - -} // namespace app -} // namespace xe - -#endif // XENIA_APP_LAUNCH_DATA_RESTART_DIALOG_QT_H_ diff --git a/src/xenia/app/moc_launch_data_restart_dialog_qt.cc b/src/xenia/app/moc_launch_data_restart_dialog_qt.cc deleted file mode 100644 index 841074f04..000000000 --- a/src/xenia/app/moc_launch_data_restart_dialog_qt.cc +++ /dev/null @@ -1,107 +0,0 @@ -/**************************************************************************** -** Meta object code from reading C++ file 'launch_data_restart_dialog_qt.h' -** -** Created by: The Qt Meta Object Compiler version 69 (Qt 6.9.2) -** -** WARNING! All changes made in this file will be lost! -*****************************************************************************/ - -#include -#include "launch_data_restart_dialog_qt.h" - -#include - -#include - -#include -#if !defined(Q_MOC_OUTPUT_REVISION) -#error \ - "The header file 'launch_data_restart_dialog_qt.h' doesn't include ." -#elif Q_MOC_OUTPUT_REVISION != 69 -#error "This file was generated using the moc from 6.9.2. It" -#error "cannot be used with the include files from this version of Qt." -#error "(The moc has changed too much.)" -#endif - -#ifndef Q_CONSTINIT -#define Q_CONSTINIT -#endif - -QT_WARNING_PUSH -QT_WARNING_DISABLE_DEPRECATED -QT_WARNING_DISABLE_GCC("-Wuseless-cast") -namespace { -struct qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t {}; -} // unnamed namespace - -template <> -constexpr inline auto -xe::app::LaunchDataRestartDialogQt::qt_create_metaobjectdata< - qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t>() { - namespace QMC = QtMocConstants; - QtMocHelpers::StringRefStorage qt_stringData{ - "xe::app::LaunchDataRestartDialogQt", "OnOKClicked", ""}; - - QtMocHelpers::UintData qt_methods{ - // Slot 'OnOKClicked' - QtMocHelpers::SlotData(1, 2, QMC::AccessPrivate, QMetaType::Void), - }; - QtMocHelpers::UintData qt_properties{}; - QtMocHelpers::UintData qt_enums{}; - return QtMocHelpers::metaObjectData< - LaunchDataRestartDialogQt, - qt_meta_tag_ZN2xe3app25LaunchDataRestartDialogQtE_t>( - QMC::MetaObjectFlag{}, qt_stringData, qt_methods, qt_properties, - qt_enums); -} -Q_CONSTINIT const QMetaObject xe::app::LaunchDataRestartDialogQt::staticMetaObject = { { - QMetaObject::SuperData::link(), - qt_staticMetaObjectStaticContent.stringdata, - qt_staticMetaObjectStaticContent.data, - qt_static_metacall, - nullptr, - qt_staticMetaObjectRelocatingContent.metaTypes, - nullptr -} }; - -void xe::app::LaunchDataRestartDialogQt::qt_static_metacall( - QObject* _o, QMetaObject::Call _c, int _id, void** _a) { - auto* _t = static_cast(_o); - if (_c == QMetaObject::InvokeMetaMethod) { - switch (_id) { - case 0: - _t->OnOKClicked(); - break; - default:; - } - } - (void)_a; -} - -const QMetaObject* xe::app::LaunchDataRestartDialogQt::metaObject() const { - return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() - : &staticMetaObject; -} - -void* xe::app::LaunchDataRestartDialogQt::qt_metacast(const char* _clname) { - if (!_clname) return nullptr; - if (!strcmp(_clname, qt_staticMetaObjectStaticContent.strings)) - return static_cast(this); - return QDialog::qt_metacast(_clname); -} - -int xe::app::LaunchDataRestartDialogQt::qt_metacall(QMetaObject::Call _c, - int _id, void** _a) { - _id = QDialog::qt_metacall(_c, _id, _a); - if (_id < 0) return _id; - if (_c == QMetaObject::InvokeMetaMethod) { - if (_id < 1) qt_static_metacall(this, _c, _id, _a); - _id -= 1; - } - if (_c == QMetaObject::RegisterMethodArgumentMetaType) { - if (_id < 1) *reinterpret_cast(_a[0]) = QMetaType(); - _id -= 1; - } - return _id; -} -QT_WARNING_POP diff --git a/src/xenia/emulator.cc b/src/xenia/emulator.cc index f85dc7d9b..8d6fe89de 100644 --- a/src/xenia/emulator.cc +++ b/src/xenia/emulator.cc @@ -1434,42 +1434,73 @@ void Emulator::RemoveGameConfigLoadCallback(GameConfigLoadCallback* callback) { game_config_load_callbacks_.erase(it); } +std::string Emulator::RemountAndResolveLaunchPath( + const std::string& launch_path) { + // Normalize path separators for the platform + std::string normalized_path = launch_path; +#if XE_PLATFORM_LINUX + // Convert backslashes to forward slashes for consistent paths on Linux + std::replace(normalized_path.begin(), normalized_path.end(), '\\', '/'); +#endif + + // Get the current game:\ symbolic link path + std::string symbolic_link_path; + if (!kernel_state_->file_system()->FindSymbolicLink(kDefaultGameSymbolicLink, + symbolic_link_path)) { + return ""; + } + + std::filesystem::path file_path = symbolic_link_path; + + // Remove previous symbolic links. + // Some titles can provide root within specific directory. + kernel_state_->file_system()->UnregisterSymbolicLink( + kDefaultPartitionSymbolicLink); + kernel_state_->file_system()->UnregisterSymbolicLink( + kDefaultGameSymbolicLink); + + file_path /= std::filesystem::path(normalized_path); + + // Re-register symbolic links to point to the parent directory of the module + kernel_state_->file_system()->RegisterSymbolicLink( + kDefaultPartitionSymbolicLink, xe::path_to_utf8(file_path.parent_path())); + kernel_state_->file_system()->RegisterSymbolicLink( + kDefaultGameSymbolicLink, xe::path_to_utf8(file_path.parent_path())); + + return xe::path_to_utf8(file_path); +} + std::string Emulator::FindLaunchModule() { std::string path("game:\\"); auto xam = kernel_state()->GetKernelModule("xam.xex"); if (!xam->loader_data().launch_path.empty()) { - std::string symbolic_link_path; - if (kernel_state_->file_system()->FindSymbolicLink(kDefaultGameSymbolicLink, - symbolic_link_path)) { - std::filesystem::path file_path = symbolic_link_path; - // Remove previous symbolic links. - // Some titles can provide root within specific directory. - kernel_state_->file_system()->UnregisterSymbolicLink( - kDefaultPartitionSymbolicLink); - kernel_state_->file_system()->UnregisterSymbolicLink( - kDefaultGameSymbolicLink); - - std::string launch_path = xam->loader_data().launch_path; -#if XE_PLATFORM_LINUX - // Convert backslashes to forward slashes for consistent paths on Linux - std::replace(launch_path.begin(), launch_path.end(), '\\', '/'); -#endif - file_path /= std::filesystem::path(launch_path); - - kernel_state_->file_system()->RegisterSymbolicLink( - kDefaultPartitionSymbolicLink, - xe::path_to_utf8(file_path.parent_path())); - kernel_state_->file_system()->RegisterSymbolicLink( - kDefaultGameSymbolicLink, xe::path_to_utf8(file_path.parent_path())); - - return xe::path_to_utf8(file_path); + std::string result = + RemountAndResolveLaunchPath(xam->loader_data().launch_path); + if (!result.empty()) { + return result; } } if (!cvars::launch_module.empty()) { - return path + cvars::launch_module; + std::string launch_module = cvars::launch_module; +#if XE_PLATFORM_LINUX + // Convert backslashes to forward slashes for consistent paths on Linux + std::replace(launch_module.begin(), launch_module.end(), '\\', '/'); +#endif + + // If the module is in a subdirectory, remount game:\ to that subdirectory + std::filesystem::path module_path(launch_module); + if (module_path.has_parent_path() && module_path.parent_path() != ".") { + std::string result = RemountAndResolveLaunchPath(launch_module); + if (!result.empty()) { + return result; + } + } + + // No subdirectory, just return as guest path + return path + launch_module; } return path + "default.xex"; diff --git a/src/xenia/emulator.h b/src/xenia/emulator.h index e6ce895c7..1360aa69e 100644 --- a/src/xenia/emulator.h +++ b/src/xenia/emulator.h @@ -367,6 +367,7 @@ class Emulator { void AddGameConfigLoadCallback(GameConfigLoadCallback* callback); void RemoveGameConfigLoadCallback(GameConfigLoadCallback* callback); + std::string RemountAndResolveLaunchPath(const std::string& launch_path); std::string FindLaunchModule(); X_STATUS CompleteLaunch(const std::filesystem::path& path,