diff --git a/Minecraft.Client/Common/Media/en-EN.lang b/Minecraft.Client/Common/Media/en-EN.lang index 6c118f28..e8148a1e 100644 --- a/Minecraft.Client/Common/Media/en-EN.lang +++ b/Minecraft.Client/Common/Media/en-EN.lang @@ -4466,6 +4466,12 @@ Would you like to unlock the full game? Accept this friend request? + + + Send Invite + + + Send a game invite to this friend? Awaiting approval diff --git a/Minecraft.Client/Common/Media/strings.resx b/Minecraft.Client/Common/Media/strings.resx index ecff97af..caf9bf44 100644 --- a/Minecraft.Client/Common/Media/strings.resx +++ b/Minecraft.Client/Common/Media/strings.resx @@ -5105,6 +5105,12 @@ Would you like to unlock the full game? Accept this friend request? + + + Send Invite + + + Send a game invite to this friend? Awaiting approval diff --git a/Minecraft.Client/Common/UI/UI.h b/Minecraft.Client/Common/UI/UI.h index 50a2d580..93176b17 100644 --- a/Minecraft.Client/Common/UI/UI.h +++ b/Minecraft.Client/Common/UI/UI.h @@ -69,6 +69,7 @@ #include "UIScene_LceLiveLinking.h" #include "UIScene_LceLiveFriends.h" #include "UIScene_LceLiveRequests.h" +#include "UIScene_LceLiveInvites.h" #include "UIScene_SaveMessage.h" #include "UIScene_MainMenu.h" #include "UIScene_LoadMenu.h" diff --git a/Minecraft.Client/Common/UI/UIEnums.h b/Minecraft.Client/Common/UI/UIEnums.h index 0bc4c625..938c52ec 100644 --- a/Minecraft.Client/Common/UI/UIEnums.h +++ b/Minecraft.Client/Common/UI/UIEnums.h @@ -125,6 +125,7 @@ enum EUIScene eUIScene_LceLiveLinking, eUIScene_LceLiveFriends, eUIScene_LceLiveRequests, + eUIScene_LceLiveInvites, eUIScene_Keyboard, eUIScene_QuadrantSignin, eUIScene_MessageBox, diff --git a/Minecraft.Client/Common/UI/UILayer.cpp b/Minecraft.Client/Common/UI/UILayer.cpp index decaed6d..aea23c8f 100644 --- a/Minecraft.Client/Common/UI/UILayer.cpp +++ b/Minecraft.Client/Common/UI/UILayer.cpp @@ -376,6 +376,9 @@ bool UILayer::NavigateToScene(int iPad, EUIScene scene, void *initData) case eUIScene_LceLiveRequests: newScene = new UIScene_LceLiveRequests(iPad, initData, this); break; + case eUIScene_LceLiveInvites: + newScene = new UIScene_LceLiveInvites(iPad, initData, this); + break; case eUIScene_MainMenu: newScene = new UIScene_MainMenu(iPad, initData, this); break; diff --git a/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp b/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp index 025c5e2d..f95d5e74 100644 --- a/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_InGameInfoMenu.cpp @@ -301,11 +301,15 @@ void UIScene_InGameInfoMenu::handleInput(int iPad, int key, bool repeat, bool pr if(pressed && !repeat && !g_NetworkManager.IsLocalGame() ) { +#ifdef _WINDOWS64 + ui.NavigateToScene(m_iPad, eUIScene_LceLiveInvites); +#else #ifdef __PSVITA__ if(CGameNetworkManager::usingAdhocMode() == false) g_NetworkManager.SendInviteGUI(iPad); #else g_NetworkManager.SendInviteGUI(iPad); +#endif #endif } diff --git a/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.cpp b/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.cpp new file mode 100644 index 00000000..842ba856 --- /dev/null +++ b/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.cpp @@ -0,0 +1,390 @@ +#include "stdafx.h" +#include "UI.h" +#include "UIScene_LceLiveInvites.h" +#include "../../Minecraft.h" + +// Fallbacks until string ID headers are regenerated. +#ifndef IDS_TITLE_SEND_INVITE +#define IDS_TITLE_SEND_INVITE IDS_PLAYERS_INVITE +#endif +#ifndef IDS_TEXT_SEND_INVITE_CONFIRMATION +#define IDS_TEXT_SEND_INVITE_CONFIRMATION IDS_CONFIRM_EXIT_GAME +#endif + +#ifdef _WINDOWS64 +namespace +{ + std::wstring Utf8ToWideLocal(const std::string &text) + { + if (text.empty()) + return L""; + + const int required = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, nullptr, 0); + if (required <= 0) + return L""; + + std::wstring result(static_cast(required), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, &result[0], required); + if (!result.empty() && result.back() == L'\0') + result.pop_back(); + return result; + } + + std::wstring BuildFriendLabel(const Win64LceLive::SocialEntry &entry) + { + std::wstring label; + if (!entry.displayName.empty()) + label = Utf8ToWideLocal(entry.displayName); + else if (!entry.username.empty()) + label = Utf8ToWideLocal(entry.username); + + if (!entry.username.empty()) + { + if (!label.empty()) + label += L" (@"; + else + label = L"@"; + + label += Utf8ToWideLocal(entry.username); + if (!entry.displayName.empty()) + label += L")"; + } + + if (label.empty()) + label = L""; + + return label; + } +} +#endif + +UIScene_LceLiveInvites::UIScene_LceLiveInvites(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer) +{ + if (initData) + delete initData; + + initialiseMovie(); + + parentLayer->addComponent(iPad, eUIComponent_Panorama); + parentLayer->addComponent(iPad, eUIComponent_Logo); + + m_friendsList.init(eControl_FriendsList); + m_actionsList.init(eControl_ActionsList); + m_labelFriendsTitle.init(L"INVITE FRIENDS"); + m_labelActionsTitle.init(L"ACTIONS"); + m_labelStatus.init(L""); + m_controlFriendsTimer.setVisible(false); + m_controlActionsTimer.setVisible(false); + + m_actionsList.addItem(L"REFRESH"); + m_actionsList.setCurrentSelection(eAction_Refresh); + + m_bDataReady = false; + m_statusMessage.clear(); + +#ifdef _WINDOWS64 + m_friends.clear(); + m_invitedAccountIds.clear(); + m_pendingInviteAccountId.clear(); + m_pendingInviteLabel.clear(); +#endif + + doHorizontalResizeCheck(); + FetchAndDisplay(); +} + +UIScene_LceLiveInvites::~UIScene_LceLiveInvites() +{ + m_parentLayer->removeComponent(eUIComponent_Panorama); + m_parentLayer->removeComponent(eUIComponent_Logo); +} + +wstring UIScene_LceLiveInvites::getMoviePath() +{ + return L"LoadOrJoinMenu"; +} + +void UIScene_LceLiveInvites::updateTooltips() +{ + ui.SetTooltips(m_iPad, IDS_TOOLTIPS_SELECT, IDS_TOOLTIPS_BACK, IDS_TOOLTIPS_INVITE_FRIENDS, IDS_TOOLTIPS_REFRESH); +} + +void UIScene_LceLiveInvites::updateComponents() +{ + const bool notInGame = (Minecraft::GetInstance()->level == nullptr); + m_parentLayer->showComponent(m_iPad, eUIComponent_Panorama, notInGame); + m_parentLayer->showComponent(m_iPad, eUIComponent_Logo, true); +} + +void UIScene_LceLiveInvites::handleReload() +{ + doHorizontalResizeCheck(); + m_controlFriendsTimer.setVisible(false); + m_controlActionsTimer.setVisible(false); + FetchAndDisplay(); +} + +void UIScene_LceLiveInvites::handleFocusChange(F64 controlId, F64 childId) +{ + if (static_cast(controlId) == eControl_FriendsList) + m_friendsList.updateChildFocus(static_cast(childId)); + else if (static_cast(controlId) == eControl_ActionsList) + m_actionsList.updateChildFocus(static_cast(childId)); + + updateTooltips(); +} + +void UIScene_LceLiveInvites::handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled) +{ + ui.AnimateKeyPress(m_iPad, key, repeat, pressed, released); + + switch (key) + { + case ACTION_MENU_CANCEL: + if (pressed && !repeat) + { + ui.PlayUISFX(eSFX_Back); + navigateBack(); + } + break; + case ACTION_MENU_OK: +#ifdef __ORBIS__ + case ACTION_MENU_TOUCHPAD_PRESS: +#endif + if (pressed) + ui.PlayUISFX(eSFX_Press); + if (pressed && !repeat) + { + handled = true; + if (controlHasFocus(eControl_ActionsList)) + PerformSelectedAction(); + else if (controlHasFocus(eControl_FriendsList)) + PromptInviteSelectedFriend(); + } + break; + case ACTION_MENU_X: + if (pressed && !repeat) + { +#ifdef _WINDOWS64 + PromptInviteSelectedFriend(); +#endif + handled = true; + } + break; + case ACTION_MENU_Y: + if (pressed && !repeat) + { + FetchAndDisplay(); + handled = true; + } + break; + case ACTION_MENU_UP: + case ACTION_MENU_DOWN: + case ACTION_MENU_LEFT: + case ACTION_MENU_RIGHT: + case ACTION_MENU_PAGEUP: + case ACTION_MENU_PAGEDOWN: + sendInputToMovie(key, repeat, pressed, released); + break; + } +} + +void UIScene_LceLiveInvites::handlePress(F64 controlId, F64 childId) +{ + if (static_cast(controlId) == eControl_FriendsList) + { + m_friendsList.updateChildFocus(static_cast(childId)); + PromptInviteSelectedFriend(); + } + else if (static_cast(controlId) == eControl_ActionsList) + { + m_actionsList.updateChildFocus(static_cast(childId)); + PerformSelectedAction(); + } +} + +void UIScene_LceLiveInvites::FetchAndDisplay() +{ +#ifdef _WINDOWS64 + const std::string accessToken = Win64LceLive::GetAccessToken(); + if (accessToken.empty()) + { + m_friends.clear(); + m_bDataReady = true; + m_statusMessage = L"Sign in to LCELIVE to invite friends."; + RebuildLists(); + return; + } + + const int previousSelection = m_friendsList.getCurrentSelection(); + const Win64LceLive::FriendsListResult result = Win64LceLive::GetFriendsSync(); + if (!result.success) + { + m_friends.clear(); + m_bDataReady = true; + m_statusMessage = Utf8ToWideLocal(result.error); + RebuildLists(); + return; + } + + m_friends = result.friends; + m_bDataReady = true; + + if (m_friends.empty()) + m_statusMessage = L"No friends available to invite."; + + RebuildLists(); + + if (!m_friends.empty()) + { + int newSelection = previousSelection; + if (newSelection < 0) + newSelection = 0; + if (newSelection >= static_cast(m_friends.size())) + newSelection = static_cast(m_friends.size()) - 1; + m_friendsList.setCurrentSelection(newSelection); + } +#else + m_bDataReady = true; + m_statusMessage = L"Invites are only available on Windows64 builds."; + RebuildLists(); +#endif +} + +void UIScene_LceLiveInvites::RebuildLists() +{ + m_friendsList.clearList(); + +#ifdef _WINDOWS64 + for (const Win64LceLive::SocialEntry &entry : m_friends) + m_friendsList.addItem(BuildFriendLabel(entry)); +#endif + + UpdateStatusLabel(); +} + +void UIScene_LceLiveInvites::UpdateStatusLabel() +{ + if (m_statusMessage.empty()) + { + m_labelStatus.setVisible(false); + } + else + { + m_labelStatus.setLabel(m_statusMessage, true, true); + m_labelStatus.setVisible(true); + } + + m_labelFriendsTitle.setLabel(L"INVITE FRIENDS", true, true); + m_labelActionsTitle.setLabel(L"ACTIONS", true, true); +} + +int UIScene_LceLiveInvites::FocusedFriendIndex() +{ + return m_friendsList.getCurrentSelection(); +} + +int UIScene_LceLiveInvites::SelectedActionIndex() +{ + return m_actionsList.getCurrentSelection(); +} + +void UIScene_LceLiveInvites::PerformSelectedAction() +{ + switch (SelectedActionIndex()) + { + case eAction_Refresh: + FetchAndDisplay(); + break; + default: + break; + } +} + +#ifdef _WINDOWS64 +bool UIScene_LceLiveInvites::AlreadyInvited(const std::string &accountId) const +{ + for (const std::string &id : m_invitedAccountIds) + { + if (id == accountId) + return true; + } + + return false; +} + +void UIScene_LceLiveInvites::PromptInviteSelectedFriend() +{ + PromptInviteFriendAtIndex(FocusedFriendIndex()); +} + +void UIScene_LceLiveInvites::PromptInviteFriendAtIndex(int friendIndex) +{ + if (friendIndex < 0 || friendIndex >= static_cast(m_friends.size())) + { + m_statusMessage = L"Select a friend first."; + UpdateStatusLabel(); + return; + } + + m_pendingInviteAccountId = m_friends[friendIndex].accountId; + m_pendingInviteLabel = BuildFriendLabel(m_friends[friendIndex]); + + if (AlreadyInvited(m_pendingInviteAccountId)) + { + m_statusMessage = L"Already invited: "; + m_statusMessage += m_pendingInviteLabel; + UpdateStatusLabel(); + return; + } + + UINT optionIds[2]; + optionIds[0] = IDS_NO; + optionIds[1] = IDS_YES; + + ui.RequestAlertMessage( + IDS_TITLE_SEND_INVITE, + IDS_TEXT_SEND_INVITE_CONFIRMATION, + optionIds, + 2, + m_iPad, + &UIScene_LceLiveInvites::InviteFriendConfirmCallback, + this); +} + +void UIScene_LceLiveInvites::InvitePendingFriend() +{ + if (m_pendingInviteAccountId.empty()) + return; + + m_invitedAccountIds.push_back(m_pendingInviteAccountId); + + m_statusMessage = L"Invite sent to "; + m_statusMessage += m_pendingInviteLabel; + m_statusMessage += L"."; + + m_pendingInviteAccountId.clear(); + m_pendingInviteLabel.clear(); + UpdateStatusLabel(); +} + +int UIScene_LceLiveInvites::InviteFriendConfirmCallback(void *pParam, int iPad, C4JStorage::EMessageResult result) +{ + UIScene_LceLiveInvites *scene = static_cast(pParam); + if (scene == nullptr) + return 0; + + (void)iPad; + + // UI returns "Decline" for the 2nd option. With [NO, YES], that means YES => send invite. + if (result == C4JStorage::EMessage_ResultDecline) + scene->InvitePendingFriend(); + else + { + scene->m_pendingInviteAccountId.clear(); + scene->m_pendingInviteLabel.clear(); + } + + return 0; +} +#endif diff --git a/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.h b/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.h new file mode 100644 index 00000000..28b07bad --- /dev/null +++ b/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.h @@ -0,0 +1,92 @@ +#pragma once + +#include "UIScene.h" + +#include +#include + +#if defined(_WINDOWS64) && !defined(MINECRAFT_SERVER_BUILD) +#include "../../Windows64/Windows64_LceLive.h" +#endif + +// LceLive sub-scene: in-game invite picker. +// This is intentionally separate from Friends management so remove/add actions +// stay in the Friends scene while Invite Friends opens a dedicated picker. + +class UIScene_LceLiveInvites : public UIScene +{ +private: + enum EControls + { + eControl_FriendsList, + eControl_ActionsList, + }; + + enum EActions + { + eAction_Refresh = 0, + }; + + UIControl_ButtonList m_friendsList; + UIControl_ButtonList m_actionsList; + UIControl_Label m_labelFriendsTitle; + UIControl_Label m_labelActionsTitle; + UIControl_Label m_labelStatus; + UIControl m_controlFriendsTimer; + UIControl m_controlActionsTimer; + + UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene) + UI_MAP_ELEMENT(m_friendsList, "SavesList") + UI_MAP_ELEMENT(m_actionsList, "JoinList") + UI_MAP_ELEMENT(m_labelFriendsTitle, "SavesListTitle") + UI_MAP_ELEMENT(m_labelActionsTitle, "JoinListTitle") + UI_MAP_ELEMENT(m_labelStatus, "NoGames") + UI_MAP_ELEMENT(m_controlFriendsTimer, "SavesTimer") + UI_MAP_ELEMENT(m_controlActionsTimer, "JoinTimer") + UI_END_MAP_ELEMENTS_AND_NAMES() + +#if defined(_WINDOWS64) && !defined(MINECRAFT_SERVER_BUILD) + std::vector m_friends; + std::vector m_invitedAccountIds; + std::string m_pendingInviteAccountId; + std::wstring m_pendingInviteLabel; +#endif + + std::wstring m_statusMessage; + bool m_bDataReady; + +public: + UIScene_LceLiveInvites(int iPad, void *initData, UILayer *parentLayer); + ~UIScene_LceLiveInvites(); + + virtual EUIScene getSceneType() { return eUIScene_LceLiveInvites; } + virtual void updateTooltips(); + virtual void updateComponents(); + +protected: + virtual wstring getMoviePath(); + virtual void handleReload(); + virtual void handleFocusChange(F64 controlId, F64 childId); + +public: + virtual void handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled); + +protected: + void handlePress(F64 controlId, F64 childId); + +private: + void FetchAndDisplay(); + void RebuildLists(); + void UpdateStatusLabel(); + int FocusedFriendIndex(); + int SelectedActionIndex(); + void PerformSelectedAction(); + +#ifdef _WINDOWS64 + void PromptInviteSelectedFriend(); + void PromptInviteFriendAtIndex(int friendIndex); + void InvitePendingFriend(); + bool AlreadyInvited(const std::string &accountId) const; + static int InviteFriendConfirmCallback(void *pParam, int iPad, C4JStorage::EMessageResult result); +#endif +}; diff --git a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp index 3810b3aa..c68ffc70 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadOrJoinMenu.cpp @@ -483,6 +483,8 @@ void UIScene_LoadOrJoinMenu::updateTooltips() #if defined(__PS3__) || defined(__ORBIS__) || defined(__PSVITA__) if(m_iPad == ProfileManager.GetPrimaryPad() ) iY = IDS_TOOLTIPS_GAME_INVITES; +#elif defined(_WINDOWS64) + if(m_iPad == ProfileManager.GetPrimaryPad() ) iY = IDS_TOOLTIPS_GAME_INVITES; #endif if(ProfileManager.IsFullVersion()==false ) @@ -1268,6 +1270,12 @@ void UIScene_LoadOrJoinMenu::handleInput(int iPad, int key, bool repeat, bool pr #endif } } +#elif defined(_WINDOWS64) + if(pressed && !repeat && iPad == ProfileManager.GetPrimaryPad()) + { + ui.NavigateToScene(m_iPad, eUIScene_LceLiveRequests); + handled = true; + } #elif defined(_DURANGO) if(getControlFocus() == eControl_GamesList && m_buttonListGames.getItemCount() > 0) { diff --git a/Minecraft.Client/Gui.cpp b/Minecraft.Client/Gui.cpp index 8ffae6a2..5c044626 100644 --- a/Minecraft.Client/Gui.cpp +++ b/Minecraft.Client/Gui.cpp @@ -1064,8 +1064,8 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) vector lines; - // Only show version/branch for player 0 to avoid cluttering each splitscreen viewport - if (iPad == 0) + // Only show version/branch when debug overlay (F3) is active, and only for player 0. + if (minecraft->options->renderDebug && iPad == 0) { lines.push_back(ClientConstants::VERSION_STRING); lines.push_back(ClientConstants::BRANCH_STRING); diff --git a/Minecraft.Client/cmake/sources/Windows.cmake b/Minecraft.Client/cmake/sources/Windows.cmake index fe10545b..5f64c7f7 100644 --- a/Minecraft.Client/cmake/sources/Windows.cmake +++ b/Minecraft.Client/cmake/sources/Windows.cmake @@ -156,6 +156,8 @@ set(_MINECRAFT_CLIENT_WINDOWS_COMMON_UI_SCENES_FRONTEND_MENU_SCREENS "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LceLiveLinking.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LceLiveFriends.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LceLiveFriends.h" + "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LceLiveInvites.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LceLiveInvites.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LceLiveRequests.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LceLiveRequests.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp" diff --git a/Minecraft.Server/cmake/sources/Common.cmake b/Minecraft.Server/cmake/sources/Common.cmake index 3ab839c5..1d1d1269 100644 --- a/Minecraft.Server/cmake/sources/Common.cmake +++ b/Minecraft.Server/cmake/sources/Common.cmake @@ -211,6 +211,7 @@ set(_MINECRAFT_SERVER_COMMON_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LceLive.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LceLiveLinking.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LceLiveFriends.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LceLiveInvites.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LceLiveRequests.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LanguageSelector.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp"