feat(vehicle): Personal Vehicle Spawner (#579)

* feat(vehicle): Personal Vehicle Spawner

* Add Vinewood Club Garage & Clone PV

* Use different approach for clone PV

* some fixes

* Stuff

* Update lua bindings
This commit is contained in:
Arthur
2025-07-01 22:59:47 +03:00
committed by GitHub
parent dacfa665f7
commit 08cb3887b6
12 changed files with 882 additions and 54 deletions
+517
View File
@@ -0,0 +1,517 @@
#include "PersonalVehicles.hpp"
#include "core/backend/FiberPool.hpp"
#include "core/backend/ScriptMgr.hpp"
#include "game/backend/Self.hpp"
#include "game/gta/data/VehicleValues.hpp"
#include "game/gta/Natives.hpp"
#include "game/gta/ScriptFunction.hpp"
#include "game/gta/ScriptLocal.hpp"
#include "game/gta/ScriptGlobal.hpp"
#include "game/gta/Stats.hpp"
#include "types/script/globals/GPBD_FM.hpp"
#include "types/script/globals/MPSV.hpp"
#include "types/script/globals/FreemodeGeneral.hpp"
#include "types/script/globals/g_SavedMPGlobals.hpp"
#define MAX_GARAGE_NUM 33
namespace YimMenu
{
// TO-DO: Use script functions for these instead?
static int GetPropertyGarageOffset(int property)
{
switch (property)
{
case 0: return 13 * 0;
case 1: return 13 * 1;
case 2: return 13 * 2;
case 3: return 13 * 3;
case 4: return 13 * 4;
//Property 5 is not used.
case 6: return 65;
case 7: return 75;
case 8: return 88;
case 9: return 108;
case 10: return 128;
case 11: return 148;
case 12: return 159;
case 13: return 179;
case 14: return 191;
case 15: return 192;
case 16: return 202;
case 17: return 212;
case 18: return 227;
case 19: return 237;
case 20: return 247;
case 21: return 258;
case 22: return 268;
case 23: return 281;
case 24: return 294;
case 25: return 307;
case 26: return 317;
case 27: return 337;
case 28: return 350;
case 29: return 363;
case 30: return 415; // The Vinewood Club Garage
case 31: return 515;
case 32: return 537;
case MAX_GARAGE_NUM + 0: return 156; // Mobile Operations Center
case MAX_GARAGE_NUM + 1: return 224; // Nightclub B1
case MAX_GARAGE_NUM + 2: return 223; // Terrorbyte
case MAX_GARAGE_NUM + 3: return 278; // Kosatka
}
return -1;
}
static int GetPropertyGarageSize(int property)
{
switch (property)
{
case MAX_GARAGE_NUM + 0: // Mobile Operations Center
case MAX_GARAGE_NUM + 2: // Terrorbyte
case MAX_GARAGE_NUM + 3: // Kosatka
case 14: return 1;
case 31: return 2;
case MAX_GARAGE_NUM + 1: return 3; // Nightclub B1
case 11: return 8;
case 6:
case 15:
case 16:
case 17:
case 18:
case 19:
case 20:
case 21:
case 22:
case 32:
case 25: return 10;
case 13: return 11;
case 0:
case 1:
case 2:
case 3:
case 4:
case 7:
case 23:
case 24:
case 27:
case 28: return 13;
case 8:
case 9:
case 10:
case 12:
case 26: return 20;
case 29: return 50;
case 30: return 100; // The Vinewood Club Garage
}
return -1;
}
static int GetPropertyStatState(int property)
{
std::string stat = "";
switch (property)
{
case 0: stat = "MPX_PROPERTY_HOUSE"; break;
case 1: stat = "MPX_MULTI_PROPERTY_1"; break;
case 2: stat = "MPX_MULTI_PROPERTY_2"; break;
case 3: stat = "MPX_MULTI_PROPERTY_3"; break;
case 4: stat = "MPX_MULTI_PROPERTY_4"; break;
case 5: stat = "MPX_PROP_OFFICE"; break;
case 6: stat = "MPX_PROP_CLUBHOUSE"; break;
case 7: stat = "MPX_MULTI_PROPERTY_5"; break;
case 8: stat = "MPX_PROP_OFFICE_GAR1"; break;
case 9: stat = "MPX_PROP_OFFICE_GAR2"; break;
case 10: stat = "MPX_PROP_OFFICE_GAR3"; break;
case 11: stat = "MPX_PROP_IE_WAREHOUSE"; break;
case 12: stat = "MPX_PROP_HANGAR"; break;
case 13: stat = "MPX_PROP_DEFUNCBASE"; break;
case 14: stat = "MPX_PROP_NIGHTCLUB"; break;
case 15: stat = "MPX_PROP_MEGAWARE_GAR1"; break;
case 16: stat = "MPX_PROP_MEGAWARE_GAR2"; break;
case 17: stat = "MPX_PROP_MEGAWARE_GAR3"; break;
case 18: stat = "MPX_PROP_ARENAWARS_GAR1"; break;
case 19: stat = "MPX_PROP_ARENAWARS_GAR2"; break;
case 20: stat = "MPX_PROP_ARENAWARS_GAR3"; break;
case 21: stat = "MPX_PROP_CASINO_GAR1"; break;
case 22: stat = "MPX_PROP_ARCADE_GAR1"; break;
case 23: stat = "MPX_MULTI_PROPERTY_6"; break;
case 24: stat = "MPX_MULTI_PROPERTY_7"; break;
case 25: stat = "MPX_PROP_AUTO_SHOP"; break;
case 26: stat = "MPX_PROP_SECURITY_OFFICE_GAR"; break;
case 27: stat = "MPX_MULTI_PROPERTY_8"; break;
case 28: stat = "MPX_MULTI_PROPERTY_9"; break;
case 29: stat = "MPX_MULTSTOREY_GAR_OWNED"; break;
case 31: stat = "MPX_PROP_BAIL_OFFICE"; break;
case 32: stat = "MPX_PROP_HACKER_DEN"; break;
case 30:
case MAX_GARAGE_NUM + 0:
case MAX_GARAGE_NUM + 1:
case MAX_GARAGE_NUM + 2:
case MAX_GARAGE_NUM + 3: return 1;
default: return -1;
}
return Stats::GetInt(stat);
}
static std::string GetStaticPropertyName(int property, int garageSlotIterator)
{
switch (property)
{
case 12: // Hangar
{
auto id = GPBD_FM::Get()->Entries[Self::GetPlayer().GetId()].PropertyData.HangarData.Index;
switch (id)
{
case 1: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_HANGAR_1"); // LSIA Hangar 1
case 2: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_HANGAR_2"); // LSIA Hangar A17
case 3: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_HANGAR_3"); // Fort Zancudo Hangar A2
case 4: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_HANGAR_4"); // Fort Zancudo Hangar 3497
case 5: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_HANGAR_5"); // Fort Zancudo Hangar 3499
}
break;
}
case 13: // Facility
{
auto id = GPBD_FM::Get()->Entries[Self::GetPlayer().GetId()].PropertyData.DefunctBaseData.Index;
switch (id)
{
case 1: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_DBASE_1"); // Grand Senora Desert Facility
case 2: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_DBASE_2"); // Route 68 Facility
case 3: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_DBASE_3"); // Sandy Shores Facility
case 4: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_DBASE_4"); // Mount Gordo Facility
case 5: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_DBASE_6"); // Paleto Bay Facility
case 6: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_DBASE_7"); // Lago Zancudo Facility
case 7: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_DBASE_8"); // Zancudo River Facility
case 8: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_DBASE_9"); // Ron Alternates Wind Farm Facility
case 9: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_DBASE_10"); // Land Act Reservoir Facility
}
break;
}
case 14: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_BHUB_CLUBG"); // Nightclub Service Entrance
case 15: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_BHUB_GAR1"); // Nightclub B2
case 16: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_BHUB_GAR2"); // Nightclub B3
case 17: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_BHUB_GAR3"); // Nightclub B4
case 18: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("ARENA_GAR_F0"); // Arena Workshop
case 19: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("ARENA_GAR_F1"); // Arena Workshop B1
case 20: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("ARENA_GAR_F2"); // Arena Workshop B2
case 21: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("CASINO_GARNAME"); // Casino Penthouse
case 22: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("ARCADE_GARNAME"); // Arcade
case 25: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("AUT_SHP_GAR"); // Auto Shop
case 26: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("FIXER_GARNAME"); // Agency
case 29: // Eclipse Blvd Garage
{
int level = (garageSlotIterator - 1) / 10;
switch (level)
{
case 0: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MSG_B1"); // Eclipse Blvd Garage B1
case 1: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MSG_B2"); // Eclipse Blvd Garage B2
case 2: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MSG_B3"); // Eclipse Blvd Garage B3
case 3: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MSG_B4"); // Eclipse Blvd Garage B4
case 4: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MSG_B5"); // Eclipse Blvd Garage B5
}
}
case 30: // The Vinewood Club Garage
{
int level = (garageSlotIterator - 1) / 10;
switch (level)
{
case 0: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("VPG_B1"); // Basement Level 1
case 1: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("VPG_B2"); // Basement Level 2
case 2: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("VPG_B3"); // Basement Level 3
case 3: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("VPG_B4"); // Basement Level 4
case 4: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("VPG_B5"); // Basement Level 5
}
}
case 31: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("BO_GARNAME"); // Bail Office
case 32: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("HD_GARNAME"); // Garment Factory
case MAX_GARAGE_NUM + 0: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("GRTRUCK"); // Mobile Operations Center
case MAX_GARAGE_NUM + 1: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_BHUB_GAR0"); // Nightclub B1
case MAX_GARAGE_NUM + 2: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_BHUB_CLUBT"); // Terrorbyte
case MAX_GARAGE_NUM + 3: return HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION("MP_BHUB_SUB"); // Kosatka
}
return "";
}
PersonalVehicles::PersonalVehicle::PersonalVehicle(int id, MPSV_Entry* data) :
m_Id(id),
m_Data(data)
{
m_Model = m_Data->VehicleModel;
m_Plate = m_Data->NumberPlateText;
m_Name = std::format("{} ({})", HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION(VEHICLE::GET_DISPLAY_NAME_FROM_VEHICLE_MODEL(m_Model)), m_Plate);
SetGarage();
}
int PersonalVehicles::PersonalVehicle::GetId()
{
return m_Id;
}
MPSV_Entry* PersonalVehicles::PersonalVehicle::GetData()
{
return m_Data;
}
joaat_t PersonalVehicles::PersonalVehicle::GetModel()
{
return m_Model;
}
std::string PersonalVehicles::PersonalVehicle::GetPlate()
{
return m_Plate;
}
std::string PersonalVehicles::PersonalVehicle::GetName()
{
return m_Name;
}
std::string PersonalVehicles::PersonalVehicle::GetGarage()
{
return m_Garage;
}
void PersonalVehicles::PersonalVehicle::SetGarage()
{
for (int propertyIterator = 0; propertyIterator < MAX_GARAGE_NUM + 4; propertyIterator++)
{
auto propertyStatState = GetPropertyStatState(propertyIterator);
if (propertyStatState > 0)
{
auto garageSize = GetPropertyGarageSize(propertyIterator);
auto garageOffset = GetPropertyGarageOffset(propertyIterator);
for (int garageSlotIterator = 1; garageSlotIterator <= garageSize; garageSlotIterator++)
{
auto itemInSlot = *ScriptGlobal(1940530).At(garageOffset).At(garageSlotIterator).As<int*>() - 1;
if (itemInSlot == m_Id)
{
auto staticPropertyString = GetStaticPropertyName(propertyIterator, garageSlotIterator);
if (staticPropertyString.empty())
{
m_Garage = HUD::GET_FILENAME_FOR_AUDIO_CONVERSATION(ScriptGlobal(1312335).At(propertyStatState, 1951).At(16).As<const char*>());
}
else
{
m_Garage = staticPropertyString;
}
return;
}
}
}
}
}
bool PersonalVehicles::PersonalVehicle::Despawn()
{
if (auto veh = GetCurrentHandle(); veh.IsValid())
{
veh.BringToHalt();
m_Data->PersonalVehicleFlags.Clear(ePersonalVehicleFlags::TRIGGER_SPAWN_TOGGLE);
for (int i = 0; veh.IsValid(); i++)
{
ScriptMgr::Yield(100ms);
if (i > 30)
{
LOG(WARNING) << "Despawn() Timed out despawning Personal Vehicle.";
return false;
}
}
}
return true;
}
bool PersonalVehicles::PersonalVehicle::Repair()
{
if (m_Data->PersonalVehicleFlags.IsSet(ePersonalVehicleFlags::DESTROYED) && m_Data->PersonalVehicleFlags.IsSet(ePersonalVehicleFlags::HAS_INSURANCE))
{
m_Data->PersonalVehicleFlags.Clear(ePersonalVehicleFlags::DESTROYED);
m_Data->PersonalVehicleFlags.Clear(ePersonalVehicleFlags::IMPOUNDED);
m_Data->PersonalVehicleFlags.Clear(ePersonalVehicleFlags::UNK2);
return true;
}
return false;
}
bool PersonalVehicles::PersonalVehicle::Request(bool bring)
{
if (auto freemodeGeneral = FreemodeGeneral::Get())
{
if (freemodeGeneral->RequestedPersonalVehicleId != -1)
return false;
if (!GetCurrent()->Despawn())
return false;
Repair();
ScriptMgr::Yield(100ms);
if (bring)
freemodeGeneral->NodeDistanceCheck = 0;
freemodeGeneral->PersonalVehicleRequested = TRUE; // am_pi_menu also sets the field 956 but I guess we don't need that
freemodeGeneral->Exec1Impound = FALSE; // not really sure what this does
freemodeGeneral->RequestedPersonalVehicleId = m_Id;
ScriptMgr::Yield(100ms);
*ScriptLocal("freemode"_J, 19447).At(176).As<int*>() = 0;
if (bring)
{
for (int i = 0; !GetCurrentHandle().IsValid(); i++)
{
ScriptMgr::Yield(100ms);
if (i > 30)
break;
}
if (auto veh = GetCurrentHandle(); veh.IsValid())
{
auto coords = Self::GetPed().GetPosition();
auto heading = Self::GetPed().GetHeading();
veh.SetPosition(coords);
veh.SetHeading(heading);
veh.SetOnGroundProperly();
Self::GetPed().SetInVehicle(veh);
}
}
return true;
}
return false;
}
Vehicle PersonalVehicles::PersonalVehicle::Clone(rage::fvector3 coords, float heading)
{
if (auto veh = Vehicle::Create(m_Model, coords, heading))
{
auto oldVal = m_Data->IsPersonalVehicle;
m_Data->IsPersonalVehicle = 0;
// This sometimes fails to apply the crew logo since ADD_VEHICLE_EMBLEM needs to be called in a loop
static ScriptFunction applyMPSVData("freemode"_J, ScriptPointer("ApplyMPSVData", "5D ? ? ? 38 2A 71").Add(1).Rip());
applyMPSVData.Call<void>(veh.GetHandle(), m_Data, true, true, false);
m_Data->IsPersonalVehicle = oldVal;
return veh;
}
return nullptr;
}
std::unique_ptr<PersonalVehicles::PersonalVehicle> PersonalVehicles::GetCurrentImpl()
{
auto savedMPGlobals = g_SavedMPGlobals::Get();
auto MPSV = MPSV::Get();
if (savedMPGlobals && MPSV)
{
auto id = savedMPGlobals->Entries[0].GeneralSaved.LastSavedCar;
auto data = &MPSV->Entries[id];
return std::make_unique<PersonalVehicle>(id, data);
}
return nullptr;
}
Vehicle PersonalVehicles::GetCurrentHandleImpl()
{
if (auto freemodeGeneral = FreemodeGeneral::Get())
{
if (auto veh = freemodeGeneral->PersonalVehicleIndex; veh != -1)
return Vehicle(veh);
}
return nullptr;
}
void PersonalVehicles::UpdateImpl()
{
const auto now = std::chrono::high_resolution_clock::now();
if (std::chrono::duration_cast<std::chrono::seconds>(now - m_LastUpdate) < 10s)
return;
m_LastUpdate = std::chrono::high_resolution_clock::now();
FiberPool::Push([] {
RegisterVehicles();
RegisterGarages();
});
}
void PersonalVehicles::RegisterVehiclesImpl()
{
if (auto mpsv = MPSV::Get())
{
const auto arraySize = *(int*)mpsv;
for (int i = 0; i < arraySize; i++)
{
if (i % 100 == 0)
ScriptMgr::Yield();
auto data = &MPSV::Get()->Entries[i];
const auto model = data->VehicleModel;
const auto& it = m_PVLookup.find(i);
const auto exists = it != m_PVLookup.end();
if (STREAMING::IS_MODEL_A_VEHICLE(model))
{
auto veh = std::make_unique<PersonalVehicle>(i, data);
if (exists)
{
if (veh->GetName() != it->second)
{
m_PersonalVehicles.erase(it->second);
it->second = veh->GetName();
m_PersonalVehicles.emplace(veh->GetName(), std::move(veh));
}
else
{
m_PersonalVehicles[veh->GetName()]->SetGarage();
}
continue;
}
m_PVLookup.emplace(i, veh->GetName());
m_PersonalVehicles.emplace(veh->GetName(), std::move(veh));
continue;
}
if (exists)
{
m_PersonalVehicles.erase(it->second);
m_PVLookup.erase(i);
}
}
}
}
void PersonalVehicles::RegisterGaragesImpl()
{
m_Garages.clear();
for (const auto& [name, ptr] : GetPersonalVehicles())
{
if (auto garageName = ptr->GetGarage(); !garageName.empty())
{
m_Garages.emplace(garageName);
}
}
}
}
+93
View File
@@ -0,0 +1,93 @@
#pragma once
#include "core/util/Joaat.hpp"
#include "game/gta/Vehicle.hpp"
#include <set>
struct MPSV_Entry;
namespace YimMenu
{
class PersonalVehicles
{
public:
class PersonalVehicle
{
int m_Id;
MPSV_Entry* m_Data;
joaat_t m_Model;
std::string m_Plate;
std::string m_Name;
std::string m_Garage;
public:
PersonalVehicle(int id, MPSV_Entry* data);
int GetId();
MPSV_Entry* GetData();
joaat_t GetModel();
std::string GetPlate();
std::string GetName();
std::string GetGarage();
void SetGarage();
bool Despawn();
bool Repair();
bool Request(bool bring = false);
Vehicle Clone(rage::fvector3 coords, float heading);
};
static std::unique_ptr<PersonalVehicle> GetCurrent()
{
return GetInstance().GetCurrentImpl();
}
static Vehicle GetCurrentHandle()
{
return GetInstance().GetCurrentHandleImpl();
}
static std::map<std::string, std::unique_ptr<PersonalVehicle>>& GetPersonalVehicles()
{
return GetInstance().m_PersonalVehicles;
}
static std::set<std::string>& GetGarages()
{
return GetInstance().m_Garages;
}
static void Update()
{
GetInstance().UpdateImpl();
}
static void RegisterGarages()
{
GetInstance().RegisterGaragesImpl();
}
static void RegisterVehicles()
{
GetInstance().RegisterVehiclesImpl();
}
private:
static PersonalVehicles& GetInstance()
{
static PersonalVehicles instance;
return instance;
}
std::unique_ptr<PersonalVehicle> GetCurrentImpl();
Vehicle GetCurrentHandleImpl();
void UpdateImpl();
void RegisterVehiclesImpl();
void RegisterGaragesImpl();
std::map<std::string, std::unique_ptr<PersonalVehicle>> m_PersonalVehicles;
std::map<int, std::string> m_PVLookup;
std::set<std::string> m_Garages;
std::chrono::time_point<std::chrono::steady_clock> m_LastUpdate;
};
}
@@ -0,0 +1,21 @@
#include "core/commands/Command.hpp"
#include "game/backend/PersonalVehicles.hpp"
#include "game/pointers/Pointers.hpp"
namespace YimMenu
{
class DespawnPersonalVehicle : public Command
{
using Command::Command;
virtual void OnCall() override
{
if (!*Pointers.IsSessionStarted)
return;
PersonalVehicles::GetCurrent()->Despawn();
}
};
static DespawnPersonalVehicle _DespawnPersonalVehicle{"despawnpv", "Despawn Personal Vehicle", "Returns your current Personal Vehicle to storage."};
}
+15 -20
View File
@@ -1,6 +1,7 @@
#include "core/commands/Command.hpp"
#include "core/frontend/Notifications.hpp"
#include "types/script/globals/MPSV.hpp"
#include "game/backend/PersonalVehicles.hpp"
#include "game/pointers/Pointers.hpp"
namespace YimMenu::Features
{
@@ -10,27 +11,21 @@ namespace YimMenu::Features
virtual void OnCall() override
{
if (auto mpsv = MPSV::Get())
{
int count = 0;
for (int i = 0; i < *(int*)mpsv; i++)
{
if (mpsv->Entries[i].PersonalVehicleFlags.IsSet(1) && mpsv->Entries[i].PersonalVehicleFlags.IsSet(2))
{
mpsv->Entries[i].PersonalVehicleFlags.Clear(1);
mpsv->Entries[i].PersonalVehicleFlags.Clear(6);
mpsv->Entries[i].PersonalVehicleFlags.Clear(16);
mpsv->Entries[i].PersonalVehicleFlags.Set(0);
mpsv->Entries[i].PersonalVehicleFlags.Set(11);
count++;
}
}
if (!*Pointers.IsSessionStarted)
return;
if (count > 0)
Notifications::Show("Fix All Vehicles", std::format("{} vehicles fixed.", count), NotificationType::Success);
else
Notifications::Show("Fix All Vehicles", "No vehicles to fix.");
int count = 0;
for (const auto& it : PersonalVehicles::GetPersonalVehicles())
{
const auto& personalVeh = it.second;
if (personalVeh->Repair())
count++;
}
if (count > 0)
Notifications::Show("Fix All Vehicles", std::format("{} vehicles fixed.", count), NotificationType::Success);
else
Notifications::Show("Fix All Vehicles", "No vehicles to fix.");
}
};
@@ -0,0 +1,21 @@
#include "core/commands/Command.hpp"
#include "game/backend/PersonalVehicles.hpp"
#include "game/pointers/Pointers.hpp"
namespace YimMenu
{
class RequestPersonalVehicle : public Command
{
using Command::Command;
virtual void OnCall() override
{
if (!*Pointers.IsSessionStarted)
return;
PersonalVehicles::GetCurrent()->Request();
}
};
static RequestPersonalVehicle _RequestPersonalVehicle{"requestpv", "Request Personal Vehicle", "Requests your current Personal Vehicle."};
}
+3 -1
View File
@@ -13,7 +13,7 @@ namespace YimMenu::Submenus
auto main = std::make_shared<Category>("Main");
auto globals = std::make_shared<Group>("Globals");
auto tools = std::make_shared<Group>("Tools", 1);
auto tools = std::make_shared<Group>("Tools", 2);
auto misc = std::make_shared<Group>("Misc");
globals->AddItem(std::make_shared<BoolCommandItem>("vehiclegodmode"_J, "Godmode"));
@@ -26,6 +26,8 @@ namespace YimMenu::Submenus
tools->AddItem(std::make_shared<CommandItem>("repairvehicle"_J));
tools->AddItem(std::make_shared<CommandItem>("fixallvehicles"_J));
tools->AddItem(std::make_shared<CommandItem>("callmechanic"_J));
tools->AddItem(std::make_shared<CommandItem>("requestpv"_J));
tools->AddItem(std::make_shared<CommandItem>("despawnpv"_J));
tools->AddItem(std::make_shared<CommandItem>("savepersonalvehicle"_J));
misc->AddItem(std::make_shared<BoolCommandItem>("speedometer"_J));
@@ -2,15 +2,18 @@
#include "core/commands/BoolCommand.hpp"
#include "core/backend/ScriptMgr.hpp"
#include "core/backend/FiberPool.hpp"
#include "core/frontend/Notifications.hpp"
#include "game/backend/Self.hpp"
#include "game/backend/PersonalVehicles.hpp"
#include "game/gta/data/Vehicles.hpp"
#include "game/gta/Vehicle.hpp"
#include "game/gta/Natives.hpp"
namespace YimMenu::Submenus
{
static BoolCommand spawnInsideVehicle{"spawninsideveh", "Spawn Inside", "Spawn inside the vehicle."};
static BoolCommand spawnVehicleMaxed{"spawnvehmaxed", "Spawn Maxed", "Spawn the vehicle maxed."};
static BoolCommand spawnInsidePersonalVehicle{"spawninsidepv", "Spawn Inside", "Spawn inside the personal vehicle."};
static BoolCommand spawnClonePersonalVehicle{"spawnclonepv", "Spawn Clone", "Spawn a clone of the persone vehicle."};
static Vector3 GetVehicleSpawnLoc(joaat_t hash, bool spawnInside)
{
@@ -31,15 +34,18 @@ namespace YimMenu::Submenus
return ENTITY::GET_OFFSET_FROM_ENTITY_IN_WORLD_COORDS(Self::GetPed().GetHandle(), 0.f, y_offset, 0.f);
}
std::shared_ptr<Category> BuildSpawnVehicleMenu()
std::shared_ptr<TabItem> RenderSpawnNewVehicle()
{
auto menu = std::make_shared<Category>("Spawn");
auto tab = std::make_shared<TabItem>("New Vehicle");
auto spawn = std::make_shared<Group>("Spawn");
auto settings = std::make_shared<Group>("Settings");
static std::vector<std::string> vehicleNames{};
static std::vector<int> vehicleClasses{};
static int selectedClass{-1};
menu->AddItem(std::make_unique<ImGuiItem>([] {
spawn->AddItem(std::make_unique<ImGuiItem>([] {
static bool init = [] {
FiberPool::Push([] {
std::unordered_map<std::string, int> nameCount;
@@ -96,40 +102,44 @@ namespace YimMenu::Submenus
ImGui::EndCombo();
}
const int visible = std::min(10, static_cast<int>(vehicleNames.size()));
const int visible = std::min(20, static_cast<int>(vehicleNames.size()));
const float height = visible * ImGui::GetTextLineHeightWithSpacing();
if (ImGui::BeginListBox("##vehicles", {300.f, height}))
{
std::string lower = search;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
for (int veh = 0; veh < vehicleNames.size(); veh++)
if (vehicleNames.empty())
{
if (vehicleNames[veh].empty())
continue;
auto hash = g_VehicleHashes[veh];
auto name = vehicleNames[veh];
auto lowerName = name;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
bool matchesSearch = lowerName.find(lower) != std::string::npos;
bool matchesClass = selectedClass == -1 || vehicleClasses[veh] == selectedClass;
if (matchesSearch && matchesClass)
ImGui::Text("Natives not cached yet.");
}
else
{
std::string lower = search;
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
for (int veh = 0; veh < vehicleNames.size(); veh++)
{
ImGui::PushID(hash);
if (ImGui::Selectable(name.c_str()))
auto hash = g_VehicleHashes[veh];
auto name = vehicleNames[veh];
auto lowerName = name;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
bool matchesSearch = lowerName.find(lower) != std::string::npos;
bool matchesClass = selectedClass == -1 || vehicleClasses[veh] == selectedClass;
if (matchesSearch && matchesClass)
{
FiberPool::Push([hash] {
auto handle = Vehicle::Create(hash, GetVehicleSpawnLoc(hash, spawnInsideVehicle.GetState()), Self::GetPed().GetHeading());
ImGui::PushID(hash);
if (ImGui::Selectable(name.c_str()))
{
FiberPool::Push([hash] {
auto handle = Vehicle::Create(hash, GetVehicleSpawnLoc(hash, spawnInsideVehicle.GetState()), Self::GetPed().GetHeading());
if (spawnInsideVehicle.GetState())
Self::GetPed().SetInVehicle(handle);
if (spawnInsideVehicle.GetState())
Self::GetPed().SetInVehicle(handle);
if (spawnVehicleMaxed.GetState())
handle.Upgrade();
});
if (spawnVehicleMaxed.GetState())
handle.Upgrade();
});
}
ImGui::PopID();
}
ImGui::PopID();
}
}
@@ -137,8 +147,126 @@ namespace YimMenu::Submenus
}
}));
menu->AddItem(std::make_shared<BoolCommandItem>("spawninsideveh"_J));
menu->AddItem(std::make_shared<BoolCommandItem>("spawnvehmaxed"_J));
settings->AddItem(std::make_shared<BoolCommandItem>("spawninsideveh"_J));
settings->AddItem(std::make_shared<BoolCommandItem>("spawnvehmaxed"_J));
tab->AddItem(spawn);
tab->AddItem(settings);
return tab;
}
std::shared_ptr<TabItem> RenderSpawnPersonalVehicle()
{
auto tab = std::make_shared<TabItem>("Personal Vehicle");
auto spawn = std::make_shared<Group>("Spawn");
auto settings = std::make_shared<Group>("Settings");
static std::string selectedGarageStr{""};
spawn->AddItem(std::make_unique<ImGuiItem>([] {
if (!*Pointers.IsSessionStarted)
return ImGui::TextDisabled("Join GTA Online.");
PersonalVehicles::Update();
static char search[64];
ImGui::SetNextItemWidth(300.f);
ImGui::InputTextWithHint("Name", "Search", search, sizeof(search));
ImGui::SetNextItemWidth(300.f);
if (ImGui::BeginCombo("Garage", selectedGarageStr.empty() ? "All" : selectedGarageStr.c_str()))
{
if (ImGui::Selectable("All", selectedGarageStr.empty()))
{
selectedGarageStr.clear();
}
for (auto garage : PersonalVehicles::GetGarages())
{
if (ImGui::Selectable(garage.c_str(), garage == selectedGarageStr))
{
selectedGarageStr = garage;
}
}
ImGui::EndCombo();
}
const int visible = std::min(20, static_cast<int>(PersonalVehicles::GetPersonalVehicles().size()));
const float height = visible * ImGui::GetTextLineHeightWithSpacing();
if (ImGui::BeginListBox("##personalvehicles", {300.f, height}))
{
if (PersonalVehicles::GetPersonalVehicles().empty())
{
ImGui::Text("Stats not loaded yet.");
}
else
{
std::string lowerSearch = search;
std::transform(lowerSearch.begin(), lowerSearch.end(), lowerSearch.begin(), tolower);
for (const auto& it : PersonalVehicles::GetPersonalVehicles())
{
const auto& label = it.first;
const auto& personalVeh = it.second;
auto lowerName = label;
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), ::tolower);
bool matchesSearch = lowerName.find(lowerSearch) != std::string::npos;
bool matchesGarage = selectedGarageStr.empty() || personalVeh->GetGarage() == selectedGarageStr;
if (matchesSearch && matchesGarage)
{
ImGui::PushID(personalVeh->GetId());
if (ImGui::Selectable(label.c_str()))
{
FiberPool::Push([&personalVeh] {
if (spawnClonePersonalVehicle.GetState())
{
auto coords = GetVehicleSpawnLoc(personalVeh->GetModel(), spawnInsidePersonalVehicle.GetState());
auto heading = Self::GetPed().GetHeading();
auto handle = personalVeh->Clone(coords, heading);
if (spawnInsidePersonalVehicle.GetState())
Self::GetPed().SetInVehicle(handle);
}
else
{
if (!personalVeh->Request(spawnInsidePersonalVehicle.GetState()))
Notifications::Show("Spawn Personal Vehicle", "Failed to spawn Personal Vehicle.", NotificationType::Error);
}
});
}
ImGui::PopID();
}
}
}
ImGui::EndListBox();
}
}));
settings->AddItem(std::make_shared<BoolCommandItem>("spawninsidepv"_J));
settings->AddItem(std::make_shared<BoolCommandItem>("spawnclonepv"_J));
tab->AddItem(spawn);
tab->AddItem(settings);
return tab;
}
std::shared_ptr<Category> BuildSpawnVehicleMenu()
{
auto menu = std::make_shared<Category>("Spawn");
menu->AddItem(std::make_shared<ImGuiItem>([] {
ImGui::BeginTabBar("Spawn");
}));
menu->AddItem(RenderSpawnNewVehicle());
menu->AddItem(RenderSpawnPersonalVehicle());
menu->AddItem(std::make_shared<ImGuiItem>([] {
ImGui::EndTabBar();
}));
return menu;
}
}
+14
View File
@@ -174,6 +174,20 @@ namespace YimMenu
VEHICLE::SET_REDUCED_SUSPENSION_FORCE(GetHandle(), lower);
}
void Vehicle::BringToHalt(float distance, int duration)
{
ENTITY_ASSERT_VALID();
VEHICLE::BRING_VEHICLE_TO_HALT(GetHandle(), distance, duration, FALSE);
}
bool Vehicle::SetOnGroundProperly()
{
ENTITY_ASSERT_VALID();
return VEHICLE::SET_VEHICLE_ON_GROUND_PROPERLY(GetHandle(), 5.f);
}
std::string Vehicle::GetFullName()
{
auto model = ENTITY::GET_ENTITY_MODEL(GetHandle());
+3
View File
@@ -36,6 +36,9 @@ namespace YimMenu
// stance
void LowerStance(bool lower);
void BringToHalt(float distance = 1.0f, int duration = 1);
bool SetOnGroundProperly();
std::string GetFullName();
};
}
+14
View File
@@ -100,6 +100,18 @@ namespace YimMenu::Lua
return 0;
}
static int BringToHalt(lua_State* state)
{
GetObject<YimMenu::Vehicle>(state, 1).BringToHalt(luaL_checknumber(state, 2), luaL_checkinteger(state, 3));
return 0;
}
static int SetOnGroundProperly(lua_State* state)
{
lua_pushboolean(state, GetObject<YimMenu::Vehicle>(state, 1).SetOnGroundProperly());
return 1;
}
static int GetFullName(lua_State* state)
{
auto name = GetObject<YimMenu::Vehicle>(state, 1).GetFullName();
@@ -126,6 +138,8 @@ namespace YimMenu::Lua
SetFunction(state, IsBoostActive, "is_boost_active");
SetFunction(state, SetBoostCharge, "set_boost_charge");
SetFunction(state, LowerStance, "lower_stance");
SetFunction(state, BringToHalt, "bring_to_halt");
SetFunction(state, SetOnGroundProperly, "set_on_ground_properly");
SetFunction(state, GetFullName, "get_full_name");
}
lua_setfield(state, -2, "__index"); // prototype
+9 -1
View File
@@ -58,7 +58,15 @@ struct FreemodeGeneral
{
uint64_t PAD_0000[301];
VEHICLE_INDEX PersonalVehicleIndex; // 301
uint64_t PAD_0302[6553];
uint64_t PAD_0302[653];
SCR_BOOL PersonalVehicleRequested; // 955
uint64_t PAD_0956[13];
SCR_BOOL NodeDistanceCheck; // 969
uint64_t PAD_0970[48];
SCR_INT RequestedPersonalVehicleId; // 1018
uint64_t PAD_1019[2];
SCR_BOOL Exec1Impound; // 1021
uint64_t PAD_1022[5833];
STREET_DEALERS StreetDealers; // 6855
uint64_t PAD_6879[71];
DAILY_RESET_DATA DailyReset; // 6950
+13 -1
View File
@@ -1,6 +1,18 @@
#pragma once
#include "types/script/types.hpp"
enum class ePersonalVehicleFlags
{
TRIGGER_SPAWN_TOGGLE = 0,
DESTROYED = 1,
HAS_INSURANCE = 2,
UNK0 = 3,
IMPOUNDED = 6,
UNK1 = 10,
SPAWN_AT_MORS_MUTUAL = 11,
UNK2 = 16
};
struct MPSV_Entry
{
SCR_INT NumberPlateTextIndex; // 0 (SET/GET_VEHICLE_NUMBER_PLATE_TEXT)
@@ -41,7 +53,7 @@ struct MPSV_Entry
SCR_INT ImportExportVehicle; // 101
SCR_INT TyreType; // 102
SCR_BITSET<uint64_t> Flags3; // 103 (flag 0 is set if the MLJ decor exists)
SCR_BITSET<uint64_t> PersonalVehicleFlags; // 104
SCR_BITSET<ePersonalVehicleFlags> PersonalVehicleFlags; // 104
SCR_INT ImpoundTime; // 105 (MP_STAT_MPSV_IMPOUND_TIME)
SCR_INT PremiumPaid; // 106 (MP_STAT_MPSV_PREMIUM_PAID)
TEXT_LABEL_63 DestroyerName; // 107