diff --git a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp index e1cf6661..ec63bd06 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp @@ -6,6 +6,7 @@ #ifdef _WINDOWS64 #include "../../Windows64/Network/WinsockNetLayer.h" #include "../../Windows64/Windows64_Xuid.h" +#include "../../Windows64/Windows64_LceLive.h" #include "../../Minecraft.h" #include "../../User.h" #include "../../MinecraftServer.h" @@ -422,6 +423,7 @@ bool CPlatformNetworkManagerStub::LeaveGame(bool bMigrateHost) #ifdef _WINDOWS64 WinsockNetLayer::StopAdvertising(); + Win64LceLive::DeactivateGameInvitesSync(); #endif // If we are the host wait for the game server to end @@ -1039,7 +1041,37 @@ bool CPlatformNetworkManagerStub::IsHost() bool CPlatformNetworkManagerStub::JoinGameFromInviteInfo( int userIndex, int userMask, const INVITE_INFO *pInviteInfo) { + #ifdef _WINDOWS64 + if (pInviteInfo == nullptr || !pInviteInfo->sessionActive || pInviteInfo->hostPort <= 0 || pInviteInfo->hostIP[0] == 0) + return false; + + m_bLeavingGame = false; + m_bLeaveGameOnTick = false; + IQNet::s_isHosting = false; + m_pIQNet->ClientJoinGame(); + + IQNet::m_player[0].m_smallId = 0; + IQNet::m_player[0].m_isRemote = true; + IQNet::m_player[0].m_isHostPlayer = true; + IQNet::m_player[0].m_resolvedXuid = Win64Xuid::GetLegacyEmbeddedHostXuid(); + wcsncpy_s(IQNet::m_player[0].m_gamertag, 32, pInviteInfo->hostName, _TRUNCATE); + + WinsockNetLayer::StopDiscovery(); + + wcsncpy_s(m_joinHostName, 32, pInviteInfo->hostName, _TRUNCATE); + m_joinLocalUsersMask = userMask; + + if (!WinsockNetLayer::BeginJoinGame(pInviteInfo->hostIP, pInviteInfo->hostPort)) + { + app.DebugPrintf("Win64 invite: Failed to connect to %s:%d\n", pInviteInfo->hostIP, pInviteInfo->hostPort); + return false; + } + + m_bJoinPending = true; + return true; + #else return ( m_pIQNet->JoinGameFromInviteInfo( userIndex, userMask, pInviteInfo ) == S_OK); + #endif } void CPlatformNetworkManagerStub::SetSessionTexturePackParentId( int id ) diff --git a/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.cpp b/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.cpp index 36da9beb..9cb0874f 100644 --- a/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LceLiveInvites.cpp @@ -2,6 +2,7 @@ #include "UI.h" #include "UIScene_LceLiveInvites.h" #include "../../Minecraft.h" +#include "../../Windows64/Network/WinsockNetLayer.h" // Fallbacks until string ID headers are regenerated. #ifndef IDS_TITLE_SEND_INVITE @@ -212,6 +213,7 @@ void UIScene_LceLiveInvites::FetchAndDisplay() if (accessToken.empty()) { m_friends.clear(); + m_invitedAccountIds.clear(); m_bDataReady = true; m_statusMessage = L"Sign in to LCELIVE to invite friends."; RebuildLists(); @@ -230,6 +232,16 @@ void UIScene_LceLiveInvites::FetchAndDisplay() } m_friends = result.friends; + m_invitedAccountIds.clear(); + const Win64LceLive::GameInvitesResult inviteState = Win64LceLive::GetGameInvitesSync(); + if (inviteState.success) + { + for (const Win64LceLive::GameInviteEntry &invite : inviteState.outgoing) + { + if (invite.status == "pending") + m_invitedAccountIds.push_back(invite.recipientAccountId); + } + } m_bDataReady = true; if (m_friends.empty()) @@ -259,7 +271,12 @@ void UIScene_LceLiveInvites::RebuildLists() #ifdef _WINDOWS64 for (const Win64LceLive::SocialEntry &entry : m_friends) - m_friendsList.addItem(BuildFriendLabel(entry)); + { + std::wstring label = BuildFriendLabel(entry); + if (AlreadyInvited(entry.accountId)) + label += L" [SENT]"; + m_friendsList.addItem(label); + } #endif UpdateStatusLabel(); @@ -332,14 +349,6 @@ void UIScene_LceLiveInvites::PromptInviteFriendAtIndex(int friendIndex) 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; @@ -359,6 +368,29 @@ void UIScene_LceLiveInvites::InvitePendingFriend() if (m_pendingInviteAccountId.empty()) return; + if (!g_NetworkManager.IsHost() || !WinsockNetLayer::IsHosting() || g_Win64MultiplayerIP[0] == 0 || g_Win64MultiplayerPort <= 0) + { + m_statusMessage = L"The game session is no longer active."; + m_pendingInviteAccountId.clear(); + m_pendingInviteLabel.clear(); + UpdateStatusLabel(); + return; + } + + const Win64LceLive::SocialActionResult result = Win64LceLive::SendGameInviteSync( + m_pendingInviteAccountId, + g_Win64MultiplayerIP, + g_Win64MultiplayerPort, + ""); + if (!result.success) + { + m_statusMessage = Utf8ToWideLocal(result.error); + m_pendingInviteAccountId.clear(); + m_pendingInviteLabel.clear(); + UpdateStatusLabel(); + return; + } + m_invitedAccountIds.push_back(m_pendingInviteAccountId); m_statusMessage = L"Invite sent to "; diff --git a/Minecraft.Client/Common/UI/UIScene_LceLiveRequests.cpp b/Minecraft.Client/Common/UI/UIScene_LceLiveRequests.cpp index ec7e52a6..1934d37b 100644 --- a/Minecraft.Client/Common/UI/UIScene_LceLiveRequests.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LceLiveRequests.cpp @@ -10,6 +10,12 @@ #ifndef IDS_TEXT_ACCEPT_REQUEST_CONFIRMATION #define IDS_TEXT_ACCEPT_REQUEST_CONFIRMATION IDS_CONFIRM_EXIT_GAME #endif +#ifndef IDS_TITLE_SEND_INVITE +#define IDS_TITLE_SEND_INVITE IDS_TITLE_FRIEND_REQUEST +#endif +#ifndef IDS_TEXT_SEND_INVITE_CONFIRMATION +#define IDS_TEXT_SEND_INVITE_CONFIRMATION IDS_TEXT_ACCEPT_REQUEST_CONFIRMATION +#endif #ifdef _WINDOWS64 namespace @@ -30,24 +36,37 @@ namespace return result; } - std::wstring BuildRequestLabel(bool incoming, const std::string &username, const std::string &displayName) + std::wstring BuildInviteLabel( + const std::string &senderDisplayName, + const std::string &senderUsername, + const std::string &hostName, + bool sessionActive) { - std::wstring line = incoming ? L"[IN] " : L"[OUT] "; + std::wstring line = L"[INVITE] "; - if (!displayName.empty()) - line += Utf8ToWideLocal(displayName); - else if (!username.empty()) - line += Utf8ToWideLocal(username); + if (!senderDisplayName.empty()) + line += Utf8ToWideLocal(senderDisplayName); + else if (!senderUsername.empty()) + line += Utf8ToWideLocal(senderUsername); else line += L""; - if (!username.empty()) + if (!senderUsername.empty()) { line += L" (@"; - line += Utf8ToWideLocal(username); + line += Utf8ToWideLocal(senderUsername); line += L")"; } + if (!hostName.empty()) + { + line += L" - "; + line += Utf8ToWideLocal(hostName); + } + + if (!sessionActive) + line += L" [inactive]"; + return line; } } @@ -62,7 +81,7 @@ UIScene_LceLiveRequests::UIScene_LceLiveRequests(int iPad, void *initData, UILay m_requestsList.init(eControl_RequestsList); m_actionsList.init(eControl_ActionsList); - m_labelRequestsTitle.init(L"REQUESTS"); + m_labelRequestsTitle.init(L"GAME INVITES"); m_labelActionsTitle.init(L"ACTIONS"); m_labelStatus.init(L""); m_controlRequestsTimer.setVisible(false); @@ -75,8 +94,10 @@ UIScene_LceLiveRequests::UIScene_LceLiveRequests(int iPad, void *initData, UILay m_statusMessage.clear(); m_bDataReady = false; #ifdef _WINDOWS64 - m_pendingRequestAccountId.clear(); - m_pendingRequestIncoming = false; + m_pendingInviteId.clear(); + m_pendingInviteHostIp.clear(); + m_pendingInviteHostPort = 0; + m_pendingInviteHostName.clear(); #endif doHorizontalResizeCheck(); @@ -199,13 +220,13 @@ void UIScene_LceLiveRequests::FetchAndDisplay() { m_entries.clear(); m_bDataReady = true; - m_statusMessage = L"Sign in to view and manage friend requests."; + m_statusMessage = L"Sign in to view and manage game invites."; RebuildLists(); return; } const int previousSelection = m_requestsList.getCurrentSelection(); - const Win64LceLive::PendingRequestsResult result = Win64LceLive::GetPendingRequestsSync(); + const Win64LceLive::GameInvitesResult result = Win64LceLive::GetGameInvitesSync(); if (!result.success) { m_entries.clear(); @@ -216,29 +237,28 @@ void UIScene_LceLiveRequests::FetchAndDisplay() } m_entries.clear(); - for (const Win64LceLive::SocialEntry &entry : result.incoming) + for (const Win64LceLive::GameInviteEntry &entry : result.incoming) { RequestEntry row = {}; - row.incoming = true; - row.accountId = entry.accountId; - row.username = entry.username; - row.displayName = entry.displayName; - m_entries.push_back(row); - } - for (const Win64LceLive::SocialEntry &entry : result.outgoing) - { - RequestEntry row = {}; - row.incoming = false; - row.accountId = entry.accountId; - row.username = entry.username; - row.displayName = entry.displayName; + row.inviteId = entry.inviteId; + row.senderAccountId = entry.senderAccountId; + row.senderUsername = entry.senderUsername; + row.senderDisplayName = entry.senderDisplayName; + row.recipientAccountId = entry.recipientAccountId; + row.recipientUsername = entry.recipientUsername; + row.recipientDisplayName = entry.recipientDisplayName; + row.hostIp = entry.hostIp; + row.hostPort = entry.hostPort; + row.hostName = entry.hostName; + row.status = entry.status; + row.sessionActive = entry.sessionActive; m_entries.push_back(row); } m_bDataReady = true; if (m_entries.empty() && m_statusMessage.empty()) - m_statusMessage = L"No pending requests."; - else if (!m_entries.empty() && m_statusMessage == L"No pending requests.") + m_statusMessage = L"No pending game invites."; + else if (!m_entries.empty() && m_statusMessage == L"No pending game invites.") m_statusMessage.clear(); RebuildLists(); @@ -255,7 +275,7 @@ void UIScene_LceLiveRequests::FetchAndDisplay() #else m_entries.clear(); m_bDataReady = true; - m_statusMessage = L"Requests are only available on Windows64 builds."; + m_statusMessage = L"Game invites are only available on Windows64 builds."; RebuildLists(); #endif } @@ -266,7 +286,7 @@ void UIScene_LceLiveRequests::RebuildLists() #ifdef _WINDOWS64 for (const RequestEntry &entry : m_entries) - m_requestsList.addItem(BuildRequestLabel(entry.incoming, entry.username, entry.displayName)); + m_requestsList.addItem(BuildInviteLabel(entry.senderDisplayName, entry.senderUsername, entry.hostName, entry.sessionActive)); #endif UpdateStatusLabel(); @@ -284,7 +304,7 @@ void UIScene_LceLiveRequests::UpdateStatusLabel() m_labelStatus.setVisible(true); } - m_labelRequestsTitle.setLabel(L"REQUESTS", true, true); + m_labelRequestsTitle.setLabel(L"GAME INVITES", true, true); m_labelActionsTitle.setLabel(L"ACTIONS", true, true); } @@ -315,29 +335,24 @@ void UIScene_LceLiveRequests::PromptResolveSelectedRequest() const int selectedIndex = FocusedRequestIndex(); if (selectedIndex < 0 || selectedIndex >= static_cast(m_entries.size())) { - m_statusMessage = L"Select a request first."; - UpdateStatusLabel(); - return; - } - - if (!m_entries[selectedIndex].incoming) - { - m_statusMessage = L"Outgoing requests are waiting on the other player."; + m_statusMessage = L"Select an invite first."; UpdateStatusLabel(); return; } #ifdef _WINDOWS64 - m_pendingRequestAccountId = m_entries[selectedIndex].accountId; - m_pendingRequestIncoming = true; + m_pendingInviteId = m_entries[selectedIndex].inviteId; + m_pendingInviteHostIp = m_entries[selectedIndex].hostIp; + m_pendingInviteHostPort = m_entries[selectedIndex].hostPort; + m_pendingInviteHostName = m_entries[selectedIndex].hostName; UINT optionIds[2]; optionIds[0] = IDS_NO; optionIds[1] = IDS_YES; ui.RequestAlertMessage( - IDS_TITLE_FRIEND_REQUEST, - IDS_TEXT_ACCEPT_REQUEST_CONFIRMATION, + IDS_TITLE_SEND_INVITE, + IDS_TEXT_SEND_INVITE_CONFIRMATION, optionIds, 2, m_iPad, @@ -352,26 +367,11 @@ void UIScene_LceLiveRequests::PerformAccept() const int selectedIndex = FocusedRequestIndex(); if (selectedIndex < 0 || selectedIndex >= static_cast(m_entries.size())) { - m_statusMessage = L"Select a request first."; + m_statusMessage = L"Select an invite first."; UpdateStatusLabel(); return; } - - if (!m_entries[selectedIndex].incoming) - { - m_statusMessage = L"Only incoming requests can be accepted."; - UpdateStatusLabel(); - return; - } - - const std::string accountId = m_entries[selectedIndex].accountId; - const Win64LceLive::SocialActionResult result = Win64LceLive::AcceptFriendRequestSync(accountId); - if (result.success) - m_statusMessage = L"Friend request accepted."; - else - m_statusMessage = Utf8ToWideLocal(result.error); - - FetchAndDisplay(); + ResolvePendingRequest(true); #endif } @@ -381,48 +381,95 @@ void UIScene_LceLiveRequests::PerformDecline() const int selectedIndex = FocusedRequestIndex(); if (selectedIndex < 0 || selectedIndex >= static_cast(m_entries.size())) { - m_statusMessage = L"Select a request first."; + m_statusMessage = L"Select an invite first."; UpdateStatusLabel(); return; } - - if (!m_entries[selectedIndex].incoming) - { - m_statusMessage = L"Only incoming requests can be declined."; - UpdateStatusLabel(); - return; - } - - const std::string accountId = m_entries[selectedIndex].accountId; - const Win64LceLive::SocialActionResult result = Win64LceLive::DeclineFriendRequestSync(accountId); - if (result.success) - m_statusMessage = L"Friend request declined."; - else - m_statusMessage = Utf8ToWideLocal(result.error); - - FetchAndDisplay(); + ResolvePendingRequest(false); #endif } #ifdef _WINDOWS64 void UIScene_LceLiveRequests::ResolvePendingRequest(bool accept) { - if (!m_pendingRequestIncoming || m_pendingRequestAccountId.empty()) + if (m_pendingInviteId.empty()) return; - const std::string accountId = m_pendingRequestAccountId; - m_pendingRequestAccountId.clear(); - m_pendingRequestIncoming = false; + const std::string inviteId = m_pendingInviteId; - const Win64LceLive::SocialActionResult result = accept - ? Win64LceLive::AcceptFriendRequestSync(accountId) - : Win64LceLive::DeclineFriendRequestSync(accountId); + if (accept) + { + const Win64LceLive::GameInviteActionResult result = Win64LceLive::AcceptGameInviteSync(inviteId); + if (result.success) + { + m_pendingInviteHostIp = result.hostIp; + m_pendingInviteHostPort = result.hostPort; + m_pendingInviteHostName = result.hostName; + JoinAcceptedInvite(); + m_pendingInviteId.clear(); + return; + } - if (result.success) - m_statusMessage = accept ? L"Friend request accepted." : L"Friend request declined."; - else m_statusMessage = Utf8ToWideLocal(result.error); + UpdateStatusLabel(); + FetchAndDisplay(); + m_pendingInviteId.clear(); + return; + } + const Win64LceLive::SocialActionResult declineResult = Win64LceLive::DeclineGameInviteSync(inviteId); + if (declineResult.success) + m_statusMessage = L"Game invite declined."; + else + m_statusMessage = Utf8ToWideLocal(declineResult.error); + m_pendingInviteId.clear(); + + FetchAndDisplay(); +} + +void UIScene_LceLiveRequests::JoinAcceptedInvite() +{ + if (m_pendingInviteHostIp.empty() || m_pendingInviteHostPort <= 0) + { + m_statusMessage = L"The game session is no longer active."; + UpdateStatusLabel(); + FetchAndDisplay(); + return; + } + + ProfileManager.SetLockedProfile(m_iPad); + ProfileManager.SetPrimaryPad(m_iPad); + g_NetworkManager.SetLocalGame(false); + ProfileManager.QuerySigninStatus(); + Minecraft::GetInstance()->clearConnectionFailed(); + + int localUsersMask = 0; + for (unsigned int index = 0; index < XUSER_MAX_COUNT; ++index) + { + if (ProfileManager.IsSignedIn(index)) + localUsersMask |= g_NetworkManager.GetLocalPlayerMask(index); + } + + INVITE_INFO inviteInfo = {}; + inviteInfo.netVersion = MINECRAFT_NET_VERSION; + strcpy_s(inviteInfo.hostIP, m_pendingInviteHostIp.c_str()); + inviteInfo.hostPort = m_pendingInviteHostPort; + inviteInfo.sessionActive = true; + const std::wstring hostNameWide = m_pendingInviteHostName.empty() ? L"LCELive" : Utf8ToWideLocal(m_pendingInviteHostName); + wcsncpy_s(inviteInfo.hostName, hostNameWide.c_str(), _TRUNCATE); + strcpy_s(inviteInfo.inviteId, m_pendingInviteId.c_str()); + + const bool success = g_NetworkManager.JoinGameFromInviteInfo(m_iPad, localUsersMask, &inviteInfo); + if (!success) + m_statusMessage = L"Could not join this game."; + else + m_statusMessage = L"Joining game..."; + + m_pendingInviteId.clear(); + m_pendingInviteHostIp.clear(); + m_pendingInviteHostPort = 0; + m_pendingInviteHostName.clear(); + UpdateStatusLabel(); FetchAndDisplay(); } diff --git a/Minecraft.Client/Common/UI/UIScene_LceLiveRequests.h b/Minecraft.Client/Common/UI/UIScene_LceLiveRequests.h index 07155a7d..169481cb 100644 --- a/Minecraft.Client/Common/UI/UIScene_LceLiveRequests.h +++ b/Minecraft.Client/Common/UI/UIScene_LceLiveRequests.h @@ -9,7 +9,7 @@ #include "../../Windows64/Windows64_LceLive.h" #endif -// LceLive sub-scene: friend requests list. +// LceLive sub-scene: game invite inbox. // Uses the native Start Game style (LoadOrJoinMenu): left list of requests, // right list of actions. @@ -29,10 +29,18 @@ private: struct RequestEntry { - bool incoming; - std::string accountId; - std::string username; - std::string displayName; + std::string inviteId; + std::string senderAccountId; + std::string senderUsername; + std::string senderDisplayName; + std::string recipientAccountId; + std::string recipientUsername; + std::string recipientDisplayName; + std::string hostIp; + int hostPort; + std::string hostName; + std::string status; + bool sessionActive; }; UIControl_ButtonList m_requestsList; @@ -46,8 +54,10 @@ private: std::wstring m_statusMessage; bool m_bDataReady; #ifdef _WINDOWS64 - std::string m_pendingRequestAccountId; - bool m_pendingRequestIncoming; + std::string m_pendingInviteId; + std::string m_pendingInviteHostIp; + int m_pendingInviteHostPort; + std::string m_pendingInviteHostName; #endif UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene) UI_MAP_ELEMENT(m_requestsList, "SavesList") @@ -90,6 +100,7 @@ private: void PerformDecline(); #ifdef _WINDOWS64 void ResolvePendingRequest(bool accept); + void JoinAcceptedInvite(); static int ResolveRequestConfirmCallback(void *pParam, int iPad, C4JStorage::EMessageResult result); #endif }; diff --git a/Minecraft.Client/Windows64/Windows64_LceLive.cpp b/Minecraft.Client/Windows64/Windows64_LceLive.cpp index 3a406fa5..a832e54b 100644 --- a/Minecraft.Client/Windows64/Windows64_LceLive.cpp +++ b/Minecraft.Client/Windows64/Windows64_LceLive.cpp @@ -148,6 +148,22 @@ namespace return it->get(); } + int JsonIntOrDefault(const Json &object, const char *key, int defaultValue) + { + const Json::const_iterator it = object.find(key); + if (it == object.end() || !it->is_number_integer()) + return defaultValue; + return it->get(); + } + + bool JsonBoolOrDefault(const Json &object, const char *key, bool defaultValue) + { + const Json::const_iterator it = object.find(key); + if (it == object.end() || !it->is_boolean()) + return defaultValue; + return it->get(); + } + std::string ParseErrorMessage(const std::string &responseBody, const std::string &fallback); void TrimTrailingSlashes(std::string *value) @@ -1401,6 +1417,214 @@ namespace Win64LceLive return { true, std::string() }; } + + GameInvitesResult GetGameInvitesSync() + { + EnsureInitialized(); + std::string accessToken; + EnterCriticalSection(&g_state.lock); + if (g_state.session.valid) + accessToken = g_state.session.accessToken; + LeaveCriticalSection(&g_state.lock); + + if (accessToken.empty()) + return { false, {}, {}, "Not signed in to LCELive." }; + + RequestContext req = {}; + req.type = ERequestType::None; + req.path = "/api/sessions/invites"; + req.authorization = "Bearer " + accessToken; + + DWORD status = 0; + std::string responseBody; + if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + return { false, {}, {}, ParseErrorMessage(responseBody, "Failed to get game invites.") }; + + const Json responseJson = Json::parse(responseBody, nullptr, false); + if (!responseJson.is_object()) + return { false, {}, {}, "Invalid game invites response." }; + + auto parseList = [&](const char *key) + { + std::vector result; + const Json::const_iterator it = responseJson.find(key); + if (it != responseJson.end() && it->is_array()) + { + for (const Json &entry : *it) + { + if (!entry.is_object()) + continue; + + GameInviteEntry invite = {}; + invite.inviteId = JsonStringOrEmpty(entry, "inviteId"); + invite.senderAccountId = JsonStringOrEmpty(entry, "senderAccountId"); + invite.senderUsername = JsonStringOrEmpty(entry, "senderUsername"); + invite.senderDisplayName = JsonStringOrEmpty(entry, "senderDisplayName"); + invite.recipientAccountId = JsonStringOrEmpty(entry, "recipientAccountId"); + invite.recipientUsername = JsonStringOrEmpty(entry, "recipientUsername"); + invite.recipientDisplayName = JsonStringOrEmpty(entry, "recipientDisplayName"); + invite.hostIp = JsonStringOrEmpty(entry, "hostIp"); + invite.hostPort = JsonIntOrDefault(entry, "hostPort", 0); + invite.hostName = JsonStringOrEmpty(entry, "hostName"); + invite.status = JsonStringOrEmpty(entry, "status"); + invite.sessionActive = JsonBoolOrDefault(entry, "sessionActive", false); + invite.createdUtc = JsonStringOrEmpty(entry, "createdAtUtc"); + invite.expiresUtc = JsonStringOrEmpty(entry, "expiresAtUtc"); + result.push_back(invite); + } + } + return result; + }; + + return { true, parseList("incoming"), parseList("outgoing"), std::string() }; + } + + SocialActionResult SendGameInviteSync(const std::string &recipientAccountId, const std::string &hostIp, int hostPort, const std::string &hostName) + { + EnsureInitialized(); + std::string accessToken; + std::string refreshToken; + EnterCriticalSection(&g_state.lock); + if (g_state.session.valid) + { + accessToken = g_state.session.accessToken; + refreshToken = g_state.session.refreshToken; + } + LeaveCriticalSection(&g_state.lock); + + if (accessToken.empty()) + return { false, "Not signed in to LCELive." }; + + Json bodyJson; + bodyJson["recipientAccountId"] = recipientAccountId; + bodyJson["hostIp"] = hostIp; + bodyJson["hostPort"] = hostPort; + bodyJson["hostName"] = hostName; + + RequestContext req = {}; + req.type = ERequestType::None; + req.path = "/api/sessions/invites"; + req.body = bodyJson.dump(); + req.authorization = "Bearer " + accessToken; + + DWORD status = 0; + std::string responseBody; + if (!PerformJsonRequest(req, &status, &responseBody)) + return { false, "Failed to contact LCELive while sending game invite." }; + + if (status == 401 && !refreshToken.empty()) + { + std::string refreshError; + if (RefreshSessionSync(&refreshError)) + { + EnterCriticalSection(&g_state.lock); + accessToken = g_state.session.accessToken; + LeaveCriticalSection(&g_state.lock); + + req.authorization = "Bearer " + accessToken; + responseBody.clear(); + if (!PerformJsonRequest(req, &status, &responseBody)) + return { false, "Failed to contact LCELive while sending game invite." }; + } + else + { + return { false, refreshError }; + } + } + + if (status < 200 || status >= 300) + return { false, ParseErrorMessage(responseBody, "Failed to send game invite.") }; + + return { true, std::string() }; + } + + GameInviteActionResult AcceptGameInviteSync(const std::string &inviteId) + { + EnsureInitialized(); + std::string accessToken; + EnterCriticalSection(&g_state.lock); + if (g_state.session.valid) + accessToken = g_state.session.accessToken; + LeaveCriticalSection(&g_state.lock); + + if (accessToken.empty()) + return { false, std::string(), std::string(), 0, std::string(), "Not signed in to LCELive." }; + + RequestContext req = {}; + req.type = ERequestType::None; + req.path = "/api/sessions/invites/" + inviteId + "/accept"; + req.body = "{}"; + req.authorization = "Bearer " + accessToken; + + DWORD status = 0; + std::string responseBody; + if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + return { false, std::string(), std::string(), 0, std::string(), ParseErrorMessage(responseBody, "Failed to accept game invite.") }; + + const Json responseJson = Json::parse(responseBody, nullptr, false); + if (!responseJson.is_object()) + return { false, std::string(), std::string(), 0, std::string(), "Invalid game invite response." }; + + GameInviteActionResult result = {}; + result.success = true; + result.inviteId = JsonStringOrEmpty(responseJson, "inviteId"); + result.hostIp = JsonStringOrEmpty(responseJson, "hostIp"); + result.hostPort = JsonIntOrDefault(responseJson, "hostPort", 0); + result.hostName = JsonStringOrEmpty(responseJson, "hostName"); + return result; + } + + SocialActionResult DeclineGameInviteSync(const std::string &inviteId) + { + EnsureInitialized(); + std::string accessToken; + EnterCriticalSection(&g_state.lock); + if (g_state.session.valid) + accessToken = g_state.session.accessToken; + LeaveCriticalSection(&g_state.lock); + + if (accessToken.empty()) + return { false, "Not signed in to LCELive." }; + + RequestContext req = {}; + req.type = ERequestType::None; + req.path = "/api/sessions/invites/" + inviteId + "/decline"; + req.body = "{}"; + req.authorization = "Bearer " + accessToken; + + DWORD status = 0; + std::string responseBody; + if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + return { false, ParseErrorMessage(responseBody, "Failed to decline game invite.") }; + + return { true, std::string() }; + } + + SocialActionResult DeactivateGameInvitesSync() + { + EnsureInitialized(); + std::string accessToken; + EnterCriticalSection(&g_state.lock); + if (g_state.session.valid) + accessToken = g_state.session.accessToken; + LeaveCriticalSection(&g_state.lock); + + if (accessToken.empty()) + return { false, "Not signed in to LCELive." }; + + RequestContext req = {}; + req.type = ERequestType::None; + req.path = "/api/sessions/invites/deactivate"; + req.body = "{}"; + req.authorization = "Bearer " + accessToken; + + DWORD status = 0; + std::string responseBody; + if (!PerformJsonRequest(req, &status, &responseBody) || status < 200 || status >= 300) + return { false, ParseErrorMessage(responseBody, "Failed to deactivate game invites.") }; + + return { true, std::string() }; + } } #endif diff --git a/Minecraft.Client/Windows64/Windows64_LceLive.h b/Minecraft.Client/Windows64/Windows64_LceLive.h index 1d60a25b..ec200380 100644 --- a/Minecraft.Client/Windows64/Windows64_LceLive.h +++ b/Minecraft.Client/Windows64/Windows64_LceLive.h @@ -67,6 +67,42 @@ namespace Win64LceLive std::string error; }; + struct GameInviteEntry + { + std::string inviteId; + std::string senderAccountId; + std::string senderUsername; + std::string senderDisplayName; + std::string recipientAccountId; + std::string recipientUsername; + std::string recipientDisplayName; + std::string hostIp; + int hostPort; + std::string hostName; + std::string status; + bool sessionActive; + std::string createdUtc; + std::string expiresUtc; + }; + + struct GameInvitesResult + { + bool success; + std::vector incoming; + std::vector outgoing; + std::string error; + }; + + struct GameInviteActionResult + { + bool success; + std::string inviteId; + std::string hostIp; + int hostPort; + std::string hostName; + std::string error; + }; + void Tick(); Snapshot GetSnapshot(); bool StartDeviceLink(); @@ -95,6 +131,12 @@ namespace Win64LceLive SocialActionResult AcceptFriendRequestSync(const std::string& fromAccountId); SocialActionResult DeclineFriendRequestSync(const std::string& fromAccountId); SocialActionResult RemoveFriendSync(const std::string& accountId); + + GameInvitesResult GetGameInvitesSync(); + SocialActionResult SendGameInviteSync(const std::string& recipientAccountId, const std::string& hostIp, int hostPort, const std::string& hostName); + GameInviteActionResult AcceptGameInviteSync(const std::string& inviteId); + SocialActionResult DeclineGameInviteSync(const std::string& inviteId); + SocialActionResult DeactivateGameInvitesSync(); } #endif diff --git a/Minecraft.World/x64headers/extraX64.h b/Minecraft.World/x64headers/extraX64.h index 03f1b6ac..714b722a 100644 --- a/Minecraft.World/x64headers/extraX64.h +++ b/Minecraft.World/x64headers/extraX64.h @@ -66,7 +66,25 @@ typedef DQRNetworkManager::SessionInfo INVITE_INFO; typedef ULONGLONG PlayerUID; typedef ULONGLONG SessionID; typedef PlayerUID GameSessionUID; -class INVITE_INFO; +struct INVITE_INFO +{ + INVITE_INFO() + : netVersion(0) + , hostPort(0) + , sessionActive(false) + { + hostIP[0] = 0; + hostName[0] = 0; + inviteId[0] = 0; + } + + unsigned short netVersion; + char hostIP[64]; + int hostPort; + wchar_t hostName[32]; + char inviteId[64]; + bool sessionActive; +}; #endif // __PS3__