From 5cd7d1c8f01c8ff8f016bb0712013713bf60afcb Mon Sep 17 00:00:00 2001 From: maybegreat48 <96936658+maybegreat48@users.noreply.github.com> Date: Wed, 9 Apr 2025 13:12:31 -0400 Subject: [PATCH] feat(network): join by username --- src/game/backend/SavedPlayers.cpp | 13 ++++ src/game/backend/SavedPlayers.hpp | 6 ++ src/game/frontend/submenus/Network.cpp | 19 ++++++ .../submenus/Network/SavedPlayers.cpp | 32 +++++++++- src/game/gta/Network.cpp | 39 ++++++++++++ src/game/gta/Network.hpp | 1 + src/game/pointers/Pointers.cpp | 5 ++ src/game/pointers/Pointers.hpp | 5 ++ src/types/rage/atArray.hpp | 10 ++-- src/types/socialclub/rlGetAvatarsContext.hpp | 59 +++++++++++++++++++ .../socialclub/rlQueryAccountsResult.hpp | 27 +++++++++ 11 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 src/types/socialclub/rlGetAvatarsContext.hpp create mode 100644 src/types/socialclub/rlQueryAccountsResult.hpp diff --git a/src/game/backend/SavedPlayers.cpp b/src/game/backend/SavedPlayers.cpp index b6f9301..fae47e7 100644 --- a/src/game/backend/SavedPlayers.cpp +++ b/src/game/backend/SavedPlayers.cpp @@ -270,6 +270,19 @@ namespace YimMenu return res; } + void SavedPlayers::AddPlayerDataImpl(std::uint64_t id, std::string_view username) + { + if (auto data = GetPlayerDataImpl(id)) + return; + + SavedPlayerData data; + data.m_Name = username; + + m_SavedPlayers.emplace(id, data); + + Save(); + } + void SavedPlayers::UpdateRockstarIdImpl(std::uint64_t id, std::uint64_t new_id) { if (auto player = GetPlayerDataImpl(id)) diff --git a/src/game/backend/SavedPlayers.hpp b/src/game/backend/SavedPlayers.hpp index 9bf5809..020eefe 100644 --- a/src/game/backend/SavedPlayers.hpp +++ b/src/game/backend/SavedPlayers.hpp @@ -23,6 +23,7 @@ namespace YimMenu void FetchPlayerInfoImpl(bool tracked_only = false); SavedPlayerData* GetPlayerDataImpl(std::uint64_t id); SavedPlayerData* GetPlayerDataImpl(Player player, bool create = true); + void AddPlayerDataImpl(std::uint64_t id, std::string_view username); void UpdateRockstarIdImpl(std::uint64_t id, std::uint64_t new_id); void RemovePlayerDataImpl(std::uint64_t id); void LoadImpl(); @@ -56,6 +57,11 @@ namespace YimMenu return GetInstance().GetPlayerDataImpl(player, create); } + static void AddPlayerData(std::uint64_t id, std::string_view username) + { + GetInstance().AddPlayerDataImpl(id, username); + } + // TODO: this cannot be safely called outside the saved players frontend menu. Invalidates all SavedPlayerData pointers due to potential map reallocation static void UpdateRockstarId(std::uint64_t old_id, std::uint64_t new_id) { diff --git a/src/game/frontend/submenus/Network.cpp b/src/game/frontend/submenus/Network.cpp index 992c199..cce7d4c 100644 --- a/src/game/frontend/submenus/Network.cpp +++ b/src/game/frontend/submenus/Network.cpp @@ -1,5 +1,6 @@ #include "Network.hpp" #include "core/backend/FiberPool.hpp" +#include "core/frontend/Notifications.hpp" #include "game/frontend/items/Items.hpp" #include "game/frontend/submenus/Network/SavedPlayers.hpp" #include "game/gta/Network.hpp" @@ -25,6 +26,24 @@ namespace YimMenu::Submenus joinGroup->AddItem(joinSession); joinGroup->AddItem(std::make_shared([] { static std::uint64_t rockstar_id{}; + static char name_buf[24]{}; + + ImGui::SetNextItemWidth(180.0f); + ImGui::InputText("Username", name_buf, sizeof(name_buf)); + ImGui::SameLine(); + if (ImGui::Button("Join##username")) + FiberPool::Push([] { + auto rid = YimMenu::Network::ResolveRockstarId(name_buf); + if (rid) + { + YimMenu::Network::JoinRockstarId(*rid); + } + else + { + Notifications::Show("Joiner", "Failed to get RID from username", NotificationType::Error); + } + }); + ImGui::SetNextItemWidth(150.0f); ImGui::InputScalar("Rockstar Id", ImGuiDataType_U64, &rockstar_id); ImGui::SameLine(); diff --git a/src/game/frontend/submenus/Network/SavedPlayers.cpp b/src/game/frontend/submenus/Network/SavedPlayers.cpp index 20ce1ab..cf516cd 100644 --- a/src/game/frontend/submenus/Network/SavedPlayers.cpp +++ b/src/game/frontend/submenus/Network/SavedPlayers.cpp @@ -1,6 +1,7 @@ #include "SavedPlayers.hpp" #include "core/backend/FiberPool.hpp" #include "core/frontend/widgets/imgui_colors.h" +#include "core/frontend/Notifications.hpp" #include "game/backend/SavedPlayers.hpp" #include "game/gta/Network.hpp" #include "game/pointers/Pointers.hpp" @@ -72,7 +73,7 @@ namespace YimMenu::Submenus ImGui::SetNextItemWidth(200.f); ImGui::InputTextWithHint("Search", "Search", g_NameToSearch, sizeof(g_NameToSearch)); - if (ImGui::BeginListBox("###player-list", {180, static_cast(*Pointers.ScreenResY - 700 - 38 * 4)})) + if (ImGui::BeginListBox("###player-list", {180, -100 /* static_cast(*Pointers.ScreenResY - 700 - 38 * 4) */})) { auto& players = SavedPlayers::GetSavedPlayers(); @@ -108,7 +109,7 @@ namespace YimMenu::Submenus if (!g_SelectedPlayer || g_SelectedRid == 0) return; - if (ImGui::BeginChild("##player-editor", {500, static_cast(*Pointers.ScreenResY - 688 - 38 * 4)}, 0, ImGuiWindowFlags_NoBackground)) + if (ImGui::BeginChild("##player-editor", {500, -100 /* static_cast(*Pointers.ScreenResY - 688 - 38 * 4) */}, 0, ImGuiWindowFlags_NoBackground)) { ImGui::SetNextItemWidth(180.f); if (ImGui::InputText("Name", g_SelectedPlayerName, sizeof(g_SelectedPlayerName))) @@ -178,10 +179,32 @@ namespace YimMenu::Submenus RenderPlayerEditor(); } + static void RenderAddNewPlayer() + { + static char name_buf[24]{}; + + ImGui::SetNextItemWidth(180.0f); + ImGui::InputText("Username", name_buf, sizeof(name_buf)); + ImGui::SameLine(); + if (ImGui::Button("Add")) + FiberPool::Push([] { + auto rid = YimMenu::Network::ResolveRockstarId(name_buf); + if (rid) + { + SavedPlayers::AddPlayerData(*rid, name_buf); + } + else + { + Notifications::Show("Saved Players", "Failed to get RID from username", NotificationType::Error); + } + }); + } + std::shared_ptr BuildSavedPlayersMenu() { auto menu = std::make_shared("Saved Players"); auto players = std::make_shared("Players"); + auto new_player = std::make_shared("New"); auto tracking = std::make_shared("Tracking"); auto notifications = std::make_shared("Notifications"); @@ -189,6 +212,10 @@ namespace YimMenu::Submenus RenderSavedPlayers(); })); + new_player->AddItem(std::make_shared([] { + RenderAddNewPlayer(); + })); + notifications->AddItem(std::make_shared("playerdbnotifywhenjoinable"_J)); notifications->AddItem(std::make_shared("playerdbnotifywhenunjoinable"_J)); notifications->AddItem(std::make_shared("playerdbnotifywhenonline"_J)); @@ -206,6 +233,7 @@ namespace YimMenu::Submenus tracking->AddItem(std::make_shared("playerdbnotify"_J, std::move(notifications))); menu->AddItem(players); + menu->AddItem(new_player); menu->AddItem(tracking); return menu; diff --git a/src/game/gta/Network.cpp b/src/game/gta/Network.cpp index 2914047..e7116f0 100644 --- a/src/game/gta/Network.cpp +++ b/src/game/gta/Network.cpp @@ -6,9 +6,13 @@ #include "game/gta/ScriptGlobal.hpp" #include "game/gta/ScriptFunction.hpp" #include "game/pointers/Pointers.hpp" +#include "types/rage/atArray.hpp" #include "types/network/rlSessionInfo.hpp" #include "types/network/rlSessionByGamerHandleTaskResult.hpp" #include "types/network/rlTaskStatus.hpp" +#include "types/socialclub/rlQueryAccountsResult.hpp" +#include "types/socialclub/rlGetAvatarsContext.hpp" +#include "types/socialclub/rlScTaskStatus.hpp" namespace YimMenu::Network { @@ -37,6 +41,7 @@ namespace YimMenu::Network { ctx->SetReturnValue(TRUE); } + session_to_join = std::nullopt; } else { @@ -84,4 +89,38 @@ namespace YimMenu::Network return; } } + + std::optional ResolveRockstarId(std::string_view name) + { + if (!Pointers.GetAvatars) + return std::nullopt; + + // these are probably too large to keep on the stack + auto results = std::make_unique(); + auto players = std::make_unique(); + + rage::rlGetAvatarsContext context; + + strncpy(players->m_PlayerNames[0], name.data(), sizeof(players->m_PlayerNames[0])); + players->m_NumEntries = 1; + + context.m_Type = rage::rlGetAvatarsContext::Type::BY_NICKNAME; + context.m_AvatarSize = rage::rlGetAvatarsContext::AvatarSize::SMALL; // we don't need the avatars, but you gotta give it something + context.m_Result = results.get(); + + if (Pointers.GetAvatars(&context, players.get())) + { + while (context.m_Status.m_Status == 1) + { + ScriptMgr::Yield(); + } + + if (context.m_Status.m_Status == 3 && results->m_NumResults == 1) + { + return results->m_Results[0].m_RockstarId; + } + } + + return std::nullopt; + } } \ No newline at end of file diff --git a/src/game/gta/Network.hpp b/src/game/gta/Network.hpp index 953eff3..170f3f9 100644 --- a/src/game/gta/Network.hpp +++ b/src/game/gta/Network.hpp @@ -25,4 +25,5 @@ namespace YimMenu::Network void LaunchJoinType(JoinType to_launch); void JoinSessionInfo(rage::rlSessionInfo* info); void JoinRockstarId(std::uint64_t id); + std::optional ResolveRockstarId(std::string_view name); } \ No newline at end of file diff --git a/src/game/pointers/Pointers.cpp b/src/game/pointers/Pointers.cpp index 0675b08..cab287e 100644 --- a/src/game/pointers/Pointers.cpp +++ b/src/game/pointers/Pointers.cpp @@ -352,6 +352,11 @@ namespace YimMenu BytePatches::Add(ptr.As(), std::vector{0xB0, 0x01})->Apply(); }); + constexpr auto getAvatarsPtrn = Pattern<"89 4E 7C 48 8B CE E8 ? ? ? ? 84 C0">("GetAvatars"); + scanner.Add(getAvatarsPtrn, [this](PointerCalculator ptr) { + GetAvatars = ptr.Add(6).Add(1).Rip().As(); + }); + if (!scanner.Scan()) { LOG(WARNING) << "Some socialclub patterns could not be found"; diff --git a/src/game/pointers/Pointers.hpp b/src/game/pointers/Pointers.hpp index 916cf0a..cd50a86 100644 --- a/src/game/pointers/Pointers.hpp +++ b/src/game/pointers/Pointers.hpp @@ -27,6 +27,9 @@ namespace rage class rlSessionByGamerTaskResult; class rlQueryPresenceAttributesContext; class rlScGamerHandle; + class rlQueryAccountsResult; + class rlGetAvatarsContext; + class rlGetAvatarsPlayerList; } class CPedFactory; class CNetGamePlayer; @@ -60,6 +63,7 @@ namespace YimMenu using JoinSessionByInfo = bool (*)(CNetworkSession* network, rage::rlSessionInfo* info, int unk, int flags, rage::rlGamerHandle* handles, int num_handles); using GetSessionByGamerHandle = bool (*)(int profile_index, rage::rlGamerHandle* handles, int num_handles, rage::rlSessionByGamerTaskResult* results, int num_results, bool* success, rage::rlTaskStatus* state); using GetPresenceAttributes = bool (*)(int profile_index, rage::rlScGamerHandle* handles, int num_handles, rage::rlQueryPresenceAttributesContext** contexts, int count, rage::rlScTaskStatus* state); + using GetAvatars = bool (*)(rage::rlGetAvatarsContext* context, rage::rlGetAvatarsPlayerList* players); } struct PointerData @@ -129,6 +133,7 @@ namespace YimMenu Functions::JoinSessionByInfo JoinSessionByInfo; Functions::GetSessionByGamerHandle GetSessionByGamerHandle; Functions::GetPresenceAttributes GetPresenceAttributes; + Functions::GetAvatars GetAvatars; }; struct Pointers : PointerData diff --git a/src/types/rage/atArray.hpp b/src/types/rage/atArray.hpp index bf4565c..7a41aef 100644 --- a/src/types/rage/atArray.hpp +++ b/src/types/rage/atArray.hpp @@ -13,7 +13,7 @@ namespace rage atArray() : m_Data(nullptr), m_Size(0), - m_Count(0) + m_Capacity(0) { } @@ -37,9 +37,9 @@ namespace rage return m_Size; } - std::uint16_t count() const + std::uint16_t capacity() const { - return m_Count; + return m_Capacity; } T& operator[](std::uint16_t index) const @@ -59,10 +59,10 @@ namespace rage return false; } - private: + public: T* m_Data; std::uint16_t m_Size; - std::uint16_t m_Count; + std::uint16_t m_Capacity; // includes items allocated due to reservations and alignment }; static_assert(sizeof(rage::atArray) == 0x10, "rage::atArray is not properly sized"); } \ No newline at end of file diff --git a/src/types/socialclub/rlGetAvatarsContext.hpp b/src/types/socialclub/rlGetAvatarsContext.hpp new file mode 100644 index 0000000..55508cb --- /dev/null +++ b/src/types/socialclub/rlGetAvatarsContext.hpp @@ -0,0 +1,59 @@ +#pragma once +#include "rlScTaskStatus.hpp" + +namespace rage +{ + struct rlGetAvatarsPlayerList + { + union + { + char m_PlayerNames[51][250]; + char m_PlayerHandles[51][250]; + }; + int m_NumEntries; + }; + static_assert(sizeof(rlGetAvatarsPlayerList) == 0x31D4); + + struct rlGetAvatarsResult + { + std::uint64_t m_RockstarId; // 0x00 + char m_Nickname[51]; // 0x08 + char m_AvatarUrl[127]; // 0x3B + }; + static_assert(sizeof(rlGetAvatarsResult) == 0xC0); + + struct rlGetAvatarsResults + { + rlGetAvatarsResult m_Results[250]; + int m_NumResults; + }; + static_assert(sizeof(rlGetAvatarsResults) == 0xBB88); + + // this is probably something else that LLVM decided to inline into the GetAvatars func + class rlGetAvatarsContext + { + public: + enum class Type : std::uint8_t + { + BY_NICKNAME, + BY_ROCKSTAR_ID + }; + + enum class AvatarSize + { + SMALL = 1, + MEDIUM = 2, + LARGE = 4 + }; + + virtual ~rlGetAvatarsContext() = default; + + char m_Pad[80]; // 0x08 + void* m_Result; // 0x58 + char m_Pad2[4]; // 0x60 + Type m_Type; // 0x64 + AvatarSize m_AvatarSize; // 0x68 + rage::rlScTaskStatus m_Status{}; // 0x70 + }; + static_assert(sizeof(rlGetAvatarsContext) == 0x80); +} \ No newline at end of file diff --git a/src/types/socialclub/rlQueryAccountsResult.hpp b/src/types/socialclub/rlQueryAccountsResult.hpp new file mode 100644 index 0000000..ae88a0d --- /dev/null +++ b/src/types/socialclub/rlQueryAccountsResult.hpp @@ -0,0 +1,27 @@ +#pragma once + +namespace rage +{ + class rlQueryAccountsResult + { + public: + enum class Relationship : std::uint32_t + { + NONE = 1, + BLOCKED_BY_ME = 2, + BLOCKED_BY_THEM = 4, + BLOCKED_BY_BOTH = 6, + INVITED_BY_ME = 8, + INVITED_BY_THEM = 16, + FRIEND = 32 + }; + + std::uint64_t m_RockstarId; // 0x00 (gets parsed as a 32 bit value? wtf?) + char m_Nickname[51]; // 0x08 + char m_AvatarUrl[2048]; // 0x3B + bool m_IsVisible; // 0x83B + Relationship m_Relationship; // 0x83C + bool m_IsClanmate; // 0x840 + }; + static_assert(sizeof(rlQueryAccountsResult) == 0x848); +} \ No newline at end of file