diff --git a/vrp/cfg/modules.lua b/vrp/cfg/modules.lua index bfbe848..4e7be5b 100644 --- a/vrp/cfg/modules.lua +++ b/vrp/cfg/modules.lua @@ -15,6 +15,7 @@ local modules = { weapon = true, user = true, identity = true, + vehicle = true, logs = true -- discord logs } diff --git a/vrp/cfg/vehicles.lua b/vrp/cfg/vehicles.lua new file mode 100644 index 0000000..ec8235c --- /dev/null +++ b/vrp/cfg/vehicles.lua @@ -0,0 +1,94 @@ +local cfg = {} + +cfg.vehicle_update_interval = 15 -- seconds +cfg.vehicle_check_interval = 15 -- seconds, re-own/respawn task + + +-- list of all purchasable vehicles +-- model = spawn code +-- name = display name +-- price = purchase price +-- category = as per GetVehicleClass() (see FiveM docs) +-- type = as per IsThisModelACar()/IsThisModelABike(), etc. +-- shop = single shop key or array of shop keys +cfg.vehicles = { + { model = 'alpha', name = 'Alpha', price = 53000, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'banshee', name = 'Banshee', price = 56000, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'bestiagts', name = 'Bestia GTS', price = 37000, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'buffalo', name = 'Buffalo', price = 18750, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'buffalo2', name = 'Buffalo S', price = 24500, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'carbonizzare', name = 'Carbonizzare', brand = 'Grotti', price = 155000, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'comet2', name = 'Comet', brand = 'Pfister', price = 130000, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'comet3', name = 'Comet Retro Custom', brand = 'Pfister', price = 175000, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'comet4', name = 'Comet Safari', brand = 'Pfister', price = 110000, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'comet5', name = 'Comet SR', brand = 'Pfister', price = 155000, category = 'sports', type = 'automobile', shop = 'pdm' }, + { model = 'coquette', name = 'Coquette', brand = 'Invetero', price = 145000, category = 'sports', type = 'automobile', shop = 'pdm' }, + + + + { model = "blista", name = "Blista", price = 100, category = "compacts", type = "automobile", shop = {"pdm","luxury"} }, + { model = "buzzard", name = "Buzzard Attack", price = 100, category = "helicopter", type = "aircraft", shop = "aircraft" }, + { model = "dinghy", name = "Dinghy", price = 100, category = "boats", type = "boat", shop = "marina" } + -- add your own entries below +} + +-- configuration for each vehicle shop +-- key = shop identifier (matches cfg.vehicles.shop) +-- showroom_location = vec3(x,y,z) +-- preview = vec4(x,y,z,heading) +-- purchaseSpawn = vec4(x,y,z,heading) +-- blip = { id = blipId, color = blipColor } +-- marker = { id = markerId, scale = { x,y,z }, color = { r,g,b,a } } +cfg.vehicleshops = { + pdm = { + shop_name = "Luxury Car Dealership", + showroom_location = vec3(-54.94, -1111.50, 26.44), + preview = vec4(-60.0, -1110.0, 26.4, 120.0), + purchaseSpawn = { + vec4(-61.744976043701,-1117.8952636719,26.432458877563, 10), -- Location where purchased vehicles will spawn + vec4(-56.407440185547,-1116.4392089844,26.434999465942, 10) + + }, + blip = { id = 326, color = 69 }, + marker = { id = 1, scale = {1.5,1.5,1.0}, color = {255,215,0,100} } + }, + aircraft = { + shop_name = "LSIA Hangar", + showroom_location = vec3(-970.63586425781,-2999.8103027344,13.945083618164), + preview = vec4(-1345.1208496094,-2722.6687011719,13.944955825806, 330.0), + purchaseSpawn = { + vec4(-979.73876953125,-2997.0693359375,13.945078849792,0), -- x,y,z,heading + vec4(-974.91973876953,-2986.9838867188,13.945068359375, 0), + vec4(-984.21917724609,-3004.3366699219,13.945056915283,0) + }, + blip = { id = 90, color = 38 }, + marker = { id = 1, scale = {2.0,2.0,1.0}, color = {0,191,255,120} } + } + } + +-- where players can sell back vehicles +cfg.sellvehicle = { + cars = { + name = "Sell Cars", + type = "automobile", + sellPrice = 75, -- players get 75% of original price + blip = { id = 369, color = 25 }, + marker = { id = 1, scale = {1.5, 1.5, 1.0}, color = {0, 128, 255, 100} }, + coords = { + vec3(-61.744976043701,-1117.8952636719,26.432458877563) + } + }, + + aircraft = { + name = "Sell Aircraft", + type = "aircraft", + sellPrice = 50, -- players get 50% of original price + blip = { id = 370, color = 38 }, + marker = { id = 1, scale = {2.0, 2.0, 1.0}, color = {0, 191, 255, 120} }, + coords = { + vec3(-965.33026123047,-3004.2521972656,13.9426612854) + } + } + } + +return cfg diff --git a/vrp/client/vehicle.lua b/vrp/client/vehicle.lua new file mode 100644 index 0000000..49b7f41 --- /dev/null +++ b/vrp/client/vehicle.lua @@ -0,0 +1,469 @@ +-- https://github.com/ImagicTheCat/vRP +-- MIT license (see LICENSE or vrp/vRPShared.lua) + +if not vRP.modules.vehicle then return end + +local Vehicle = class("Vehicle", vRP.Extension) + +-- METHODS + +function Vehicle:__construct() + vRP.Extension.__construct(self) + + -- init decorators + DecorRegister("vRP.owner", 3) + + self.vehicles = {} -- map of vehicle model => veh id (owned vehicles) + self.hash_models = {} -- map of hash => model + + self.update_interval = 30 -- seconds + self.check_interval = 15 -- seconds + self.respawn_radius = 200 + + self.state_ready = false -- flag, if true will try to re-own/spawn periodically out vehicles + + self.out_vehicles = {} -- map of vehicle model => {cstate, position, rotation}, unloaded out vehicles to spawn + + -- task: save vehicle states + Citizen.CreateThread(function() + while true do + Citizen.Wait(self.update_interval*1000) + + if self.state_ready then + local states = {} + + for model, veh in pairs(self.vehicles) do + if IsEntityAVehicle(veh) then + local state = self:getVehicleState(model) + state.position = {table.unpack(GetEntityCoords(veh, true))} + state.rotation = {GetEntityQuaternion(veh)} + + states[model] = state + + if self.out_vehicles[model] then -- update out vehicle state data + self.out_vehicles[model] = {state, state.position, state.rotation} + end + end + end + + self.remote._updateVehicleStates(states) + vRP.EXT.PlayerState.remote._update({ in_owned_vehicle = self:getInOwnedVehicleModel() or false}) + end + end + end) + + -- task: vehicles check + Citizen.CreateThread(function() + while true do + Citizen.Wait(self.check_interval*1000) + + if self.state_ready then + self:cleanupVehicles() + self:tryOwnVehicles() -- get back network lost vehicles + self:trySpawnOutVehicles() + end + end + end) +end + +local custom = {} + +-- veh: vehicle game id +-- return owner character id and model or nil if not managed by vRP +-- Refactored getVehicleInfo function +function Vehicle:getVehicleInfo(veh) + if DecorExistOn(veh, "vRP.owner") then + local model = self.hash_models[GetEntityModel(veh)] + if model then + return DecorGetInt(veh, "vRP.owner"), model + end + end +end + +-- spawn vehicle +-- will be placed on ground properly +-- one vehicle per model allowed at the same time +-- +-- state: (optional) vehicle state (client) +-- position: (optional) {x,y,z}, if not passed the vehicle will be spawned on the player (and will be put inside the vehicle) +-- rotation: (optional) quaternion {x,y,z,w}, if passed with the position, will be applied to the vehicle entity +function Vehicle:spawnVehicle(model, state, position, rotation) + self:despawnVehicle(model) + + -- Load vehicle model + local mhash = GetHashKey(model) + RequestModel(mhash) + local i = 0 + while not HasModelLoaded(mhash) and i < 10000 do + Citizen.Wait(10) + i = i + 1 + end + + if HasModelLoaded(mhash) then + local ped = GetPlayerPed(-1) + local x, y, z + + if position and position.x and position.y and position.z then + x, y, z = position.x, position.y, position.z + else + x, y, z = table.unpack(GetEntityCoords(ped)) + end + + local nveh = CreateVehicle(mhash, x, y, z + 0.5, 0.0, true, false) + + -- Set rotation and heading if provided + if rotation then + SetEntityQuaternion(nveh, table.unpack(rotation)) + else + SetEntityHeading(nveh, GetEntityHeading(ped)) + end + + -- Finalize vehicle setup + SetVehicleOnGroundProperly(nveh) + SetEntityInvincible(nveh, false) + + -- Put the player inside the vehicle if no position is provided + if not position then + SetPedIntoVehicle(ped, nveh, -1) + end + + -- Set vehicle plate and ownership + local plateText = custom.plate_txt or ("LS " .. vRP.EXT.Identity.registration) + SetVehicleNumberPlateText(nveh, plateText) + SetEntityAsMissionEntity(nveh, true, true) + SetVehicleHasBeenOwnedByPlayer(nveh, true) + DecorSetInt(nveh, "vRP.owner", vRP.EXT.Base.cid) + self.vehicles[model] = nveh + + -- Set vehicle state if provided + if state then + self:setVehicleState(nveh, state) + end + + -- Mark the model as no longer needed and trigger event + SetModelAsNoLongerNeeded(mhash) + vRP:triggerEvent("VehicleVehicleSpawn", model) + end +end + + +-- return true if despawned +function Vehicle:despawnVehicle(model) + local veh = self.vehicles[model] + if veh then + -- Before despawning, save its state + self:saveVehicleState() + + vRP:triggerEvent("VehicleVehicleDespawn", model) + + -- remove vehicle + SetVehicleHasBeenOwnedByPlayer(veh,false) + SetEntityAsMissionEntity(veh, false, true) + SetVehicleAsNoLongerNeeded(Citizen.PointerValueIntInitialized(veh)) + Citizen.InvokeNative(0xEA386986E786A54F, Citizen.PointerValueIntInitialized(veh)) + self.vehicles[model] = nil + + return true + end +end + +function Vehicle:despawnVehicles() + for model in pairs(self.vehicles) do + self:despawnVehicle(model) + end +end + +-- get all game vehicles +-- return list of veh +function Vehicle:getAllVehicles() + local vehs = {} + local it, veh = FindFirstVehicle() + if veh then table.insert(vehs, veh) end + local ok + repeat + ok, veh = FindNextVehicle(it) + if ok and veh then table.insert(vehs, veh) end + until not ok + EndFindVehicle(it) + + return vehs +end + +-- try re-own vehicles +function Vehicle:tryOwnVehicles() + for _, veh in pairs(self:getAllVehicles()) do + local cid, model = self:getVehicleInfo(veh) + if cid and vRP.EXT.Base.cid == cid then -- owned + local old_veh = self.vehicles[model] + if old_veh and IsEntityAVehicle(old_veh) then -- still valid + if old_veh ~= veh then -- remove this new one + SetVehicleHasBeenOwnedByPlayer(veh,false) + SetEntityAsMissionEntity(veh, false, true) + SetVehicleAsNoLongerNeeded(Citizen.PointerValueIntInitialized(veh)) + Citizen.InvokeNative(0xEA386986E786A54F, Citizen.PointerValueIntInitialized(veh)) + end + else -- no valid old veh + self.vehicles[model] = veh -- re-own + end + end + end +end + +function Vehicle:trySpawnOutVehicles() + if not self.respawn_radius then return end -- early check to prevent nil compare + + local x,y,z = vRP.EXT.Base:getPosition() + + for model, data in pairs(self.out_vehicles) do + if not self.vehicles[model] then + local vx,vy,vz = table.unpack(data[2]) + local distance = GetDistanceBetweenCoords(x,y,z,vx,vy,vz,true) + + if distance <= self.respawn_radius then + self:spawnVehicle(model, data[1], data[2], data[3]) + end + end + end +end + + +-- cleanup invalid owned vehicles +function Vehicle:cleanupVehicles() + for model, veh in pairs(self.vehicles) do + if not IsEntityAVehicle(veh) then + self.vehicles[model] = nil + end + end +end + +-- return model or nil if not in owned vehicle +function Vehicle:getInOwnedVehicleModel() + local veh = GetVehiclePedIsIn(GetPlayerPed(-1),false) + local cid, model = self:getVehicleInfo(veh) + if cid and cid == vRP.EXT.Base.cid then + return model + end +end + +-- VEHICLE STATE + +function Vehicle:getVehicleCustomization(veh) + + custom.colours = {GetVehicleColours(veh)} + custom.extra_colours = {GetVehicleExtraColours(veh)} + custom.plate_index = GetVehicleNumberPlateTextIndex(veh) + custom.plate_txt = GetVehicleNumberPlateText(veh) + custom.wheel_type = GetVehicleWheelType(veh) + custom.window_tint = GetVehicleWindowTint(veh) + custom.livery = GetVehicleLivery(veh) + custom.neons = {} + for i=0,3 do + custom.neons[i] = IsVehicleNeonLightEnabled(veh, i) + end + custom.neon_colour = {GetVehicleNeonLightsColour(veh)} + custom.tyre_smoke_color = {GetVehicleTyreSmokeColor(veh)} + + custom.mods = {} + for i=0,49 do + custom.mods[i] = GetVehicleMod(veh, i) + end + + custom.turbo_enabled = IsToggleModOn(veh, 18) + custom.smoke_enabled = IsToggleModOn(veh, 20) + custom.xenon_enabled = IsToggleModOn(veh, 22) + + return custom +end + +-- partial update per property +-- Refactored setVehicleCustomization function +function Vehicle:setVehicleCustomization(veh, custom) + if not veh then + veh = GetVehiclePedIsIn(GetPlayerPed(-1), false) + end + SetVehicleModKit(veh, 0) + + -- Apply each customization if it's provided + if custom.colours then SetVehicleColours(veh, table.unpack(custom.colours)) end + if custom.extra_colours then SetVehicleExtraColours(veh, table.unpack(custom.extra_colours)) end + if custom.plate_index then SetVehicleNumberPlateTextIndex(veh, custom.plate_index) end + if custom.plate_txt then SetVehicleNumberPlateText(veh, custom.plate_txt) end + if custom.wheel_type then SetVehicleWheelType(veh, custom.wheel_type) end + if custom.window_tint then SetVehicleWindowTint(veh, custom.window_tint) end + if custom.livery then SetVehicleLivery(veh, custom.livery) end + + -- Neons + if custom.neons then + for i = 0, 3 do + SetVehicleNeonLightEnabled(veh, i, custom.neons[i]) + end + end + if custom.neon_colour then SetVehicleNeonLightsColour(veh, table.unpack(custom.neon_colour)) end + if custom.tyre_smoke_color then SetVehicleTyreSmokeColor(veh, table.unpack(custom.tyre_smoke_color)) end + + -- Mods + if custom.mods then + for i, mod in pairs(custom.mods) do + SetVehicleMod(veh, i, mod, false) + end + end + + -- Toggle mods if enabled + if custom.turbo_enabled ~= nil then ToggleVehicleMod(veh, 18, custom.turbo_enabled) end + if custom.smoke_enabled ~= nil then ToggleVehicleMod(veh, 20, custom.smoke_enabled) end + if custom.xenon_enabled ~= nil then ToggleVehicleMod(veh, 22, custom.xenon_enabled) end +end + + +function Vehicle:getVehicleState(veh) + local state = { + customization = self:getVehicleCustomization(veh), + condition = { + health = GetEntityHealth(veh), + engine_health = GetVehicleEngineHealth(veh), + petrol_tank_health = GetVehiclePetrolTankHealth(veh), + dirt_level = GetVehicleDirtLevel(veh) + } + } + + state.condition.windows = {} + for i=0,7 do + state.condition.windows[i] = IsVehicleWindowIntact(veh, i) + end + + state.condition.tyres = {} + for i=0,7 do + local tyre_state = 2 -- 2: fine, 1: burst, 0: completely burst + if IsVehicleTyreBurst(veh, i, true) then + tyre_state = 0 + elseif IsVehicleTyreBurst(veh, i, false) then + tyre_state = 1 + end + + state.condition.tyres[i] = tyre_state + end + + state.condition.doors = {} + for i=0,5 do + state.condition.doors[i] = not IsVehicleDoorDamaged(veh, i) + end + + state.locked = (GetVehicleDoorLockStatus(veh) >= 2) + + return state +end + +-- partial update per property +function Vehicle:setVehicleState(veh, state) + -- apply state + if state.customization then + self:setVehicleCustomization(veh, state.customization) + end + + if state.condition then + if state.condition.health then + SetEntityHealth(veh, state.condition.health) + end + + if state.condition.engine_health then + SetVehicleEngineHealth(veh, state.condition.engine_health) + end + + if state.condition.petrol_tank_health then + SetVehiclePetrolTankHealth(veh, state.condition.petrol_tank_health) + end + + if state.condition.dirt_level then + SetVehicleDirtLevel(veh, state.condition.dirt_level) + end + + if state.condition.windows then + for i, window_state in pairs(state.condition.windows) do + if not window_state then + SmashVehicleWindow(veh, i) + end + end + end + + if state.condition.tyres then + for i, tyre_state in pairs(state.condition.tyres) do + if tyre_state < 2 then + SetVehicleTyreBurst(veh, i, (tyre_state == 1), 1000.01) + end + end + end + + if state.condition.doors then + for i, door_state in pairs(state.condition.doors) do + if not door_state then + SetVehicleDoorBroken(veh, i, true) + end + end + end + end + + if state.locked ~= nil then + if state.locked then -- lock + SetVehicleDoorsLocked(veh,2) + SetVehicleDoorsLockedForAllPlayers(veh, true) + else -- unlock + SetVehicleDoorsLockedForAllPlayers(veh, false) + SetVehicleDoorsLocked(veh,1) + SetVehicleDoorsLockedForPlayer(veh, PlayerId(), false) + end + end +end + + +-- TUNNEL +Vehicle.tunnel = {} + +function Vehicle.tunnel:setConfig(update_interval, check_interval, respawn_radius) + self.update_interval = update_interval + self.check_interval = check_interval + self.respawn_radius = respawn_radius +end + +function Vehicle.tunnel:setStateReady(state) + self.state_ready = state +end + +function Vehicle.tunnel:registerModels(models) + -- generate models hashes + for model in pairs(models) do + local hash = GetHashKey(model) + if hash then + self.hash_models[hash] = model + end + end +end + +function Vehicle.tunnel:setOutVehicles(out_vehicles) + for model, data in pairs(out_vehicles) do + self.out_vehicles[model] = data + end +end + +function Vehicle.tunnel:removeOutVehicles(out_vehicles) + for model in pairs(out_vehicles) do + self.out_vehicles[model] = nil + end +end + +function Vehicle.tunnel:clearOutVehicles() + self.out_vehicles = {} +end + +Vehicle.tunnel.spawnVehicle = Vehicle.spawnVehicle +Vehicle.tunnel.despawnVehicle = Vehicle.despawnVehicle +Vehicle.tunnel.despawnVehicles = Vehicle.despawnVehicles + +Vehicle.tunnel.tryOwnVehicles = Vehicle.tryOwnVehicles +Vehicle.tunnel.trySpawnOutVehicles = Vehicle.trySpawnOutVehicles +Vehicle.tunnel.cleanupVehicles = Vehicle.cleanupVehicles +Vehicle.tunnel.getInOwnedVehicleModel = Vehicle.getInOwnedVehicleModel + +Vehicle.tunnel.setVehicleCustomization = Vehicle.setVehicleCustomization + + +vRP:registerExtension(Vehicle) \ No newline at end of file diff --git a/vrp/modules/vehicle.lua b/vrp/modules/vehicle.lua new file mode 100644 index 0000000..64931e0 --- /dev/null +++ b/vrp/modules/vehicle.lua @@ -0,0 +1,377 @@ +-- https://github.com/ImagicTheCat/vRP +-- MIT license (see LICENSE or vrp/vRPShared.lua) + +if not vRP.modules.vehicle then return end + +-- A basic Vehicle implementation +local Vehicle = class("Vehicle", vRP.Extension) + +-- SUBCLASS +Vehicle.User = class("User") + +-- Get owned vehicles +-- Return map of id => {state, price, model} +function Vehicle.User:getVehicles() + return self.cdata.vehicles +end + +-- Simplified send_out_vehicles function with better early returns and clearer structure +local function send_out_vehicles(self, user) + local out_vehicles = {} + + -- Get all vehicles from the user + for model, state in pairs(user:getVehicles()) do + if state == 0 then -- Only 'out' vehicles + local vstate = user:getVehicleState(model) + if vstate.position then + -- Build vehicle state and position information + out_vehicles[model] = { + customization = vstate.customization, + condition = vstate.condition, + locked = vstate.locked, + position = vstate.position, + rotation = vstate.rotation + } + end + end + end + + -- Send out the vehicles via remote + self.remote._setOutVehicles(user.source, out_vehicles) +end + +-- detect which vehicle shop area the user is in +function Vehicle:getUserVehicleShop(user) + local shops = self.cfg.vehicleshops + for shop_key, shop_data in pairs(shops) do + local area_id = "vRP:vehicleshop:" .. shop_key + if user:inArea(area_id) then + return shop_key, shop_data.shop_name or shop_key + end + end + return nil, "Unknown Shop" +end + +function Vehicle:getVehicleByModel(model) + for _, veh in ipairs(self.cfg.vehicles) do + if veh.model == model then return veh end + end + return nil +end + +-- Improved function +function Vehicle.User:getVehicleState(model) + if not self.vehicle_states then + self.vehicle_states = {} + end + local state = self.vehicle_states[model] + if not state then + local sdata = vRP:getCData(self.cid, "vRP:vehicle_state:"..model) + if sdata and #sdata > 0 then + state = msgpack.unpack(sdata) + else + state = {} + end + self.vehicle_states[model] = state + end + + return state +end + + +-- Improved vehicle spawning and buying +local function menu_garage_buy(self) + local function m_buy(menu, model) + local user = menu.user + local uvehicles = user:getVehicles() + local veh = menu.data.vehicles[model] + + if not veh then + return vRP.EXT.Base.remote._notify(user.source, "Vehicle not found.") + end + + + if not user:tryPayment(veh.price) then + return vRP.EXT.Base.remote._notify(user.source, "Not enough money") + end + + + local shop_key = menu.data.shop_key + local shop_cfg = self.cfg.vehicleshops[shop_key] + if not shop_cfg or not shop_cfg.purchaseSpawn or #shop_cfg.purchaseSpawn == 0 then + return vRP.EXT.Base.remote._notify(user.source, "Vehicle shop configuration error.") + end + + + local spawn = shop_cfg.purchaseSpawn[math.random(#shop_cfg.purchaseSpawn)] + if not spawn or spawn.x == 0 then + return vRP.EXT.Base.remote._notify(user.source, "No valid spawn point configured.") + end + + + vRP.EXT.Base.remote.teleport(user.source, spawn.x, spawn.y, spawn.z, spawn.w) + + local vstate = user:getVehicleState(model) + local state = { + customization = vstate.customization, + condition = vstate.condition, + locked = vstate.locked + } + + + uvehicles[model] = 0 + self.remote._spawnVehicle(user.source, model, state) + self.remote._setOutVehicles(user.source, { [model] = {} }) + + vRP.EXT.Base.remote._notify(user.source, "Purchased for $" .. veh.price) + user:actualizeMenu() + user:closeMenu(menu) + end + + + vRP.EXT.GUI:registerMenuBuilder("garage.buy", function(menu) + local user = menu.user + local uvehicles = user:getVehicles() + local shop_key, shop_name = self:getUserVehicleShop(user) + local shop_vehicles = {} + + + menu.title = shop_name or "Vehicle Shop" + menu.css.header_color = "rgba(255,125,0,0.75)" + menu.data.shop_key = shop_key + + + if shop_key then + for _, veh in ipairs(self.cfg.vehicles) do + if veh.shop == shop_key or (type(veh.shop) == "table" and table.contains(veh.shop, shop_key)) then + shop_vehicles[veh.model] = { + name = veh.name, + price = veh.price, + category = veh.category, + vtype = veh.type + } + end + end + end + + menu.data.vehicles = shop_vehicles + + + for model, veh in pairs(shop_vehicles) do + if not uvehicles[model] then + menu:addOption(veh.name, m_buy, string.format("Price: $%d\n
Category: %s\n
Type: %s", veh.price, veh.category, veh.vtype), model) + end + end + end) +end + +-- Helper function to check if a value exists in a table +function table.contains(tbl, val) + for _, v in pairs(tbl) do + if v == val then return true end + end + return false +end + +local function menu_garage_sell(self) + local function m_sell(menu, model) + local user = menu.user + local uvehicles = user:getVehicles() + local veh = self:getVehicleByModel(model) + + if veh and uvehicles[model] then + local zone_type = menu.data.zone_type + local sell_cfg = self.cfg.sellvehicle[zone_type] + + if sell_cfg then + local price = math.floor((veh.price or 0) * (sell_cfg.sellPrice / 100)) + uvehicles[model] = nil + user:addWallet(price) + vRP.EXT.Base.remote._notify(user.source, "Vehicle sold for $" .. price) + user:actualizeMenu() + else + vRP.EXT.Base.remote._notify(user.source, "Invalid sell zone.") + end + else + vRP.EXT.Base.remote._notify(user.source, "You do not own this vehicle.") + end + end + + vRP.EXT.GUI:registerMenuBuilder("garage.sell", function(menu) + local user = menu.user + local uvehicles = user:getVehicles() + local zone_type = menu.data.zone_type or "cars" + local sell_cfg = self.cfg.sellvehicle[zone_type] + + if not sell_cfg then return end -- safety check + + menu.title = sell_cfg.name or "Sell Vehicle" + menu.css.header_color = "rgba(200, 30, 30, 0.75)" + menu.data.zone_type = zone_type + + for model, data in pairs(uvehicles) do + local veh = self:getVehicleByModel(model) + + if veh and veh.type == sell_cfg.type then + local price = math.floor((veh.price or 0) * (sell_cfg.sellPrice / 100)) + menu:addOption(veh.name, m_sell, string.format("Model: %s\n
Original Price: $%d\n
Sell Price: $%d", model, veh.price or 0, price), model) + + end + end + end) +end + + +function Vehicle:__construct() + vRP.Extension.__construct(self) + self.cfg = module("cfg/vehicles") + + self.models = {} + menu_garage_buy(self) + menu_garage_sell(self) +end + +-- EVENT HANDLERS -- +Vehicle.event = {} + +function Vehicle.event:playerSpawn(user, first_spawn) + if first_spawn then + self.remote._setConfig(user.source, + self.cfg.vehicle_update_interval, + self.cfg.vehicle_check_interval, + self.cfg.vehicle_respawn_radius + ) + self.remote._registerModels(user.source, self.models) + + -- setup each shop’s blip + area + for shop_key, shop_data in pairs(self.cfg.vehicleshops) do + local p = shop_data.showroom_location + if p then + local enterArea = function(u) + user:openMenu("garage.buy") + end + local leaveArea = function(u) + user:closeMenu("Vehicle Shop") + end + + local blip_id = (shop_data.blip and shop_data.blip.id) or 326 + local blip_color = (shop_data.blip and shop_data.blip.color) or 69 + local marker_id = (shop_data.marker and shop_data.marker.id) or 1 + + local poi = { + "PoI", + { + blip_id = blip_id, + blip_color = blip_color, + title = shop_data.shop_name or shop_key, + marker_id = marker_id, + pos = { p.x, p.y, p.z - 1 } + } + } + + vRP.EXT.Map.remote._addEntity(user.source, poi[1], poi[2]) + user:setArea( + "vRP:vehicleshop:"..shop_key, + p.x, p.y, p.z, + 1.5, 2.0, + enterArea, leaveArea + ) + end + end + -- setup each sell vehicle zone based on cfg + for zone_type, zone in pairs(self.cfg.sellvehicle) do + for i, pos in ipairs(zone.coords) do + local area_id = "vRP:sellvehicle:" .. zone_type .. ":" .. i + + local enterArea = function(u) + user:openMenu("garage.sell", { zone_type = zone_type }) + end + local leaveArea = function(u) + user:closeMenu("Sell Vehicle") + end + + local poi = { + "PoI", + { + blip_id = zone.blip.id, + blip_color = zone.blip.color, + title = zone.name, + marker_id = zone.marker.id, + pos = { pos.x, pos.y, pos.z - 1 }, + } + } + + vRP.EXT.Map.remote._addEntity(user.source, poi[1], poi[2]) + user:setArea(area_id, pos.x, pos.y, pos.z, 1.5, 2.0, enterArea, leaveArea) + end + end + end +end + + +function Vehicle.event:characterLoad(user) + if not user.cdata.vehicles then + user.cdata.vehicles = {} + end + + user.vehicle_states = {} + + send_out_vehicles(self, user) +end + +function Vehicle.event:characterUnload(user) + self.remote._setStateReady(user.source, false) + + -- save vehicle states + for model, state in pairs(user.vehicle_states) do + vRP:setCData(user.cid, "vRP:vehicle_state:"..model, msgpack.pack(state)) + end + + -- despawn vehicles + self.remote._despawnVehicles(user.source) + self.remote._clearOutVehicles(user.source) +end + + +function Vehicle.event:save() + for _, user in pairs(vRP.users) do + for model, state in pairs(user.vehicle_states) do + vRP:setCData(user.cid, "vRP:vehicle_state:"..model, msgpack.pack(state)) + end + end +end + +function Vehicle.event:playerStateLoaded(user) + self.remote._tryOwnVehicles(user.source) + self.remote.trySpawnOutVehicles(user.source) + if user.cdata.state.in_owned_vehicle then + self.remote._putInOwnedVehicle(user.source, user.cdata.state.in_owned_vehicle) + end + self.remote._setStateReady(user.source, true) +end + +-- TUNNEL +Vehicle.tunnel = {} + +function Vehicle.tunnel:updateVehicleStates(states) + local user = vRP.users_by_source[source] + + if user then + for model, state in pairs(states) do + if user.cdata.vehicles[model] then -- has model + local vstate = user:getVehicleState(model) + if state.customization then + vstate.customization = state.customization + end + if state.condition then + vstate.condition = state.condition + end + if state.position then vstate.position = state.position end + if state.rotation then vstate.rotation = state.rotation end + if state.locked ~= nil then vstate.locked = state.locked end + end + end + end +end + +vRP:registerExtension(Vehicle)