From 171bb62d76d2a27d9343a4c1d8faea1e05289df3 Mon Sep 17 00:00:00 2001 From: maybegreat48 <96936658+maybegreat48@users.noreply.github.com> Date: Mon, 7 Apr 2025 00:43:09 -0400 Subject: [PATCH] feat(world): add weather options --- src/game/features/world/Weather.cpp | 135 ++++++++++++++++++ .../frontend/submenus/Recovery/StatEditor.cpp | 36 +++++ .../submenus/Recovery/Transactions.cpp | 31 ++-- src/game/frontend/submenus/World.cpp | 7 +- 4 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 src/game/features/world/Weather.cpp diff --git a/src/game/features/world/Weather.cpp b/src/game/features/world/Weather.cpp new file mode 100644 index 0000000..c4c2639 --- /dev/null +++ b/src/game/features/world/Weather.cpp @@ -0,0 +1,135 @@ +#include "core/commands/Command.hpp" +#include "core/commands/ListCommand.hpp" +#include "core/commands/LoopedCommand.hpp" +#include "game/backend/ScriptPatches.hpp" +#include "game/gta/Natives.hpp" + +namespace YimMenu::Features +{ + static std::vector> g_WeatherTypes = { + {0, "Clear"}, + {1, "Extra Sunny"}, + {2, "Clouds"}, + {3, "Overcast"}, + {4, "Rain"}, + {5, "Clearing"}, + {6, "Thunder"}, + {7, "Smog"}, + {8, "Foggy"}, + {9, "Xmas"}, + {10, "Snow"}, + {11, "Snow Light"}, + {12, "Blizzard"}, + {13, "Halloween"}, + {14, "Neutral"}, + {15, "Rain Halloween"}, + {16, "Snow Halloween"}, + }; + + constexpr std::array g_WeatherCodes = { + "CLEAR", + "EXTRASUNNY", + "CLOUDS", + "OVERCAST", + "RAIN", + "CLEARING", + "THUNDER", + "SMOG", + "FOGGY", + "XMAS", + "SNOW", + "SNOWLIGHT", + "BLIZZARD", + "HALLOWEEN", + "NEUTRAL", + "RAIN_HALLOWEEN", + "SNOW_HALLOWEEN", + }; + + static ListCommand _Weather{"weather", "Weather", "Weather to set or force. Note that this is local and cannot be seen by other players", g_WeatherTypes, 0}; + + class SetWeather : public Command + { + using Command::Command; + + virtual void OnCall() override + { + MISC::SET_WEATHER_TYPE_PERSIST(g_WeatherCodes[_Weather.GetState()]); + } + }; + static SetWeather _SetWeather{"setweather", "Set Weather", "Sets the specifed weather. Note that this effect is local and cannot be seen by other players"}; + + class ForceWeather : public LoopedCommand + { + using LoopedCommand::LoopedCommand; + + ScriptPatch m_ShopControllerWeatherPatch{}; + bool m_SnowEnabled{}; + + void EnableSnow() + { + if (!m_SnowEnabled) + { + STREAMING::REQUEST_NAMED_PTFX_ASSET("core_snow"); + + if (AUDIO::REQUEST_SCRIPT_AUDIO_BANK("SNOW_FOOTSTEPS", false, -1) && STREAMING::HAS_NAMED_PTFX_ASSET_LOADED("core_snow")) + { + GRAPHICS::USE_SNOW_FOOT_VFX_WHEN_UNSHELTERED(true); + GRAPHICS::USE_SNOW_WHEEL_VFX_WHEN_UNSHELTERED(true); + GRAPHICS::_FORCE_ALLOW_SNOW_FOOT_VFX_ON_ICE(true); + GRAPHICS::_FORCE_GROUND_SNOW_PASS(true); + + m_SnowEnabled = true; + } + } + } + + void DisableSnow() + { + if (m_SnowEnabled) + { + GRAPHICS::USE_SNOW_FOOT_VFX_WHEN_UNSHELTERED(false); + GRAPHICS::USE_SNOW_WHEEL_VFX_WHEN_UNSHELTERED(false); + GRAPHICS::_FORCE_ALLOW_SNOW_FOOT_VFX_ON_ICE(false); + GRAPHICS::_FORCE_GROUND_SNOW_PASS(false); + STREAMING::REMOVE_NAMED_PTFX_ASSET("core_snow"); + AUDIO::RELEASE_NAMED_SCRIPT_AUDIO_BANK("SNOW_FOOTSTEPS"); + + m_SnowEnabled = false; + } + } + + virtual void OnEnable() override + { + // ensure shop controller can't control the weather + if (!m_ShopControllerWeatherPatch) + { + m_ShopControllerWeatherPatch = ScriptPatches::AddPatch("shop_controller"_J, "2D 00 07 00 00 71 39 02 71", 5, {0x2E, 0x00, 0x00}); + } + m_ShopControllerWeatherPatch->Enable(); + } + + virtual void OnTick() override + { + auto weather = _Weather.GetState(); + MISC::SET_OVERRIDE_WEATHER(g_WeatherCodes[weather]); + if (weather == 9 || weather == 16) + EnableSnow(); + else + DisableSnow(); + + } + + virtual void OnDisable() override + { + if (m_ShopControllerWeatherPatch) + { + m_ShopControllerWeatherPatch->Disable(); + } + DisableSnow(); + MISC::CLEAR_OVERRIDE_WEATHER(); + } + }; + + static ForceWeather _ForceWeather{"forceweather", "Force Weather", "Forces the specified weather. Note that this effect is local and cannot be seen by other players"}; +} \ No newline at end of file diff --git a/src/game/frontend/submenus/Recovery/StatEditor.cpp b/src/game/frontend/submenus/Recovery/StatEditor.cpp index c845c79..0435f55 100644 --- a/src/game/frontend/submenus/Recovery/StatEditor.cpp +++ b/src/game/frontend/submenus/Recovery/StatEditor.cpp @@ -214,6 +214,21 @@ namespace YimMenu::Submenus STATS::SET_PACKED_STAT_INT_CODE(info.m_Index, value.m_AsInt, -1); } + static void WritePackedStatRange(int start, int end, int value) + { + for (int i = start; i <= end; i++) + { + auto info = GetPackedStatInfo(i); + if (!info.m_IsValid) + break; // the rest are probably not valid, either + + if (info.m_IsBoolStat) + STATS::SET_PACKED_STAT_BOOL_CODE(info.m_Index, static_cast(value), -1); + else + STATS::SET_PACKED_STAT_INT_CODE(info.m_Index, value, -1); + } + } + static bool RenderPackedStatEditor(StatValue& value, const PackedStatInfo& info) { ImGui::SetNextItemWidth(150.f); @@ -228,6 +243,7 @@ namespace YimMenu::Submenus auto menu = std::make_shared("Stat Editor"); auto normal = std::make_shared("Regular"); auto packed = std::make_shared("Packed"); + auto packed_range = std::make_shared("Packed Range"); normal->AddItem(std::make_unique([] { if (!NativeInvoker::AreHandlersCached()) @@ -303,9 +319,29 @@ namespace YimMenu::Submenus }); })); + packed_range->AddItem(std::make_unique([] { + if (!NativeInvoker::AreHandlersCached()) + return ImGui::TextDisabled("Natives not cached yet"); + + static int start{}, end{}, value{}; + + ImGui::SetNextItemWidth(150.f); + ImGui::InputInt("Start", &start); + ImGui::SameLine(); + ImGui::SetNextItemWidth(150.f); + ImGui::InputInt("End", &end); + ImGui::SetNextItemWidth(150.f); + ImGui::InputScalar("Value##packed_range", ImGuiDataType_U8, &value); + ImGui::SameLine(); + if (ImGui::Button("Write##packed_range")) + FiberPool::Push([] { + WritePackedStatRange(start, end, value); + }); + })); menu->AddItem(std::move(normal)); menu->AddItem(std::move(packed)); + menu->AddItem(std::move(packed_range)); return menu; } } \ No newline at end of file diff --git a/src/game/frontend/submenus/Recovery/Transactions.cpp b/src/game/frontend/submenus/Recovery/Transactions.cpp index 4a6cdee..c3d1936 100644 --- a/src/game/frontend/submenus/Recovery/Transactions.cpp +++ b/src/game/frontend/submenus/Recovery/Transactions.cpp @@ -359,7 +359,7 @@ namespace YimMenu::Submenus } } - static void PrintWarningMessage(std::string_view message) + static void SetTransactionError(std::string_view message) { ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::Colors::Red.Value); @@ -367,6 +367,14 @@ namespace YimMenu::Submenus ImGui::PopStyleColor(); } + static void SetTransactionWarning(std::string_view message) + { + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::Colors::Yellow.Value); + ImGui::Text("%s", message.data()); + ImGui::PopStyleColor(); + } + static bool EditTransactionItem(std::string_view label, TransactionInfo& info, TransactionItemHash& item, bool& is_valid, bool required = true, bool validate_category = true) { bool modified = false; @@ -396,7 +404,7 @@ namespace YimMenu::Submenus if (!item.m_IsValid && !empty) { - PrintWarningMessage("Item not found!"); + SetTransactionError("Item not found!"); is_valid = false; return false; } @@ -404,17 +412,24 @@ namespace YimMenu::Submenus // TODO: maybe not check this every tick? if (BANNED_ITEM_HASHES.contains(item.m_Hash)) { - PrintWarningMessage("This item has been blocked for your safety"); + SetTransactionError("This item has been blocked for your safety"); is_valid = false; return false; } - // TODO: is this really needed? useful? could we abuse more without this check? if (validate_category && !IsPriceModifier(item.m_IntendedCategory) && item.m_IntendedCategory != info.m_Category.m_Hash && !empty) { - PrintWarningMessage(std::format("Item category {} does not match txn category {}", CategoryNameFromHash(item.m_IntendedCategory), info.m_Category.m_Name)); - is_valid = false; - return false; + if (info.m_Type == TransactionInfo::Type::SERVICE) + { + SetTransactionError(std::format("Item category {} does not match txn category {}", CategoryNameFromHash(item.m_IntendedCategory), info.m_Category.m_Name)); + is_valid = false; + return false; + } + else + { + SetTransactionWarning(std::format("Item category {} does not match txn category {}", CategoryNameFromHash(item.m_IntendedCategory), info.m_Category.m_Name)); + } + } return modified; @@ -481,7 +496,7 @@ namespace YimMenu::Submenus ImGui::InputInt("Price", &info.m_Service.m_Price); if (info.m_Service.m_Price > info.m_Service.m_Item.m_IntendedPrice && info.m_Action.m_Hash == "NET_SHOP_ACTION_EARN"_J) { - PrintWarningMessage(std::format("Item price exceeds maximum allowed ({})", info.m_Service.m_Item.m_IntendedPrice)); + SetTransactionError(std::format("Item price exceeds maximum allowed ({})", info.m_Service.m_Item.m_IntendedPrice)); txn_valid = false; } } diff --git a/src/game/frontend/submenus/World.cpp b/src/game/frontend/submenus/World.cpp index 22d847b..7a23e5f 100644 --- a/src/game/frontend/submenus/World.cpp +++ b/src/game/frontend/submenus/World.cpp @@ -9,7 +9,6 @@ namespace YimMenu::Submenus auto main = std::make_shared("Main"); auto spawnersGroup = std::make_shared("Spawners"); auto iplsGroup = std::make_shared("IPLs"); - auto killPeds = std::make_shared("Kill", 1); killPeds->AddItem(std::make_shared("killallpeds"_J)); @@ -23,9 +22,15 @@ namespace YimMenu::Submenus bringOpts->AddItem(std::make_shared("bringvehs"_J)); bringOpts->AddItem(std::make_shared("bringobjs"_J)); + auto weatherOpts = std::make_shared("Weather", 1); + weatherOpts->AddItem(std::make_shared("weather"_J)); + weatherOpts->AddItem(std::make_shared("forceweather"_J, std::make_shared("setweather"_J), true)); + weatherOpts->AddItem(std::make_shared("forceweather"_J)); + main->AddItem(std::move(killPeds)); main->AddItem(std::move(deleteOpts)); main->AddItem(std::move(bringOpts)); + main->AddItem(std::move(weatherOpts)); auto spawnGroup = std::make_shared("Vehicle"); auto modsGroup = std::make_shared("Modifications");