/** ****************************************************************************** * Xenia : Xbox 360 Emulator Research Project * ****************************************************************************** * Copyright 2022 Ben Vanik. All rights reserved. * * Released under the BSD license - see LICENSE in the root for more details. * ****************************************************************************** */ #include "xenia/kernel/xam/xam_ui.h" #include "xenia/app/emulator_window.h" #include "xenia/base/png_utils.h" #include "xenia/base/system.h" #include "xenia/hid/input_system.h" #include "xenia/kernel/kernel_state.h" #include "xenia/kernel/util/shim_utils.h" #include "xenia/kernel/xam/xam_content_device.h" #include "xenia/kernel/xam/xam_private.h" #include "xenia/ui/file_picker.h" #include "xenia/ui/imgui_dialog.h" #include "xenia/ui/imgui_drawer.h" #include "xenia/ui/imgui_guest_notification.h" #include "xenia/kernel/xam/ui/create_profile_ui.h" #include "xenia/kernel/xam/ui/game_achievements_ui.h" #include "xenia/kernel/xam/ui/gamercard_ui.h" #include "xenia/kernel/xam/ui/passcode_ui.h" #include "xenia/kernel/xam/ui/signin_ui.h" #include "xenia/kernel/xam/ui/title_info_ui.h" DEFINE_bool(storage_selection_dialog, false, "Show storage device selection dialog when the game requests it.", "UI"); DECLARE_int32(license_mask); namespace xe { namespace kernel { namespace xam { // TODO(gibbed): This is all one giant WIP that seems to work better than the // previous immediate synchronous completion of dialogs. // // The deferred execution of dialog handling is done in such a way that there is // a pre-, peri- (completion), and post- callback steps. // // pre(); // result = completion(); // CompleteOverlapped(result); // post(); // // There are games that are batshit insane enough to wait for the X_OVERLAPPED // to be completed (ie not X_ERROR_PENDING) before creating a listener to // receive a notification, which is why we have distinct pre- and post- steps. // // We deliberately delay the XN_SYS_UI = false notification to give games time // to create a listener (if they're insane enough do this). template X_RESULT xeXamDispatchDialog(T* dialog, std::function close_callback, uint32_t overlapped) { auto pre = []() { kernel_state()->BroadcastNotification(kXNotificationSystemUI, true); }; auto run = [dialog, close_callback]() -> X_RESULT { X_RESULT result; dialog->set_close_callback([&dialog, &result, &close_callback]() { result = close_callback(dialog); }); xe::threading::Fence fence; xe::ui::WindowedAppContext& app_context = kernel_state()->emulator()->display_window()->app_context(); if (app_context.CallInUIThreadSynchronous( [&dialog, &fence]() { dialog->Then(&fence); })) { kernel_state()->xam_state()->xam_dialogs_shown_++; fence.Wait(); kernel_state()->xam_state()->xam_dialogs_shown_--; } else { delete dialog; } // dialog should be deleted at this point! return result; }; auto post = []() { xe::threading::Sleep(std::chrono::milliseconds(100)); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; if (!overlapped) { pre(); auto result = run(); post(); return result; } else { kernel_state()->CompleteOverlappedDeferred(run, overlapped, pre, post); return X_ERROR_IO_PENDING; } } template X_RESULT xeXamDispatchDialogEx( T* dialog, std::function close_callback, uint32_t overlapped) { auto pre = []() { kernel_state()->BroadcastNotification(kXNotificationSystemUI, true); }; auto run = [dialog, close_callback](uint32_t& extended_error, uint32_t& length) -> X_RESULT { auto display_window = kernel_state()->emulator()->display_window(); X_RESULT result; dialog->set_close_callback( [&dialog, &result, &extended_error, &length, &close_callback]() { result = close_callback(dialog, extended_error, length); }); xe::threading::Fence fence; if (display_window->app_context().CallInUIThreadSynchronous( [&dialog, &fence]() { dialog->Then(&fence); })) { kernel_state()->xam_state()->xam_dialogs_shown_++; fence.Wait(); kernel_state()->xam_state()->xam_dialogs_shown_--; } else { delete dialog; } // dialog should be deleted at this point! return result; }; auto post = []() { xe::threading::Sleep(std::chrono::milliseconds(100)); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; if (!overlapped) { pre(); uint32_t extended_error, length; auto result = run(extended_error, length); post(); // TODO(gibbed): do something with extended_error/length? return result; } else { kernel_state()->CompleteOverlappedDeferredEx(run, overlapped, pre, post); return X_ERROR_IO_PENDING; } } X_RESULT xeXamDispatchHeadless(std::function run_callback, uint32_t overlapped) { auto pre = []() { kernel_state()->BroadcastNotification(kXNotificationSystemUI, true); }; auto post = []() { xe::threading::Sleep(std::chrono::milliseconds(100)); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; if (!overlapped) { pre(); auto result = run_callback(); post(); return result; } else { kernel_state()->CompleteOverlappedDeferred(run_callback, overlapped, pre, post); return X_ERROR_IO_PENDING; } } X_RESULT xeXamDispatchHeadlessEx( std::function run_callback, uint32_t overlapped) { auto pre = []() { kernel_state()->BroadcastNotification(kXNotificationSystemUI, true); }; auto post = []() { xe::threading::Sleep(std::chrono::milliseconds(100)); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; if (!overlapped) { pre(); uint32_t extended_error, length; auto result = run_callback(extended_error, length); post(); // TODO(gibbed): do something with extended_error/length? return result; } else { kernel_state()->CompleteOverlappedDeferredEx(run_callback, overlapped, pre, post); return X_ERROR_IO_PENDING; } } template X_RESULT xeXamDispatchDialogAsync(T* dialog, std::function close_callback) { kernel_state()->BroadcastNotification(kXNotificationSystemUI, true); kernel_state()->xam_state()->xam_dialogs_shown_++; // Important to pass captured vars by value here since we return from this // without waiting for the dialog to close so the original local vars will be // destroyed. dialog->set_close_callback([dialog, close_callback]() { close_callback(dialog); kernel_state()->xam_state()->xam_dialogs_shown_--; auto run = []() -> void { xe::threading::Sleep(std::chrono::milliseconds(100)); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; std::thread thread(run); thread.detach(); }); return X_ERROR_SUCCESS; } X_RESULT xeXamDispatchHeadlessAsync(std::function run_callback) { kernel_state()->BroadcastNotification(kXNotificationSystemUI, true); kernel_state()->xam_state()->xam_dialogs_shown_++; auto display_window = kernel_state()->emulator()->display_window(); display_window->app_context().CallInUIThread([run_callback]() { run_callback(); kernel_state()->xam_state()->xam_dialogs_shown_--; auto run = []() -> void { xe::threading::Sleep(std::chrono::milliseconds(100)); kernel_state()->BroadcastNotification(kXNotificationSystemUI, false); }; std::thread thread(run); thread.detach(); }); return X_ERROR_SUCCESS; } void MessageBoxDialog::OnDraw(ImGuiIO& io) { bool first_draw = false; if (!has_opened_) { ImGui::OpenPopup(title_.c_str()); has_opened_ = true; first_draw = true; } if (ImGui::BeginPopupModal(title_.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { if (description_.size()) { ImGui::Text("%s", description_.c_str()); } if (first_draw) { ImGui::SetKeyboardFocusHere(); } for (size_t i = 0; i < buttons_.size(); ++i) { if (ImGui::Button(buttons_[i].c_str())) { chosen_button_ = static_cast(i); ImGui::CloseCurrentPopup(); Close(); } ImGui::SameLine(); } ImGui::Spacing(); ImGui::Spacing(); ImGui::EndPopup(); } else { Close(); } } void KeyboardInputDialog::OnDraw(ImGuiIO& io) { bool first_draw = false; if (!has_opened_) { ImGui::OpenPopup(title_.c_str()); has_opened_ = true; first_draw = true; } if (ImGui::BeginPopupModal(title_.c_str(), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { if (description_.size()) { ImGui::TextWrapped("%s", description_.c_str()); } if (first_draw) { ImGui::SetKeyboardFocusHere(); } ImGui::PushID("input_text"); bool input_submitted = ImGui::InputText("##body", text_buffer_.data(), text_buffer_.size(), ImGuiInputTextFlags_EnterReturnsTrue); // Context menu for paste functionality if (ImGui::BeginPopupContextItem("input_context_menu")) { if (ImGui::MenuItem("Paste")) { if (ImGui::GetClipboardText() != nullptr) { std::string clipboard_text = ImGui::GetClipboardText(); xe::string_util::copy_truncating(text_buffer_.data(), clipboard_text, text_buffer_.size()); } } ImGui::EndPopup(); } ImGui::PopID(); if (input_submitted) { text_ = std::string(text_buffer_.data(), text_buffer_.size()); cancelled_ = false; ImGui::CloseCurrentPopup(); Close(); } if (ImGui::Button("OK")) { text_ = std::string(text_buffer_.data(), text_buffer_.size()); cancelled_ = false; ImGui::CloseCurrentPopup(); Close(); } ImGui::SameLine(); if (ImGui::Button("Cancel")) { text_ = ""; cancelled_ = true; ImGui::CloseCurrentPopup(); Close(); } ImGui::Spacing(); ImGui::EndPopup(); } else { Close(); } } static dword_result_t XamShowMessageBoxUi( dword_t user_index, lpu16string_t title_ptr, lpu16string_t text_ptr, dword_t button_count, lpdword_t button_ptrs, dword_t active_button, dword_t flags, pointer_t result_ptr, pointer_t overlapped) { std::string title = title_ptr ? xe::to_utf8(title_ptr.value()) : ""; std::string text = text_ptr ? xe::to_utf8(text_ptr.value()) : ""; std::vector buttons; for (uint32_t i = 0; i < button_count; ++i) { uint32_t button_ptr = button_ptrs[i]; auto button = xe::load_and_swap( kernel_state()->memory()->TranslateVirtual(button_ptr)); buttons.push_back(xe::to_utf8(button)); } X_RESULT result; if (cvars::headless) { // Auto-pick the focused button. auto run = [result_ptr, active_button]() -> X_RESULT { result_ptr->ButtonPressed = static_cast(active_button); return X_ERROR_SUCCESS; }; result = xeXamDispatchHeadless(run, overlapped); } else { switch (flags & 0xF) { case XMBox_NOICON: { } break; case XMBox_ERRORICON: { } break; case XMBox_WARNINGICON: { } break; case XMBox_ALERTICON: { } break; } const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); if (flags & XMBox_PASSCODEMODE || flags & XMBox_VERIFYPASSCODEMODE) { auto close = [result_ptr, active_button](ui::ProfilePasscodeUI* dialog) -> X_RESULT { if (dialog->SelectedSignedIn()) { // Logged in return X_ERROR_SUCCESS; } else { return X_ERROR_FUNCTION_FAILED; } }; result = xeXamDispatchDialog( new ui::ProfilePasscodeUI(imgui_drawer, title, text, result_ptr), close, overlapped); } else { auto close = [result_ptr](MessageBoxDialog* dialog) -> X_RESULT { result_ptr->ButtonPressed = dialog->chosen_button(); return X_ERROR_SUCCESS; }; result = xeXamDispatchDialog( new MessageBoxDialog(imgui_drawer, title, text, buttons, static_cast(active_button)), close, overlapped); } } return result; } dword_result_t XamIsUIActive_entry() { return kernel_state()->xam_state()->xam_dialogs_shown_ > 0; } DECLARE_XAM_EXPORT2(XamIsUIActive, kUI, kImplemented, kHighFrequency); // https://www.se7ensins.com/forums/threads/working-xshowmessageboxui.844116/ dword_result_t XamShowMessageBoxUI_entry( dword_t user_index, lpu16string_t title_ptr, lpu16string_t text_ptr, dword_t button_count, lpdword_t button_ptrs, dword_t active_button, dword_t flags, pointer_t result_ptr, pointer_t overlapped) { return XamShowMessageBoxUi(user_index, title_ptr, text_ptr, button_count, button_ptrs, active_button, flags, result_ptr, overlapped); } DECLARE_XAM_EXPORT1(XamShowMessageBoxUI, kUI, kImplemented); dword_result_t XamShowMessageBoxUIEx_entry( dword_t user_index, lpu16string_t title_ptr, lpu16string_t text_ptr, dword_t button_count, lpdword_t button_ptrs, dword_t active_button, dword_t flags, dword_t unknown_unused, pointer_t result_ptr, pointer_t overlapped) { return XamShowMessageBoxUi(user_index, title_ptr, text_ptr, button_count, button_ptrs, active_button, flags, result_ptr, overlapped); } DECLARE_XAM_EXPORT1(XamShowMessageBoxUIEx, kUI, kImplemented); dword_result_t XNotifyQueueUI_entry(dword_t exnq, dword_t dwUserIndex, qword_t qwAreas, lpu16string_t displayText_ptr, lpvoid_t contextData) { std::string displayText = ""; const uint8_t position_id = static_cast(qwAreas); if (displayText_ptr) { displayText = xe::to_utf8(displayText_ptr.value()); } XELOGI("XNotifyQueueUI: {}", displayText); const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); new xe::ui::XNotifyWindow(imgui_drawer, "", displayText, dwUserIndex, position_id); // XNotifyQueueUI -> XNotifyQueueUIEx -> XMsgProcessRequest -> // XMsgStartIORequestEx & XMsgInProcessCall return X_ERROR_SUCCESS; } DECLARE_XAM_EXPORT1(XNotifyQueueUI, kUI, kSketchy); // https://www.se7ensins.com/forums/threads/release-how-to-use-xshowkeyboardui-release.906568/ dword_result_t XamShowKeyboardUI_entry( dword_t user_index, dword_t flags, lpu16string_t default_text, lpu16string_t title, lpu16string_t description, lpu16string_t buffer, dword_t buffer_length, pointer_t overlapped) { if (!buffer) { return X_ERROR_INVALID_PARAMETER; } assert_not_null(overlapped); auto buffer_size = static_cast(buffer_length) * 2; X_RESULT result; if (cvars::headless) { auto run = [default_text, buffer, buffer_length, buffer_size]() -> X_RESULT { // Redirect default_text back into the buffer. if (!default_text) { std::memset(buffer, 0, buffer_size); } else { string_util::copy_and_swap_truncating(buffer, default_text.value(), buffer_length); } return X_ERROR_SUCCESS; }; result = xeXamDispatchHeadless(run, overlapped); } else { auto close = [buffer, buffer_length](KeyboardInputDialog* dialog, uint32_t& extended_error, uint32_t& length) -> X_RESULT { if (dialog->cancelled()) { extended_error = X_ERROR_CANCELLED; length = 0; return X_ERROR_SUCCESS; } else { // Zero the output buffer. auto text = xe::to_utf16(dialog->text()); string_util::copy_and_swap_truncating(buffer, text, buffer_length); extended_error = X_ERROR_SUCCESS; length = 0; return X_ERROR_SUCCESS; } }; const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); std::string title_str = title ? xe::to_utf8(title.value()) : ""; std::string desc_str = description ? xe::to_utf8(description.value()) : ""; std::string def_text_str = default_text ? xe::to_utf8(default_text.value()) : ""; result = xeXamDispatchDialogEx( new KeyboardInputDialog(imgui_drawer, title_str, desc_str, def_text_str, buffer_length), close, overlapped); } return result; } DECLARE_XAM_EXPORT1(XamShowKeyboardUI, kUI, kImplemented); dword_result_t XamShowDeviceSelectorUI_entry( dword_t user_index, dword_t content_type, dword_t content_flags, qword_t total_requested, lpdword_t device_id_ptr, pointer_t overlapped) { if (!overlapped) { return X_ERROR_INVALID_PARAMETER; } if ((user_index >= XUserMaxUserCount && user_index != XUserIndexAny) || (content_flags & 0x83F00008) != 0 || !device_id_ptr) { XOverlappedSetExtendedError(overlapped, X_ERROR_INVALID_PARAMETER); return X_ERROR_INVALID_PARAMETER; } std::vector devices = ListStorageDevices(); if (cvars::headless || !cvars::storage_selection_dialog) { // Default to the first storage device (HDD) if headless. return xeXamDispatchHeadless( [device_id_ptr, devices]() -> X_RESULT { if (devices.empty()) return X_ERROR_CANCELLED; const DummyDeviceInfo* device_info = devices.front(); *device_id_ptr = static_cast(device_info->device_id); return X_ERROR_SUCCESS; }, overlapped); } auto close = [device_id_ptr, devices](MessageBoxDialog* dialog) -> X_RESULT { uint32_t button = dialog->chosen_button(); if (button >= devices.size()) return X_ERROR_CANCELLED; const DummyDeviceInfo* device_info = devices.at(button); *device_id_ptr = static_cast(device_info->device_id); return X_ERROR_SUCCESS; }; std::string title = "Select storage device"; std::string desc = ""; cxxopts::OptionNames buttons; for (auto& device_info : devices) { buttons.push_back(to_utf8(device_info->name)); } buttons.push_back("Cancel"); const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); return xeXamDispatchDialog( new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close, overlapped); } DECLARE_XAM_EXPORT1(XamShowDeviceSelectorUI, kUI, kImplemented); void XamShowDirtyDiscErrorUI_entry(dword_t user_index) { if (cvars::headless) { assert_always(); exit(1); return; } std::string title = "Disc Read Error"; std::string desc = "There's been an issue reading content from the game disc.\nThis is " "likely caused by bad or unimplemented file IO calls."; const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); xeXamDispatchDialog( new MessageBoxDialog(imgui_drawer, title, desc, {"OK"}, 0), [](MessageBoxDialog*) -> X_RESULT { return X_ERROR_SUCCESS; }, 0); // This is death, and should never return. // TODO(benvanik): cleaner exit. exit(1); } DECLARE_XAM_EXPORT1(XamShowDirtyDiscErrorUI, kUI, kImplemented); dword_result_t XamShowPartyUI_entry(dword_t user_index) { return X_ERROR_FUNCTION_FAILED; } DECLARE_XAM_EXPORT1(XamShowPartyUI, kNone, kStub); dword_result_t XamShowCommunitySessionsUI_entry(unknown_t r3, unknown_t r4) { return X_ERROR_FUNCTION_FAILED; } DECLARE_XAM_EXPORT1(XamShowCommunitySessionsUI, kNone, kStub); // this is supposed to do a lot more, calls another function that triggers some // cbs dword_result_t XamSetDashContext_entry(dword_t value, const ppc_context_t& ctx) { ctx->kernel_state->dash_context_ = value; kernel_state()->BroadcastNotification(kXNotificationSystemDashContextChanged, 0); return 0; } DECLARE_XAM_EXPORT1(XamSetDashContext, kNone, kImplemented); dword_result_t XamGetDashContext_entry(const ppc_context_t& ctx) { return ctx->kernel_state->dash_context_; } DECLARE_XAM_EXPORT1(XamGetDashContext, kNone, kImplemented); // https://gitlab.com/GlitchyScripts/xlivelessness/-/blob/master/xlivelessness/xlive/xdefs.hpp?ref_type=heads#L1235 dword_result_t XamShowMarketplaceUIEx_entry(dword_t user_index, dword_t ui_type, qword_t offer_id, dword_t offer_type, dword_t content_category, unknown_t unk6, unknown_t unk7, dword_t title_id) { // ui_type: // 0 - view all content for the current title // 1 - view content specified by offer id // offer_types: // filter for content list, usually just -1 // content_category: // filter on item types for games (e.g. cars, maps, weapons, etc) if (user_index >= XUserMaxUserCount) { return X_ERROR_INVALID_PARAMETER; } if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { return X_ERROR_NO_SUCH_USER; } if (cvars::headless) { return xeXamDispatchHeadlessAsync([]() {}); } bool is_xbla_unlock_offer = (offer_id == ((uint64_t(kernel_state()->title_id()) << 32) | 1ull)); auto close = [ui_type, is_xbla_unlock_offer](MessageBoxDialog* dialog) -> void { if (ui_type == 1 && is_xbla_unlock_offer) { uint32_t button = dialog->chosen_button(); if (button == 0) { cvars::license_mask = 1; kernel_state()->BroadcastNotification( kXNotificationLiveContentInstalled, 0); } } }; std::string title = "Xbox Marketplace"; std::string desc = ""; cxxopts::OptionNames buttons; switch (ui_type) { case X_MARKETPLACE_ENTRYPOINT::ContentList: desc = "Game requested to open marketplace page with all content for the " "current title ID."; break; case X_MARKETPLACE_ENTRYPOINT::ContentItem: desc = fmt::format( "Game requested to open marketplace page for offer ID 0x{:016X}.", static_cast(offer_id)); break; case X_MARKETPLACE_ENTRYPOINT::MembershipList: desc = "Game requested to open marketplace page with all Xbox Live " "memberships."; break; case X_MARKETPLACE_ENTRYPOINT::MembershipItem: desc = fmt::format( "Game requested to open marketplace page for an Xbox Live " "membership offer 0x{:016X}.", static_cast(offer_id)); break; case X_MARKETPLACE_ENTRYPOINT::ContentList_Background: // Used when accessing microsoft points desc = fmt::format( "Xbox Marketplace requested access to Microsoft Points offer page " "0x{:016X}.", static_cast(offer_id)); break; case X_MARKETPLACE_ENTRYPOINT::ContentItem_Background: // Used when accessing credit card information and calls // XamShowCreditCardUI desc = fmt::format( "Xbox Marketplace requested access to credit card information page " "0x{:016X}.", static_cast(offer_id)); break; case X_MARKETPLACE_ENTRYPOINT::ForcedNameChangeV1: // Used by XamShowForcedNameChangeUI v1888 desc = fmt::format("Changing gamertag currently not implemented."); break; case X_MARKETPLACE_ENTRYPOINT::ForcedNameChangeV2: // Used by XamShowForcedNameChangeUI NXE and up desc = fmt::format("Changing gamertag currently not implemented."); break; case X_MARKETPLACE_ENTRYPOINT::ProfileNameChange: // Used by dashboard when selecting change gamertag in profile menu desc = fmt::format("Changing gamertag currently not implemented."); break; case X_MARKETPLACE_ENTRYPOINT::ActiveDownloads: // Used in profile tabs when clicking active downloads desc = fmt::format( "There are no current plans to download files from Xbox " "Marketplace."); break; default: desc = fmt::format("Unknown marketplace op {:d}", static_cast(ui_type)); break; } desc += "\nNote that since Xenia cannot access Xbox Marketplace, any DLC must be " "installed manually using File -> Install Content."; switch (ui_type) { case X_MARKETPLACE_ENTRYPOINT::ContentList: default: buttons.push_back("OK"); break; case X_MARKETPLACE_ENTRYPOINT::ContentItem: if (is_xbla_unlock_offer) { desc += "\n\nTo start trial games in full mode, set license_mask to 1 in " "Xenia config file.\n\nDo you wish to change license_mask to 1 for " "*this session*?"; buttons.push_back("Yes"); buttons.push_back("No"); } else { buttons.push_back("OK"); } break; } const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); return xeXamDispatchDialogAsync( new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close); } DECLARE_XAM_EXPORT1(XamShowMarketplaceUIEx, kUI, kSketchy); dword_result_t XamShowMarketplaceUI_entry(dword_t user_index, dword_t ui_type, qword_t offer_id, dword_t offer_type, dword_t content_category, dword_t title_id) { return XamShowMarketplaceUIEx_entry(user_index, ui_type, offer_id, offer_type, content_category, 0, 0, title_id); } DECLARE_XAM_EXPORT1(XamShowMarketplaceUI, kUI, kSketchy); dword_result_t XamShowMarketplaceDownloadItemsUI_entry( dword_t user_index, dword_t ui_type, lpqword_t offers, dword_t num_offers, lpdword_t hresult_ptr, pointer_t overlapped) { if (user_index >= XUserMaxUserCount || !offers || num_offers > 6) { return X_ERROR_INVALID_PARAMETER; } if (!kernel_state()->xam_state()->IsUserSignedIn(user_index)) { if (overlapped) { kernel_state()->CompleteOverlappedImmediate(overlapped, X_ERROR_NO_SUCH_USER); return X_ERROR_IO_PENDING; } return X_ERROR_NO_SUCH_USER; } if (cvars::headless) { return xeXamDispatchHeadless( [hresult_ptr]() -> X_RESULT { if (hresult_ptr) { *hresult_ptr = X_E_SUCCESS; } return X_ERROR_SUCCESS; }, overlapped); } auto close = [hresult_ptr](MessageBoxDialog* dialog) -> X_RESULT { if (hresult_ptr) { // TODO *hresult_ptr = X_E_SUCCESS; } return X_ERROR_SUCCESS; }; std::string title = "Xbox Marketplace"; std::string desc = ""; cxxopts::OptionNames buttons = {"OK"}; switch (ui_type) { case X_MARKETPLACE_DOWNLOAD_ITEMS_ENTRYPOINTS::FREEITEMS: desc = "Game requested to open download page for the following free offer " "IDs:"; break; case X_MARKETPLACE_DOWNLOAD_ITEMS_ENTRYPOINTS::PAIDITEMS: desc = "Game requested to open download page for the following offer IDs:"; break; default: return X_ERROR_INVALID_PARAMETER; } for (uint32_t i = 0; i < num_offers; i++) { desc += fmt::format("\n0x{:16X}", offers[i].get()); } desc += "\n\nNote that since Xenia cannot access Xbox Marketplace, any DLC " "must " "be installed manually using File -> Install Content."; const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); return xeXamDispatchDialog( new MessageBoxDialog(imgui_drawer, title, desc, buttons, 0), close, overlapped); } DECLARE_XAM_EXPORT1(XamShowMarketplaceDownloadItemsUI, kUI, kSketchy); dword_result_t XamShowForcedNameChangeUI_entry(dword_t user_index) { // Changes from 6 to 8 past NXE return XamShowMarketplaceUIEx_entry(user_index, 6, 0, 0xffffffff, 0, 0, 0, 0); } DECLARE_XAM_EXPORT1(XamShowForcedNameChangeUI, kUI, kImplemented); bool xeDrawProfileContent(xe::ui::ImGuiDrawer* imgui_drawer, const uint64_t xuid, const uint8_t user_index, const X_XAMACCOUNTINFO* account, const xe::ui::ImmediateTexture* profile_icon, std::function context_menu, std::function on_profile_change, uint64_t* selected_xuid) { const ImVec2 start_position = ImGui::GetCursorPos(); ImGui::BeginGroup(); { if (profile_icon) { ImGui::Image(reinterpret_cast(profile_icon), xe::ui::default_image_icon_size); } else { if (user_index < XUserMaxUserCount) { const auto icon = imgui_drawer->GetNotificationIcon(user_index); ImGui::Image(reinterpret_cast(icon), xe::ui::default_image_icon_size); } else { ImGui::Dummy(xe::ui::default_image_icon_size); } } ImGui::SameLine(); ImGui::BeginGroup(); { ImGui::TextUnformatted( fmt::format("User: {}\n", account->GetGamertagString()).c_str()); ImGui::TextUnformatted(fmt::format("XUID: {:016X} \n", xuid).c_str()); if (user_index != XUserIndexAny) { ImGui::TextUnformatted( fmt::format("Assigned to slot: {}\n", user_index + 1).c_str()); } else { ImGui::TextUnformatted(fmt::format("Profile is not signed in").c_str()); } } ImGui::EndGroup(); } ImGui::EndGroup(); if (xuid && selected_xuid) { const ImVec2 end_draw_position = ImVec2(ImGui::GetCursorPos().x - start_position.x, ImGui::GetCursorPos().y - start_position.y); ImGui::SetCursorPos(start_position); if (ImGui::Selectable("##Selectable", *selected_xuid == xuid, ImGuiSelectableFlags_SpanAllColumns, end_draw_position)) { *selected_xuid = xuid; ImGui::OpenPopup("Profile Menu"); } if (context_menu) { return context_menu(); } } return true; } X_RESULT xeXamShowSigninUI(uint32_t user_index, uint32_t users_needed, uint32_t flags) { // Mask values vary. Probably matching user types? Local/remote? // Games seem to sit and loop until we trigger sign in notification. if (users_needed != 1 && users_needed != 2 && users_needed != 4) { return X_ERROR_INVALID_PARAMETER; } if (cvars::headless) { return xeXamDispatchHeadlessAsync([users_needed]() { std::map xuids; for (uint32_t i = 0; i < XUserMaxUserCount; i++) { UserProfile* profile = kernel_state()->xam_state()->GetUserProfile(i); if (profile) { xuids[i] = profile->xuid(); if (xuids.size() >= users_needed) break; } } kernel_state()->xam_state()->profile_manager()->LoginMultiple(xuids); }); } auto close = [](ui::SigninUI* dialog) -> void {}; const Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); return xeXamDispatchDialogAsync( new ui::SigninUI( imgui_drawer, kernel_state()->xam_state()->profile_manager(), emulator->input_system()->GetLastUsedSlot(), users_needed), close); } X_RESULT xeXamShowCreateProfileUIEx(uint32_t user_index, dword_t flag, char* unkn2_ptr) { Emulator* emulator = kernel_state()->emulator(); xe::ui::ImGuiDrawer* imgui_drawer = emulator->imgui_drawer(); if (cvars::headless) { return X_ERROR_SUCCESS; } auto close = [](ui::CreateProfileUI* dialog) -> void {}; return xeXamDispatchDialogAsync( new ui::CreateProfileUI(imgui_drawer, emulator), close); } dword_result_t XamShowSigninUI_entry(dword_t users_needed, dword_t flags) { return xeXamShowSigninUI(XUserIndexAny, users_needed, flags); } DECLARE_XAM_EXPORT1(XamShowSigninUI, kUserProfiles, kImplemented); dword_result_t XamShowSigninUIEx_entry( dword_t users_needed, dword_t flags, pointer_t overlapped_ptr) { X_RESULT result = xeXamShowSigninUI(XUserIndexAny, users_needed, flags); if (overlapped_ptr) { kernel_state()->CompleteOverlappedImmediate(overlapped_ptr, result); return X_ERROR_IO_PENDING; } else { return result; } } DECLARE_XAM_EXPORT1(XamShowSigninUIEx, kUserProfiles, kSketchy); dword_result_t XamShowNuiSigninUI_entry(dword_t unk, dword_t user_index, dword_t flags) { uint32_t users_needed = 1; uint32_t sent_flags = flags | static_cast(SigninUiFlags::NUI); // xeXamNuiHudCheck(unk) = success then continue else return return xeXamShowSigninUI(user_index, users_needed, sent_flags); } DECLARE_XAM_EXPORT1(XamShowNuiSigninUI, kUserProfiles, kSketchy); dword_result_t XamShowSigninUIp_entry(dword_t user_index, dword_t users_needed, dword_t flags) { return xeXamShowSigninUI(user_index, users_needed, flags); } DECLARE_XAM_EXPORT1(XamShowSigninUIp, kUserProfiles, kImplemented); dword_result_t XamShowCreateProfileUIEx_entry(dword_t user_index, dword_t flag, lpstring_t unkn2_ptr) { return xeXamShowCreateProfileUIEx(user_index, flag, unkn2_ptr); } DECLARE_XAM_EXPORT1(XamShowCreateProfileUIEx, kUserProfiles, kImplemented); dword_result_t XamShowCreateProfileUI_entry(dword_t user_index, dword_t flag) { return xeXamShowCreateProfileUIEx(user_index, flag, 0); } DECLARE_XAM_EXPORT1(XamShowCreateProfileUI, kUserProfiles, kImplemented); dword_result_t XamShowAchievementsUI_entry(dword_t user_index, dword_t title_id) { auto user = kernel_state()->xam_state()->GetUserProfile(user_index); if (!user) { return X_ERROR_NO_SUCH_USER; } uint32_t proper_title_id = title_id ? title_id.value() : kernel_state()->xam_state()->spa_info()->title_id(); const auto info = kernel_state()->xam_state()->user_tracker()->GetUserTitleInfo( user->xuid(), proper_title_id); if (!info) { return X_ERROR_NO_SUCH_USER; } xe::ui::ImGuiDrawer* imgui_drawer = kernel_state()->emulator()->imgui_drawer(); auto close = [](ui::GameAchievementsUI* dialog) -> void {}; return xeXamDispatchDialogAsync( new ui::GameAchievementsUI(imgui_drawer, ImVec2(100.f, 100.f), &info.value(), user), close); } DECLARE_XAM_EXPORT1(XamShowAchievementsUI, kUserProfiles, kStub); dword_result_t XamShowGamerCardUI_entry(dword_t user_index) { auto user = kernel_state()->xam_state()->GetUserProfile(user_index); if (!user) { return X_ERROR_ACCESS_DENIED; } xe::ui::ImGuiDrawer* imgui_drawer = kernel_state()->emulator()->imgui_drawer(); auto close = [](ui::GamercardUI* dialog) -> void {}; return xeXamDispatchDialogAsync( new ui::GamercardUI(kernel_state()->emulator()->display_window(), imgui_drawer, kernel_state(), user->xuid()), close); } DECLARE_XAM_EXPORT1(XamShowGamerCardUI, kUserProfiles, kImplemented); dword_result_t XamShowEditProfileUI_entry(dword_t user_index) { auto user = kernel_state()->xam_state()->GetUserProfile(user_index); if (!user) { return X_ERROR_ACCESS_DENIED; } xe::ui::ImGuiDrawer* imgui_drawer = kernel_state()->emulator()->imgui_drawer(); auto close = [](ui::GamercardUI* dialog) -> void {}; return xeXamDispatchDialogAsync( new ui::GamercardUI(kernel_state()->emulator()->display_window(), imgui_drawer, kernel_state(), user->xuid()), close); } DECLARE_XAM_EXPORT1(XamShowEditProfileUI, kUserProfiles, kImplemented); } // namespace xam } // namespace kernel } // namespace xe DECLARE_XAM_EMPTY_REGISTER_EXPORTS(UI);