diff --git a/src/core/commands/HotkeySystem.cpp b/src/core/commands/HotkeySystem.cpp index 9e860e2..40a9277 100644 --- a/src/core/commands/HotkeySystem.cpp +++ b/src/core/commands/HotkeySystem.cpp @@ -25,6 +25,8 @@ namespace YimMenu } LOG(INFO) << "Registered " << m_CommandHotkeys.size() << " commands"; + m_CommandHotkeys.at("chathelper"_J).m_Chain.clear(); // ensure chat is always bound + m_CommandHotkeys.at("chathelper"_J).m_Chain.push_back(0x54); } bool HotkeySystem::ListenAndApply(int& Hotkey, std::vector Blacklist) diff --git a/src/core/hooking/Hooking.cpp b/src/core/hooking/Hooking.cpp index 571536c..2d0f63c 100644 --- a/src/core/hooking/Hooking.cpp +++ b/src/core/hooking/Hooking.cpp @@ -21,6 +21,8 @@ namespace YimMenu BaseHook::Add(new DetourHook("AssignPhysicalIndex", Pointers.AssignPhysicalIndex, Hooks::Info::AssignPhysicalIndex)); + BaseHook::Add(new DetourHook("ReceiveNetMessage", Pointers.ReceiveNetMessage, Hooks::Network::ReceiveNetMessage)); + BaseHook::Add(new DetourHook("IsNodeInScope", Pointers.IsNodeInScope, Hooks::Spoofing::IsNodeInScope)); BaseHook::Add(new DetourHook("ShouldUseNodeCache", Pointers.ShouldUseNodeCache, Hooks::Spoofing::ShouldUseNodeCache)); BaseHook::Add(new DetourHook("WriteNodeData", Pointers.WriteNodeData, Hooks::Spoofing::WriteNodeData)); diff --git a/src/game/features/system/Chat.cpp b/src/game/features/system/Chat.cpp new file mode 100644 index 0000000..054b7d4 --- /dev/null +++ b/src/game/features/system/Chat.cpp @@ -0,0 +1,98 @@ +#include "core/backend/ScriptMgr.hpp" +#include "core/commands/Command.hpp" +#include "core/frontend/Notifications.hpp" +#include "core/frontend/widgets/imgui_colors.h" +#include "game/backend/Players.hpp" +#include "game/backend/Self.hpp" +#include "game/frontend/ChatDisplay.hpp" +#include "game/frontend/GUI.hpp" +#include "game/gta/Natives.hpp" +#include "game/gta/Packet.hpp" +#include "game/pointers/Pointers.hpp" +#include "types/network/rlGamerHandle.hpp" +#include "types/network/rlGamerInfo.hpp" + +namespace +{ + bool ShowTextBox(std::string_view title, char* buf, int max_length) + { + int font_flags = 256; + font_flags |= 64; + MISC::NEXT_ONSCREEN_KEYBOARD_RESULT_WILL_DISPLAY_USING_THESE_FONTS(font_flags); + MISC::DISPLAY_ONSCREEN_KEYBOARD(0, title.data(), nullptr, buf, nullptr, nullptr, nullptr, max_length); + + while (true) + { + YimMenu::ScriptMgr::Yield(); + int update_res = MISC::UPDATE_ONSCREEN_KEYBOARD(); + + if (update_res == 1) + { + strncpy(buf, MISC::GET_ONSCREEN_KEYBOARD_RESULT(), max_length); + return true; + } + else if (update_res == 2 || update_res == 3) + { + MISC::CANCEL_ONSCREEN_KEYBOARD(); + return false; + } + } + } +} + +namespace YimMenu::Features +{ + class Chat : public Command + { + using Command::Command; + + std::mutex m_ChatMutex; + + virtual void OnCall() override + { + if (*Pointers.IsSessionStarted && MISC::UPDATE_ONSCREEN_KEYBOARD() != 0) + { + if (!m_ChatMutex.try_lock()) + return; + + char buffer[256]{}; + if (ShowTextBox("Enter Message", buffer, sizeof(buffer))) + { + // well, chat is pretty much dead anyway so hardcoded GUIDs should be fine + // {A88961C7-B4C8-4A16-A648-AC2D8C9300DB} + static const GUID guid = {0xa88961c7, 0xb4c8, 0x4a16, {0xa6, 0x48, 0xac, 0x2d, 0x8c, 0x93, 0x0, 0xdb}}; + char guid_str[40]; + std::sprintf(guid_str, "%08lx-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]); + + Packet pkt; + pkt.WriteMessageHeader(rage::netMessage::Type::TextMessage); + pkt.GetBuffer().WriteString(buffer, sizeof(buffer)); + pkt.GetBuffer().WriteString(guid_str, sizeof(guid_str)); + Self::GetPlayer().GetGamerInfo()->m_GamerHandle.Serialize(pkt); + pkt.GetBuffer().Write(false, 1); // is team + + for (auto& player : Players::GetPlayers()) + if (player.second.IsValid() && !player.second.IsLocal()) + pkt.Send(player.second.GetMessageId()); + + ChatDisplay::Show(Self::GetPlayer().GetName(), buffer, ImGui::Colors::Green); + } + + m_ChatMutex.unlock(); + } + } + }; + + class ClearChat : public Command + { + using Command::Command; + + virtual void OnCall() override + { + ChatDisplay::Clear(); + } + }; + + static Chat _Chat{"chathelper", "Chat", "Use this to open the chat"}; + static ClearChat _ClearChat{"clearchat", "Clear Chat", "Use this to clear the chat"}; +} \ No newline at end of file diff --git a/src/game/frontend/ChatDisplay.cpp b/src/game/frontend/ChatDisplay.cpp index 631b95b..3989ccd 100644 --- a/src/game/frontend/ChatDisplay.cpp +++ b/src/game/frontend/ChatDisplay.cpp @@ -49,7 +49,7 @@ namespace YimMenu for (auto& message : m_Messages) { - ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(ImGui::Colors::Blue)); + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(message.m_Color)); ImGui::TextWrapped("%s:", message.m_Sender.data()); ImGui::PopStyleColor(); ImGui::SameLine(); diff --git a/src/game/gta/Player.cpp b/src/game/gta/Player.cpp index d9b4f6b..6d2a35a 100644 --- a/src/game/gta/Player.cpp +++ b/src/game/gta/Player.cpp @@ -24,6 +24,19 @@ namespace YimMenu return m_Handle && m_Handle->IsPhysical() && m_Handle->m_PlayerInfo; } + bool Player::IsLocal() const + { + if (!IsValid()) + { + if (!*Pointers.IsSessionStarted) + return true; + + return false; + } + + return m_Handle->IsLocal(); + } + int Player::GetId() const { if (!IsValid()) diff --git a/src/game/gta/Player.hpp b/src/game/gta/Player.hpp index cfec6ac..7397a85 100644 --- a/src/game/gta/Player.hpp +++ b/src/game/gta/Player.hpp @@ -35,6 +35,7 @@ namespace YimMenu // object bool IsValid() const; + bool IsLocal() const; int GetId() const; const char* GetName(); CNetGamePlayer* GetHandle(); diff --git a/src/game/hooks/Hooks.hpp b/src/game/hooks/Hooks.hpp index ee5b829..8199e91 100644 --- a/src/game/hooks/Hooks.hpp +++ b/src/game/hooks/Hooks.hpp @@ -42,6 +42,11 @@ namespace YimMenu::Hooks extern bool WriteSyncTree(void* tree, int type, int flags, rage::netObject* object, rage::datBitBuffer* buffer, int unk, void* a6, std::uint8_t player_id, void* a8); } + namespace Network + { + extern void ReceiveNetMessage(void* a1, rage::netConnectionManager* mgr, rage::netEvent* event); + } + namespace Window { extern LRESULT WndProc(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam); diff --git a/src/game/hooks/Network/ReceiveNetMessage.cpp b/src/game/hooks/Network/ReceiveNetMessage.cpp new file mode 100644 index 0000000..50137a8 --- /dev/null +++ b/src/game/hooks/Network/ReceiveNetMessage.cpp @@ -0,0 +1,53 @@ +#include "core/hooking/DetourHook.hpp" +#include "game/backend/Players.hpp" +#include "game/frontend/ChatDisplay.hpp" +#include "game/hooks/Hooks.hpp" +#include "types/network/netEvent.hpp" +#include "types/network/netMessage.hpp" +#include "types/rage/datBitBuffer.hpp" +#include "core/frontend/widgets/imgui_colors.h" + +namespace YimMenu::Hooks +{ + static std::optional ReadType(rage::datBitBuffer& buffer) + { + if (buffer.Read(14) != 0x3246) + return std::nullopt; + + auto extended = buffer.Read(1); + return buffer.Read(extended ? 16 : 8); + } + + void Network::ReceiveNetMessage(void* a1, rage::netConnectionManager* mgr, rage::netEvent* event) + { + if (!g_Running) + return BaseHook::Get>()->Original()(a1, mgr, event); + + if (event->GetEventType() != rage::netEvent::Type::FrameReceived) + return BaseHook::Get>()->Original()(a1, mgr, event); + + // TODO: port security ID stuff from V1 + auto fr_evt = reinterpret_cast(event); + rage::datBitBuffer buffer(fr_evt->m_Data, fr_evt->m_Length, true); + + rage::netMessage::Type type = ReadType(buffer).value_or(rage::netMessage::Type::Invalid); + + switch (type) + { + case rage::netMessage::Type::TextMessage: + { + auto player = Players::GetByMessageId(fr_evt->m_MsgId); + if (player) + { + char msg[256]{}; + buffer.ReadString(msg, sizeof(msg)); // we don't need the rest + ChatDisplay::Show(player.GetName(), msg, ImGui::Colors::LightBlue); + } + break; + } + default: + } + + BaseHook::Get>()->Original()(a1, mgr, event); + } +} \ No newline at end of file diff --git a/src/game/pointers/Pointers.cpp b/src/game/pointers/Pointers.cpp index b6eae96..b996f0b 100644 --- a/src/game/pointers/Pointers.cpp +++ b/src/game/pointers/Pointers.cpp @@ -159,6 +159,11 @@ namespace YimMenu SpectatePatch = ptr.As(); }); + constexpr auto receiveNetMessagePtrn = Pattern<"48 81 C1 00 03 00 00 4C 89 E2">("ReceiveNetMessage"); + scanner.Add(receiveNetMessagePtrn, [this](PointerCalculator ptr) { + ReceiveNetMessage = ptr.Add(0xD).Add(1).Rip().As(); + }); + if (!scanner.Scan()) { LOG(FATAL) << "Some patterns could not be found, unloading."; diff --git a/src/game/pointers/Pointers.hpp b/src/game/pointers/Pointers.hpp index 82ff04d..456dd75 100644 --- a/src/game/pointers/Pointers.hpp +++ b/src/game/pointers/Pointers.hpp @@ -68,6 +68,7 @@ namespace YimMenu Functions::GetNetObjectById GetNetObjectById; Functions::RequestControl RequestControl; std::uint8_t* SpectatePatch; // used to patch the code that prevents you from spawning network objects when spectating + PVOID ReceiveNetMessage; }; struct Pointers : PointerData diff --git a/src/types/network/netEvent.hpp b/src/types/network/netEvent.hpp index 2268859..ed5647e 100644 --- a/src/types/network/netEvent.hpp +++ b/src/types/network/netEvent.hpp @@ -1,4 +1,5 @@ #pragma once +#include "netAddress.hpp" namespace rage { @@ -29,4 +30,15 @@ namespace rage uint32_t m_PeerId; // 0x0050 }; static_assert(sizeof(rage::netEvent) == 0x58); + + // or rage::netConnection::InFrame + class netEventFrameReceived : public netEvent + { + public: + int m_SecurityId; // 0x0058 + rage::netAddress m_Address; // 0x0060 + std::uint32_t m_Length; // 0x0080 + void* m_Data; // 0x0088 + }; + static_assert(sizeof(netEventFrameReceived) == 0x90); } \ No newline at end of file diff --git a/src/types/network/rlGamerHandle.cpp b/src/types/network/rlGamerHandle.cpp new file mode 100644 index 0000000..dffa5b9 --- /dev/null +++ b/src/types/network/rlGamerHandle.cpp @@ -0,0 +1,19 @@ +#include "../rage/datBitBuffer.hpp" +#include "rlGamerHandle.hpp" + +namespace rage +{ + void rlGamerHandle::Serialize(rage::datBitBuffer& buffer) const + { + buffer.Write(m_Platform, 8); + buffer.WriteInt64(m_RockstarId, 64); + buffer.Write(m_ProfileIndex, 8); + } + + void rlGamerHandle::Deserialize(rage::datBitBuffer& buffer) + { + m_Platform = buffer.Read(8); + buffer.ReadInt64(&m_RockstarId, 64); + m_ProfileIndex = buffer.Read(8); + } +} \ No newline at end of file diff --git a/src/types/network/rlGamerHandle.hpp b/src/types/network/rlGamerHandle.hpp index ec7b330..7348dae 100644 --- a/src/types/network/rlGamerHandle.hpp +++ b/src/types/network/rlGamerHandle.hpp @@ -4,6 +4,7 @@ namespace rage { + class datBitBuffer; // this appears to have been unchanged in E&E enum rlPlatforms : uint8_t { @@ -28,6 +29,9 @@ namespace rage m_ProfileIndex(0) { } + + void Serialize(rage::datBitBuffer& buffer) const; + void Deserialize(rage::datBitBuffer& buffer); }; //Size: 0x0010 static_assert(sizeof(rlGamerHandle) == 0x10); } \ No newline at end of file diff --git a/src/types/rage/datBitBuffer.hpp b/src/types/rage/datBitBuffer.hpp index fc65b64..cbfea94 100644 --- a/src/types/rage/datBitBuffer.hpp +++ b/src/types/rage/datBitBuffer.hpp @@ -425,10 +425,29 @@ namespace rage void ReadArrayBytes(void* array, int bytes) { - ReadArray(array, bytes); + ReadArray(array, bytes * 8); } - // TODO: implement WriteString and ReadString + void WriteString(const char* string, int max_len) + { + int len = strnlen(string, max_len - 1) + 1; + bool extended = len > 127; + + Write(extended, 1); + Write(len, extended ? 15 : 7); + WriteArrayBytes(string, len); + } + + void ReadString(char* string, int max_len) + { + auto extended = Read(1); + auto len = Read(extended ? 15 : 7); + if (len <= max_len) + { + ReadArrayBytes(string, len); + string[len - 1] = 0; + } + } public: void* m_Data; //0x0000