diff --git a/src/game/frontend/submenus/Network.cpp b/src/game/frontend/submenus/Network.cpp index 9f78a3f..9bb0f96 100644 --- a/src/game/frontend/submenus/Network.cpp +++ b/src/game/frontend/submenus/Network.cpp @@ -4,6 +4,7 @@ #include "game/frontend/items/Items.hpp" #include "game/frontend/submenus/Network/SavedPlayers.hpp" #include "game/gta/Network.hpp" +#include "game/frontend/submenus/network/RandomEvents.hpp" namespace YimMenu::Submenus { @@ -96,5 +97,6 @@ namespace YimMenu::Submenus AddCategory(std::move(session)); AddCategory(std::move(spoofing)); AddCategory(std::move(BuildSavedPlayersMenu())); + AddCategory(BuildRandomEventsMenu()); } } \ No newline at end of file diff --git a/src/game/frontend/submenus/network/RandomEvents.cpp b/src/game/frontend/submenus/network/RandomEvents.cpp new file mode 100644 index 0000000..f4e65be --- /dev/null +++ b/src/game/frontend/submenus/network/RandomEvents.cpp @@ -0,0 +1,304 @@ +#include "RandomEvents.hpp" +#include "core/backend/FiberPool.hpp" +#include "core/backend/ScriptMgr.hpp" +#include "core/frontend/Notifications.hpp" +#include "game/backend/Self.hpp" +#include "game/gta/Scripts.hpp" +#include "game/gta/ScriptFunction.hpp" +#include "game/pointers/Pointers.hpp" +#include "types/script/globals/GSBD_RandomEvents.hpp" +#include "types/script/locals/FMRandomEvents.hpp" +#include "types/script/ScriptEvent.hpp" +#include "types/script/GtaThread.hpp" +#include "types/script/CGameScriptHandlerNetComponent.hpp" +#include "types/network/CNetGamePlayer.hpp" + +namespace YimMenu::Submenus +{ + enum eRandomEvent + { + DRUG_VEHICLE, + MOVIE_PROPS, + GOLDEN_GUN, + VEHICLE_LIST, + SLASHER, + PHANTOM_CAR, + SIGHTSEEING, + SMUGGLER_TRAIL, + CERBERUS, + SMUGGLER_PLANE, + CRIME_SCENE, + METAL_DETECTOR, + CONVOY, + ROBBERY, + XMAS_MUGGER, + BANK_SHOOTOUT, + ARMOURED_TRUCK, + POSSESSED_ANIMALS, + GHOSTHUNT, + XMAS_TRUCK, + COMMUNITY_OUTREACH, + + MAX_EVENTS + }; + + static constexpr std::array randomEventNames = { + "Drug Vehicle", + "Movie Props", + "Sleeping Guard", + "Exotic Exports", + "The Slashers", + "Phantom Car", + "Sightseeing", + "Smuggler Trail", + "Cerberus Surprise", + "Smuggler Plane", + "Crime Scene", + "Metal Detector", + "Finders Keepers", + "Shop Robbery", + "The Gooch", + "Weazel Plaza Shootout", + "Armored Truck", + "Possessed Animals", + "Ghosts Exposed", + "Happy Holidays Hauler", + "Community Outreach" + }; + + static constexpr std::array RandomEventScripts = { + "fm_content_drug_vehicle"_J, + "fm_content_movie_props"_J, + "fm_content_golden_gun"_J, + "fm_content_vehicle_list"_J, + "fm_content_slasher"_J, + "fm_content_phantom_car"_J, + "fm_content_sightseeing"_J, + "fm_content_smuggler_trail"_J, + "fm_content_cerberus"_J, + "fm_content_smuggler_plane"_J, + "fm_content_crime_scene"_J, + "fm_content_metal_detector"_J, + "fm_content_convoy"_J, + "fm_content_robbery"_J, + "fm_content_xmas_mugger"_J, + "fm_content_bank_shootout"_J, + "fm_content_armoured_truck"_J, + "fm_content_possessed_animals"_J, + "fm_content_ghosthunt"_J, + "fm_content_xmas_truck"_J, + "fm_content_community_outreach"_J + }; + + static YimMenu::ScriptFunction getNumFMMCVariations("GMFV", "freemode"_J, "5D ? ? ? 01 72 02 39 04", 1, true); + static GSBD_RandomEvents* GSBDRandomEvents = nullptr; + static RANDOM_EVENTS_FREEMODE_DATA* FMRandomEvents = nullptr; + static eRandomEvent selectedEvent = DRUG_VEHICLE; + static int selectedSubvariation = 0; + static int numSubVariations = 30; + + std::string GetEventTimeLeftStr(int eventTime, int timer) + { + int timePassed = (*Pointers.NetworkTime - timer) < 0 ? -(*Pointers.NetworkTime - timer) : (*Pointers.NetworkTime - timer); + int diff = (eventTime - timePassed); + int totalSeconds = diff / 1000; + int hours = totalSeconds / 3600; + int minutes = (totalSeconds % 3600) / 60; + int seconds = totalSeconds % 60; + + if (hours < 1) + return std::format("{:02}:{:02}", minutes, seconds); + else + return std::format("{:02}:{:02}:{:02}", hours, minutes, seconds); + } + + static std::string GetEventStateString() + { + switch (GSBDRandomEvents->EventData[selectedEvent].State) + { + case eRandomEventState::INACTIVE: + return "Inactive - launching in " + GetEventTimeLeftStr(FMRandomEvents->EventData[selectedEvent].InactiveTime, GSBDRandomEvents->EventData[selectedEvent].TimerState.Time); + case eRandomEventState::AVAILABLE: + return "Available - deactivating in " + GetEventTimeLeftStr(FMRandomEvents->EventData[selectedEvent].AvailableTime, GSBDRandomEvents->EventData[selectedEvent].TimerState.Time); + case eRandomEventState::ACTIVE: + return "Active"; + case eRandomEventState::CLEANUP: + return "Cleanup"; + } + + return "N/A"; + } + + std::shared_ptr BuildRandomEventsMenu() + { + auto menu = std::make_shared("Random Events"); + + menu->AddItem(std::make_unique([] { + GSBDRandomEvents = nullptr; + FMRandomEvents = nullptr; + + GSBDRandomEvents = GSBD_RandomEvents::Get(); + if (!GSBDRandomEvents) + return ImGui::Text("Freemode global block is not loaded yet."); + + if (GSBDRandomEvents->InitState != eRandomEventInitState::INITIALIZED) + return ImGui::Text("Random Events are not initialized yet."); + + if (auto freemode = Scripts::FindScriptThread("freemode"_J)) + { + FMRandomEvents = RANDOM_EVENTS_FREEMODE_DATA::Get(freemode); + if (!FMRandomEvents) + return ImGui::Text("Freemode stack is not valid."); + } + else + { + return ImGui::Text("Freemode is not running."); + } + + if (ImGui::BeginCombo("Select Event", randomEventNames[selectedEvent])) + { + for (int event = DRUG_VEHICLE; event < MAX_EVENTS; event++) + { + switch (GSBDRandomEvents->EventData[event].State) + { + case eRandomEventState::INACTIVE: + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); + break; + case eRandomEventState::AVAILABLE: + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + break; + case eRandomEventState::ACTIVE: + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.0f, 1.0f, 0.0f, 1.0f)); + break; + } + + if (ImGui::Selectable(randomEventNames[event], event == selectedEvent)) + { + FiberPool::Push([event] { + selectedEvent = (eRandomEvent)event; + numSubVariations = getNumFMMCVariations.Call(FMRandomEvents->MissionData.FMMCData[selectedEvent].FMMCType, 0) - 1; + }); + } + + ImGui::PopStyleColor(); + } + + ImGui::EndCombo(); + } + + if (ImGui::InputInt(std::format("Select Location (0-{})", numSubVariations).c_str(), &selectedSubvariation)) + { + selectedSubvariation = std::clamp(selectedSubvariation, 0, numSubVariations); + } + + if (ImGui::Button("Launch Event")) + { + FiberPool::Push([] { + if (GSBDRandomEvents->EventData[selectedEvent].State != eRandomEventState::ACTIVE) + { + SCRIPT_EVENT_REQUEST_RANDOM_EVENT eventData; + eventData.FMMCType = FMRandomEvents->MissionData.FMMCData[selectedEvent].FMMCType; + eventData.Subvariation = selectedSubvariation; + eventData.PlayersToSend = 1; // Set FORCE_LAUNCH bit of all players + eventData.Send(); + ScriptMgr::Yield(100ms); + if (GSBDRandomEvents->EventData[selectedEvent].State == eRandomEventState::INACTIVE) + { + Notifications::Show("Random Events", "Failed to launch event. Are you freemode host?", NotificationType::Error); + } + } + else + { + Notifications::Show("Random Events", "Event is already active.", NotificationType::Error); + } + }); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Force launches the event."); + } + + ImGui::SameLine(); + + if (ImGui::Button("Kill Event")) + { + FiberPool::Push([] { + if (GSBDRandomEvents->EventData[selectedEvent].State == eRandomEventState::AVAILABLE) + { + GSBDRandomEvents->EventData[selectedEvent].State = eRandomEventState::CLEANUP; + } + else if (GSBDRandomEvents->EventData[selectedEvent].State == eRandomEventState::ACTIVE) + { + ScriptFunction terminateFMContentScript("TFMCS", RandomEventScripts[(int)selectedEvent], "2D 01 05 00 00 5D ? ? ?"); + terminateFMContentScript.Call(false); // Is it ok to do this when we're not host? + } + else + { + Notifications::Show("Random Events", "Event is not active.", NotificationType::Error); + } + }); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Terminates the event."); + } + + ImGui::SameLine(); + + if (ImGui::Button("Teleport to Event")) + { + FiberPool::Push([] { + if (GSBDRandomEvents->EventData[selectedEvent].State >= eRandomEventState::AVAILABLE) + { + auto coords = GSBDRandomEvents->EventData[selectedEvent].TriggerPosition; + if (coords != Vector3(0.f, 0.f, 0.f)) + { + Self::GetPed().TeleportTo(coords); + } + else // Either update event coords TSE not sent yet or event doesn't register a trigger point + { + Notifications::Show("Random Events", "Failed to teleport to event. Coordinates are not valid.", NotificationType::Error); + } + } + else + { + Notifications::Show("Random Events", "Event is not active.", NotificationType::Error); + } + }); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("Teleports to the event."); + } + + if (GSBDRandomEvents->EventData[selectedEvent].State == eRandomEventState::ACTIVE) + { + if (auto eventThread = Scripts::FindScriptThread(RandomEventScripts[(int)selectedEvent])) + { + if (auto netComponent = reinterpret_cast(eventThread)->m_NetComponent) + { + if (auto host = netComponent->GetHost()) + { + ImGui::Text("Host: %s", host->GetName()); + } + ImGui::SameLine(); + ImGui::BeginDisabled(netComponent->IsLocalPlayerHost()); + if (ImGui::SmallButton("Take Control")) + { + FiberPool::Push([eventThread] { + Scripts::ForceScriptHost(eventThread); + }); + } + ImGui::EndDisabled(); + } + } + } + + ImGui::Text("State: %s", GetEventStateString().c_str()); + ImGui::Text("Location: %d", GSBDRandomEvents->EventData[selectedEvent].Subvariation); + ImGui::Text("Trigger Range: %.2f", GSBDRandomEvents->EventData[selectedEvent].TriggerRange); // Default value is 400, it will be updated once the event switches to the available state + })); + + return menu; + } +} \ No newline at end of file diff --git a/src/game/frontend/submenus/network/RandomEvents.hpp b/src/game/frontend/submenus/network/RandomEvents.hpp new file mode 100644 index 0000000..bd4a4fa --- /dev/null +++ b/src/game/frontend/submenus/network/RandomEvents.hpp @@ -0,0 +1,8 @@ +#pragma once +#include "core/frontend/manager/Category.hpp" +#include "game/frontend/items/Items.hpp" + +namespace YimMenu::Submenus +{ + std::shared_ptr BuildRandomEventsMenu(); +} \ No newline at end of file diff --git a/src/game/gta/ScriptFunction.cpp b/src/game/gta/ScriptFunction.cpp index eb883bd..9875e4a 100644 --- a/src/game/gta/ScriptFunction.cpp +++ b/src/game/gta/ScriptFunction.cpp @@ -2,10 +2,12 @@ namespace YimMenu { - ScriptFunction::ScriptFunction(const std::string& name, const joaat_t script, const SimplePattern& pattern) : + ScriptFunction::ScriptFunction(const std::string& name, const joaat_t script, const SimplePattern& pattern, std::int32_t offset, bool rip) : m_Name(name), m_Script(script), m_Pattern(pattern), + m_Offset(offset), + m_Rip(rip), m_PC(0) { } @@ -17,7 +19,9 @@ namespace YimMenu if (auto location = Scripts::GetCodeLocationByPattern(program, m_Pattern)) { - m_PC = location.value(); + m_PC = location.value() + m_Offset; + if (m_Rip) + m_PC = ReadThreeByte(program->GetCodeAddress(m_PC)); LOG(INFO) << "Found pattern for " << m_Name << " at " << HEX(m_PC) << " in script " << program->m_Name; } else diff --git a/src/game/gta/ScriptFunction.hpp b/src/game/gta/ScriptFunction.hpp index 1641908..96c3b37 100644 --- a/src/game/gta/ScriptFunction.hpp +++ b/src/game/gta/ScriptFunction.hpp @@ -13,6 +13,8 @@ namespace YimMenu { joaat_t m_Script; const SimplePattern m_Pattern; + std::int32_t m_Offset; + bool m_Rip; std::uint32_t m_PC; std::string m_Name; @@ -24,8 +26,13 @@ namespace YimMenu *reinterpret_cast>*>(reinterpret_cast(stack) + (stackPtr++)) = std::forward(value); } + std::uint32_t ReadThreeByte(std::uint8_t* arr) + { + return arr[0] + (arr[1] << 8) + (arr[2] << 16); + } + public: - ScriptFunction(const std::string& name, const joaat_t script, const SimplePattern& pattern); + ScriptFunction(const std::string& name, const joaat_t script, const SimplePattern& pattern, std::int32_t offset = 0, bool rip = false); template Ret Call(Args... args) diff --git a/src/game/pointers/Pointers.cpp b/src/game/pointers/Pointers.cpp index cab287e..51ab222 100644 --- a/src/game/pointers/Pointers.cpp +++ b/src/game/pointers/Pointers.cpp @@ -303,6 +303,11 @@ namespace YimMenu GetSessionByGamerHandle = ptr.Sub(0x4A).Add(1).Rip().As(); }); + constexpr auto networkTimePtrn = Pattern<"89 05 ? ? ? ? 80 3D ? ? ? ? ? 0F 84 ? ? ? ? E9">("NetworkTime"); + scanner.Add(networkTimePtrn, [this](PointerCalculator ptr) { + NetworkTime = ptr.Add(2).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 cd50a86..23fef46 100644 --- a/src/game/pointers/Pointers.hpp +++ b/src/game/pointers/Pointers.hpp @@ -134,6 +134,7 @@ namespace YimMenu Functions::GetSessionByGamerHandle GetSessionByGamerHandle; Functions::GetPresenceAttributes GetPresenceAttributes; Functions::GetAvatars GetAvatars; + std::uint32_t* NetworkTime; }; struct Pointers : PointerData diff --git a/src/types/script/ScriptEvent.hpp b/src/types/script/ScriptEvent.hpp index 60b4129..b888c18 100644 --- a/src/types/script/ScriptEvent.hpp +++ b/src/types/script/ScriptEvent.hpp @@ -76,7 +76,9 @@ enum class ScriptEventIndex TriggerCEORaid = -1906536929, StartScriptBegin = -366707054, - StartScriptProceed = 1757622014 + StartScriptProceed = 1757622014, + + RequestRandomEvent = -126218586 }; struct SCRIPT_EVENT @@ -169,4 +171,15 @@ struct SCRIPT_EVENT_SEND_TO_INTERIOR : public SCRIPT_EVENT }; static_assert(sizeof(SCRIPT_EVENT_SEND_TO_INTERIOR) == 16 * 8); +struct SCRIPT_EVENT_REQUEST_RANDOM_EVENT : public SCRIPT_EVENT +{ + REGISTER_SCRIPT_EVENT(SCRIPT_EVENT_REQUEST_RANDOM_EVENT, RequestRandomEvent); + + SCR_INT FMMCType; + SCR_INT Variation; + SCR_INT Subvariation; + SCR_INT PlayersToSend; +}; +static_assert(sizeof(SCRIPT_EVENT_REQUEST_RANDOM_EVENT) == 7 * 8); + #undef REGISTER_SCRIPT_EVENT \ No newline at end of file diff --git a/src/types/script/globals/GSBD_RandomEvents.hpp b/src/types/script/globals/GSBD_RandomEvents.hpp new file mode 100644 index 0000000..b9c595e --- /dev/null +++ b/src/types/script/globals/GSBD_RandomEvents.hpp @@ -0,0 +1,72 @@ +#pragma once +#include "../Timer.hpp" +#include "../types.hpp" + +enum class eRandomEventInitState +{ + INITIALIZING, + INITIALIZED +}; + +enum class eRandomEventState +{ + INACTIVE, + AVAILABLE, + ACTIVE, + CLEANUP +}; + +enum class eRandomEventBitset +{ + TRIGGERED_BY_CLIENT, + FORCE_LAUNCH, + UNK_2, // unused + FORCE_CLEANUP +}; + +struct RANDOM_EVENTS_RESERVATION_DATA +{ + SCR_INT ReservedPeds; // 0 + SCR_INT ReservedVehicles; // 1 + SCR_INT ReservedObjects; // 2 +}; +static_assert(sizeof(RANDOM_EVENTS_RESERVATION_DATA) == 3 * 8); + +struct RANDOM_EVENTS_DATA +{ + alignas(8) eRandomEventState State; // 0 + TIMER TimerState; // 1 + SCR_ARRAY, 1> Bitset; // 3 + SCR_INT Variation; // 5 + SCR_INT Subvariation; // 6 + RANDOM_EVENTS_RESERVATION_DATA ReservationData; // 7 + SCR_VEC3 TriggerPosition; // 10 + SCR_FLOAT TriggerRange; // 13 + SCR_INT Retries; // 14 +}; +static_assert(sizeof(RANDOM_EVENTS_DATA) == 15 * 8); + +struct RANDOM_EVENTS_VEHICLE_LIST_DATA +{ + SCR_INT UNK_0000; // 0 + SCR_INT Variation; // 1 +}; +static_assert(sizeof(RANDOM_EVENTS_VEHICLE_LIST_DATA) == 2 * 8); + +struct RANDOM_EVENTS_TARGET_DATA +{ + PLAYER_INDEX PhantomCarTarget; // 0 + PLAYER_INDEX XMASMuggerTarget; // 1 +}; +static_assert(sizeof(RANDOM_EVENTS_TARGET_DATA) == 2 * 8); + +struct GSBD_RandomEvents +{ + alignas(8) eRandomEventInitState InitState; // 0 + SCR_ARRAY EventData; // 1 + RANDOM_EVENTS_VEHICLE_LIST_DATA VehicleListData; // 317 + RANDOM_EVENTS_TARGET_DATA TargetData; // 319 + + static GSBD_RandomEvents* Get(); +}; +static_assert(sizeof(GSBD_RandomEvents) == 321 * 8); \ No newline at end of file diff --git a/src/types/script/globals/Globals.cpp b/src/types/script/globals/Globals.cpp index 1e9469e..f89d8fe 100644 --- a/src/types/script/globals/Globals.cpp +++ b/src/types/script/globals/Globals.cpp @@ -10,6 +10,7 @@ #include "GSBD_Kicking.hpp" #include "GSBD_PropertyInstances.hpp" #include "g_AMC_playerBD.hpp" +#include "GSBD_RandomEvents.hpp" #include "game/gta/ScriptGlobal.hpp" #define DEFINE_GLOBAL_ACCESSOR(str, global) str* str::Get()\ @@ -31,4 +32,5 @@ DEFINE_GLOBAL_ACCESSOR(GSBD_FM, 1835557); DEFINE_GLOBAL_ACCESSOR(GSBD_FM_Events, 1917230); DEFINE_GLOBAL_ACCESSOR(GSBD_Kicking, 1877529); DEFINE_GLOBAL_ACCESSOR(GSBD_PropertyInstances, 1937699); -DEFINE_GLOBAL_ACCESSOR(g_AMC_playerBD, 2699624); \ No newline at end of file +DEFINE_GLOBAL_ACCESSOR(g_AMC_playerBD, 2699624); +DEFINE_GLOBAL_ACCESSOR(GSBD_RandomEvents, 1882524); \ No newline at end of file diff --git a/src/types/script/locals/FMRandomEvents.hpp b/src/types/script/locals/FMRandomEvents.hpp new file mode 100644 index 0000000..1d4adfb --- /dev/null +++ b/src/types/script/locals/FMRandomEvents.hpp @@ -0,0 +1,51 @@ +#pragma once +#include "../types.hpp" +#include "../Timer.hpp" + +namespace rage +{ + class scrThread; +} + +enum class eRandomEventFlags +{ + RESTART = 2, + DISABLE_DISTANCE_CHECK = 4 +}; + +struct RANDOM_EVENTS_LOCAL_DATA +{ + SCR_INT RegisteredEvents; // 0 + SCR_ARRAY TriggerPtr; // 1 + SCR_INT TriggerPositionPtr; // 3 + SCR_INT TriggerRangePtr; // 4 + SCR_INT AvailablePtr; // 5 + SCR_INT InactiveTime; // 6 + SCR_INT AvailableTime; // 7 + SCR_BITSET Flags; // 8 + TIMER LastTriggerAttempt; // 9 + SCR_INT UNK_0011; // 11 (unused, ranges from 0 to 4) +}; +static_assert(sizeof(RANDOM_EVENTS_LOCAL_DATA) == 12 * 8); + +struct RANDOM_EVENTS_FMMC_DATA +{ + SCR_INT FMMCType; // 0 +}; +static_assert(sizeof(RANDOM_EVENTS_FMMC_DATA) == 1 * 8); + +struct RANDOM_EVENTS_MISSION_DATA +{ + SCR_INT EventsCount; // 0 + SCR_ARRAY FMMCData; // 1 +}; +static_assert(sizeof(RANDOM_EVENTS_MISSION_DATA) == 23 * 8); + +struct RANDOM_EVENTS_FREEMODE_DATA +{ + SCR_ARRAY EventData; // 0 + RANDOM_EVENTS_MISSION_DATA MissionData; // 253 + + static RANDOM_EVENTS_FREEMODE_DATA* Get(rage::scrThread* thread); +}; +static_assert(sizeof(RANDOM_EVENTS_FREEMODE_DATA) == 276 * 8); \ No newline at end of file diff --git a/src/types/script/locals/Locals.cpp b/src/types/script/locals/Locals.cpp index e313e5a..fd51f5d 100644 --- a/src/types/script/locals/Locals.cpp +++ b/src/types/script/locals/Locals.cpp @@ -1,4 +1,5 @@ #include "VehicleRewardData.hpp" +#include "FMRandomEvents.hpp" #include "game/gta/ScriptLocal.hpp" #define DEFINE_LOCAL_ACCESSOR(str, local) \ @@ -10,4 +11,5 @@ return lcl.As(); \ } -DEFINE_LOCAL_ACCESSOR(VEHICLE_REWARD_DATA, 129); \ No newline at end of file +DEFINE_LOCAL_ACCESSOR(VEHICLE_REWARD_DATA, 129); +DEFINE_LOCAL_ACCESSOR(RANDOM_EVENTS_FREEMODE_DATA, 15919); \ No newline at end of file