diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index bafd2a7..0000000
--- a/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-
-config_server.lua
diff --git a/README.md b/README.md
index 4039f51..5919f0a 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,24 @@
-
+
+
+
-Discord
+
Documentation
-
Documentation
+
-## Dependency:
-[oxmysql](https://github.com/overextended/oxmysql/releases/download/v2.4.0/oxmysql.zip)
+## Dependencies:
+* [oxmysql](https://github.com/overextended/oxmysql/releases)
+* [ox_lib](https://github.com/overextended/ox_lib/releases)
-## Addons
-
-* [Ox Inventory](https://github.com/overextended/ox_inventory/releases)
-* [Character Selection](https://github.com/ND-Framework/ND_CharacterSelection)
-* [No Characters (use framework without character selection)](https://github.com/ND-Framework/ND_NoCharacters)
-* [Vehicle System](https://github.com/ND-Framework/ND_VehicleSystem)
-* [Dealerships](https://github.com/ND-Framework/ND_Dealership)
-* [Fuel (with nozzle and hose)](https://github.com/ND-Framework/ND_Fuel)
-* [Nitro](https://github.com/ND-Framework/ND_Nitro)
-* [Shot Spotter](https://github.com/ND-Framework/ND_ShotSpotter)
-* [Banking](https://github.com/ND-Framework/ND_Banking)
-* [Doorlocks](https://github.com/ND-Framework/ND_Doorlocks)
-* [Properties](https://github.com/ND-Framework/ND_Properties)
-* [Appearance Shops](https://github.com/ND-Framework/ND_AppearanceShops)
-
-# Need support?
-[](https://discord.gg/Z9Mxu72zZ6)
+## v2 Addons
+* [Character selection](https://github.com/ND-Framework/ND_Characters/tree/wip-v2)
+* [Banking](https://github.com/ND-Framework/ND_Banking/tree/wip-v2)
+* [Appearance shops](https://github.com/ND-Framework/ND_AppearanceShops/tree/wip-v2)
+* [Dealership](https://github.com/ND-Framework/ND_Dealership/tree/v2)
+* [Inventory](https://github.com/overextended/ox_inventory/pull/1403)
diff --git a/client/death.lua b/client/death.lua
new file mode 100644
index 0000000..475ed6e
--- /dev/null
+++ b/client/death.lua
@@ -0,0 +1,55 @@
+local ambulance
+local usingAmbulance = false
+local alreadyEliminated = false
+
+NDCore.isResourceStarted("ND_Ambulance", function(started)
+ usingAmbulance = started
+ if not usingAmbulance then return end
+ ambulance = exports["ND_Ambulance"]
+end)
+
+local function PlayerEliminated(deathCause, killerServerId, killerClientId)
+ if alreadyEliminated then return end
+ alreadyEliminated = true
+ local info = {
+ deathCause = deathCause,
+ killerServerId = killerServerId,
+ killerClientId = killerClientId,
+ damagedBones = usingAmbulance and ambulance:getBodyDamage() or {}
+ }
+ TriggerEvent("ND:playerEliminated", info)
+ TriggerServerEvent("ND:playerEliminated", info)
+ Wait(1000)
+ alreadyEliminated = false
+end
+
+AddEventHandler("gameEventTriggered", function(name, args)
+ if name ~= "CEventNetworkEntityDamage" then return end
+
+ local victim = args[1]
+ if not IsPedAPlayer(victim) or NetworkGetPlayerIndexFromPed(victim) ~= cache.playerId then return end
+
+ if not IsPedDeadOrDying(victim, true) and GetEntityHealth(victim) > 0 then
+ local hit, bone = GetPedLastDamageBone(victim)
+ if hit and usingAmbulance then
+ local damageWeapon = ambulance:getLastDamagingWeapon(victim)
+ return damageWeapon and ambulance:updateBodyDamage(bone, damageWeapon)
+ end
+ return
+ end
+
+ local killerEntity, deathCause = GetPedSourceOfDeath(cache.ped), GetPedCauseOfDeath(cache.ped)
+ local killerClientId = NetworkGetPlayerIndexFromPed(killerEntity)
+ if killerEntity ~= cache.ped and killerClientId and NetworkIsPlayerActive(killerClientId) then
+ return PlayerEliminated(deathCause, GetPlayerServerId(killerClientId), killerClientId)
+ end
+ PlayerEliminated(deathCause)
+end)
+
+local firstSpawn = true
+exports.spawnmanager:setAutoSpawnCallback(function()
+ if firstSpawn then
+ firstSpawn = false
+ return exports.spawnmanager:spawnPlayer() and exports.spawnmanager:setAutoSpawn(false)
+ end
+end)
diff --git a/client/events.lua b/client/events.lua
index d2a034f..54bd635 100644
--- a/client/events.lua
+++ b/client/events.lua
@@ -1,24 +1,26 @@
-RegisterNetEvent("ND:returnCharacters", function(characters)
- NDCore.Characters = characters
-end)
-
-- updates the money on the client.
RegisterNetEvent("ND:updateMoney", function(cash, bank)
- NDCore.SelectedCharacter.cash = cash
- NDCore.SelectedCharacter.bank = bank
+ if not NDCore.player then return end
+ NDCore.player.cash = cash
+ NDCore.player.bank = bank
end)
-- Sets main character.
-RegisterNetEvent("ND:setCharacter", function(character)
- NDCore.SelectedCharacter = character
+RegisterNetEvent("ND:characterLoaded", function(character)
+ NDCore.player = character
end)
-- Update main character info.
RegisterNetEvent("ND:updateCharacter", function(character)
- NDCore.SelectedCharacter = character
+ NDCore.player = character
end)
-- Updates last lcoation.
RegisterNetEvent("ND:updateLastLocation", function(location)
- NDCore.SelectedCharacter.lastLocation = location
+ if not NDCore.player then return end
+ NDCore.player.lastLocation = location
+end)
+
+RegisterNetEvent("ND:revivePlayer", function()
+ NDCore.revivePlayer(true)
end)
diff --git a/client/functions.lua b/client/functions.lua
index 6e2b4a4..be8d39f 100644
--- a/client/functions.lua
+++ b/client/functions.lua
@@ -1,19 +1,14 @@
-function GetCoreObject()
- return NDCore
+function NDCore.getPlayer()
+ return NDCore.player
end
-function NDCore.Functions.GetSelectedCharacter()
- return NDCore.SelectedCharacter
+function NDCore.getCharacters()
+ return NDCore.characters
end
-function NDCore.Functions.GetCharacters()
- return NDCore.Characters
-end
-
-
-function NDCore.Functions.GetPlayersFromCoords(distance, coords)
+function NDCore.getPlayersFromCoords(distance, coords)
if coords then
- coords = type(coords) == 'table' and vec3(coords.x, coords.y, coords.z) or coords
+ coords = type(coords) == "table" and vec3(coords.x, coords.y, coords.z) or coords
else
coords = GetEntityCoords(PlayerPedId())
end
@@ -31,50 +26,60 @@ function NDCore.Functions.GetPlayersFromCoords(distance, coords)
return closePlayers
end
--- Callbacks are licensed under LGPL v3.0
---
-NDCore.callback = {}
-local events = {}
-
-RegisterNetEvent("ND:callbacks", function(key, ...)
- local cb = events[key]
- return cb and cb(...)
-end)
-
-local function triggerCallback(_, name, cb, ...)
- local key = ("%s:%s"):format(name, math.random(0, 100000))
- TriggerServerEvent(("ND:%s_cb"):format(name), key, ...)
-
- local promise = not cb and promise.new()
-
- events[key] = function(response, ...)
- response = { response, ... }
- events[key] = nil
-
- if promise then
- return promise:resolve(response)
- end
-
- if cb then
- cb(table.unpack(response))
+function NDCore.revivePlayer(reset, keepDead)
+ local usingAmbulance = GetResourceState("ND_Ambulance") == "started"
+ if not keepDead then
+ LocalPlayer.state.dead = false
+ if usingAmbulance then
+ local state = Player(cache.serverId).state
+ state:set("isDead", false, true)
end
- end
+ end
- if promise then
- return table.unpack(Citizen.Await(promise))
- end
+ local veh = GetVehiclePedIsIn(cache.ped)
+ local seat = cache.seat
+ local coords = GetEntityCoords(cache.ped)
+ NetworkResurrectLocalPlayer(coords.x, coords.y, coords.z, GetEntityHeading(cache.ped), true, true, false)
+
+ local ped = PlayerPedId()
+ if cache.ped ~= ped then
+ DeleteEntity(cache.ped)
+ ClearAreaOfPeds(coords.x, coords.y, coords.z, 0.2, false)
+ end
+
+ SetEntityInvincible(ped, false)
+ FreezeEntityPosition(ped, false)
+ SetEntityVisible(ped, true)
+ SetEveryoneIgnorePlayer(ped, false)
+ SetPedCanBeTargetted(ped, true)
+ SetEntityCanBeDamaged(ped, true)
+ SetBlockingOfNonTemporaryEvents(ped, false)
+ SetPedCanRagdollFromPlayerImpact(ped, true)
+ ClearPedTasksImmediately(ped)
+
+ if veh and veh ~= 0 then
+ SetPedIntoVehicle(ped, veh, seat)
+ end
+ if reset and GetPedMovementClipset(ped) == `move_m@injured` then
+ ClearEntityLastDamageEntity(ped)
+ SetPedMoveRateOverride(ped, 1.0)
+ ResetPedMovementClipset(ped, 0)
+ end
+ if reset and usingAmbulance then
+ exports["ND_Ambulance"]:resetBodyDamage()
+ end
end
-setmetatable(NDCore.callback, {
- __call = triggerCallback
-})
-
-function NDCore.callback.await(name, ...)
- return triggerCallback(nil, name, false, ...)
+function NDCore.notify(...)
+ if GetResourceState("ModernHUD") == "started" then
+ exports["ModernHUD"]:notify(...)
+ elseif GetResourceState("ox_lib") == "started" then
+ lib.notify(...)
+ end
end
-function NDCore.callback.register(name, callback)
- RegisterNetEvent(("ND:%s_cb"):format(name), function(key, ...)
- TriggerServerEvent("ND:callbacks", key, callback(...))
- end)
+for name, func in pairs(NDCore) do
+ if type(func) == "function" then
+ exports(name, func)
+ end
end
diff --git a/client/main.lua b/client/main.lua
index 56d2691..6dafd98 100644
--- a/client/main.lua
+++ b/client/main.lua
@@ -1,54 +1,69 @@
--- For support join my discord: https://discord.gg/Z9Mxu72zZ6
-
NDCore = {}
-NDCore.SelectedCharacter = nil
-NDCore.Characters = {}
-NDCore.Functions = {}
-NDCore.Config = config
--- discord rich precense will show on a users profile.
-if config.enableRichPresence then
- Citizen.CreateThread(function()
- while true do
- if NDCore.SelectedCharacter then
- SetDiscordAppId(config.appId)
- SetRichPresence(" Playing : " .. config.serverName .. " as " .. NDCore.SelectedCharacter.firstName .. " " .. NDCore.SelectedCharacter.lastName)
- SetDiscordRichPresenceAsset(config.largeLogo)
- SetDiscordRichPresenceAssetText("Playing: " .. config.serverName)
- SetDiscordRichPresenceAssetSmall(config.smallLogo)
- SetDiscordRichPresenceAssetSmallText("Playing as: " .. NDCore.SelectedCharacter.firstName .. " " .. NDCore.SelectedCharacter.lastName)
- SetDiscordRichPresenceAction(0, config.firstButtonName, config.firstButtonLink)
- SetDiscordRichPresenceAction(1, config.secondButtonName, config.secondButtonLink)
- end
- Citizen.Wait(config.updateIntervall * 1000)
+Config = {
+ serverName = GetConvar("core:serverName", "Unconfigured ND-Core Server"),
+ discordInvite = GetConvar("core:discordInvite", "https://discord.gg/Z9Mxu72zZ6"),
+ discordAppId = GetConvar("core:discordAppId", "858146067018416128"),
+ discordAsset = GetConvar("core:discordAsset", "andyyy"),
+ discordAssetSmall = GetConvar("core:discordAssetSmall", "andyyy"),
+ discordActionText = GetConvar("core:discordActionText", "DISCORD"),
+ discordActionLink = GetConvar("discordActionLink", "https://discord.gg/Z9Mxu72zZ6"),
+ discordActionText2 = GetConvar("core:discordActionText2", "STORE"),
+ discordActionLink2 = GetConvar("core:discordActionLink2", "https://andyyy.tebex.io/category/fivem-scripts"),
+ randomUnlockedVehicleChance = GetConvarInt("core:randomUnlockedVehicleChance", 30),
+ disableVehicleAirControl = GetConvarInt("core:disableVehicleAirControl", 1) == 1,
+ useInventoryForKeys = GetConvarInt("core:useInventoryForKeys", 1) == 1,
+ groups = json.decode(GetConvar("core:groups", "[]")),
+ compatibility = json.decode(GetConvar("core:compatibility", "[]"))
+}
+
+-- Discord rich presence.
+CreateThread(function()
+ SetDiscordAppId(Config.discordAppId)
+ SetDiscordRichPresenceAsset(Config.discordAsset)
+ SetDiscordRichPresenceAssetSmall(Config.discordAssetSmall)
+ SetDiscordRichPresenceAction(0, Config.discordActionText, Config.discordActionLink)
+ SetDiscordRichPresenceAction(1, Config.discordActionText2, Config.discordActionLink2)
+ local presenceText = ("Playing: %s"):format(Config.serverName)
+ while true do
+ if NDCore.player then
+ local presence = ("Playing: %s as %s %s"):format(Config.serverName, NDCore.player.firstname, NDCore.player.lastname)
+ local presenceTextSmall = ("Playing as: %s %s"):format(NDCore.player.firstname, NDCore.player.lastname)
+ SetRichPresence(presence)
+ SetDiscordRichPresenceAssetText(presenceText)
+ SetDiscordRichPresenceAssetSmallText(presenceTextSmall)
end
- end)
-end
-
--- show server name, first name, last name, and the amount of money the character has in the pause menu.
-if config.customPauseMenu then
- Citizen.CreateThread(function()
- while true do
- Citizen.Wait(0)
- if NDCore.SelectedCharacter then
- if IsPauseMenuActive() then
- BeginScaleformMovieMethodOnFrontendHeader("SET_HEADING_DETAILS")
- AddTextEntry("FE_THDR_GTAO", config.serverName)
- ScaleformMovieMethodAddParamPlayerNameString(NDCore.SelectedCharacter.firstName .. " " .. NDCore.SelectedCharacter.lastName)
- PushScaleformMovieFunctionParameterString("Cash: $" .. tostring(NDCore.SelectedCharacter.cash))
- PushScaleformMovieFunctionParameterString("Bank: $" .. tostring(NDCore.SelectedCharacter.bank))
- EndScaleformMovieMethod()
- end
- end
- end
- end)
-end
-
--- Enables pvp if it's selected in the config.
-AddEventHandler("playerSpawned", function()
- if config.enablePVP then
- SetCanAttackFriendly(PlayerPedId(), true, false)
- NetworkSetFriendlyFireOption(true)
+ Wait(60000)
end
- print("^0This framework is created by ^5Andyyy#7666 ^0for support you can join the ^5discord: ^0https://discord.gg/Z9Mxu72zZ6")
-end)
\ No newline at end of file
+end)
+
+-- Pause menu information.
+CreateThread(function()
+ AddTextEntry("FE_THDR_GTAO", Config.serverName)
+ local sleep = 500
+ while true do
+ Wait(sleep)
+ if NDCore.player and IsPauseMenuActive() then
+ sleep = 0
+ BeginScaleformMovieMethodOnFrontendHeader("SET_HEADING_DETAILS")
+ ScaleformMovieMethodAddParamPlayerNameString(("%s %s"):format(NDCore.player.firstname, NDCore.player.lastname))
+ ScaleformMovieMethodAddParamTextureNameString(("Cash: $%d"):format(NDCore.player.cash))
+ ScaleformMovieMethodAddParamTextureNameString(("Bank: $%d"):format(NDCore.player.bank))
+ EndScaleformMovieMethod()
+ elseif sleep == 0 then
+ sleep = 500
+ end
+ end
+end)
+
+AddEventHandler("playerSpawned", function()
+ print("^0ND Framework support discord: ^5https://discord.gg/Z9Mxu72zZ6")
+ SetCanAttackFriendly(PlayerPedId(), true, false)
+ NetworkSetFriendlyFireOption(true)
+end)
+
+AddEventHandler("onResourceStart", function(resourceName)
+ if resourceName ~= GetCurrentResourceName() then return end
+ SetCanAttackFriendly(PlayerPedId(), true, false)
+ NetworkSetFriendlyFireOption(true)
+end)
diff --git a/client/peds.lua b/client/peds.lua
new file mode 100644
index 0000000..4ecc0fb
--- /dev/null
+++ b/client/peds.lua
@@ -0,0 +1,227 @@
+local target, ox_target
+local locations = {}
+local pedBlips = {}
+local clothingComponents = {
+ face = 0,
+ mask = 1,
+ hair = 2,
+ torso = 3,
+ leg = 4,
+ bag = 5,
+ shoes = 6,
+ accessory = 7,
+ undershirt = 8,
+ kevlar = 9,
+ badge = 10,
+ torso2 = 11
+}
+local clothingProps = {
+ hat = 0,
+ glasses = 1,
+ ears = 2,
+ watch = 6,
+ bracelets = 7
+}
+
+NDCore.isResourceStarted("ox_target", function(started)
+ target = started
+ if not target then return end
+ ox_target = exports.ox_target
+end)
+
+local function configPed(ped)
+ SetPedCanBeTargetted(ped, false)
+ SetEntityCanBeDamaged(ped, false)
+ SetBlockingOfNonTemporaryEvents(ped, true)
+ SetPedCanRagdollFromPlayerImpact(ped, false)
+ SetPedResetFlag(ped, 249, true)
+ SetPedConfigFlag(ped, 185, true)
+ SetPedConfigFlag(ped, 108, true)
+ SetPedConfigFlag(ped, 208, true)
+ SetPedCanRagdoll(ped, false)
+end
+
+local function setClothing(ped, clothing)
+ if not clothing then return end
+ for component, clothingInfo in pairs(clothing) do
+ if clothingComponents[component] then
+ SetPedComponentVariation(ped, clothingComponents[component], clothingInfo.drawable, clothingInfo.texture, 0)
+ elseif clothingProps[component] then
+ SetPedPropIndex(ped, clothingProps[component], clothingInfo.drawable, clothingInfo.texture, true)
+ end
+ end
+end
+
+local function groupCheck(groups, playerGroups)
+ if not groups or #groups == 0 then return true end
+ for i=1, #groups do
+ if playerGroups and playerGroups[groups[i]] then
+ return true
+ end
+ end
+end
+
+local function createBlip(coords, info, i)
+ if not info then return end
+ local groups = info.groups
+ local playerGroups = NDCore.player?.groups
+
+ local key = i or #pedBlips+1
+ pedBlips[key] = {
+ groups = groups,
+ info = info,
+ coords = coords
+ }
+
+ if not groupCheck(groups, playerGroups) then return end
+ local blip = AddBlipForCoord(coords.x, coords.y, coords.z)
+ SetBlipSprite(blip, info.sprite or 280)
+ SetBlipScale(blip, info.scale or 0.8)
+ SetBlipColour(blip, info.color or 3)
+ SetBlipAsShortRange(blip, true)
+ if info.label then
+ BeginTextCommandSetBlipName("STRING")
+ AddTextComponentString(info.label)
+ EndTextCommandSetBlipName(blip)
+ end
+
+ pedBlips[key].blip = blip
+ return blip
+end
+
+local function updateBlips(playerGroups)
+ for i=1, #pedBlips do
+ local blipInfo = pedBlips[i]
+ local access = groupCheck(blipInfo.groups, playerGroups)
+ if access and not blipInfo.blip or not DoesBlipExist(blipInfo.blip) then
+ createBlip(blipInfo.coords, blipInfo.info, i)
+ elseif not access and blipInfo.blip and DoesBlipExist(blipInfo.blip) then
+ RemoveBlip(blipInfo.blip)
+ end
+ end
+end
+
+function NDCore.createAiPed(info)
+ local ped
+ local model = type(info.model) == "string" and GetHashKey(info.model) or info.model
+ local blipInfo = info.blip
+ local anim = info.anim
+ local clothing = info.clothing
+ local coords = info.coords
+ local options = info.options
+ local blip = createBlip(coords, blipInfo)
+ local point = lib.points.new({
+ coords = vec3(coords.x, coords.y, coords.z),
+ distance = info.distance or 25.0
+ })
+
+ local id = #locations+1
+ locations[id] = {
+ point = point,
+ blip = blip,
+ options = info.options,
+ resource = info.resource or GetInvokingResource()
+ }
+
+ function point:onEnter()
+ local found, ground = GetGroundZFor_3dCoord(coords.x, coords.y, coords.z, true)
+ lib.requestModel(model)
+ ped = CreatePed(4, model, coords.x, coords.y, found and ground or coords.z, coords.w or coords.h or info.heading, false, false)
+
+ local time = GetCloudTimeAsInt()
+ while not DoesEntityExist(ped) and time-GetCloudTimeAsInt() < 5 do
+ Wait(100)
+ end
+
+ configPed(ped)
+ setClothing(ped, clothing)
+ locations[id].ped = ped
+
+ if anim and anim.dict and anim.clip then
+ lib.requestAnimDict(anim.dict)
+ TaskPlayAnim(ped, anim.dict, anim.clip, 2.0, 8.0, -1, 1, 0, 0, 0, 0)
+ end
+
+ if target and options then
+ Wait(500)
+ ox_target:addLocalEntity({ped}, options)
+ end
+
+ if blip and blipInfo and blipInfo.showWhenNear and DoesBlipExist(blip) then
+ SetBlipAlpha(blip, 255)
+ end
+ end
+
+ function point:onExit()
+ if blip and blipInfo and blipInfo.showWhenNear and DoesBlipExist(blip) then
+ SetBlipAlpha(blip, 0)
+ end
+ if ped and DoesEntityExist(ped) then
+ if target and options then
+ ox_target:removeLocalEntity({ped})
+ end
+ Wait(500)
+ DeleteEntity(ped)
+ end
+ end
+
+ return id
+end
+
+function NDCore.removeAiPed(id)
+ local info = locations[id]
+ if not info then return end
+
+ local ped = info.ped
+ local blip = info.blip
+ info.point:remove()
+ locations[id] = nil
+
+ if blip and DoesBlipExist(blip) then
+ RemoveBlip(blip)
+ end
+
+ if ped and DoesEntityExist(ped) then
+ if info.options then
+ ox_target:removeLocalEntity({ped})
+ end
+ DeleteEntity(ped)
+ end
+end
+
+RegisterNetEvent("ND:updateCharacter", function(character)
+ Wait(3000)
+ if character.id ~= NDCore.player?.id then return end
+ updateBlips(character.groups)
+end)
+
+RegisterNetEvent("ND:characterLoaded", function(character)
+ Wait(3000)
+ if character.id ~= NDCore.player?.id then return end
+ updateBlips(character.groups)
+end)
+
+AddEventHandler("onResourceStop", function(name)
+ if name == GetCurrentResourceName() then
+ for i, _ in ipairs(locations) do
+ NDCore.removeAiPed(i)
+ end
+ else
+ for i, v in ipairs(locations) do
+ if v.resource == name then
+ NDCore.removeAiPed(i)
+ end
+ end
+ end
+end)
+
+RegisterCommand("getclothing", function(source, args, rawCommand)
+ local info = ""
+ for k, v in pairs(clothingComponents) do
+ info = ("%s\n%s = {\n drawable = %s,\n texture = %s\n},"):format(info, k, GetPedDrawableVariation(cache.ped, v), GetPedTextureVariation(cache.ped, v))
+ end
+ for k, v in pairs(clothingProps) do
+ info = ("%s\n%s = {\n drawable = %s,\n texture = %s\n},"):format(info, k, GetPedPropIndex(cache.ped, v), GetPedPropTextureIndex(cache.ped, v))
+ end
+ lib.setClipboard(info)
+end, false)
diff --git a/client/vehicle/data.lua b/client/vehicle/data.lua
new file mode 100644
index 0000000..d16bc8a
--- /dev/null
+++ b/client/vehicle/data.lua
@@ -0,0 +1,243 @@
+return {
+ {
+ garageType = "land",
+ groups = {"sahp", "lspd", "bcso"},
+ ped = vector4(452.83, -1027.79, 28.54, 2.49),
+ vehicleSpawns = {
+ vector4(446.19, -1025.47, 28.24, 185.96),
+ vector4(438.75, -1026.09, 28.38, 184.29),
+ vector4(434.98, -1026.61, 28.46, 185.99),
+ vector4(431.26, -1027.31, 28.53, 185.59),
+ vector4(427.52, -1026.84, 28.58, 184.25)
+ }
+ },
+ {
+ garageType = "land",
+ impound = true,
+ ped = vector4(407.99, -1624.74, 29.29, 229.93),
+ vehicleSpawns = {
+ vector4(396.34, -1644.28, 28.86, 319.10),
+ vector4(398.56, -1646.42, 28.8616, 319.18),
+ vector4(400.68, -1648.86, 28.86, 140.78),
+ vector4(403.32, -1650.54, 28.86, 319.92),
+ vector4(403.21, -1650.68, 28.86, 139.40)
+ }
+ },
+ {
+ garageType = "plane",
+ ped = vector4(-941.05, -2966.04, 13.95, 136.06),
+ vehicleSpawns = {
+ vector4(-974.98, -2977.90, 14.55, 59.91)
+ }
+ },
+ {
+ garageType = "heli",
+ ped = vector4(-731.00, -1394.54, 5.00, 245.52),
+ vehicleSpawns = {
+ vector4(-746.01, -1469.39, 5.68, 139.27),
+ vector4(-725.26, -1444.72, 5.68, 139.58)
+ }
+ },
+ {
+ garageType = "plane",
+ ped = vector4(1742.77, 3298.25, 41.22, 132.91),
+ vehicleSpawns = {
+ vector4(1734.26, 3250.98, 41.96, 81.11),
+ vector4(1729.32, 3270.69, 41.74, 145.39)
+ }
+ },
+ {
+ garageType = "plane",
+ ped = vector4(-1241.26, -3391.48, 13.94, 35.03),
+ vehicleSpawns = {
+ vector4(-1254.71, -3388.15, 14.54, 330.04),
+ vector4(-1270.36, -3378.80, 14.54, 330.20),
+ vector4(-1286.05, -3369.44, 14.54, 329.99)
+ }
+ },
+ {
+ garageType = "plane",
+ ped = vector4(-1621.10, -3151.63, 13.99, 41.26),
+ vehicleSpawns = {
+ vector4(-1634.04, -3147.84, 14.60, 330.28),
+ vector4(-1649.75, -3138.34, 14.60, 329.20),
+ vector4(-1665.42, -3129.26, 14.60, 330.10)
+ }
+ },
+ {
+ garageType = "heli",
+ ped = vector4(-1121.87, -2839.88, 13.95, 150.58),
+ vehicleSpawns = {
+ vector4(-1178.71, -2846.51, 14.62, 150.16),
+ vector4(-1146.40, -2865.22, 14.62, 151.08),
+ vector4(-1112.85, -2884.66, 14.62, 149.70)
+ }
+ },
+ {
+ garageType = "water",
+ ped = vector4(-831.03, -1359.53, 5.00, 299.80),
+ vehicleSpawns = {
+ vector4(-846.16, -1362.07, 0.39, 110.54),
+ vector4(-842.53, -1372.00, 0.39, 111.54),
+ vector4(-849.29, -1353.26, 0.38, 109.53),
+ vector4(-852.26, -1345.09, 0.41, 110.52),
+ vector4(-855.49, -1336.60, 0.40, 107.43),
+ vector4(-858.63, -1328.30, 0.40, 109.82),
+ vector4(-839.35, -1380.30, 0.39, 109.27),
+ vector4(-836.38, -1388.93, 0.41, 110.11),
+ vector4(-833.29, -1397.32, 0.40, 110.80),
+ vector4(-830.34, -1405.65, 0.37, 110.16)
+ }
+ },
+ {
+ garageType = "land",
+ ped = vector4(-280.32, -888.42, 31.32, 250.68),
+ vehicleSpawns = {
+ vector4(-282.36, -915.11, 30.38, 68.81),
+ vector4(-284.27, -918.37, 30.38, 70.58),
+ vector4(-285.44, -921.82, 30.38, 250.22),
+ vector4(-285.70, -887.62, 30.38, 167.04),
+ vector4(-292.70, -885.93, 30.38, 167.09),
+ vector4(-285.76, -887.47, 30.38, 169.59),
+ vector4(-300.44, -885.14, 30.38, 347.05),
+ vector4(-303.59, -883.71, 30.38, 167.69),
+ vector4(-309.38, -896.88, 30.38, 167.15),
+ vector4(-312.98, -896.30, 30.38, 166.46),
+ vector4(-316.68, -896.26, 30.37, 347.94),
+ vector4(-311.06, -881.94, 30.38, 168.21),
+ vector4(-314.78, -881.86, 30.37, 348.45)
+ }
+ },
+ {
+ garageType = "land",
+ ped = vector4(597.53, 91.08, 93.13, 250.54),
+ vehicleSpawns = {
+ vector4(598.53, 98.37, 92.27, 69.47),
+ vector4(599.79, 102.00, 92.27, 249.38),
+ vector4(608.21, 103.90, 92.18, 248.98),
+ vector4(600.60, 111.38, 92.27, 73.07),
+ vector4(609.91, 107.59, 92.23, 68.77),
+ vector4(601.36, 114.99, 92.27, 250.19),
+ vector4(611.07, 111.39, 92.29, 249.69),
+ vector4(603.52, 118.52, 92.26, 67.62),
+ vector4(612.74, 114.94, 92.28, 69.01),
+ vector4(604.00, 122.58, 92.27, 249.76),
+ vector4(613.78, 118.83, 92.29, 248.82),
+ vector4(622.43, 115.48, 91.99, 70.39),
+ vector4(628.56, 110.25, 91.47, 249.30),
+ vector4(620.67, 111.89, 92.03, 250.24),
+ vector4(618.41, 104.51, 91.97, 72.22),
+ vector4(624.87, 99.25, 91.36, 63.32),
+ vector4(616.59, 100.82, 91.97, 249.00)
+ }
+ },
+ {
+ garageType = "land",
+ ped = vector4(100.64, -1072.88, 29.37, 341.55),
+ vehicleSpawns = {
+ vector4(106.13, -1063.24, 28.51, 66.86),
+ vector4(107.82, -1059.73, 28.51, 66.99),
+ vector4(112.39, -1049.71, 28.52, 67.40),
+ vector4(110.76, -1053.09, 28.51, 68.13),
+ vector4(109.06, -1056.38, 28.51, 66.45),
+ vector4(117.53, -1081.03, 28.50, 181.40),
+ vector4(119.09, -1069.44, 28.50, 181.12),
+ vector4(121.22, -1081.42, 28.50, 0.57),
+ vector4(122.32, -1070.46, 28.50, 0.67),
+ vector4(125.62, -1069.54, 28.50, 179.25),
+ vector4(124.83, -1081.17, 28.50, 180.37),
+ vector4(128.62, -1081.71, 28.50, 1.36),
+ vector4(128.93, -1070.62, 28.50, 359.89),
+ vector4(132.24, -1069.35, 28.50, 181.54),
+ vector4(132.31, -1081.45, 28.50, 180.86),
+ vector4(135.93, -1081.35, 28.50, 358.82),
+ vector4(135.64, -1070.75, 28.50, 0.43),
+ vector4(139.75, -1081.91, 28.50, 182.18),
+ vector4(138.86, -1070.37, 28.50, 181.90),
+ vector4(143.55, -1081.41, 28.50, 0.89),
+ vector4(147.13, -1081.48, 28.50, 179.02),
+ vector4(150.95, -1081.49, 28.50, 358.79),
+ vector4(154.64, -1081.32, 28.50, 179.51),
+ vector4(158.38, -1082.03, 28.50, 1.57),
+ vector4(162.10, -1081.32, 28.50, 179.88)
+ }
+ },
+ {
+ garageType = "land",
+ ped = vector4(214.84, -806.24, 30.81, 342.23),
+ vehicleSpawns = {
+ vector4(251.26, -774.63, 29.98, 67.34),
+ vector4(245.36, -772.34, 30.02, 66.81),
+ vector4(219.19, -765.93, 30.14, 249.78),
+ vector4(228.11, -768.94, 30.10, 249.66),
+ vector4(233.68, -771.18, 30.07, 249.82),
+ vector4(244.01, -775.06, 29.99, 249.41),
+ vector4(249.53, -777.07, 29.95, 251.09),
+ vector4(248.74, -779.52, 29.92, 69.14),
+ vector4(243.64, -777.63, 29.96, 69.23),
+ vector4(233.47, -773.91, 30.05, 69.88),
+ vector4(218.62, -768.58, 30.14, 69.81),
+ vector4(217.02, -770.83, 30.16, 248.11),
+ vector4(226.09, -773.95, 30.09, 249.30),
+ vector4(231.82, -776.14, 30.04, 249.17),
+ vector4(242.05, -779.91, 29.93, 246.99),
+ vector4(247.72, -782.02, 29.88, 251.04),
+ vector4(216.19, -773.65, 30.16, 67.90),
+ vector4(225.82, -776.85, 30.08, 65.64),
+ vector4(230.77, -778.89, 30.03, 69.40),
+ vector4(241.72, -782.84, 29.90, 69.13),
+ vector4(247.12, -784.93, 29.84, 67.56),
+ vector4(246.23, -787.13, 29.82, 247.56),
+ vector4(239.40, -784.68, 29.90, 248.06),
+ vector4(230.42, -781.41, 30.01, 247.48),
+ vector4(224.14, -779.01, 30.07, 247.39),
+ vector4(215.32, -775.95, 30.17, 248.99),
+ vector4(243.81, -792.19, 29.77, 246.23),
+ vector4(238.08, -789.95, 29.85, 250.62),
+ vector4(228.14, -786.30, 30.01, 249.07),
+ vector4(221.98, -783.92, 30.08, 248.25),
+ vector4(213.41, -781.01, 30.19, 249.21),
+ vector4(214.77, -778.70, 30.17, 67.86),
+ vector4(224.02, -781.91, 30.07, 68.82),
+ vector4(229.34, -784.05, 30.01, 66.57),
+ vector4(239.87, -787.83, 29.86, 69.37),
+ vector4(245.18, -789.87, 29.79, 68.00),
+ vector4(241.85, -797.20, 29.72, 245.51),
+ vector4(236.57, -795.07, 29.82, 247.54),
+ vector4(226.12, -791.34, 29.99, 247.84),
+ vector4(220.61, -789.19, 30.08, 248.09),
+ vector4(212.00, -786.22, 30.21, 248.93),
+ vector4(212.91, -783.71, 30.19, 68.61),
+ vector4(222.11, -786.89, 30.07, 70.10),
+ vector4(227.84, -789.16, 29.99, 67.55),
+ vector4(237.73, -792.73, 29.82, 69.40),
+ vector4(243.54, -794.88, 29.73, 68.74),
+ vector4(237.03, -812.64, 29.59, 243.63),
+ vector4(207.66, -798.70, 30.29, 70.06),
+ vector4(216.56, -801.92, 30.10, 70.31),
+ vector4(222.28, -804.17, 29.98, 69.97),
+ vector4(232.41, -807.95, 29.74, 69.42),
+ vector4(238.16, -810.17, 29.60, 68.95),
+ vector4(238.32, -807.43, 29.63, 248.28),
+ vector4(233.32, -805.44, 29.75, 248.13),
+ vector4(222.46, -801.41, 29.98, 248.50),
+ vector4(216.85, -799.11, 30.10, 248.54),
+ vector4(207.60, -795.90, 30.29, 248.64),
+ vector4(209.28, -793.74, 30.25, 69.82),
+ vector4(218.31, -796.92, 30.08, 68.55),
+ vector4(224.50, -799.34, 29.96, 68.63),
+ vector4(234.40, -803.02, 29.76, 66.67),
+ vector4(240.09, -805.27, 29.64, 69.02),
+ vector4(240.53, -802.44, 29.67, 248.95),
+ vector4(234.46, -800.13, 29.79, 249.40),
+ vector4(224.30, -796.51, 29.98, 248.31),
+ vector4(218.23, -794.00, 30.09, 249.09),
+ vector4(209.84, -791.10, 30.24, 251.56),
+ vector4(211.09, -788.68, 30.22, 69.53),
+ vector4(220.17, -791.73, 30.06, 70.56),
+ vector4(225.84, -794.12, 29.98, 67.53),
+ vector4(236.38, -797.85, 29.79, 69.39),
+ vector4(216.56, -801.90, 30.79, 69.39)
+ }
+ },
+}
\ No newline at end of file
diff --git a/client/vehicle/garages.lua b/client/vehicle/garages.lua
new file mode 100644
index 0000000..2cce0e9
--- /dev/null
+++ b/client/vehicle/garages.lua
@@ -0,0 +1,291 @@
+local locations = require "client.vehicle.data"
+local sprite = {
+ ["water"] = 356,
+ ["heli"] = 360,
+ ["plane"] = 359,
+ ["land"] = 357
+}
+local garageTypes = {
+ ["water"] = 14,
+ ["heli"] = 15,
+ ["plane"] = 16
+}
+
+local clothing = {
+ {
+ face = {
+ drawable = 1,
+ texture = 1
+ },
+ undershirt = {
+ drawable = 0,
+ texture = 0
+ },
+ torso = {
+ drawable = 1,
+ texture = 1
+ },
+ leg = {
+ drawable = 0,
+ texture = 0
+ },
+ glasses = {
+ drawable = 1,
+ texture = 0
+ },
+ hat = {
+ drawable = -1,
+ texture = -1
+ },
+ },
+ {
+ leg = {
+ drawable = 0,
+ texture = 1
+ },
+ undershirt = {
+ drawable = 0,
+ texture = 0
+ },
+ face = {
+ drawable = 0,
+ texture = 0
+ },
+ torso = {
+ drawable = 0,
+ texture = 2
+ },
+ glasses = {
+ drawable = -1,
+ texture = -1
+ },
+ hat = {
+ drawable = 0,
+ texture = 0
+ },
+ },
+ {
+ face = {
+ drawable = 0,
+ texture = 2
+ },
+ undershirt = {
+ drawable = 0,
+ texture = 0
+ },
+ torso = {
+ drawable = 1,
+ texture = 2
+ },
+ leg = {
+ drawable = 0,
+ texture = 0
+ },
+ hat = {
+ drawable = -1,
+ texture = -1
+ },
+ glasses = {
+ drawable = -1,
+ texture = -1
+ },
+ }
+}
+
+local function getClosestOwnedVehicle()
+ local coords = GetEntityCoords(cache.ped)
+ local vehicles = lib.getNearbyVehicles(coords, 50.0, true)
+ local nearestVeh = {}
+
+ local function setNearestVehicle(veh)
+ local state = Entity(veh.vehicle).state
+ if not state.owner or state.owner ~= NDCore.player?.id then return end
+
+ local nearestDist = nearestVeh.dist
+ local dist = #(coords-veh.coords)
+ if not nearestDist or dist < nearestDist then
+ nearestVeh.dist = dist
+ nearestVeh.coords = veh.coords
+ nearestVeh.entity = veh.vehicle
+ end
+ end
+
+ for i=1, #vehicles do
+ setNearestVehicle(vehicles[i])
+ end
+ return nearestVeh.entity, nearestVeh.coords, nearestVeh.dist
+end
+
+local function parkVehicle(veh)
+ if not veh or not DoesEntityExist(veh) then
+ return NDCore.notify({
+ title = "Garage",
+ description = "No owned vehicle found nearby.",
+ type = "error",
+ position = "bottom",
+ duration = 3000
+ })
+ end
+ if GetPedInVehicleSeat(veh, -1) ~= 0 then
+ NDCore.notify({
+ title = "Garage",
+ description = "Player in vehicle!",
+ type = "error",
+ position = "bottom",
+ duration = 3000
+ })
+ return
+ end
+
+ local properties = lib.getVehicleProperties(veh)
+ properties.class = GetVehicleClass(veh)
+ TriggerServerEvent("ND_Vehicles:storeVehicle", VehToNet(veh))
+end
+
+local function isVehicleAvailable(vehicle, garageType, impound)
+ local class = vehicle.properties.class
+ local available = vehicle.available and not impound or vehicle.impounded and impound
+ if available and not garageTypes[garageType] then return true end
+
+ for garType, garClass in pairs(garageTypes) do
+ if available and garType == garageType and garClass == class then
+ return true
+ end
+ end
+end
+
+local function getEngineStatus(health)
+ if health > 950 then
+ return "Perfect"
+ elseif health > 750 then
+ return "Good"
+ elseif health > 500 then
+ return "Bad"
+ end
+ return "Very bad"
+end
+
+local function createMenuOptions(vehicle, vehicleSpawns)
+ local props = vehicle.properties
+ local makeName = GetLabelText(GetMakeNameFromVehicleModel(props.model))
+ local modelName = GetLabelText(GetDisplayNameFromVehicleModel(props.model))
+ local metadata = {}
+
+ if not makeName or makeName == "NULL" then
+ makeName = ""
+ else
+ metadata[#metadata+1] = {label = "Make", value = makeName}
+ makeName = makeName .. " "
+ end
+ if not modelName or modelName == "NULL" then
+ modelName = ""
+ else
+ metadata[#metadata+1] = {label = "Model", value = modelName}
+ end
+
+ if props?.plate then
+ metadata[#metadata+1] = {label = "Plate", value = props.plate}
+ end
+ if props?.engineHealth then
+ metadata[#metadata+1] = {
+ label = "Engine status",
+ value = getEngineStatus(props.engineHealth),
+ progress = props.engineHealth/10,
+ colorScheme = "blue"
+ }
+ end
+
+ if props?.fuelLevel then
+ metadata[#metadata+1] = {
+ label = "Fuel",
+ value = ("%d%s"):format(props.fuelLevel, "%"),
+ progress = props.fuelLevel,
+ colorScheme = "yellow"
+ }
+ end
+
+ return {
+ title = ("Vehicle: %s%s\nPlate: %s"):format(makeName, modelName, props?.plate or "not found"),
+ metadata = metadata,
+ onSelect = function(args)
+ TriggerServerEvent("ND_Vehicles:takeVehicle", vehicle.id, vehicleSpawns)
+ end,
+ }
+end
+
+local function createMenu(vehicles, garageType, vehicleSpawns, impound)
+ local options = {}
+ if not impound then
+ options[#options+1] = {
+ title = "Park vehicle",
+ onSelect = function(args)
+ local veh = getClosestOwnedVehicle()
+ parkVehicle(veh)
+ end
+ }
+ end
+ for _, vehicle in ipairs(vehicles) do
+ if isVehicleAvailable(vehicle, garageType, impound) then
+ options[#options+1] = createMenuOptions(vehicle, vehicleSpawns)
+ end
+ end
+ if impound and #options == 0 then
+ options[#options+1] = {
+ title = "No vehicles found",
+ readOnly = true
+ }
+ end
+ return {
+ id = ("garage_%s"):format(garageType),
+ title = impound and "Vehicle impound" or "Parking garage",
+ options = options,
+ onExit = function()
+ garageOpen = false
+ end
+ }
+end
+
+for i=1, #locations do
+ local location = locations[i]
+ NDCore.createAiPed({
+ model = `s_m_y_airworker`,
+ coords = location.ped,
+ distance = 45.0,
+ clothing = clothing[math.random(1, #clothing)],
+ blip = {
+ label = location.impound and ("Impound (%s)"):format(location.garageType) or ("Parking garage (%s)"):format(location.garageType),
+ sprite = location.impound and 285 or sprite[location.garageType],
+ scale = 0.7,
+ color = 3,
+ groups = location.groups
+ },
+ anim = {
+ dict = "anim@amb@casino@valet_scenario@pose_d@",
+ clip = "base_a_m_y_vinewood_01"
+ },
+ options = {
+ {
+ name = "nd_core:garagePed",
+ icon = "fa-solid fa-warehouse",
+ label = location.impound and "View impounded vehicles" or "View garage",
+ distance = 2.0,
+ canInteract = function(entity, distance, coords, name, bone)
+ if not location.groups then return true end
+ local groups = location.groups
+ local playerGroups = NDCore.player?.groups
+ for i=1, #groups do
+ if playerGroups?[groups[i]] then
+ return true
+ end
+ end
+ end,
+ onSelect = function(data)
+ local vehicles = lib.callback.await("ND_Vehicles:getOwnedVehicles") or {}
+ local menu = createMenu(vehicles, location.garageType, location.vehicleSpawns, location.impound)
+ lib.registerContext(menu)
+ lib.showContext(menu.id)
+ end
+ }
+ },
+ })
+end
diff --git a/client/vehicle/main.lua b/client/vehicle/main.lua
new file mode 100644
index 0000000..7a44c22
--- /dev/null
+++ b/client/vehicle/main.lua
@@ -0,0 +1,750 @@
+local vehicleColorNames = {
+ [0] = "Black",
+ [1] = "Black",
+ [2] = "Black",
+ [3] = "Silver",
+ [4] = "Silver",
+ [5] = "Silver",
+ [6] = "Gray",
+ [7] = "Silver",
+ [8] = "Silver",
+ [9] = "Silver",
+ [10] = "Metal",
+ [11] = "Grey",
+ [12] = "Black",
+ [13] = "Gray",
+ [14] = "Grey",
+ [15] = "Black",
+ [16] = "Black",
+ [17] = "Silver",
+ [18] = "Silver",
+ [19] = "Metal",
+ [20] = "Silver",
+ [21] = "Black",
+ [22] = "Graphite",
+ [23] = "Silver",
+ [24] = "Silver",
+ [25] = "Silver",
+ [26] = "Silver",
+ [27] = "Red",
+ [28] = "Red",
+ [29] = "Red",
+ [30] = "Red",
+ [31] = "Red",
+ [32] = "Red",
+ [33] = "Red",
+ [34] = "Red",
+ [35] = "Red",
+ [36] = "Orange",
+ [37] = "Gold",
+ [38] = "Orange",
+ [39] = "Red",
+ [40] = "Red",
+ [41] = "Orange",
+ [42] = "Yellow",
+ [43] = "Red",
+ [44] = "Red",
+ [45] = "Red",
+ [46] = "Red",
+ [47] = "Red",
+ [48] = "Red",
+ [49] = "Green",
+ [50] = "Green",
+ [51] = "Green",
+ [52] = "Green",
+ [53] = "Green",
+ [54] = "Green",
+ [55] = "Green",
+ [56] = "Green",
+ [57] = "Green",
+ [58] = "Green",
+ [59] = "Green",
+ [60] = "Green",
+ [61] = "Blue",
+ [62] = "Blue",
+ [63] = "Blue",
+ [64] = "Blue",
+ [65] = "Blue",
+ [66] = "Blue",
+ [67] = "Blue",
+ [68] = "Blue",
+ [69] = "Blue",
+ [70] = "Blue",
+ [71] = "Blue",
+ [72] = "Blue",
+ [73] = "Blue",
+ [74] = "Blue",
+ [75] = "Blue",
+ [76] = "Blue",
+ [77] = "Blue",
+ [78] = "Blue",
+ [79] = "Bblue",
+ [80] = "Blue",
+ [81] = "Blue",
+ [82] = "Blue",
+ [83] = "Blue",
+ [84] = "Blue",
+ [85] = "Blue",
+ [86] = "Blue",
+ [87] = "Blue",
+ [88] = "Yellow",
+ [89] = "Yellow",
+ [90] = "Bronze",
+ [91] = "Yellow",
+ [92] = "Lime",
+ [93] = "Champagne",
+ [94] = "Beige",
+ [95] = "Ivory",
+ [96] = "Brown",
+ [97] = "Brown",
+ [98] = "Brown",
+ [99] = "Beige",
+ [100] = "Brown",
+ [101] = "Brown",
+ [102] = "Beechwood",
+ [103] = "Beechwood",
+ [104] = "Orange",
+ [105] = "Sand",
+ [106] = "Sand",
+ [107] = "Cream",
+ [108] = "Brown",
+ [109] = "Brown",
+ [110] = "Brown",
+ [111] = "White",
+ [112] = "White",
+ [113] = "Beige",
+ [114] = "Brown",
+ [115] = "Brown",
+ [116] = "Beige",
+ [117] = "Steel",
+ [118] = "Steel",
+ [119] = "Aluminium",
+ [120] = "Chrome",
+ [121] = "White",
+ [122] = "White",
+ [123] = "Orange",
+ [124] = "Orange",
+ [125] = "Green",
+ [126] = "Yellow",
+ [127] = "Blue",
+ [128] = "Green",
+ [129] = "Brown",
+ [130] = "Orange",
+ [131] = "White",
+ [132] = "White",
+ [133] = "Green",
+ [134] = "White",
+ [135] = "Pink",
+ [136] = "pink",
+ [137] = "Pink",
+ [138] = "Orange",
+ [139] = "Green",
+ [140] = "Blue",
+ [141] = "Black",
+ [142] = "Black",
+ [143] = "Black",
+ [144] = "Green",
+ [145] = "Purple",
+ [146] = "Blue",
+ [147] = "Black",
+ [148] = "Purple",
+ [149] = "Purple",
+ [150] = "Red",
+ [151] = "Green",
+ [152] = "Green",
+ [153] = "Brown",
+ [154] = "Tan",
+ [155] = "Green",
+ [156] = "ALLOY",
+ [157] = "Blue",
+}
+
+local vehicleClassNames = {
+ [0] = "Compact",
+ [1] = "Sedan",
+ [2] = "SUV",
+ [3] = "Coupe",
+ [4] = "Muscle",
+ [5] = "Sports Classic",
+ [6] = "Sport",
+ [7] = "Super",
+ [8] = "Motorcycle",
+ [9] = "Off-road",
+ [10] = "Industrial",
+ [11] = "Utility",
+ [12] = "Van",
+ [13] = "Cycle",
+ [14] = "Boat",
+ [15] = "Helicopter",
+ [16] = "Plane",
+ [17] = "Service",
+ [18] = "Emergency",
+ [19] = "Military",
+ [20] = "Commercial",
+ [21] = "Train",
+ [22] = "Open wheel"
+}
+
+local cruiseSpeedSet = 0
+local cruiseSpeedVehicle = 0
+local cruiseControlEnabled = false
+local playerVehicle = cache.seat == -1 and cache.vehicle
+local cloudTime = GetCloudTimeAsInt()
+
+local vehicleLockCheckTime = {
+ lastCheck = cloudTime,
+ lastUse = cloudTime
+}
+
+local keyCheckTime = {
+ lastCheck = cloudTime,
+ hasKey = false
+}
+
+local vehicleClassNotDisableAirControl = {
+ [8] = true, --motorcycle
+ [13] = true, --bicycles
+ [14] = true, --boats
+ [15] = true, --helicopter
+ [16] = true, --plane
+ [19] = true --military
+}
+
+local function getVehicleBlipSprite(entity)
+ if not IsEntityAVehicle(entity) then
+ return 148 -- circle blip
+ end
+
+ local class = GetVehicleClass(entity)
+ local model = GetEntityModel(entity)
+ local classBlip = {
+ [16] = 423, -- plane
+ [8] = 226, -- motorcycle
+ [15] = 64, -- helicopter
+ [14] = 427, -- boat
+ [6] = 825, -- sports
+ [7] = 523, -- super
+ [2] = 821, -- SUV
+ [4] = 663 -- muscle
+ }
+ local typeBlip = {
+ [`seashark`] = 471,
+ [`marquis`] = 410,
+ [`rhino`] = 421,
+ [`hydra`] = 424,
+ [`lazer`] = 424,
+ [`taxi`] = 198,
+ [`trash`] = 318,
+ [`trash2`] = 318
+ }
+
+ return typeBlip[model] or classBlip[class] or 225 -- 255 is default car blip
+end
+
+local function getVehFromNetId(netId)
+ local time = GetCloudTimeAsInt()
+ while not NetworkDoesNetworkIdExist(netId) or not NetworkDoesEntityExistWithNetworkId(netId) and time-GetCloudTimeAsInt() < 5 do
+ Wait(100)
+ end
+ return NetToVeh(netId)
+end
+
+RegisterNetEvent("ND_Vehicles:blip", function(netId, status)
+ local veh = getVehFromNetId(netId)
+ if not veh then return end
+ if not status then
+ local blip = GetBlipFromEntity(veh)
+ if not blip or not DoesBlipExist(blip) then return end
+ return RemoveBlip(blip)
+ end
+
+ local blip = AddBlipForEntity(veh)
+ SetBlipSprite(blip, getVehicleBlipSprite(veh))
+ SetBlipColour(blip, 0)
+ SetBlipScale(blip, 0.8)
+ SetBlipAsShortRange(blip, true)
+ BeginTextCommandSetBlipName("STRING")
+ AddTextComponentSubstringPlayerName("Personal vehicle")
+ EndTextCommandSetBlipName(blip)
+end)
+
+RegisterNetEvent("ND_Vehicles:syncAlarm", function(netId)
+ local veh = getVehFromNetId(netId)
+ if not veh then return end
+ SetVehicleAlarmTimeLeft(veh, 1)
+ SetVehicleAlarm(veh, true)
+ StartVehicleAlarm(veh)
+end)
+
+RegisterNetEvent("ND_VehicleSystem:setOwnedIfNot", function(netId)
+ local veh = getVehFromNetId(netId)
+ if not veh then return end
+ setVehicleOwned(veh, true)
+ setVehicleLocked(veh, true)
+end)
+
+AddStateBagChangeHandler("props", nil, function(bagName, key, value, reserved, replicated)
+ local entity = GetEntityFromStateBagName(bagName)
+ if not value or not DoesEntityExist(entity) or NetworkGetEntityOwner(entity) ~= cache.playerId then return end
+ local props = value
+ if type(value) == "string" then
+ props = json.decode(value)
+ end
+ lib.setVehicleProperties(entity, props)
+end)
+
+local function playKeyFob(veh)
+ local keyFob
+ local ped = cache.ped
+ local coords = GetEntityCoords(ped)
+ if #(coords-GetEntityCoords(veh)) > 25.0 then return end
+
+ if not playerVehicle then
+ ClearPedTasks(ped)
+ lib.requestAnimDict("anim@mp_player_intmenu@key_fob@")
+ TaskPlayAnim(ped, "anim@mp_player_intmenu@key_fob@", "fob_click_fp", 8.0, 8.0, -1, 48, 1, false, false, false)
+ keyFob = CreateObject(`lr_prop_carkey_fob`, 0, 0, 0, true, true, true)
+ AttachEntityToEntity(keyFob, ped, GetPedBoneIndex(ped, 0xDEAD), 0.12, 0.04, -0.025, -100.0, 100.0, 0.0, true, true, false, true, 1, true)
+ Wait(700)
+ end
+
+ PlaySoundFromEntity(-1, "Remote_Control_Fob", ped, "PI_Menu_Sounds", true, 0)
+ SetVehicleLights(veh, 2)
+ Wait(100)
+ SetVehicleLights(veh, 0)
+ Wait(200)
+ SetVehicleLights(veh, 2)
+ Wait(100)
+ SetVehicleLights(veh, 0)
+ return keyFob and DeleteEntity(keyFob)
+end
+
+RegisterNetEvent("ND_Vehicles:keyFob", function(netId)
+ playKeyFob(getVehFromNetId(netId))
+end)
+
+local dontLock = {
+ [8] = true, -- Motorcycles
+ [13] = true, -- Cycles
+ [14] = true -- boats
+}
+
+AddStateBagChangeHandler("locked", nil, function(bagName, key, value, reserved, replicated)
+ local entity = GetEntityFromStateBagName(bagName)
+ if entity == 0 or value == nil or dontLock[GetVehicleClass(entity)] then return end
+
+ if value then
+ -- SetVehicleDoorsLockedForAllPlayers(entity, true)
+ return SetVehicleDoorsLocked(entity, 2)
+ end
+
+ CreateThread(function()
+ while GetVehiclePedIsEntering(cache.ped) == entity do Wait(10) end
+ -- SetVehicleDoorsLockedForAllPlayers(entity, false)
+
+ SetVehicleDoorsLocked(entity, 0)
+ end)
+end)
+
+lib.callback.register("ND_Vehicles:getProps", function(netId)
+ local veh = getVehFromNetId(netId)
+ local props = lib.getVehicleProperties(veh)
+ local colorPrimary, colorSecondary = GetVehicleColours(veh)
+ if not props then return end
+
+ props.colorNamePrimary = vehicleColorNames[colorPrimary]
+ props.colorNameSecondary = vehicleColorNames[colorSecondary]
+ props.colorName = props.colorNamePrimary == props.colorNameSecondary and props.colorNamePrimary or ("%s & %s"):format(props.colorNamePrimary, props.colorNameSecondary)
+ props.className = vehicleClassNames[GetVehicleClass(veh)]
+ props.makeName = GetLabelText(GetMakeNameFromVehicleModel(props.model))
+ props.modelName = GetLabelText(GetDisplayNameFromVehicleModel(props.model))
+ return props
+end)
+
+lib.callback.register("ND_Vehicles:getVehicleModelMakeLabel", function(model)
+ local make = GetLabelText(GetMakeNameFromVehicleModel(model))
+ local name = GetLabelText(GetDisplayNameFromVehicleModel(model))
+ if make == "NULL" then
+ return name
+ elseif name == "NULL" then
+ return make
+ end
+ return ("%s %s"):format(make, name)
+end)
+
+local function hasVehicleKeys(veh, checkEngine)
+ local state = Entity(veh).state
+ if Config.ox_inventory and Config.useInventoryForKeys then
+ local metadata = {
+ vehId = state.id,
+ keyEnabled = true
+ }
+ local hasKey = exports.ox_inventory:GetItemCount("keys", metadata) > 0
+ return hasKey or checkEngine and state.hotwired
+ end
+
+ local keys = state and state.keys
+ local player = NDCore.getPlayer()
+ local hasKey = player and keys and keys[player.id]
+ return hasKey or checkEngine and state.hotwired
+end
+
+local function hasVehicleKeysCheck(veh)
+ local time = GetCloudTimeAsInt()
+ if time-keyCheckTime.lastCheck < 5 then
+ return keyCheckTime.hasKey
+ end
+
+ local hasKey = hasVehicleKeys(veh, true)
+ keyCheckTime.lastCheck = time
+ keyCheckTime.hasKey = hasKey
+ return hasKey
+end
+
+local function getNearestVehicle(hasKeysForOnly)
+ local coords = GetEntityCoords(cache.ped)
+ local vehicles = lib.getNearbyVehicles(coords, 25.0, true)
+ local nearestVeh = {}
+
+ local function setNearestVehicle(veh)
+ if hasKeysForOnly and not hasVehicleKeys(veh.vehicle) then return end
+ local nearestDist = nearestVeh.dist
+ local dist = #(coords-veh.coords)
+ if not nearestDist or dist < nearestDist then
+ nearestVeh.dist = dist
+ nearestVeh.coords = veh.coords
+ nearestVeh.entity = veh.vehicle
+ end
+ end
+
+ for i=1, #vehicles do
+ setNearestVehicle(vehicles[i])
+ end
+ return nearestVeh.entity, nearestVeh.coords, nearestVeh.dist
+end
+
+local vehicleLockKeybind = lib.addKeybind({
+ name = "vehicleKey",
+ description = "Unlock/lock vehicle (double click)",
+ defaultKey = "E",
+ onPressed = function(self)
+ local time = GetCloudTimeAsInt()
+ if time-vehicleLockCheckTime.lastCheck < 1 and time-vehicleLockCheckTime.lastUse > 1 then
+ vehicleLockCheckTime.lastUse = time
+ local veh = getNearestVehicle(true)
+ if not veh then return end
+ TriggerServerEvent("ND_Vehicles:toggleVehicleLock", VehToNet(veh))
+ end
+ vehicleLockCheckTime.lastCheck = time
+ end
+})
+
+NDCore.isResourceStarted("ox_inventory", function(started)
+ Config.ox_inventory = started
+ if not started or not Config.useInventoryForKeys then
+ return vehicleLockKeybind:disable(false)
+ end
+ Wait(1000)
+ vehicleLockKeybind:disable(true)
+ exports.ox_inventory:displayMetadata({
+ vehPlate = "Plate",
+ vehModel = "Model"
+ })
+end)
+
+-- save wheels steering angle.
+CreateThread(function()
+ local angle = 0.0
+ while true do
+ Wait(300)
+ if playerVehicle then
+ if GetIsTaskActive(cache.ped, 2) then
+ SetVehicleSteeringAngle(playerVehicle, angle)
+ end
+ angle = DoesEntityExist(playerVehicle) and GetVehicleSteeringAngle(playerVehicle)
+ end
+ end
+end)
+
+CreateThread(function()
+ local wait = 500
+ while true do
+ Wait(wait)
+ playerVehicle = cache.seat == -1 and cache.vehicle
+ if playerVehicle then
+ if Config.disableVehicleAirControl and not vehicleClassNotDisableAirControl[GetVehicleClass(playerVehicle)] and (IsEntityInAir(playerVehicle) or IsEntityUpsidedown(playerVehicle)) then
+ wait = 0
+ DisableControlAction(0, 59) -- disable vehicle air control.
+ DisableControlAction(0, 60)
+ elseif not GetIsVehicleEngineRunning(playerVehicle) and not hasVehicleKeysCheck(playerVehicle) then
+ wait = 0
+ DisableControlAction(0, 59)
+ if DoesEntityExist(playerVehicle) and IsVehicleEngineStarting(playerVehicle) then
+ SetVehicleEngineOn(playerVehicle, false, true, true) -- don't turn on engine if no keys.
+ end
+ else
+ wait = 500
+ end
+ elseif wait ~= 500 then
+ wait = 500
+ end
+ end
+end)
+
+local function hotwireVehicle()
+ local state = playerVehicle and Entity(playerVehicle).state
+ if not playerVehicle or state.hotwired then return end
+
+ local finished = false
+ lib.requestModel(`imp_prop_impexp_pliers_02`)
+ lib.requestModel(`prop_tool_screwdvr01`)
+ lib.requestAnimDict("veh@handler@base")
+
+ if GetFollowVehicleCamViewMode() > 2 then
+ SetFollowVehicleCamViewMode(0)
+ end
+
+ CreateThread(function()
+ while not finished do
+ Wait(0)
+ DisableFirstPersonCamThisFrame()
+ end
+ end)
+
+ local modelWithHandles = {`seashark`, `seashark2`, `seashark3`}
+ local vehicleModel = GetEntityModel(playerVehicle)
+ local bikeHandles = GetVehicleClass(playerVehicle) == 8 or lib.table.contains(modelWithHandles, vehicleModel)
+
+ local success = lib.progressCircle({
+ duration = math.random(10000, 20000),
+ label = "Hotwiring",
+ useWhileDead = false,
+ allowRagdoll = false,
+ allowCuffed = false,
+ allowFalling = false,
+ canCancel = true,
+ anim = {
+ dict = bikeHandles and "anim@veh@boat@jetski@front@base" or "veh@handler@base",
+ clip = "hotwire"
+ },
+ disable = {
+ move = true,
+ car = true,
+ combat = true
+ },
+ prop = {
+ {
+ model = `imp_prop_impexp_pliers_02`,
+ bone = 0x49D9, -- SKEL_R_Hand
+ pos = vec3(0.1, -0.05, 0.0),
+ rot = vec3(-1.5, -15.0, -1.5)
+ },
+ {
+ model = `prop_tool_screwdvr01`,
+ bone = 0xDEAD, -- SKEL_R_Hand
+ pos = vec3(0.1, 0.08, -0.03),
+ rot = vec3(90.0, 0.0, 0.0)
+ }
+ }
+ })
+
+ finished = true
+ if not success then return end
+ if not playerVehicle then return false, true end
+ TriggerServerEvent("ND_Vehicles:hotwire", VehToNet(playerVehicle))
+ return true, true
+end
+
+local function lockpickVehicle()
+ if cache.vehicle then return end
+ local pos = GetEntityCoords(cache.ped)
+ local rot = GetEntityRotation(cache.ped, 2)
+ local veh = lib.getClosestVehicle(pos, 2.5, false)
+ if not veh then return end
+
+ local dificulties = {
+ "easy",
+ "medium",
+ "hard"
+ }
+ local dificultyTime = {
+ easy = 500,
+ medium = 800,
+ hard = 1000
+ }
+
+ lib.requestAnimDict("veh@break_in@0h@p_m_one@")
+ for i=1, 7 do
+ TaskPlayAnimAdvanced(cache.ped, "veh@break_in@0h@p_m_one@", "std_force_entry_ds", pos.x, pos.y, pos.z+0.025, rot.x, rot.y, rot.z, 8.0, 8.0, 1800, 28, 0.1)
+ local dificulty = dificulties[math.random(1, #dificulties)]
+ local success = lib.skillCheck(dificulty)
+ if not success or not DoesEntityExist(veh) or #(pos-GetEntityCoords(veh)) > 2.5 then
+ TriggerServerEvent("ND_Vehicles:lockpick", VehToNet(veh), false)
+ return false, true
+ end
+ Wait(dificultyTime[dificulty])
+ end
+
+ veh = lib.getClosestVehicle(pos, 2.5, false)
+ if not veh then return false, true end
+ TriggerServerEvent("ND_Vehicles:lockpick", VehToNet(veh), true)
+ PlaySoundFromEntity(-1, "Remote_Control_Fob", cache.ped, "PI_Menu_Sounds", true, 0)
+ return true, true
+end
+
+exports("lockpick", function(data, slot)
+ local _, used = lockpickVehicle()
+ if used then
+ exports.ox_inventory:useItem(data)
+ end
+end)
+
+exports("hotwire", function(data, slot)
+ local _, used = hotwireVehicle()
+ if used then
+ exports.ox_inventory:useItem(data)
+ end
+end)
+
+exports("keyControl", function(action, slot)
+ for item, data in pairs(exports.ox_inventory:Items()) do
+ local metadata = data.metadata
+ if data.slot == slot then
+ if metadata and not metadata.keyEnabled then
+ return lib.notify({
+ title = "No signal",
+ description = "Vehicle key disabled.",
+ type = "error",
+ position = "bottom-right",
+ duration = 3000
+ })
+ end
+ break
+ end
+ end
+ exports.ox_inventory:closeInventory()
+ if action == "trunk" then
+ local veh = getNearestVehicle(true)
+ if not veh then return end
+ playKeyFob(veh)
+ if GetVehicleDoorAngleRatio(veh, 5) > 0.0 then
+ SetVehicleDoorShut(veh, 5)
+ else
+ SetVehicleDoorOpen(veh, 5, false)
+ end
+ elseif action == "disable" then
+ TriggerServerEvent("ND_Vehicles:disableKey", slot)
+ lib.notify({
+ title = "Key disabled",
+ description = "This vehicle key has been disabled and cannot be used anymore.",
+ type = "inform",
+ position = "bottom-right",
+ duration = 3000
+ })
+ end
+end)
+
+local function cruiseControl()
+ cruiseSpeedVehicle = GetEntitySpeed(playerVehicle) * 2.236936
+ if not playerVehicle then
+ lib.notify({
+ title = "Cruise control",
+ description = "Vehicle cruise control disabled.",
+ type = "inform",
+ position = "bottom-right",
+ duration = 3000
+ })
+ return
+ end
+ if cruiseSpeedVehicle < cruiseSpeedSet/3 then
+ lib.notify({
+ title = "Cruise control",
+ description = "Vehicle cruise control disabled.",
+ type = "inform",
+ position = "bottom-right",
+ duration = 3000
+ })
+ return
+ end
+ if cruiseSpeedVehicle < cruiseSpeedSet then
+ SetControlNormal(0, 71, 0.6)
+ end
+ return true
+end
+
+lib.addKeybind({
+ name = "vehicleCruiseControl",
+ description = "Toggle vehicle cruise control",
+ defaultKey = "",
+ onPressed = function(self)
+ if cruiseControlEnabled and cruiseSpeedVehicle-1 > cruiseSpeedSet then
+ cruiseSpeedSet = cruiseSpeedVehicle
+ return lib.notify({
+ title = "Cruise control",
+ description = ("Increased to %d mph."):format(math.floor(cruiseSpeedSet)),
+ type = "inform",
+ position = "bottom-right",
+ duration = 3000
+ })
+ elseif cruiseControlEnabled then
+ cruiseControlEnabled = false
+ return lib.notify({
+ title = "Cruise control",
+ description = "Vehicle cruise control disabled.",
+ type = "inform",
+ position = "bottom-right",
+ duration = 3000
+ })
+ end
+ if not playerVehicle then return end
+ cruiseControlEnabled = true
+ cruiseSpeedVehicle = math.floor(GetEntitySpeed(playerVehicle) * 2.236936)
+ cruiseSpeedSet = cruiseSpeedVehicle
+ lib.notify({
+ title = "Cruise control",
+ description = ("Set to %d mph."):format(math.floor(cruiseSpeedSet)),
+ type = "inform",
+ position = "bottom-right",
+ duration = 3000
+ })
+ CreateThread(function()
+ while cruiseControlEnabled and cruiseControl() do Wait(0) end
+ cruiseControlEnabled = false
+ end)
+ end
+})
+
+lib.addKeybind({
+ name = "vehicleShuffleSeat",
+ description = "Suffle vehicle seat.",
+ defaultKey = "",
+ onPressed = function(self)
+ if not cache.vehicle then return end
+ local seats = {
+ [-1] = 0,
+ [0] = -1,
+ [1] = 2,
+ [2] = 1,
+ [3] = 4,
+ [4] = 3,
+ [5] = 6,
+ [6] = 5
+ }
+ SetPedIntoVehicle(cache.ped, cache.vehicle, seats[cache.seat])
+ end
+})
+
+lib.onCache("ped", function(value)
+ SetPedConfigFlag(value, 184, true)
+end)
+
+lib.onCache("vehicle", function(value)
+ local veh = value or cache.vehicle
+ local blip = GetBlipFromEntity(veh)
+ if not blip or not DoesBlipExist(blip) then return end
+ SetBlipAlpha(blip, value and 0 or 255)
+end)
+
+SetTimeout(500, function()
+ SetPedConfigFlag(cache.ped, 184, true)
+end)
diff --git a/compatibility/backwards/client.lua b/compatibility/backwards/client.lua
new file mode 100644
index 0000000..5f5ecb1
--- /dev/null
+++ b/compatibility/backwards/client.lua
@@ -0,0 +1,18 @@
+if not lib.table.contains(Config.compatibility, "backwards") then return end
+
+NDCore.Functions = {}
+NDCore.Functions.GetSelectedCharacter = NDCore.getPlayer
+NDCore.Functions.GetCharacters = NDCore.getCharacters
+NDCore.Functions.GetPlayersFromCoords = NDCore.getPlayersFromCoords
+
+exports("GetCoreObject", function()
+ return NDCore
+end)
+
+RegisterNetEvent("ND:returnCharacters", function(characters)
+ NDCore.characters = characters
+end)
+
+RegisterNetEvent("ND:setCharacter", function(character)
+ NDCore.player = character
+end)
diff --git a/compatibility/backwards/server.lua b/compatibility/backwards/server.lua
new file mode 100644
index 0000000..fb72827
--- /dev/null
+++ b/compatibility/backwards/server.lua
@@ -0,0 +1,259 @@
+if not lib.table.contains(Config.compatibility, "backwards") then return end
+
+exports("GetCoreObject", function()
+ return NDCore
+end)
+
+NDCore.Functions = {}
+NDCore.Functions.GetPlayer = NDCore.getPlayer
+NDCore.Functions.GetPlayers = NDCore.getPlayers
+NDCore.Functions.GetUserDiscordInfo = NDCore.getDiscordInfo
+NDCore.Functions.SetActiveCharacter = NDCore.setActiveCharacter
+NDCore.Functions.GetPlayerCharacters = NDCore.fetchAllCharacters
+NDCore.Functions.GetPlayerByCharacterId = NDCore.fetchCharacter
+
+function NDCore.Functions.GetPlayerIdentifierFromType(identifierType, src)
+ return GetPlayerIdentifierByType(src, identifierType)
+end
+
+function NDCore.Functions.GetNearbyPedToPlayer(src)
+ local pedCoords = GetEntityCoords(GetPlayerPed(src))
+ for targetId, targetInfo in pairs(NDCore.players) do
+ local targetCoords = GetEntityCoords(GetPlayerPed(targetId))
+ if #(pedCoords - targetCoords) < 2.0 and targetId ~= src then
+ return targetId, targetInfo
+ end
+ end
+end
+
+function NDCore.Functions.UpdateMoney(src)
+ local player = NDCore.getPlayer(src)
+ player.triggerEvent("ND:updateMoney", player.cash, player.bank)
+end
+
+function NDCore.Functions.TransferBank(amount, source, target, descriptionSender, descriptionReceiver)
+ local amount = tonumber(amount)
+ local src = tonumber(source)
+ local target = tonumber(target)
+ local player = NDCore.getPlayer(src)
+ if not player then return end
+
+ if src == target then
+ return false, "Can't transfer money to same account"
+ elseif GetPlayerPing(target) == 0 then
+ return false, "Account not found"
+ elseif amount <= 0 then
+ return false, "Invalid amount"
+ elseif player.bank < amount then
+ return false, "Insufficient funds"
+ end
+
+ local targetPlayer = NDCore.getPlayer(target)
+ if not targetPlayer then return end
+ return player.deductMoney("bank", amount, descriptionSender or "Transfer") and targetPlayer.addMoney("bank", amount, descriptionReceiver or "Transfer")
+end
+
+function NDCore.Functions.GiveCash(amount, source, target)
+ local amount = tonumber(amount)
+ local src = tonumber(source)
+ local target = tonumber(target)
+ local player = NDCore.getPlayer(src)
+ if not player then return end
+
+ if src == target then
+ return false, "Can't give to self"
+ elseif GetPlayerPing(target) == 0 then
+ return false, "Target not found"
+ elseif amount <= 0 then
+ return false, "Invalid amount"
+ elseif player.bank < amount then
+ return false, "Not enough money"
+ end
+
+ local targetPlayer = NDCore.getPlayer(target)
+ if not targetPlayer then return endn end
+ return player.deductMoney("cash", amount) and targetPlayer.addMoney("cash", amount)
+end
+
+function NDCore.Functions.GiveCashToNearbyPlayer(source, amount)
+ local targetId = NDCore.Functions.GetNearbyPedToPlayer(source)
+ if not targetId then return end
+ return NDCore.Functions.GiveCash(amount, source, targetId)
+end
+
+function NDCore.Functions.WithdrawMoney(amount, source)
+ local player = NDCore.getPlayer(source)
+ return player and player.withdrawMoney(amount)
+end
+
+function NDCore.Functions.DepositMoney(amount, source)
+ local player = NDCore.getPlayer(source)
+ return player and player.depositMoney(amount)
+end
+
+function NDCore.Functions.DeductMoney(amount, source, account, description)
+ local player = NDCore.getPlayer(source)
+ return player and player.deductMoney(amount, account, description)
+end
+
+function NDCore.Functions.AddMoney(amount, source, account, description)
+ local player = NDCore.getPlayer(source)
+ return player and player.addMoney(amount, account, description)
+end
+
+function NDCore.Functions.CreateCharacter(src, firstName, lastName, dob, gender, cb)
+ local player = NDCore.newCharacter(src, {
+ firstname = firstName,
+ lastname = lastName,
+ dob = dob,
+ gender = gender,
+ })
+
+ if cb then cb(player.id) end
+ player.triggerEvent("ND:returnCharacters", NDCore.fetchAllCharacters(src))
+ return player.id
+end
+
+function NDCore.Functions.UpdateCharacter(characterId, firstName, lastName, dob, gender)
+ local player = NDCore.fetchCharacter(characterId)
+ player.setData({
+ source = src,
+ firstname = firstName,
+ lastname = lastName,
+ dob = dob,
+ gender = gender
+ })
+ return player
+end
+
+function NDCore.Functions.DeleteCharacter(characterId)
+ local player = NDCore.fetchCharacter(characterId)
+ return player and player.delete()
+end
+
+function NDCore.Functions.SetPlayerData(characterId, key, value)
+ local player = NDCore.fetchCharacter(characterId)
+ if not player then return end
+
+ if player[key] then
+ return player.setData(key, value)
+ end
+ return player.setMetadata(key, value)
+end
+
+function NDCore.Functions.CreatePlayerLicense(characterId, licenseType, expire)
+ local player = NDCore.fetchCharacter(characterId)
+ return self.createLicense(licenseType, expire)
+end
+
+-- find a players license by it's identifier.
+function NDCore.Functions.FindLicenseByIdentifier(licences, identifier)
+ for key, license in pairs(licences) do
+ if license.identifier == identifier then
+ return license, key
+ end
+ end
+ return {}
+end
+
+-- Edit a license by the license identifier.
+function NDCore.Functions.EditPlayerLicense(characterId, identifier, newData)
+ local player = NDCore.fetchCharacter(characterId)
+ if not player then return end
+ player.updateLicense(identifier, newData)
+end
+
+-- Set the players job and job rank.
+function NDCore.Functions.SetPlayerJob(characterId, job, rank)
+ local player = NDCore.fetchCharacter(characterId)
+ if not player then return end
+ if player.source then
+ local oldJob = player.getJob(job)
+ TriggerEvent("ND:jobChanged", player.source, {name = job, rank = rank or 1}, {name = player.job, rank = oldJob and oldJob.rank or 1})
+ TriggerClientEvent("ND:jobChanged", player.source, {name = job, rank = rank or 1}, {name = player.job, rank = oldJob and oldJob.rank or 1})
+ end
+ return player.setJob(job, rank)
+end
+
+-- Set a player to a group.
+function NDCore.Functions.SetPlayerToGroup(characterId, group, rank)
+ local player = NDCore.fetchCharacter(characterId)
+ return player and player.addGroup(group, rank)
+end
+
+-- Remove a player from a group.
+function NDCore.Functions.RemovePlayerFromGroup(characterId, group)
+ local player = NDCore.fetchCharacter(characterId)
+ return player and player.removeGroup(group)
+end
+
+-- Update the characters last location into the database.
+function NDCore.Functions.UpdateLastLocation(characterId, location)
+ local player = NDCore.fetchCharacter(characterId)
+ return player and player.setMetadata("location", {
+ x = location.x,
+ y = location.y,
+ x = location.z,
+ w = location.h or location.heading or location.w or 0.0
+ })
+end
+
+function NDCore.Functions.IsPlayerAdmin(src)
+ local player = NDCore.getPlayer(src)
+ if player.groups["admin"] then
+ return true
+ end
+end
+
+-- Getting all the characters the player has and returning them to the client.
+RegisterNetEvent("ND:GetCharacters", function()
+ local src = source
+ TriggerClientEvent("ND:returnCharacters", src, NDCore.fetchAllCharacters(src))
+end)
+
+-- Creating a new character.
+RegisterNetEvent("ND:newCharacter", function(newCharacter)
+ local src = source
+ NDCore.newCharacter(src, {
+ firstname = newCharacter.firstName,
+ lastname = newCharacter.lastName,
+ dob = newCharacter.dob,
+ gender = newCharacter.gender,
+ cash = 0,
+ bank = 0,
+ })
+end)
+
+-- Update the character info when edited.
+RegisterNetEvent("ND:editCharacter", function(newCharacter)
+ local src = source
+ local player = NDCore.fetchCharacter(newCharacter.id, src)
+ return player and player.setData({
+ source = src,
+ firstname = newCharacter.firstName,
+ lastname = newCharacter.lastName,
+ dob = newCharacter.dob,
+ gender = newCharacter.gender
+ })
+end)
+
+-- Delete character from database.
+RegisterNetEvent("ND:deleteCharacter", function(characterId)
+ local src = source
+ local player = NDCore.fetchCharacter(characterId, src)
+ if not player or player.source ~= source then return end
+ return player.delete()
+end)
+
+-- add a player to the table.
+RegisterNetEvent("ND:setCharacterOnline", function(id)
+ local src = source
+ NDCore.setActiveCharacter(src, tonumber(id))
+end)
+
+-- Update the characters clothes.
+RegisterNetEvent("ND:updateClothes", function(clothing)
+ local src = source
+ local player = NDCore.getPlayer(src)
+ player.setMetadata("clothing", clothing)
+end)
diff --git a/compatibility/esx/client.lua b/compatibility/esx/client.lua
new file mode 100644
index 0000000..68d65f2
--- /dev/null
+++ b/compatibility/esx/client.lua
@@ -0,0 +1,412 @@
+if not lib.table.contains(Config.compatibility, "esx") then return end
+
+NDCore.Game = {}
+NDCore.Game.Utils = {}
+NDCore.Scaleform = {}
+NDCore.Scaleform.Utils = {}
+NDCore.Streaming = {}
+NDCore.UI = {}
+NDCore.UI.HUD = {}
+NDCore.UI.Menu = {}
+NDCore.PlayerData = {}
+
+local uiMetatable = {
+ __index = function(table, key)
+ if type(key) == "string" and key:match("^%a+$") then
+ return function()
+ print(("[^3WARNING^7] ESX Function '%s' is not compatible with NDCore!"):format(key))
+ end
+ end
+ end
+}
+
+setmetatable(NDCore.UI, uiMetatable)
+setmetatable(NDCore.UI.HUD, uiMetatable)
+setmetatable(NDCore.UI.Menu, uiMetatable)
+
+local function exportHandler(resource, exportName, cb)
+ AddEventHandler(("__cfx_export_%s_%s"):format(resource, exportName), function(setCB)
+ setCB(cb)
+ end)
+end
+
+exportHandler("es_extended", "getSharedObject", function()
+ return NDCore
+end)
+
+function NDCore.GetPlayerData()
+ local player = NDCore.getPlayer()
+ if not player then return {} end
+
+ player.Accounts = {
+ Bank = player.bank,
+ Money = player.Cash,
+ Black = player.Cash
+ }
+
+ if player.jobInfo then
+ player.job = {
+ id = player.job,
+ name = player.job,
+ label = player.jobInfo.label,
+ grade = player.jobInfo.rank,
+ grade_name = player.jobInfo.rankName,
+ grade_label = player.jobInfo.rankName,
+ grade_salary = 0,
+ skin_male = {},
+ skin_female = {}
+ }
+ end
+
+ player.coords = GetEntityCoords(cache.ped)
+ player.loadout = {}
+ player.maxWeight = exports.ox_inventory:GetPlayerMaxWeight()
+ player.money = player.Accounts.Money
+ player.sex = player.gender
+ player.firstName = player.firstname
+ player.lastName = player.lastname
+ player.dateofbirth = player.dob
+ player.height = 120
+ player.dead = LocalPlayer.state.dead or false
+ NDCore.PlayerData = player
+
+ return player
+end
+
+function NDCore.IsPlayerLoaded()
+ return NDCore.getPlayer() ~= nil
+end
+
+function NDCore.Progressbar(message, lenght, options)
+ local newOptions = {
+ duration = lenght,
+ label = message,
+ canCancel = true
+ }
+
+ if options.animation then
+ newOptions.anim = {}
+ if options.animation.type == "anim" then
+ if options.animation.dict then
+ newOptions.anim.dict = options.animation.dict
+ end
+ if options.animation.lib then
+ newOptions.anim.clip = options.animation.lib
+ end
+ elseif options.animation.type == "Scenario" then
+ newOptions.anim.scenario = options.animation.Scenario
+ end
+ end
+
+ if options.FreezePlayer then FreezeEntityPosition(cache.ped, true) end
+ local complete = lib.progressBar(newOptions)
+
+ if options.FreezePlayer then FreezeEntityPosition(cache.ped, false) end
+ if newOptions.onFinish and complete then
+ newOptions.onFinish()
+ end
+ if newOptions.onCancel and not complete then
+ newOptions.onCancel()
+ end
+ return complete
+end
+
+function NDCore.SearchInventory(item, count)
+ local itemCount = exports.ox_inventory:Search(item)
+ return itemCount >= count and itemCount
+end
+
+function NDCore.SetPlayerData()
+ print("[^3WARNING^7] ESX Function 'SetPlayerData' is not compatible with NDCore!")
+end
+
+function NDCore.ShowAdvancedNotification()
+ print("[^3WARNING^7] ESX Function 'ShowAdvancedNotification' is not compatible with NDCore!")
+end
+
+function NDCore.ShowFloatingHelpNotification()
+ print("[^3WARNING^7] ESX Function 'ShowFloatingHelpNotification' is not compatible with NDCore!")
+end
+
+function NDCore.ShowHelpNotification()
+ print("[^3WARNING^7] ESX Function 'ShowHelpNotification' is not compatible with NDCore!")
+end
+
+function NDCore.ShowInventory()
+ exports.ox_inventory:openInventory("player", cache.serverId)
+end
+
+function NDCore.ShowNotification(msg, type, time)
+ NDCore.notify({
+ title = "Notification",
+ description = msg,
+ type = type == "info" and "inform" or type,
+ duration = time
+ })
+end
+
+function NDCore.TriggerServerCallback(name, cb, ...)
+ lib.callback(name, nil, cb, ...)
+end
+
+function NDCore.Streaming.RequestAnimDict(animDict, cb)
+ lib.requestAnimDict(animDict)
+ if cb then cb(animDict) end
+ return animDict
+end
+
+function NDCore.Streaming.RequestAnimSet(animSet, cb)
+ lib.requestAnimSet(animSet)
+ if cb then cb(animSet) end
+ return animSet
+end
+
+function NDCore.Streaming.RequestModel(model, cb)
+ lib.requestModel(model)
+ if cb then cb(model) end
+ return model
+end
+
+function NDCore.Streaming.RequestNamedPtfxAsset(assetName, cb)
+ lib.requestNamedPtfxAsset(assetName)
+ if cb then cb(assetName) end
+ return assetName
+end
+
+function NDCore.Streaming.RequestStreamedTextureDict(textureDict, cb)
+ lib.requestStreamedTextureDict(textureDict)
+ if cb then cb(textureDict) end
+ return textureDict
+end
+
+function NDCore.Streaming.RequestWeaponAsset(weaponHash, cb)
+ lib.requestWeaponAsset(weaponHash)
+ if cb then cb(weaponHash) end
+ return weaponHash
+end
+
+function NDCore.Scaleform.ShowBreakingNews()
+ print("[^3WARNING^7] ESX Function 'Scaleform.ShowBreakingNews' is not compatible with NDCore!")
+end
+
+function NDCore.Scaleform.ShowFreemodeMessage()
+ print("[^3WARNING^7] ESX Function 'Scaleform.ShowFreemodeMessage' is not compatible with NDCore!")
+end
+
+function NDCore.Scaleform.ShowPopupWarning()
+ print("[^3WARNING^7] ESX Function 'Scaleform.ShowPopupWarning' is not compatible with NDCore!")
+end
+
+function NDCore.Scaleform.ShowTrafficMovie()
+ print("[^3WARNING^7] ESX Function 'Scaleform.ShowTrafficMovie' is not compatible with NDCore!")
+end
+
+function NDCore.Scaleform.Utils.RequestScaleformMovie()
+ print("[^3WARNING^7] ESX Function 'Scaleform.Utils.RequestScaleformMovie' is not compatible with NDCore!")
+end
+
+function NDCore.Game.Utils.DrawText3D(coords, text, size, font)
+ local onScreen, x, y = World3dToScreen2d(coords.x, coords.y, coords.z)
+ if not onScreen then return end
+ SetTextScale(size or 0.4, size or 0.4)
+ SetTextFont(font or 4)
+ SetTextProportional(1)
+ SetTextEntry("STRING")
+ SetTextCentre(true)
+ SetTextColour(255, 255, 255, 255)
+ SetTextOutline()
+ AddTextComponentString(text)
+ DrawText(x, y)
+end
+
+function NDCore.Game.DeleteObject(object)
+ if not DoesEntityExist(object) then return end
+ DeleteEntity(object)
+end
+
+function NDCore.Game.DeleteVehicle(vehicle)
+ if not DoesEntityExist(vehicle) then return end
+ DeleteEntity(vehicle)
+end
+
+function NDCore.Game.GetClosestEntity(coords)
+ coords = coords or GetEntityCoords(cache.ped)
+ local loc = vec3(coords.x, coords.y, coords.z)
+ local entities = {lib.getClosestObject(loc), lib.getClosestPed(loc), lib.getClosestVehicle(loc)}
+ local closest = nil
+ local closestDistance = math.huge
+
+ for i = 1, #entities do
+ local ent = entities[i]
+ if DoesEntityExist(ent.object or ent.ped or ent.vehicle) then
+ local distance = #(loc-ent.coords)
+ if distance < closestDistance then
+ closest = ent
+ closestDistance = distance
+ end
+ end
+ end
+ return closest.object or closest.ped or closest.vehicle
+end
+
+function NDCore.Game.GetClosestObject(coords)
+ coords = coords or GetEntityCoords(cache.ped)
+ local loc = vec3(coords.x, coords.y, coords.z)
+ return lib.getClosestObject(loc).object
+end
+
+function NDCore.Game.GetClosestPed(coords)
+ coords = coords or GetEntityCoords(cache.ped)
+ local loc = vec3(coords.x, coords.y, coords.z)
+ return lib.getClosestPed(loc).ped
+end
+
+function NDCore.Game.GetClosestPlayer(coords)
+ coords = coords or GetEntityCoords(cache.ped)
+ local loc = vec3(coords.x, coords.y, coords.z)
+ local ply = lib.getClosestPlayer(loc)
+ return ply.playerId, #(ply.playerCoords-loc)
+end
+
+function NDCore.Game.GetClosestVehicle(coords)
+ coords = coords or GetEntityCoords(cache.ped)
+ local loc = vec3(coords.x, coords.y, coords.z)
+ return lib.getClosestVehicle(loc).vehicle
+end
+
+function NDCore.Game.GetObjects()
+ return GetGamePool("CObject")
+end
+
+function NDCore.Game.GetPedMugshot()
+ print("[^3WARNING^7] ESX Function 'Game.GetPedMugshot' is not compatible with NDCore!")
+end
+
+function NDCore.Game.GetPeds(onlyOtherPeds)
+ local peds = GetGamePool("CPed")
+ if onlyOtherPeds then
+ for i=1, #peds do
+ if peds[i] == cache.ped then
+ table.remove(peds, i)
+ end
+ end
+ end
+ return peds
+end
+
+function NDCore.Game.GetPlayers()
+ print("[^3WARNING^7] ESX Function 'Game.GetPlayers' is not compatible with NDCore!")
+end
+
+function NDCore.Game.GetPlayersInArea()
+ print("[^3WARNING^7] ESX Function 'Game.GetPlayersInArea' is not compatible with NDCore!")
+end
+
+function NDCore.Game.GetPlayersInArea()
+ print("[^3WARNING^7] ESX Function 'Game.GetPlayersInArea' is not compatible with NDCore!")
+end
+
+function NDCore.Game.GetVehicleInDirection()
+ print("[^3WARNING^7] ESX Function 'Game.GetVehicleInDirection' is not compatible with NDCore!")
+end
+
+function NDCore.Game.GetVehicleProperties(vehicle)
+ return lib.getVehicleProperties(vehicle)
+end
+
+function NDCore.Game.GetVehicles()
+ return GetGamePool("CVehicle")
+end
+
+function NDCore.Game.GetVehiclesInArea()
+ print("[^3WARNING^7] ESX Function 'Game.GetVehiclesInArea' is not compatible with NDCore!")
+end
+
+function NDCore.Game.IsSpawnPointClear()
+ print("[^3WARNING^7] ESX Function 'Game.IsSpawnPointClear' is not compatible with NDCore!")
+end
+
+function NDCore.Game.IsVehicleEmpty(vehicle)
+ for i=-1, 6 do
+ if not IsVehicleSeatFree(vehicle, i) then
+ return false
+ end
+ end
+ return true
+end
+
+function NDCore.Game.SetVehicleProperties(vehicle, props)
+ lib.setVehicleProperties(vehicle, props)
+end
+
+function NDCore.Game.SpawnLocalObject(model, coords, cb)
+ if type(model) == "string" then
+ model = GetHashKey(model)
+ end
+ local entity = CreateObject(model, coords.x, coords.y, coords.z, false, false, false)
+ entity = lib.waitFor(function()
+ if DoesEntityExist(entity) then return entity end
+ end)
+ if cb then cb(entity) end
+ return entity
+end
+
+function NDCore.Game.SpawnLocalVehicle(model, coords, heading, cb)
+ if type(model) == "string" then
+ model = GetHashKey(model)
+ end
+ local entity = CreateVehicle(model, coords.x, coords.y, coords.z, heading, false, false, false)
+ entity = lib.waitFor(function()
+ if DoesEntityExist(entity) then return entity end
+ end)
+ if cb then cb(entity) end
+ return entity
+end
+
+function NDCore.Game.SpawnObject(model, coords, cb)
+ if type(model) == "string" then
+ model = GetHashKey(model)
+ end
+ local entity = CreateObject(model, coords.x, coords.y, coords.z, true, false, false)
+ entity = lib.waitFor(function()
+ if DoesEntityExist(entity) then return entity end
+ end)
+ if cb then cb(entity) end
+ return entity
+end
+
+function NDCore.Game.SpawnVehicle(model, coords, heading, cb)
+ if type(model) == "string" then
+ model = GetHashKey(model)
+ end
+ local entity = CreateVehicle(model, coords.x, coords.y, coords.z, heading, true, false, false)
+ entity = lib.waitFor(function()
+ if DoesEntityExist(entity) then return entity end
+ end)
+ if cb then cb(entity) end
+ return entity
+end
+
+function NDCore.Game.Teleport(entity, coords, cb)
+ if DoesEntityExist(entity) then
+ RequestCollisionAtCoord(coords.x, coords.y, coords.z)
+ while not HasCollisionLoadedAroundEntity(entity) do
+ Wait(0)
+ end
+
+ SetEntityCoords(entity, coords.x, coords.y, coords.z, false, false, false, false)
+ SetEntityHeading(entity, coords.w or coords.heading or 0.0)
+ end
+
+ if cb then
+ cb()
+ end
+end
+
+AddEventHandler("ND:characterLoaded", function()
+ NDCore.GetPlayerData()
+end)
+
+AddEventHandler("ND:updateCharacter", function()
+ NDCore.GetPlayerData()
+end)
diff --git a/compatibility/esx/locale.lua b/compatibility/esx/locale.lua
new file mode 100644
index 0000000..9ec37e5
--- /dev/null
+++ b/compatibility/esx/locale.lua
@@ -0,0 +1,29 @@
+local config_locale = "en"
+Locales = {}
+
+function Translate(str, ...) -- Translate string
+ if not str then
+ print(("[^1ERROR^7] Resource ^5%s^7 You did not specify a parameter for the Translate function or the value is nil!"):format(GetInvokingResource() or GetCurrentResourceName()))
+ return 'Given translate function parameter is nil!'
+ end
+ if Locales[config_locale] then
+ if Locales[config_locale][str] then
+ return string.format(Locales[config_locale][str], ...)
+ elseif config_locale ~= 'en' and Locales['en'] and Locales['en'][str] then
+ return string.format(Locales['en'][str], ...)
+ else
+ return 'Translation [' .. config_locale .. '][' .. str .. '] does not exist'
+ end
+ elseif config_locale ~= 'en' and Locales['en'] and Locales['en'][str] then
+ return string.format(Locales['en'][str], ...)
+ else
+ return 'Locale [' .. config_locale .. '] does not exist'
+ end
+end
+
+function TranslateCap(str, ...) -- Translate string first char uppercase
+ return _(str, ...):gsub("^%l", string.upper)
+end
+
+_ = Translate
+_U = TranslateCap
diff --git a/compatibility/esx/server.lua b/compatibility/esx/server.lua
new file mode 100644
index 0000000..3c195d2
--- /dev/null
+++ b/compatibility/esx/server.lua
@@ -0,0 +1,371 @@
+if not lib.table.contains(Config.compatibility, "esx") then return end
+
+local itemNames
+local registeredItems = {}
+
+local function getAmmoFromWeapon(weapon)
+ if not weapon then return end
+ for item, data in pairs(exports.ox_inventory:Items()) do
+ if data.weapon and data.model and data.model:lower() == weapon:lower() then
+ return data.ammoname
+ end
+ end
+end
+
+local function createPlayerFunctions(self)
+ self.Accounts = {
+ Bank = self.bank,
+ Money = self.Cash,
+ Black = self.Cash
+ }
+
+ if self.jobInfo then
+ self.job = {
+ id = self.job,
+ name = self.job,
+ label = self.jobInfo.label,
+ grade = self.jobInfo.rank,
+ grade_name = self.jobInfo.rankName,
+ grade_label = self.jobInfo.rankName,
+ grade_salary = 0,
+ skin_male = {},
+ skin_female = {}
+ }
+ end
+
+ local ped = GetPlayerPed(self.source)
+ if DoesEntityExist(ped) then
+ self.coords = GetEntityCoords()
+ end
+
+ self.loadout = {}
+ self.maxWeight = 30000
+ self.money = self.Accounts.Money
+ self.sex = self.gender
+ self.firstName = self.firstname
+ self.lastName = self.lastname
+ self.dateofbirth = self.dob
+ self.height = 120
+ self.dead = self.getMetadata("dead")
+
+ function self.addAccountMoney(account, amount)
+ local amount = tonumber(amount)
+ if not amount or amount <= 0 or account ~= "bank" and account ~= "cash" then return end
+ self[account] += amount
+ if NDCore.players[self.source] then
+ self.triggerEvent("ND:updateMoney", self.cash, self.bank)
+ TriggerEvent("ND:moneyChange", self.source, account, amount, "add")
+ end
+ return true
+ end
+
+ function self.addInventoryItem(item, count)
+ exports.ox_inventory:AddItem(self.source, item, count)
+ end
+
+ function self.addMoney(amount)
+ local amount = tonumber(amount)
+ if not amount or amount <= 0 then return end
+ self["bank"] += amount
+ if NDCore.players[self.source] then
+ self.triggerEvent("ND:updateMoney", self.cash, self.bank)
+ TriggerEvent("ND:moneyChange", self.source, "bank", amount, "add", reason)
+ end
+ return true
+ end
+
+ function self.addWeaponAmmo(weaponName, ammoCount)
+ local ammoName = getAmmoFromWeapon(weaponName)
+ if not ammoName then return end
+ self.addInventoryItem(ammoName, ammoCount)
+ end
+
+ function self.addWeapon(weaponName, ammo)
+ local name = nil
+ if weaponName:find("weapon") then
+ name = weaponName
+ else
+ name = ("weapon_%s"):format(weaponName)
+ end
+ self.addInventoryItem(name, 1)
+ self.addWeaponAmmo(name, ammo)
+
+ -- local weapon = GetHashKey(name)
+ -- local ped = GetPlayerPed(self.source)
+ -- GiveWeaponToPed(ped, weapon, ammo, false, false)
+ end
+
+ function self.addWeaponComponent(_, component)
+ self.addInventoryItem(component, 1)
+ end
+
+ function self.canCarryItem(item, count)
+ return exports.ox_inventory:CanCarryItem(self.source, item, count)
+ end
+
+ function self.canSwapItem(firstItem, firstItemCount, testItem, testItemCount)
+ exports.ox_inventory:CanSwapItem(self.source, firstItem, firstItemCount, testItem, testItemCount)
+ end
+
+ function self.clearMeta(index)
+ self.setMetadata(index, nil)
+ end
+
+ function self.getMeta(index, subIndex)
+ local meta = self.getMetadata(index)
+ if type(meta) == "table" then
+ return meta[subIndex]
+ end
+ return meta
+ end
+
+ function self.getCoords(useVector)
+ local ped = GetPlayerPed(self.source)
+ local coords = GetEntityCoords(ped)
+ if useVector then
+ return coords
+ end
+ return {
+ x = coords.x,
+ y = coords.y,
+ z = coords.z
+ }
+ end
+
+ function self.getIdentifier()
+ return self.identifier
+ end
+
+ function self.getInventory(minimal)
+ local playerItems = exports.ox_inventory:GetInventoryItems(self.source)
+ if not minimal then
+ return playerItems
+ end
+ -- minimal stuff
+ end
+
+ function self.getInventoryItem(item)
+ local playerItems = exports.ox_inventory:GetInventoryItems(self.source)
+ for name, data in pairs(playerItems) do
+ if name == item then
+ return data
+ end
+ end
+ end
+
+ function self.getMoney()
+ return self.bank
+ end
+
+ function self.getName()
+ return self.fullname
+ end
+
+ function self.hasItem(item, metadata)
+ return exports.ox_inventory:GetItem(self.source, item, metadata)
+ end
+
+ function self.removeInventoryItem(item, count)
+ exports.ox_inventory:RemoveItem(self.source, item, count)
+ end
+
+ function self.removeMoney(amount)
+ self.deductMoney("bank", amount)
+ end
+
+ function setMoney(amount)
+ self.setData("bank", amount)
+ end
+
+ self.kick = self.drop
+ self.removeAccountMoney = self.deductMoney
+ self.setMeta = self.setMetadata
+ self.setAccountMoney = self.setData
+ self.setInventoryItem = self.addInventoryItem
+
+
+ -- self.getAccount(account)
+ -- self.getAccounts()
+ -- self.getGroup()
+ -- self.getJob()
+ -- self.getLoadout()
+ -- self.getMissingAccounts(cb)
+ -- self.getWeapon(weaponName)
+ -- self.getWeaponTint(weaponName, weaponTintIndex)
+ -- self.getWeight()
+ -- self.hasWeapon(weaponName)
+ -- self.hasWeaponComponent(weaponName, weaponComponent)
+ -- self.removeWeapon(weaponName)
+ -- self.removeWeaponAmmo(weaponName, ammoCount)
+ -- self.removeWeaponComponent(weaponName, weaponComponent)
+ -- self.setMaxWeight(newWeight)
+ -- self.setName(newName)
+ -- self.setWeaponTint(weaponName, weaponTintIndex)
+ -- self.showAdvancedNotification(sender, subject, msg, textureDict, iconType, flash, saveToBrief, hudColorIndex)
+ -- self.showHelpNotification(msg, thisFrame, beep, duration)
+ -- self.showNotification(msg, flash, saveToBrief, hudColorIndex)
+
+ return self
+end
+
+NDCore.OneSync = {}
+NDCore.RegisterServerCallback = lib.callback.register
+NDCore.SetTimeout = SetTimeout
+NDCore.Trace = Citizen.Trace
+
+local function exportHandler(resource, exportName, cb)
+ AddEventHandler(("__cfx_export_%s_%s"):format(resource, exportName), function(setCB)
+ setCB(cb)
+ end)
+end
+
+exportHandler("es_extended", "getSharedObject", function()
+ return NDCore
+end)
+
+function NDCore.ClearTimeout()
+ print("[^3WARNING^7] ESX Function 'ClearTimeout' is not compatible with NDCore!")
+end
+
+function NDCore.CreatePickup()
+ print("[^3WARNING^7] ESX Function 'CreatePickup' is not compatible with NDCore!")
+end
+
+function NDCore.DiscordLog()
+ print("[^3WARNING^7] ESX Function 'DiscordLog' is not compatible with NDCore!")
+end
+
+function NDCore.DiscordLogFields()
+ print("[^3WARNING^7] ESX Function 'DiscordLogFields' is not compatible with NDCore!")
+end
+
+function NDCore.RegisterUsableItem()
+ print("[^3WARNING^7] ESX Function 'RegisterUsableItem' is not compatible with NDCore!")
+end
+
+function NDCore.UseItem()
+ print("[^3WARNING^7] ESX Function 'UseItem' is not compatible with NDCore!")
+end
+
+
+function NDCore.GetPlayerFromId(src)
+ return createPlayerFunctions(NDCore.getPlayer(src))
+end
+
+function NDCore.GetExtendedPlayers(key, value)
+ local players = {}
+ if not key or not value then
+ for _, info in pairs(NDCore.players) do
+ players[#players+1] = createPlayerFunctions(info)
+ end
+ return players
+ end
+
+ local keyTypes = {id = "id", firstname = "firstname", lastname = "lastname", gender = "gender", groups = "groups"}
+ local findBy = keyTypes[key] or "metadata"
+ if findBy then
+ for _, info in pairs(NDCore.players) do
+ if findBy == "metadata" and info["metadata"][key] == value or info[findBy] == value then
+ players[#players+1] = createPlayerFunctions(info)
+ end
+ end
+ end
+ return players
+end
+
+NDCore.GetPlayers = NDCore.GetExtendedPlayers
+
+function NDCore.RegisterCommand(name, perms, cb, allowConsole, suggestion)
+ lib.addCommand(name, {
+ help = suggestion.help,
+ params = suggestion.arguments,
+ restricted = perms and ("group.%s"):format(perms)
+ }, function(source, args, raw)
+ if allowConsole and source == 0 then
+ return print("[^3WARNING^7] ^5Command Cannot be executed from console")
+ end
+ local player = NDCore.getPlayer(source)
+ cb(player, args, function(msg)
+ if source == 0 then
+ return print(("[^3WARNING^7] %s^7"):format(msg))
+ end
+ player.showNotification(msg)
+ end)
+ end)
+end
+
+function NDCore.DoesJobExist(job, grade)
+ local groupInfo = Config.groups[job]
+ if groupInfo and groupInfo.ranks[grade] then
+ return true
+ end
+end
+
+function NDCore.GetItemLabel(item)
+ if not itemNames then
+ itemNames = {}
+ for item, data in pairs(exports.ox_inventory:Items()) do
+ itemNames[item] = data.label
+ end
+ end
+ return itemNames[item]
+end
+
+function NDCore.GetJobs()
+ return Config.groups
+end
+
+function NDCore.GetPlayerFromIdentifier(identifier)
+ for _, info in pairs(NDCore.players) do
+ if info.identifier:find(identifier) then
+ return info
+ end
+ end
+end
+
+function NDCore.OneSync.SpawnObject(model, coords, heading, cb)
+ local entity = CreateObject(model, coords.x, coords.y, coords.z, true, false, false)
+ local value = lib.waitFor(function()
+ if DoesEntityExist(entity) then return entity end
+ end, "Failed to spawn object", 5000)
+ if not value then return end
+ SetEntityHeading(value, heading)
+ if not cb then return end
+ cb(value)
+end
+
+function NDCore.OneSync.SpawnPed(model, coords, heading, cb)
+ local entity = CreatePed(0, model, coords.x, coords.y, coords.z, heading, true, false)
+ local value = lib.waitFor(function()
+ if DoesEntityExist(entity) then return entity end
+ end, "Failed to spawn ped", 5000)
+ if not value or not cb then return end
+ cb(NetworkGetNetworkIdFromEntity(value))
+end
+
+function NDCore.OneSync.SpawnVehicle(model, coords, heading, properties, cb)
+ local vehicle = NDCore.createVehicle({
+ model = model,
+ coords = coords,
+ heading = heading
+ })
+ if not cb then return end
+ cb(vehicle.netId)
+end
+
+function NDCore.OneSync.SpawnPedInVehicle(model, vehicle, seat, cb)
+ local entity = CreatePedInsideVehicle(vehicle, 0, model, seat, true, false)
+ local value = lib.waitFor(function()
+ if DoesEntityExist(entity) then return entity end
+ end, "Failed to spawn ped", 5000)
+ if not value or not cb then return end
+ cb(value)
+end
+
+AddEventHandler("ND:characterUnloaded", function(src, character)
+ TriggerEvent("esx:playerDropped", src, "")
+end)
+
+AddEventHandler("ND:characterLoaded", function(character)
+ TriggerEvent("esx:playerLoaded", character.source, createPlayerFunctions(character))
+end)
diff --git a/compatibility/qb/client.lua b/compatibility/qb/client.lua
new file mode 100644
index 0000000..93be1f2
--- /dev/null
+++ b/compatibility/qb/client.lua
@@ -0,0 +1,2 @@
+if not lib.table.contains(Config.compatibility, "qb") then return end
+
diff --git a/compatibility/qb/server.lua b/compatibility/qb/server.lua
new file mode 100644
index 0000000..54e6f48
--- /dev/null
+++ b/compatibility/qb/server.lua
@@ -0,0 +1 @@
+if not lib.table.contains(Config.compatibility, "qb") then return end
diff --git a/database/characters.sql b/database/characters.sql
new file mode 100644
index 0000000..e70a660
--- /dev/null
+++ b/database/characters.sql
@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS `nd_characters` (
+ `charid` INT(10) NOT NULL AUTO_INCREMENT,
+ `identifier` VARCHAR(200) NOT NULL DEFAULT '0',
+ `name` VARCHAR(50) DEFAULT NULL,
+ `firstname` VARCHAR(50) DEFAULT NULL,
+ `lastname` VARCHAR(50) DEFAULT NULL,
+ `dob` VARCHAR(50) DEFAULT NULL,
+ `gender` VARCHAR(50) DEFAULT NULL,
+ `cash` INT(10) DEFAULT '0',
+ `bank` INT(10) DEFAULT '0',
+ `groups` LONGTEXT DEFAULT '[]',
+ `metadata` LONGTEXT DEFAULT '[]',
+ `inventory` LONGTEXT DEFAULT '[]',
+ PRIMARY KEY (`charid`) USING BTREE
+);
diff --git a/database/vehicles.sql b/database/vehicles.sql
new file mode 100644
index 0000000..c4ae6dd
--- /dev/null
+++ b/database/vehicles.sql
@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS `nd_vehicles` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `owner` INT(11) DEFAULT NULL,
+ `plate` VARCHAR(255) DEFAULT NULL,
+ `glovebox` LONGTEXT DEFAULT '[]',
+ `trunk` LONGTEXT DEFAULT '[]',
+ `properties` LONGTEXT DEFAULT '[]',
+ `stored` INT(11) DEFAULT '1',
+ `impounded` INT(11) DEFAULT '0',
+ `stolen` INT(11) DEFAULT '0',
+ `metadata` LONGTEXT DEFAULT '[]',
+ PRIMARY KEY (`id`) USING BTREE,
+ INDEX `owner` (`owner`) USING BTREE,
+ CONSTRAINT `vehowner` FOREIGN KEY (`owner`) REFERENCES `nd_characters` (`charid`) ON UPDATE CASCADE ON DELETE CASCADE
+);
diff --git a/fxmanifest.lua b/fxmanifest.lua
index 0069149..4fb30f1 100644
--- a/fxmanifest.lua
+++ b/fxmanifest.lua
@@ -1,39 +1,42 @@
-- For support join my discord: https://discord.gg/Z9Mxu72zZ6
-author "Andyyy#7666, N1K0#0001"
+author "Andyyy#7666"
description "ND Framework Core"
-version "1.0.3"
+version "2.0.0"
fx_version "cerulean"
game "gta5"
lua54 "yes"
-shared_scripts {
- "config_client.lua",
- "shared/main.lua"
-}
+shared_script "@ox_lib/init.lua"
client_scripts {
"client/main.lua",
+ "shared/functions.lua",
+ "client/peds.lua",
+ "client/vehicle/main.lua",
+ "client/vehicle/garages.lua",
"client/functions.lua",
"client/events.lua",
- "shared/import.lua"
+ "client/death.lua",
+ "compatibility/**/client.lua"
}
server_scripts {
"@oxmysql/lib/MySQL.lua",
- "config_server.lua",
"server/main.lua",
+ "shared/functions.lua",
+ "server/player.lua",
+ "server/vehicle.lua",
"server/functions.lua",
- "server/events.lua",
- "server/commands.lua",
- "shared/import.lua"
+ "compatibility/**/server.lua",
+ "server/commands.lua"
}
-exports {
- "GetCoreObject"
-}
-
-server_exports {
- "GetCoreObject"
+files {
+ "init.lua",
+ "client/vehicle/data.lua",
+ "compatibility/**/locale.lua"
}
dependency "oxmysql"
+-- provide "es_extended"
+-- provide "qb-Core"
diff --git a/init.lua b/init.lua
new file mode 100644
index 0000000..499944f
--- /dev/null
+++ b/init.lua
@@ -0,0 +1,11 @@
+local nd_core = exports["ND_Core"]
+
+NDCore = setmetatable({}, {
+ __index = function(self, index)
+ self[index] = function(...)
+ return nd_core[index](nil, ...)
+ end
+
+ return self[index]
+ end
+})
diff --git a/query.sql b/query.sql
deleted file mode 100644
index 3f2b7ac..0000000
--- a/query.sql
+++ /dev/null
@@ -1,16 +0,0 @@
-CREATE TABLE `characters` (
- `character_id` INT(10) NOT NULL AUTO_INCREMENT,
- `license` VARCHAR(200) NOT NULL DEFAULT '0',
- `first_name` VARCHAR(50) DEFAULT NULL,
- `last_name` VARCHAR(50) DEFAULT NULL,
- `dob` VARCHAR(50) DEFAULT NULL,
- `gender` VARCHAR(50) DEFAULT NULL,
- `cash` INT(10) DEFAULT '0',
- `bank` INT(10) DEFAULT '0',
- `job` VARCHAR(50) DEFAULT NULL,
- `phone_number` VARCHAR(20) DEFAULT NULL,
- `data` LONGTEXT DEFAULT '[]',
- `inventory` LONGTEXT DEFAULT '[]',
- `last_location` LONGTEXT DEFAULT '[]',
- PRIMARY KEY (`character_id`) USING BTREE
-);
\ No newline at end of file
diff --git a/server/commands.lua b/server/commands.lua
index e98e0d5..af2bf41 100644
--- a/server/commands.lua
+++ b/server/commands.lua
@@ -1,117 +1,446 @@
-NDCore.Functions.AddCommand("setmoney", "Admin command, manage player money.", function(source, args, rawCommand)
- if not NDCore.Functions.IsPlayerAdmin(source) then return end
-
- local target = tonumber(args[1])
- local action = args[2]
- local moneyType = args[3]:lower()
- local amount = tonumber(args[4])
-
- if not target or GetPlayerPing(target) == 0 then return end
- if action ~= "remove" and action ~= "add" and action ~= "set" then return end
- if moneyType ~= "bank" and moneyType ~= "cash" then return end
-
- if action == "remove" then
- if not amount or amount < 1 then return end
- NDCore.Functions.DeductMoney(amount, target, moneyType, "Server staff action.")
- elseif action == "add" then
- if not amount or amount < 1 then return end
- NDCore.Functions.AddMoney(amount, target, moneyType, "Server staff action.")
- elseif action == "set" then
- local character = NDCore.Functions.GetPlayer(target)
- NDCore.Functions.SetPlayerData(character.id, moneyType, amount, "Server staff action.")
+local moneyTypes = {"bank", "cash"}
+local moneyActions = {
+ remove = function(player, account, amount)
+ player.deductMoney(account, amount, "Staff action")
+ return ("Removed $%d (%s) to %s"):format(amount, account, player.name), ("removed $%d from %s"):format(amount, account)
+ end,
+ add = function(player, account, amount)
+ player.addMoney(account, amount, "Staff action")
+ return ("Added $%d (%s) to %s"):format(amount, account, player.name), ("added $%d to %s"):format(amount, account)
+ end,
+ set = function(player, account, amount)
+ player.setData(account, amount, "Staff action")
+ return ("Set %s's (%s) to $%d"):format(player.name, account, amount), ("set %s to $%d"):format(account, amount)
end
-end, true, {
- { name="player", help="Player server id" },
- { name="action", help="remove/add/set" },
- { name="type", help="bank/cash" },
- { name="amount" }
-})
+}
-NDCore.Functions.AddCommand("setjob", "Admin command, set player job.", function(source, args, rawCommand)
- if not NDCore.Functions.IsPlayerAdmin(source) then
- return {
- color = {255, 0, 0},
- args = {"Error", "you don't have access to this command."}
+lib.addCommand("setmoney", {
+ help = "Admin command, manage player money.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ },
+ {
+ name = "action",
+ type = "string",
+ help = "remove/add/set"
+ },
+ {
+ name = "type",
+ type = "string",
+ help = "bank/cash"
+ },
+ {
+ name = "amount",
+ type = "number"
}
+ }
+}, function(source, args, raw)
+ local action = moneyActions[args.action]
+ local moneyType = args.type:lower()
+ if not action or not lib.table.contains(moneyTypes, moneyType) then return end
+
+ local player = NDCore.getPlayer(args.target)
+ if not player then return end
+ local staffMessage, userMessage = action(player, moneyType, args.amount)
+
+ player.notify({
+ title = "Staff action",
+ description = userMessage,
+ type = "inform",
+ duration = 10000
+ })
+
+ if not source then return end
+ TriggerClientEvent("chat:addMessage", source, {
+ color = {50, 100, 235},
+ multiline = true,
+ args = {"Staff action", staffMessage}
+ })
+end)
+
+lib.addCommand("setjob", {
+ help = "Admin command, set a players job.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ },
+ {
+ name = "job",
+ type = "string",
+ help = "Job name"
+ },
+ {
+ name = "rank",
+ type = "number",
+ optional = true
+ }
+ }
+}, function(source, args, raw)
+ local player = NDCore.getPlayer(args.target)
+ if not player then return end
+
+ local job = args.job:lower()
+ local jobInfo = player.setJob(job, args.rank)
+ if not player or not jobInfo then return end
+ player.notify({
+ title = "Staff action",
+ description = ("Job updated to %s, rank %s"):format(jobInfo.label, jobInfo.rankName),
+ type = "inform",
+ duration = 10000
+ })
+
+ if not source then return end
+ TriggerClientEvent("chat:addMessage", source, {
+ color = {50, 100, 235},
+ multiline = true,
+ args = {"Staff action", "success"}
+ })
+end)
+
+lib.addCommand("setgroup", {
+ help = "Admin command, set a players group.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ },
+ {
+ name = "action",
+ type = "string",
+ help = "remove/add"
+ },
+ {
+ name = "group",
+ type = "string",
+ help = "group name"
+ },
+ {
+ name = "rank",
+ type = "number",
+ optional = true
+ }
+ }
+}, function(source, args, raw)
+ local player = NDCore.getPlayer(args.target)
+ if not player then return end
+
+ if args.action == "add" then
+ local groupInfo = player.addGroup(args.group, args.rank)
+ if not groupInfo then return end
+ player.notify({
+ title = "Staff action",
+ description = ("Added to group %s, rank %s."):format(groupInfo.label, groupInfo.rankName),
+ type = "inform",
+ duration = 10000
+ })
+ elseif args.action == "remove" then
+ local groupInfo = player.removeGroup(args.group)
+ if not groupInfo then return end
+ player.notify({
+ title = "Staff action",
+ description = ("Removed from group %s."):format(groupInfo.label),
+ type = "inform",
+ duration = 10000
+ })
+ else
+ return
+ end
+
+ if not source then return end
+ TriggerClientEvent("chat:addMessage", source, {
+ color = {50, 100, 235},
+ multiline = true,
+ args = {"Staff action", "success"}
+ })
+end)
+
+lib.addCommand("skin", {
+ help = "Admin command, set player into character clothing menu.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ }
+ }
+}, function(source, args, raw)
+ TriggerClientEvent("ND:clothingMenu", args.target)
+end)
+
+lib.addCommand("character", {
+ help = "Admin command, set player into character selection menu.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ }
+ }
+}, function(source, args, raw)
+ TriggerClientEvent("ND:characterMenu", args.target)
+end)
+
+lib.addCommand("pay", {
+ help = "give money to nearby player.",
+ params = {
+ {
+ name = "amount",
+ type = "number"
+ }
+ }
+}, function(source, args, raw)
+ if not source then return end
+ local targetPlayer
+ local pedCoords = GetEntityCoords(GetPlayerPed(source))
+ for targetId, targetInfo in pairs(NDCore.players) do
+ local targetCoords = GetEntityCoords(GetPlayerPed(targetId))
+ if #(pedCoords-targetCoords) < 2.0 and targetId ~= source then
+ targetPlayer = targetInfo
+ break
+ end
+ end
+
+ local player = NDCore.getPlayer(source)
+ if not player then return end
+ local success = player.deductMoney("cash", args.amount)
+
+ if not success then
+ return player.notify({
+ title = "Couldn't give money",
+ type = "error",
+ duration = 5000
+ })
end
- local target = tonumber(args[1])
- if not target or GetPlayerPing(target) == 0 then
- return {
- color = {255, 0, 0},
- args = {"Error", "target player not found."}
- }
- end
+ if not targetPlayer or not targetPlayer.addMoney("cash", args.amount) then return end
+ targetPlayer.notify({
+ title = "Money received",
+ description = ("Received $%d in cash"):format(args.amount),
+ type = "inform",
+ duration = 10000
+ })
+
+ player.notify({
+ title = "Money given",
+ description = ("You gave someone $%d in cash"):format(args.amount),
+ type = "inform",
+ duration = 10000
+ })
+end)
- local job = args[2]
- if not job then
- return {
- color = {255, 0, 0},
- args = {"Error", "job required."}
- }
- end
+lib.addCommand("unlock", {
+ help = "Admin force unlock vehicles",
+ restricted = "group.admin",
+}, function(source, args, raw)
+ local ped = GetPlayerPed(source)
+ local playerVeh = GetVehiclePedIsIn(ped)
+ local coords = GetEntityCoords(ped)
+ local vehicles = GetAllVehicles()
+ local maxDistance = 2.0
+ local closestVehicle
- local character = NDCore.Functions.GetPlayer(target)
- NDCore.Functions.SetPlayerJob(character.id, job, args[3])
- return {
- color = {0, 255, 0},
- args = {"Success", GetPlayerName(target) .. " job set to " .. job .. (args[3] and " rank " .. args[3] or " rank 1.")}
+ if not playerVeh or playerVeh == 0 or not DoesEntityExist(playerVeh) then
+ for i=1, #vehicles do
+ local vehicle = vehicles[i]
+ local vehicleCoords = GetEntityCoords(vehicle)
+ local distance = #(coords-vehicleCoords)
+
+ if distance < maxDistance then
+ maxDistance = distance
+ closestVehicle = vehicle
+ end
+ end
+ if not closestVehicle or not DoesEntityExist(closestVehicle) then return end
+ local state = Entity(closestVehicle).state
+ state.locked = false
+ else
+ local state = Entity(playerVeh).state
+ state.hotwired = true
+ end
+end)
+
+lib.addCommand("revive", {
+ help = "Admin command, revive player.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ }
}
-end, false, {
- { name="player", help="Player server id" },
- { name="job name" },
- { name="rank", help="This should be a number, default value is 1." }
-})
+}, function(source, args, raw)
+ local player = NDCore.getPlayer(args.target)
+ if not player then return end
+ player.revive()
+end)
-NDCore.Functions.AddCommand("setgroup", "Admin command, set player group.", function(source, args, rawCommand)
- if not NDCore.Functions.IsPlayerAdmin(source) then return end
-
- local target = tonumber(args[1])
- if not target or GetPlayerPing(target) == 0 then
- return {
- color = {255, 0, 0},
- args = {"Error", "target player not found."}
+lib.addCommand("dv", {
+ help = "Admin command, delete vehicles within the range.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "range",
+ type = "number",
+ help = "The range to select vehicles for deleteion from",
+ optional = true
}
- end
-
- local group = args[3]
- if not group then
- return {
- color = {255, 0, 0},
- args = {"Error", "group required."}
- }
- end
-
- local character = NDCore.Functions.GetPlayer(target)
- if args[2] == "remove" then
- NDCore.Functions.RemovePlayerFromGroup(character.id, group)
- return {
- color = {0, 255, 0},
- args = {"Success", GetPlayerName(target) .. " removed from " .. group}
- }
- elseif args[2] == "add" then
- local rank = args[4]
- NDCore.Functions.SetPlayerToGroup(character.id, group, rank)
- return {
- color = {0, 255, 0},
- args = {"Success", GetPlayerName(target) .. " added to " .. group .. (rank and " rank " .. rank or " rank 1.")}
- }
- end
-end, false, {
- { name="player", help="Player server id" },
- { name="action", help="remove/add"},
- { name="group", help="Group name, make sure it's correct or it won't work."},
- { name="rank", help="This should be a number, default value is 1 (not required if removing)." }
-})
-
-NDCore.Functions.AddCommand("pay", "give cash to a nearby player", function(source, args, rawCommand)
- local amount = args[1]
- if not amount or amount == 0 then return end
- NDCore.Functions.GiveCashToNearbyPlayer(source, amount)
- return {
- color = {0, 255, 0},
- args = {"Success", amount .. " paid."}
}
-end, true, {
- { name="Amount" },
-})
\ No newline at end of file
+}, function(source, args, raw)
+ local ped = GetPlayerPed(source)
+ local veh = GetVehiclePedIsIn(ped)
+ if veh and veh ~= 0 and DoesEntityExist(veh) then
+ DeleteEntity(veh)
+ return TriggerClientEvent("chat:addMessage", source, {
+ color = {50, 100, 235},
+ multiline = true,
+ args = {"Staff action", "deleted 1 vehicle"}
+ })
+ end
+
+ local count = 0
+ local coords = GetEntityCoords(ped)
+ if args.range then
+ for _, veh in ipairs(GetAllVehicles()) do
+ local vehDist = #(GetEntityCoords(veh) - coords)
+ if vehDist < args.range then
+ DeleteEntity(veh)
+ count += 1
+ end
+ end
+ return TriggerClientEvent("chat:addMessage", source, {
+ color = {50, 100, 235},
+ multiline = true,
+ args = {"Staff action", ("deleted %d vehicles"):format(count)}
+ })
+ end
+
+ local closest, dist
+ for _, veh in ipairs(GetAllVehicles()) do
+ local vehDist = #(GetEntityCoords(veh) - coords)
+ if vehDist < 5.0 and not closest or (dist and dist > vehDist) then
+ closest = veh
+ dist = vehDist
+ end
+ end
+
+ if not closest then return "no vehicle found nearby" end
+ DeleteEntity(closest)
+ TriggerClientEvent("chat:addMessage", source, {
+ color = {50, 100, 235},
+ multiline = true,
+ args = {"Staff action", "deleted 1 vehicle"}
+ })
+end)
+
+lib.addCommand("goto", {
+ help = "Admin command, teleport to a player.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ }
+ }
+}, function(source, args, raw)
+ if args.target == source then return end
+ local target = GetPlayerPed(args.target)
+ local coords = GetEntityCoords(target)
+ local ped = GetPlayerPed(source)
+ SetEntityCoords(ped, coords.x, coords.y, coords.z)
+end)
+
+lib.addCommand("bring", {
+ help = "Admin command, teleport a player to you.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ }
+ }
+}, function(source, args, raw)
+ if args.target == source then return end
+ local ped = GetPlayerPed(source)
+ local coords = GetEntityCoords(ped)
+ local targetPed = GetPlayerPed(args.target)
+ SetEntityCoords(targetPed, coords.x, coords.y, coords.z)
+end)
+
+lib.addCommand("freeze", {
+ help = "Admin command, freeze a player.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ }
+ }
+}, function(source, args, raw)
+ local ped = GetPlayerPed(args.target)
+ FreezeEntityPosition(ped, true)
+end)
+
+lib.addCommand("unfreeze", {
+ help = "Admin command, unfreeze a player.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "target",
+ type = "playerId",
+ help = "Target player's server id"
+ }
+ }
+}, function(source, args, raw)
+ local ped = GetPlayerPed(args.target)
+ FreezeEntityPosition(ped, false)
+end)
+
+lib.addCommand("vehicle", {
+ help = "Admin command, unfreeze a player.",
+ restricted = "group.admin",
+ params = {
+ {
+ name = "model",
+ type = "string",
+ help = "Name of the vehicle to spawn"
+ }
+ }
+}, function(source, args, raw)
+ local player = NDCore.getPlayer(source)
+ if not player then return end
+
+ local ped = GetPlayerPed(source)
+ local coords = GetEntityCoords(ped)
+ local heading = GetEntityHeading(ped)
+ local info = NDCore.createVehicle({
+ owner = player.id,
+ coords = coords,
+ heading = heading,
+ model = GetHashKey(args.model)
+ })
+
+ local veh = info.entity
+ for i=1, 10 do
+ if GetPedInVehicleSeat(veh, -1) ~= ped then
+ SetPedIntoVehicle(ped, veh, -1)
+ else
+ break
+ end
+ Wait(100)
+ end
+end)
diff --git a/server/functions.lua b/server/functions.lua
index e42e7c9..77dda58 100644
--- a/server/functions.lua
+++ b/server/functions.lua
@@ -1,766 +1,99 @@
--- For support join my discord: https://discord.gg/Z9Mxu72zZ6
-
--- Get an active players character data.
-function NDCore.Functions.GetPlayer(player)
- return NDCore.Players[player]
+---@param src number
+---@return table
+function NDCore.getPlayer(src)
+ return NDCore.players[src]
end
--- Get all active players character data.
-function NDCore.Functions.GetPlayers(getBy, value)
- if not getBy or not value then
- return NDCore.Players
- end
+---@param metadata string
+---@param data any
+---@return table
+function NDCore.getPlayers(key, value, returnArray)
+ if not key or not value then return NDCore.players end
+
local players = {}
+ local keyTypes = {id = "id", firstname = "firstname", lastname = "lastname", gender = "gender", groups = "groups"}
+ local findBy = keyTypes[key] or "metadata"
- if getBy == "groups" then
- for player, playerInfo in pairs(NDCore.Players) do
- if playerInfo.data.groups then
- local valueGroup = value:lower()
- for group, _ in pairs(playerInfo.data.groups) do
- if group and group:lower() == valueGroup then
- players[player] = playerInfo
- end
+ if findBy then
+ for src, info in pairs(NDCore.players) do
+ if findBy == "metadata" and info["metadata"][key] == value or info[findBy] == value then
+ if returnArray then
+ players[#players+1] = info
+ else
+ players[src] = info
end
end
end
- else
- for player, playerInfo in pairs(NDCore.Players) do
- if playerInfo[getBy] == value then
- players[player] = playerInfo
- end
- end
end
-
return players
end
-local discordErrors = {
- [400] = "improper http request",
- [401] = "Discord bot token might be missing or incorrect",
- [404] = "user might not be in server.",
- [429] = "Discord bot rate limited."
-}
--- Used to retrive the players discord server nickname, discord name and tag, and the roles.
-function NDCore.Functions.GetUserDiscordInfo(discordUserId)
- local data
- local timeout = 0
- PerformHttpRequest("https://discordapp.com/api/guilds/" .. server_config.guildId .. "/members/" .. discordUserId, function(errorCode, resultData, resultHeaders)
- if errorCode ~= 200 then
- print("Error: " .. errorCode .. " " .. discordErrors[errorCode])
- end
- local result = json.decode(resultData)
- local roles = {}
- local nickname = ""
- local tag = ""
- if result and result.roles then
- for _, roleId in pairs(result.roles) do
- roles[roleId] = roleId
- end
- if result.nick then
- nickname = result.nick
- end
- if result.user and result.user.username and result.user.discriminator then
- tag = tostring(result.user.username) .. "#" .. tostring(result.user.discriminator)
- end
- data = {
- nickname = nickname,
- discordTag = tag,
- roles = roles
- }
- return
- end
- data = {
- nickname = nickname,
- discordTag = tag,
- roles = roles
- }
- end, "GET", "", {["Content-Type"] = "application/json", ["Authorization"] = "Bot " .. server_config.discordServerToken})
- while not data do
- Wait(1000)
- timeout = timeout + 1
- if timeout > 5 then
- break
+---@param source number
+---@return table
+function NDCore.getPlayerServerInfo(source)
+ return PlayersInfo[source]
+end
+
+---@param fileLocation string|tabale
+---@return boolean
+function NDCore.loadSQL(fileLocation, resource)
+ local resourceName = resource or GetInvokingResource() or GetCurrentResourceName()
+
+ if type(fileLocation) == "string" then
+ local file = LoadResourceFile(resourceName, fileLocation)
+ if not file then return end
+ MySQL.query(file)
+ return true
+ end
+
+ for i=1, #fileLocation do
+ local file = LoadResourceFile(resourceName, fileLocation[i])
+ if file then
+ MySQL.query(file)
+ Wait(100)
end
end
+ return true
+end
+
+function NDCore.getDiscordInfo(discordUserId)
+ if not discordUserId or not Config.discordBotToken or not Config.discordGuildId then return end
+ local done = false
+ local data
+ local discordErrors = {
+ [400] = "Improper HTTP request",
+ [401] = "Discord bot token might be missing or incorrect",
+ [404] = "User might not be in the server",
+ [429] = "Discord bot rate limited"
+ }
+
+ if type(discordUserId) == "string" and discordUserId:find("discord:") then discordUserId:gsub("discord:", "") end
+
+ PerformHttpRequest(("https://discordapp.com/api/guilds/%s/members/%s"):format(Config.discordGuildId, discordUserId), function(errorCode, resultData, resultHeaders)
+ if errorCode ~= 200 then
+ done = true
+ return print(("^3Warning: %d %s"):format(errorCode, discordErrors[errorCode]))
+ end
+
+ local result = json.decode(resultData)
+ data = {
+ nickname = result.nick or result.user.username,
+ user = result.user,
+ roles = result.roles
+ }
+ done = true
+ end, "GET", "", {["Content-Type"] = "application/json", ["Authorization"] = ("Bot %s"):format(Config.discordBotToken)})
+
+ while not done do Wait(50) end
return data
end
--- Get player any identifier, available types: steam, license, xbl, ip, discord, live.
-function NDCore.Functions.GetPlayerIdentifierFromType(type, player)
- local identifierCount = GetNumPlayerIdentifiers(player)
- for count = 0, identifierCount do
- local identifier = GetPlayerIdentifier(player, count)
- if identifier and string.find(identifier, type) then
- return identifier
- end
- end
- return nil
+function NDCore.enableMultiCharacter(enable)
+ Config.multiCharacter = enable
end
--- This will return the server id and ND player data of a nearby player.
-function NDCore.Functions.GetNearbyPedToPlayer(player)
- local pedCoords = GetEntityCoords(GetPlayerPed(player))
- for targetId, targetInfo in pairs(NDCore.Players) do
- local targetCoords = GetEntityCoords(GetPlayerPed(targetId))
- if #(pedCoords - targetCoords) < 2.0 and targetId ~= player then
- return targetId, targetInfo
- end
- end
-end
-
--- update the players money on the client kinda like a refresh.
-function NDCore.Functions.UpdateMoney(player)
- local player = tonumber(player)
- local result = MySQL.query.await("SELECT cash, bank FROM characters WHERE character_id = ? LIMIT 1", {NDCore.Players[player].id})
- if result then
- local cash = result[1].cash
- local bank = result[1].bank
- NDCore.Players[player].cash = cash
- NDCore.Players[player].bank = bank
- TriggerClientEvent("ND:updateMoney", player, cash, bank)
+for name, func in pairs(NDCore) do
+ if type(func) == "function" then
+ exports(name, func)
end
end
-
--- Transfer money from one players bank account to another.
-function NDCore.Functions.TransferBank(amount, player, target, descriptionSender, descriptionReceiver)
- local amount = tonumber(amount)
- local player = tonumber(player)
- local target = tonumber(target)
- if player == target then
- TriggerClientEvent("chat:addMessage", player, {
- color = {255, 0, 0},
- args = {"Error", "You can't send money to yourself."}
- })
- return false
- elseif GetPlayerPing(target) == 0 then
- TriggerClientEvent("chat:addMessage", player, {
- color = {255, 0, 0},
- args = {"Error", "That player does not exist."}
- })
- return false
- elseif amount <= 0 then
- TriggerClientEvent("chat:addMessage", player, {
- color = {255, 0, 0},
- args = {"Error", "You can't send that amount."}
- })
- return false
- elseif NDCore.Players[player].bank < amount then
- TriggerClientEvent("chat:addMessage", player, {
- color = {255, 0, 0},
- args = {"Error", "You don't have enough money."}
- })
- return false
- else
- MySQL.query.await("UPDATE characters SET bank = bank - ? WHERE character_id = ?", {amount, NDCore.Players[player].id})
- NDCore.Functions.UpdateMoney(player)
- TriggerEvent("ND:moneyChange", player, "bank", amount, "remove", descriptionSender or "Transfer")
- TriggerClientEvent("chat:addMessage", player, {
- color = {0, 255, 0},
- args = {"Success", "You paid " .. NDCore.Players[target].firstName .. " " .. NDCore.Players[target].lastName .. " $" .. amount .. "."}
- })
-
- MySQL.query.await("UPDATE characters SET bank = bank + ? WHERE character_id = ?", {amount, NDCore.Players[target].id})
- NDCore.Functions.UpdateMoney(target)
- TriggerEvent("ND:moneyChange", target, "bank", amount, "add", descriptionReceiver or "Transfer")
- TriggerClientEvent("chat:addMessage", target, {
- color = {0, 255, 0},
- args = {"Success", NDCore.Players[player].firstName .. " " .. NDCore.Players[player].lastName .. " sent you $" .. amount .. "."}
- })
- return true
- end
-end
-
--- Give cash from one players wallet to another.
-function NDCore.Functions.GiveCash(amount, player, target)
- local amount = tonumber(amount)
- local player = tonumber(player)
- local target = tonumber(target)
- if player == target then
- TriggerClientEvent("chat:addMessage", player, {
- color = {255, 0, 0},
- args = {"Error", "You can't give money to yourself."}
- })
- return false
- elseif GetPlayerPing(target) == 0 then
- TriggerClientEvent("chat:addMessage", player, {
- color = {255, 0, 0},
- args = {"Error", "That player does not exist."}
- })
- return false
- elseif amount <= 0 then
- TriggerClientEvent("chat:addMessage", player, {
- color = {255, 0, 0},
- args = {"Error", "You can't give that amount."}
- })
- return false
- elseif NDCore.Players[player].cash < amount then
- TriggerClientEvent("chat:addMessage", player, {
- color = {255, 0, 0},
- args = {"Error", "You don't have enough money."}
- })
- return false
- else
- MySQL.query.await("UPDATE characters SET cash = cash - ? WHERE character_id = ?", {amount, NDCore.Players[player].id})
- NDCore.Functions.UpdateMoney(player)
- TriggerEvent("ND:moneyChange", player, "cash", amount, "remove")
- TriggerClientEvent("chat:addMessage", player, {
- color = {0, 255, 0},
- args = {"Success", "You gave " .. NDCore.Players[target].firstName .. " " .. NDCore.Players[target].lastName .. " $" .. amount .. "."}
- })
-
- MySQL.query.await("UPDATE characters SET cash = cash + ? WHERE character_id = ?", {amount, NDCore.Players[target].id})
- NDCore.Functions.UpdateMoney(target)
- TriggerEvent("ND:moneyChange", target, "cash", amount, "add")
- TriggerClientEvent("chat:addMessage", target, {
- color = {0, 255, 0},
- args = {"Success", " Received $" .. amount .. "."}
- })
- return true
- end
-end
-
--- Give money from a players wallet to a nearby player.
-function NDCore.Functions.GiveCashToNearbyPlayer(player, amount)
- local targetId = NDCore.Functions.GetNearbyPedToPlayer(player)
- if targetId then
- NDCore.Functions.GiveCash(amount, player, targetId)
- return true
- end
- TriggerClientEvent("chat:addMessage", player, {
- color = {255, 0, 0},
- args = {"Error", "No players nearby."}
- })
- return false
-end
-
--- withdraws money from a players bank account to their wallet/cash.
-function NDCore.Functions.WithdrawMoney(amount, player)
- local amount = tonumber(amount)
- local player = tonumber(player)
- if amount <= 0 then return false end
- if NDCore.Players[player].bank < amount then return false end
- MySQL.query.await("UPDATE characters SET bank = bank - ? WHERE character_id = ? LIMIT 1", {amount, NDCore.Players[player].id})
- MySQL.query.await("UPDATE characters SET cash = cash + ? WHERE character_id = ? LIMIT 1", {amount, NDCore.Players[player].id})
- NDCore.Functions.UpdateMoney(player)
- TriggerEvent("ND:moneyChange", player, "bank", amount, "remove", "Withdraw")
- TriggerEvent("ND:moneyChange", player, "cash", amount, "add", "Withdraw")
- return true
-end
-
--- deposits money from a players wallet/cash to their bank account.
-function NDCore.Functions.DepositMoney(amount, player)
- local amount = tonumber(amount)
- local player = tonumber(player)
- if amount <= 0 then return false end
- if NDCore.Players[player].cash < amount then return false end
- MySQL.query.await("UPDATE characters SET cash = cash - ? WHERE character_id = ? LIMIT 1", {amount, NDCore.Players[player].id})
- MySQL.query.await("UPDATE characters SET bank = bank + ? WHERE character_id = ? LIMIT 1", {amount, NDCore.Players[player].id})
- NDCore.Functions.UpdateMoney(player)
- TriggerEvent("ND:moneyChange", player, "cash", amount, "remove", "Deposit")
- TriggerEvent("ND:moneyChange", player, "bank", amount, "add", "Deposit")
- return true
-end
-
--- Deducts money from the player, "bank" or "cash" needs to be specified.
-function NDCore.Functions.DeductMoney(amount, player, from, description)
- local amount = tonumber(amount)
- local player = tonumber(player)
- if from == "bank" then
- MySQL.query.await("UPDATE characters SET bank = bank - ? WHERE character_id = ? LIMIT 1", {amount, NDCore.Players[player].id})
- elseif from == "cash" then
- MySQL.query.await("UPDATE characters SET cash = cash - ? WHERE character_id = ? LIMIT 1", {amount, NDCore.Players[player].id})
- end
- NDCore.Functions.UpdateMoney(player)
- TriggerEvent("ND:moneyChange", player, from, amount, "remove", description)
-end
-
--- Adds money from the player, "bank" or "cash" needs to be specified.
-function NDCore.Functions.AddMoney(amount, player, to, description)
- local amount = tonumber(amount)
- local player = tonumber(player)
- if to == "bank" then
- MySQL.query.await("UPDATE characters SET bank = bank + ? WHERE character_id = ? LIMIT 1", {amount, NDCore.Players[player].id})
- elseif to == "cash" then
- MySQL.query.await("UPDATE characters SET cash = cash + ? WHERE character_id = ? LIMIT 1", {amount, NDCore.Players[player].id})
- end
- NDCore.Functions.UpdateMoney(player)
- TriggerEvent("ND:moneyChange", player, to, amount, "add", description)
-end
-
--- Adds the players character to the NDCore.Players table, this table consists of every players selected character.
-function NDCore.Functions.SetActiveCharacter(player, characterId)
- if NDCore.Players[player] then
- TriggerEvent("ND:characterUnloaded", player, NDCore.Players[player])
- end
- local result = MySQL.query.await("SELECT * FROM characters WHERE character_id = ? LIMIT 1", {characterId})
- if result then
- local i = result[1]
- NDCore.Players[player] = {
- source = player,
- id = characterId,
- firstName = i.first_name,
- lastName = i.last_name,
- dob = i.dob,
- gender = i.gender,
- cash = i.cash,
- bank = i.bank,
- phoneNumber = i.phone_number,
- lastLocation = json.decode(i.last_location),
- inventory = json.decode(i.inventory),
- discordInfo = NDCore.PlayersDiscordInfo[player],
- data = json.decode(i.data),
- job = i.job
- }
- end
- NDCore.Functions.RefreshCommands(player)
- TriggerEvent("ND:characterLoaded", NDCore.Players[player])
- TriggerClientEvent("ND:setCharacter", player, NDCore.Players[player])
-end
-
--- This returns all the characters the player has.
-function NDCore.Functions.GetPlayerCharacters(player)
- local characters = {}
- local result = MySQL.query.await("SELECT * FROM characters WHERE license = ?", {NDCore.Functions.GetPlayerIdentifierFromType("license", player)})
- for i = 1, #result do
- local temp = result[i]
- characters[temp.character_id] = {
- id = temp.character_id,
- firstName = temp.first_name,
- lastName = temp.last_name,
- dob = temp.dob,
- gender = temp.gender,
- cash = temp.cash,
- bank = temp.bank,
- phoneNumber = temp.phone_number,
- lastLocation = json.decode(temp.last_location),
- inventory = json.decode(temp.inventory),
- discordInfo = NDCore.PlayersDiscordInfo[player],
- data = json.decode(temp.data),
- job = temp.job
- }
- end
- return characters
-end
-
--- Creates a new character for the player and returns all their characters to the client.
-function NDCore.Functions.CreateCharacter(player, firstName, lastName, dob, gender, cb)
- local characterId = false
- local license = NDCore.Functions.GetPlayerIdentifierFromType("license", player)
- local result = MySQL.query.await("SELECT character_id FROM characters WHERE license = ?", {license})
- if result and config.characterLimit > #result then
- characterId = MySQL.insert.await("INSERT INTO characters (license, first_name, last_name, dob, gender, cash, bank, data) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", {license, firstName, lastName, dob, gender, config.startingCash, config.startingBank, json.encode({groups={}})})
- if cb then cb(characterId) end
- TriggerClientEvent("ND:returnCharacters", player, NDCore.Functions.GetPlayerCharacters(player))
- end
- return characterId
-end
-
--- Update/edit a character info by character id.
-function NDCore.Functions.UpdateCharacter(characterId, firstName, lastName, dob, gender)
- local result = MySQL.query.await("UPDATE characters SET first_name = ?, last_name = ?, dob = ?, gender = ? WHERE character_id = ? LIMIT 1", {firstName, lastName, dob, gender, characterId})
- return result
-end
-
--- Delete a character by character id.
-function NDCore.Functions.DeleteCharacter(characterId)
- local result = MySQL.query.await("DELETE FROM characters WHERE character_id = ? LIMIT 1", {characterId})
- return result
-end
-
--- Updates the player's data
-function NDCore.Functions.SetPlayerData(characterId, key, value, description)
- if not key then return end
-
- local player = nil
- for id, character in pairs(NDCore.Players) do
- if character.id == characterId then
- player = id
- break
- end
- end
-
- if key == "cash" then
- if player then
- NDCore.Players[player][key] = value
- TriggerEvent("ND:moneyChange", player, "cash", tonumber(value), "set", description)
- end
- MySQL.query.await("UPDATE characters SET cash = ? WHERE character_id = ?", {tonumber(value), characterId})
- elseif key == "bank" then
- if player then
- NDCore.Players[player][key] = value
- TriggerEvent("ND:moneyChange", player, "bank", tonumber(value), "set", description)
- end
- MySQL.query.await("UPDATE characters SET bank = ? WHERE character_id = ?", {tonumber(value), characterId})
- elseif key == "job" then
- if player then
- NDCore.Players[player].job = value
- end
- MySQL.query.await("UPDATE characters SET job = ? WHERE character_id = ?", {value, characterId})
- else
- if player then
- NDCore.Players[player].data[key] = value
- MySQL.query.await("UPDATE characters SET `data` = ? WHERE character_id = ?", {json.encode(NDCore.Players[player].data), characterId})
- else
- local result = MySQL.query.await("SELECT `data` FROM characters WHERE character_id = ?", {characterId})
- if not result or not result[1] then return end
-
- local data = json.decode(result[1].data)
- data[key] = value
- MySQL.query.await("UPDATE characters SET `data` = ? WHERE character_id = ?", {json.encode(data), characterId})
- end
- end
-
- if not player then return end
- TriggerClientEvent("ND:updateCharacter", player, NDCore.Players[player])
-end
-
--- Get a character by the character id.
-function NDCore.Functions.GetPlayerByCharacterId(id)
- for _, character in pairs(NDCore.Players) do
- if character.id == id then
- return character
- end
- end
-end
-
--- Generate a random string with letters and numbers.
-function randomString(length)
- local number = {}
- for i = 1, length do
- number[i] = math.random(0, 1) == 1 and string.char(math.random(65, 90)) or math.random(0, 9)
- end
- return table.concat(number)
-end
-
--- Give a player a license.
-function NDCore.Functions.CreatePlayerLicense(characterId, licenseType, expire)
- local expireIn = tonumber(expire)
- if not expireIn then
- expireIn = 2592000
- end
-
- local time = os.time()
- local license = {
- type = licenseType,
- status = "valid",
- issued = time,
- expires = time+expireIn,
- identifier = randomString(16)
- }
-
- local character = NDCore.Functions.GetPlayerByCharacterId(characterId)
- if character then
- local data = character.data
- if not data.licences then
- data.licences = {}
- end
- character.data.licences[#character.data.licences+1] = license
- NDCore.Functions.SetPlayerData(character.id, "licences", character.data.licences)
- return true
- end
-
- local result = MySQL.query.await("SELECT data FROM characters WHERE character_id = ?", {characterId})
- if result and result[1] then
- local data = result[1].data
- if not data.licences then
- data.licences = {}
- end
- data.licences[#data.licences+1] = license
- NDCore.Functions.SetPlayerData(character.id, "licences", data.licences)
- return true
- end
-end
-
--- find a players license by it's identifier.
-function NDCore.Functions.FindLicenseByIdentifier(licences, identifier)
- for key, license in pairs(licences) do
- if license.identifier == identifier then
- return license
- end
- end
- return {}
-end
-
--- Edit a license by the license identifier.
-function NDCore.Functions.EditPlayerLicense(characterId, identifier, newData)
- local licences = {}
- local character = NDCore.Functions.GetPlayerByCharacterId(characterId)
- if character then
- licences = character.data.licences
- else
- local result = MySQL.query.await("SELECT data FROM characters WHERE character_id = ?", {characterId})
- if result and result[1] then
- local data = result[1].data
- if not data.licences then
- data.licences = {}
- end
- licences = data.licences
- end
- end
-
- local license = NDCore.Functions.FindLicenseByIdentifier(licences, identifier)
- for k, v in pairs(newData) do
- license[k] = v
- end
- NDCore.Functions.SetPlayerData(characterId, "licences", licences)
- return licences
-end
-
--- Set the players job and job rank.
-function NDCore.Functions.SetPlayerJob(characterId, job, rank)
- if not job then return end
-
- local jobRank = tonumber(rank)
- if not jobRank then
- jobRank = 1
- end
-
- local result = MySQL.query.await("SELECT job FROM characters WHERE character_id = ?", {characterId})
- if result and result[1] then
- local character = NDCore.Functions.GetPlayerByCharacterId(characterId)
- if character then
- local oldRank = 1
- if character.data.groups and character.data.groups[character.job] then
- oldRank = character.data.groups[character.job].rank
- end
- TriggerEvent("ND:jobChanged", character.source, {name = job, rank = jobRank}, {name = character.job, rank = oldRank})
- TriggerClientEvent("ND:jobChanged", character.source, {name = job, rank = jobRank}, {name = character.job, rank = oldRank})
- end
- NDCore.Functions.RemovePlayerFromGroup(characterId, result[1].job)
- end
-
- NDCore.Functions.SetPlayerData(characterId, "job", job)
- NDCore.Functions.SetPlayerToGroup(characterId, job, jobRank)
-end
-
--- Set a player to a group.
-function NDCore.Functions.SetPlayerToGroup(characterId, group, rank)
- local groupRank = tonumber(rank)
- if not groupRank then
- groupRank = 1
- end
-
- local group = group:lower()
- for groupName, groupRanks in pairs(config.groups) do
- if groupName:lower() == group then
- group = groupName
- break
- end
- end
-
- local character = NDCore.Functions.GetPlayerByCharacterId(characterId)
- if character then
- local data = character.data
- if not data.groups then
- data.groups = {}
- end
- local rankName = tostring(groupRank)
- if config.groups[group] and config.groups[group][groupRank] then
- rankName = config.groups[group][groupRank]
- end
- data.groups[group] = {
- rank = groupRank,
- rankName = rankName
- }
- NDCore.Functions.SetPlayerData(characterId, "groups", data.groups)
- return true
- end
-
- local result = MySQL.query.await("SELECT data FROM characters WHERE character_id = ?", {characterId})
- if not result or not result[1] then return end
- local data = json.decode(result[1].data)
- if not data then
- data = {}
- end
- if not data.groups then
- data.groups = {}
- end
- local rankName = tostring(groupRank)
- if config.groups[group] and config.groups[group][groupRank] then
- rankName = config.groups[group][groupRank]
- end
- data.groups[group] = {
- rank = groupRank,
- rankName = rankName
- }
- NDCore.Functions.SetPlayerData(characterId, "groups", data.groups)
- return true
-end
-
--- Remove a player from a group.
-function NDCore.Functions.RemovePlayerFromGroup(characterId, group)
- if not group then return end
- local group = group:lower()
- for groupName, groupRanks in pairs(config.groups) do
- if groupName:lower() == group then
- group = groupName
- break
- end
- end
-
- local character = NDCore.Functions.GetPlayerByCharacterId(characterId)
- if character then
- local data = character.data
- if not data.groups then
- data.groups = {}
- end
- data.groups[group] = nil
- NDCore.Functions.SetPlayerData(characterId, "groups", data.groups)
- return true
- end
-
- local result = MySQL.query.await("SELECT data FROM characters WHERE character_id = ?", {characterId})
- if result and result[1] then
- local data = result[1].data
- if not data.groups then
- data.groups = {}
- end
- data.groups[group] = nil
- NDCore.Functions.SetPlayerData(characterId, "groups", data.groups)
- return true
- end
-end
-
--- Update the characters last location into the database.
-function NDCore.Functions.UpdateLastLocation(characterId, location)
- local result = MySQL.query.await("UPDATE characters SET last_location = ? WHERE character_id = ? LIMIT 1", {json.encode(location), characterId})
- return result
-end
-
-function NDCore.Functions.AddCommand(name, help, callback, argsrequired, arguments)
- local commandName = name:lower()
- if NDCore.Commands[commandName] then print("/" .. commandName .. " has already been registered.") return end
-
- local arguments = arguments or {}
-
- RegisterCommand(commandName, function(source, args, rawCommand)
- if argsrequired and #args < #arguments then
- return TriggerClientEvent("chat:addMessage", source, {
- color = {255, 0, 0},
- multiline = true,
- args = {"Error", "all arguments required."}
- })
- end
- local message = callback(source, args, rawCommand)
- if not message then return end
- TriggerClientEvent("chat:addMessage", source, message)
- end, false)
-
- NDCore.Commands[commandName] = {
- name = commandName,
- help = help,
- callback = callback,
- argsrequired = argsrequired,
- arguments = arguments
- }
-end
-
-function NDCore.Functions.RefreshCommands(source)
- local suggestions = {}
- for command, info in pairs(NDCore.Commands) do
- suggestions[#suggestions + 1] = {
- name = "/" .. command,
- help = info.help,
- params = info.arguments
- }
- end
- TriggerClientEvent("chat:addSuggestions", source, suggestions)
-end
-
-function NDCore.Functions.IsPlayerAdmin(src)
- local discordInfo = NDCore.PlayersDiscordInfo[src]
- if not discordInfo or not discordInfo.roles then return end
- for _, adminRole in pairs(config.adminRoles) do
- for _, role in pairs(discordInfo.roles) do
- if role == adminRole then return true end
- end
- end
-end
-
-function NDCore.Functions.VersionChecker(expectedResourceName, resourceName, downloadLink, rawGithubLink)
- if expectedResourceName ~= resourceName then
- print(("^4%s ^1WARNING^0"):format(expectedResourceName))
- print(("Change the resource name to ^4%s ^0or else it won't work properly!"):format(expectedResourceName))
- StopResource(resourceName)
- return
- end
- PerformHttpRequest(rawGithubLink, function(errorCode, resultData, resultHeaders)
- local i, j = tostring(resultData):find("version")
- if not i or not j then return end
- local resultData = tostring(resultData):sub(i, j + 12)
- local resultData = resultData:gsub("version \"", "")
- local i, j = resultData:find("\"")
- local resultData = resultData:sub(1, i - 1)
- local githubVersion = resultData:gsub("%.", "")
- local fileVersion = GetResourceMetadata(expectedResourceName, "version", 0):gsub("%.", "")
-
- if not githubVersion and not fileVersion then
- print(("^4%s ^1WARNING^0"):format(expectedResourceName))
- print(("You may not have the latest version of ^4%s^0. A newer, improved version may be present at ^5%s^0"):format(expectedResourceName, downloadLink))
- elseif githubVersion > fileVersion then
- local oldVersion = ("%s.%s.%s"):format(fileVersion:sub(1, 1), fileVersion:sub(2, 2), fileVersion:sub(3, 3))
- local newVersion = ("%s.%s.%s"):format(githubVersion:sub(1, 1), githubVersion:sub(2, 2), githubVersion:sub(3, 3))
- print(("^4%s ^1WARNING^0"):format(expectedResourceName))
- print(("^4%s ^0is outdated. Please update it from ^5%s ^0| Current Version: ^1%s ^0| New Version: ^2%s ^0|"):format(expectedResourceName, downloadLink, oldVersion, newVersion))
- elseif githubVersion < fileVersion then
- local oldVersion = ("%s.%s.%s"):format(fileVersion:sub(1, 1), fileVersion:sub(2, 2), fileVersion:sub(3, 3))
- local newVersion = ("%s.%s.%s"):format(githubVersion:sub(1, 1), githubVersion:sub(2, 2), githubVersion:sub(3, 3))
- print(("^4%s ^1WARNING^0"):format(expectedResourceName))
- print(("^4%s ^0version number is higher than expected | Current Version: ^3%s ^0| Expected Version: ^2%s ^0|"):format(expectedResourceName, oldVersion, newVersion))
- else
- local newVersion = ("%s.%s.%s"):format(githubVersion:sub(1, 1), githubVersion:sub(2, 2), githubVersion:sub(3, 3))
- print(("^4%s ^0is up to date | Current Version: ^2%s ^0|"):format(expectedResourceName, newVersion))
- end
- end)
-end
-NDCore.Functions.VersionChecker("ND_Core", GetCurrentResourceName(), "https://github.com/ND-Framework/ND_Core", "https://raw.githubusercontent.com/ND-Framework/ND_Core/main/fxmanifest.lua")
-
-
--- Callbacks are licensed under LGPL v3.0
---
-NDCore.callback = {}
-local events = {}
-
-RegisterNetEvent("ND:callbacks", function(key, ...)
- local cb = events[key]
- return cb and cb(...)
-end)
-
-function triggerCallback(_, name, playerId, cb, ...)
- local key = ("%s:%s:%s"):format(name, math.random(0, 100000), playerId)
- TriggerClientEvent(("ND:%s_cb"):format(name), playerId, key, ...)
-
- local promise = not cb and promise.new()
-
- events[key] = function(response, ...)
- response = { response, ... }
- events[key] = nil
-
- if promise then
- return promise:resolve(response)
- end
-
- if cb then
- cb(table.unpack(response))
- end
- end
-
- if promise then
- return table.unpack(Citizen.Await(promise))
- end
-end
-
-setmetatable(NDCore.callback, {
- __call = triggerCallback
-})
-
-function NDCore.callback.await(name, playerId, ...)
- return triggerCallback(nil, name, playerId, false, ...)
-end
-
-function NDCore.callback.register(name, callback)
- RegisterNetEvent(("ND:%s_cb"):format(name), function(key, ...)
- local src = source
- TriggerClientEvent("ND:callbacks", src, key, callback(src, ...))
- end)
-end
diff --git a/server/main.lua b/server/main.lua
index f84ab27..fd9fa68 100644
--- a/server/main.lua
+++ b/server/main.lua
@@ -1,21 +1,154 @@
--- For support join my discord: https://discord.gg/Z9Mxu72zZ6
-
NDCore = {}
-NDCore.Players = {}
-NDCore.Functions = {}
-NDCore.Commands = {}
-NDCore.PlayersDiscordInfo = {}
-NDCore.Config = config
+NDCore.players = {}
+PlayersInfo = {}
+local resourceName = GetCurrentResourceName()
+local tempPlayersInfo = {}
-function GetCoreObject()
- return NDCore
+Config = {
+ serverName = GetConvar("core:serverName", "Unconfigured ND-Core Server"),
+ discordInvite = GetConvar("core:discordInvite", "https://discord.gg/Z9Mxu72zZ6"),
+ discordAppId = GetConvar("core:discordAppId", "858146067018416128"),
+ discordAsset = GetConvar("core:discordAsset", "andyyy"),
+ discordAssetSmall = GetConvar("core:discordAssetSmall", "andyyy"),
+ discordActionText = GetConvar("core:discordActionText", "DISCORD"),
+ discordActionLink = GetConvar("discordActionLink", "https://discord.gg/Z9Mxu72zZ6"),
+ discordActionText2 = GetConvar("core:discordActionText2", "STORE"),
+ discordActionLink2 = GetConvar("core:discordActionLink2", "https://andyyy.tebex.io/category/fivem-scripts"),
+ characterIdentifier = GetConvar("core:characterIdentifier", "license"),
+ discordGuildId = GetConvar("core:discordGuildId", "false"),
+ discordBotToken = GetConvar("core:discordBotToken", "false"),
+ randomUnlockedVehicleChance = GetConvarInt("core:randomUnlockedVehicleChance", 30),
+ disableVehicleAirControl = GetConvarInt("core:disableVehicleAirControl", 1) == 1,
+ useInventoryForKeys = GetConvarInt("core:useInventoryForKeys", 1) == 1,
+ groups = json.decode(GetConvar("core:groups", "[]")),
+ admins = json.decode(GetConvar("core:admins", "[]")),
+ adminDiscordRoles = json.decode(GetConvar("core:adminDiscordRoles", "[]")),
+ multiCharacter = false,
+ compatibility = json.decode(GetConvar("core:compatibility", "[]"))
+}
+
+SetConvarServerInfo("Discord", Config.discordInvite)
+SetConvarServerInfo("NDCore", GetResourceMetadata(resourceName, "version", 0) or "invalid")
+SetConvarReplicated("inventory:framework", "nd")
+
+local function getIdentifierList(src)
+ local list = {}
+ for i=0, GetNumPlayerIdentifiers(src) do
+ local identifier = GetPlayerIdentifier(src, i)
+ if identifier then
+ local colon = identifier:find(":")
+ local identifierType = identifier:sub(1, colon-1)
+ list[identifierType] = identifier
+ end
+ end
+ return list
end
-isResourceStarted("ox_inventory", function(started)
- if not started then return end
- SetConvarReplicated("inventory:framework", "nd")
+AddEventHandler("playerJoining", function(oldId)
+ local src = source
+ PlayersInfo[src] = tempPlayersInfo[oldId]
+ tempPlayersInfo[oldId] = nil
+
+ if Config.multiCharacter then return end
+ Wait(3000)
+
+ local characters = NDCore.fetchAllCharacters(src)
+ local id = next(characters)
+ if id then
+ return NDCore.setActiveCharacter(src, id)
+ end
+
+ local player = NDCore.newCharacter(src, {
+ firstname = GetPlayerName(src),
+ lastname = "",
+ dob = "",
+ gender = ""
+ })
+ NDCore.setActiveCharacter(src, player.id)
end)
-for _, roleid in pairs(config.adminRoles) do
- ExecuteCommand("add_principal identifier.discord:" .. roleid .. " group.admin")
-end
\ No newline at end of file
+local function checkDiscordIdentifier(identifiers)
+ if Config.discordBotToken == "false" or Config.discordGuildId == "false" then return end
+
+ local discordIdentifier = identifiers["discord"]
+ if not discordIdentifier then return end
+
+ return NDCore.getDiscordInfo(discordIdentifier:gsub("discord:", ""))
+end
+
+AddEventHandler("onResourceStart", function(name)
+ if name ~= resourceName then return end
+ for _, playerId in ipairs(GetPlayers()) do
+ local src = tonumber(playerId)
+ local identifiers = getIdentifierList(src)
+ PlayersInfo[src] = {
+ identifiers = identifiers,
+ discord = checkDiscordIdentifier(identifiers) or {}
+ }
+ Wait(65)
+ end
+end)
+
+AddEventHandler("playerConnecting", function(name, setKickReason, deferrals)
+ local tempSrc = source
+ local identifiers = getIdentifierList(tempSrc)
+ local mainIdentifier = identifiers[Config.characterIdentifier]
+ local discordInfo = {}
+
+ deferrals.defer()
+ Wait(0)
+
+ if mainIdentifier and Config.discordBotToken ~= "false" and Config.discordGuildId ~= "false" and not discordInfo then
+ discordInfo = checkDiscordIdentifier(identifiers)
+ if not discordInfo then
+ deferrals.done(("Your discord was not found, join our discord here: %s."):format(Config.discordInvite))
+ Wait(0)
+ end
+ end
+
+ deferrals.update("Connecting...")
+ Wait(0)
+
+ if mainIdentifier then
+ tempPlayersInfo[tempSrc] = {
+ identifiers = identifiers,
+ discord = discordInfo
+ }
+ deferrals.done()
+ else
+ deferrals.done(("Your %s was not found."):format(Config.characterIdentifier))
+ Wait(0)
+ end
+end)
+
+AddEventHandler("playerDropped", function()
+ local src = source
+ local char = NDCore.players[src]
+ if char then char.unload() end
+ PlayersInfo[src] = nil
+end)
+
+AddEventHandler("onResourceStop", function(name)
+ if name ~= resourceName then return end
+ for _, player in pairs(NDCore.players) do
+ player.unload()
+ Wait(10)
+ end
+end)
+
+SetTimeout(500, function()
+ NDCore.loadSQL({
+ "database/characters.sql",
+ "database/vehicles.sql"
+ }, resourceName)
+end)
+
+RegisterNetEvent("ND:playerEliminated", function(info)
+ local src = source
+ local player = NDCore.getPlayer(src)
+ if not player then return end
+ player.setMetadata({
+ dead = true,
+ deathInfo = info
+ })
+end)
diff --git a/server/player.lua b/server/player.lua
new file mode 100644
index 0000000..410ffd9
--- /dev/null
+++ b/server/player.lua
@@ -0,0 +1,486 @@
+local function removeCharacterFunctions(character)
+ local newData = {}
+ for k, v in pairs(character) do
+ if type(v) ~= "function" then
+ newData[k] = v
+ end
+ end
+ return newData
+end
+
+local function createCharacterTable(info)
+ local playerInfo = PlayersInfo[info.source] or {}
+
+ local self = {
+ id = info.id,
+ source = info.source,
+ identifier = info.identifier,
+ identifiers = playerInfo.identifiers or {},
+ discord = playerInfo.discord or {},
+ name = info.name,
+ firstname = info.firstname,
+ lastname = info.lastname,
+ fullname = ("%s %s"):format(info.firstname, info.lastname),
+ dob = info.dob,
+ gender = info.gender,
+ cash = info.cash,
+ bank = info.bank,
+ groups = info.groups,
+ metadata = info.metadata,
+ inventory = info.inventory
+ }
+
+ ---@param account string
+ ---@param amount number
+ ---@param reason string|nil
+ ---@return boolean
+ function self.deductMoney(account, amount, reason)
+ local amount = tonumber(amount)
+ if not amount or amount <= 0 or account ~= "bank" and account ~= "cash" then return end
+ self[account] -= amount
+ if NDCore.players[self.source] then
+ self.triggerEvent("ND:updateMoney", self.cash, self.bank)
+ TriggerEvent("ND:moneyChange", self.source, account, amount, "remove", reason)
+ end
+ return true
+ end
+
+ ---@param account string
+ ---@param amount number
+ ---@param reason string|nil
+ ---@return boolean
+ function self.addMoney(account, amount, reason)
+ local amount = tonumber(amount)
+ if not amount or amount <= 0 or account ~= "bank" and account ~= "cash" then return end
+ self[account] += amount
+ if NDCore.players[self.source] then
+ self.triggerEvent("ND:updateMoney", self.cash, self.bank)
+ TriggerEvent("ND:moneyChange", self.source, account, amount, "add", reason)
+ end
+ return true
+ end
+
+ ---@param amount number
+ ---@return boolean
+ function self.depositMoney(amount)
+ local amount = tonumber(amount)
+ if not amount or self.cash < amount or amount <= 0 then return end
+ return self.deductMoney("cash", amount, "Deposit") and self.addMoney("bank", amount, "Deposit")
+ end
+
+ ---@param amount number
+ ---@return boolean
+ function self.withdrawMoney(amount)
+ local amount = tonumber(amount)
+ if not amount or self.bank < amount or amount <= 0 then return end
+ return self.deductMoney("bank", amount, "Withdraw") and self.addMoney("cash", amount, "Withdraw")
+ end
+
+ ---@param data string
+ ---@return any
+ function self.getData(data)
+ return self[data]
+ end
+
+ ---@param metadata string|table
+ ---@return any
+ function self.getMetadata(metadata)
+ if type(metadata) ~= "table" then
+ return self.metadata[metadata]
+ end
+ local returnData = {}
+ for i=1, #metadata do
+ local data = metadata[i]
+ returnData[data] = self.metadata[data]
+ end
+ return returnData
+ end
+
+ ---@param key string|table
+ ---@param value any
+ function self.setData(key, value, reason)
+ if type(key) == "table" then
+ for k, v in pairs(key) do
+ self[k] = v
+ if k == "cash" or k == "bank" then
+ TriggerEvent("ND:moneyChange", self.source, k, v, "set", reason)
+ end
+ end
+ else
+ self[key] = value
+ if key == "cash" or key == "bank" then
+ TriggerEvent("ND:moneyChange", self.source, key, value, "set", reason)
+ end
+ end
+ self.triggerEvent("ND:updateCharacter", removeCharacterFunctions(self))
+ end
+
+ ---@param key string|table
+ ---@param value any
+ ---@return table
+ function self.setMetadata(key, value)
+ if type(key) == "table" then
+ for k, v in pairs(key) do
+ self.metadata[k] = v
+ end
+ else
+ self.metadata[key] = value
+ end
+ self.triggerEvent("ND:updateCharacter", removeCharacterFunctions(self))
+ return self.metadata
+ end
+
+ -- Completely delete character
+ function self.delete()
+ local result = MySQL.query.await("DELETE FROM nd_characters WHERE charid = ?", {self.id})
+ if result and NDCore.players[self.source] then
+ NDCore.players[self.source] = nil
+ end
+ return result
+ end
+
+ -- Unload and save character
+ function self.unload()
+ if not NDCore.players[self.source] then return end
+ for name, _ in pairs(self.groups) do
+ lib.removePrincipal(self.source, ("group.%s"):format(name))
+ end
+ local ped = GetPlayerPed(self.source)
+ if ped then
+ local coords = GetEntityCoords(ped)
+ local heading = GetEntityHeading(ped)
+ self.setMetadata("location", {
+ x = coords.x,
+ y = coords.y,
+ z = coords.z,
+ w = heading
+ })
+ end
+ self.triggerEvent("ND:characterUnloaded")
+ TriggerEvent("ND:characterUnloaded", self.source, self)
+ local saved = self.save()
+ NDCore.players[self.source] = nil
+ return saved
+ end
+
+ -- Save character information to database
+ function self.save()
+ local affectedRows = MySQL.update.await("UPDATE nd_characters SET name = ?, firstname = ?, lastname = ?, dob = ?, gender = ?, cash = ?, bank = ?, groups = ?, metadata = ? WHERE charid = ?", {
+ self.name,
+ self.firstname,
+ self.lastname,
+ self.dob,
+ self.gender,
+ self.cash,
+ self.bank,
+ json.encode(self.groups),
+ json.encode(self.metadata),
+ self.id
+ })
+ return affectedRows > 0
+ end
+
+ ---Create a license/permit for the character
+ ---@param licenseType string
+ ---@param expire number
+ function self.createLicense(licenseType, expire)
+ local expireIn = tonumber(expire) or 2592000
+ local time = os.time()
+ local licenses = self.metadata.licenses
+ local identifier = {}
+
+ for i=1, 16 do
+ identifier[i] = math.random(0, 1) == 1 and string.char(math.random(65, 90)) or math.random(0, 9)
+ end
+
+ local license = {
+ type = licenseType,
+ status = "valid",
+ issued = time,
+ expires = time+expireIn,
+ identifier = table.concat(identifier)
+ }
+
+ if licenses then
+ self.metadata.licenses[#licenses+1] = license
+ else
+ self.metadata.licenses = {license}
+ end
+ self.triggerEvent("ND:updateCharacter", removeCharacterFunctions(self))
+ end
+
+ function self.getLicense(identifier)
+ local licenses = self.metadata.licenses or {}
+ for i=1, #licenses do
+ local data = licenses[i]
+ if data.identifier == identifier then
+ return data, i
+ end
+ end
+ end
+
+ function self.updateLicense(identifier, newData)
+ local data, i = self.getLicense(identifier)
+ if not data then return end
+ for k, v in pairs(newData) do
+ data[k] = v
+ end
+ self.save()
+ end
+
+ ---@param coords vector3|vector4
+ ---@return boolean
+ function self.setCoords(coords)
+ if not self.source or not coords then return end
+ local ped = GetPlayerPed(self.source)
+ if not DoesEntityExist(ped) then return end
+ SetEntityCoords(ped, coords.x, coords.y, coords.z)
+ if coords.w then
+ SetEntityHeading(ped, coords.w)
+ end
+ return true
+ end
+
+ ---@param eventName string
+ ---@param ... any
+ ---@return boolean
+ function self.triggerEvent(eventName, ...)
+ if not self.source then return end
+ TriggerClientEvent(eventName, self.source, ...)
+ return true
+ end
+
+ function self.notify(...)
+ if not self.source then return end
+ if GetResourceState("ModernHUD") == "started" then
+ TriggerClientEvent("ModernHUD:notify", self.source, ...)
+ elseif GetResourceState("ox_lib") == "started" then
+ TriggerClientEvent("ox_lib:notify", self.source, ...)
+ end
+ return true
+ end
+
+ function self.revive()
+ self.triggerEvent("ND:revivePlayer")
+ self.setMetadata({
+ dead = false,
+ deathInfo = false,
+ })
+ end
+
+ ---@param reason string
+ function self.drop(reason)
+ if not self.source then return end
+ DropPlayer(self.source, reason)
+ end
+
+ -- Set the character as the players active character/currently playing character
+ function self.active()
+ local char = NDCore.players[self.source]
+ if char and char.id == self.id then return end
+ if char then char.unload() end
+ for identifierType, identifier in pairs(self.identifiers) do
+ if lib.table.contains(Config.admins, ("%s:%s"):format(identifierType, identifier)) then
+ self.addGroup("admin")
+ end
+ end
+
+ local roles = self.discord.roles
+ if roles then
+ for i=1, #Config.adminDiscordRoles do
+ local role = Config.adminDiscordRoles[i]
+ if lib.table.contains(roles, role) then
+ self.addGroup("admin")
+ end
+ end
+ end
+
+ for name, _ in pairs(self.groups) do
+ lib.addPrincipal(self.source, ("group.%s"):format(name))
+ end
+ NDCore.players[self.source] = self
+ TriggerEvent("ND:characterLoaded", self)
+ self.triggerEvent("ND:characterLoaded", removeCharacterFunctions(self))
+ end
+
+ ---@param name string
+ ---@param rank number
+ ---@param isJob boolean
+ ---@return boolean
+ function self.addGroup(name, rank, isJob)
+ local groupRank = tonumber(rank) or 1
+ local groupInfo = Config.groups[name]
+ -- if not groupInfo then return end
+ if isJob then
+ for _, group in pairs(self.groups) do
+ group.isJob = nil
+ end
+ end
+ self.groups[name] = {
+ label = groupInfo and groupInfo.label or name,
+ rankName = groupInfo and groupInfo.ranks[groupRank] or groupRank,
+ rank = groupRank,
+ isJob = isJob
+ }
+ self.triggerEvent("ND:updateCharacter", removeCharacterFunctions(self))
+ lib.addPrincipal(self.source, ("group.%s"):format(name))
+ return self.groups[name]
+ end
+
+ ---@param name string
+ ---@return table
+ function self.getGroup(name)
+ return self.groups[name]
+ end
+
+ ---@param name string
+ function self.removeGroup(name)
+ local group = self.groups[name]
+ if not group then return end
+ self.groups[name] = nil
+ self.triggerEvent("ND:updateCharacter", removeCharacterFunctions(self))
+ lib.removePrincipal(self.source, ("group.%s"):format(name))
+ return group
+ end
+
+ ---@param name string
+ ---@param rank number
+ ---@return boolean
+ function self.setJob(name, rank)
+ self.removeGroup(self.job)
+ local job = self.addGroup(name, rank, true)
+ local jobName, jobInfo = self.getJob()
+ if jobInfo then
+ self.job = jobName
+ self.jobInfo = jobInfo
+ end
+ return job
+ end
+
+ ---@param job string
+ ---@return boolean
+ function self.getJob()
+ for name, group in pairs(self.groups) do
+ if group.isJob then
+ return name, group
+ end
+ end
+ end
+
+ local jobName, jobInfo = self.getJob()
+ if jobInfo then
+ self.job = jobName
+ self.jobInfo = jobInfo
+ end
+
+ return self
+end
+
+---@param src number
+---@param info table
+---@return table
+function NDCore.newCharacter(src, info)
+ local identifier = GetPlayerIdentifierByType(src, Config.characterIdentifier)
+ if not identifier then return end
+
+ local charInfo = {
+ source = src,
+ identifier = identifier,
+ name = GetPlayerName(src) or "",
+ firstname = info.firstname or "",
+ lastname = info.lastname or "",
+ dob = info.dob or "",
+ gender = info.gender or "",
+ cash = info.cash or 0,
+ bank = info.bank or 0,
+ groups = info.groups or {},
+ metadata = info.metadata or {},
+ inventory = info.inventory or {},
+ }
+
+ charInfo.id = MySQL.insert.await("INSERT INTO nd_characters (identifier, name, firstname, lastname, dob, gender, cash, bank, groups, metadata) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", {
+ identifier,
+ charInfo.name,
+ charInfo.firstname,
+ charInfo.lastname,
+ charInfo.dob,
+ charInfo.gender,
+ charInfo.cash,
+ charInfo.bank,
+ json.encode(charInfo.groups),
+ json.encode(charInfo.metadata)
+ })
+
+ return createCharacterTable(charInfo)
+end
+
+---@param id number
+---@return table
+function NDCore.fetchCharacter(id, src)
+ local result
+ if src then
+ result = MySQL.query.await("SELECT * FROM nd_characters WHERE charid = ? and identifier = ?", {id, GetPlayerIdentifierByType(src, Config.characterIdentifier)})
+ else
+ result = MySQL.query.await("SELECT * FROM nd_characters WHERE charid = ?", {id})
+ end
+
+ if not result then return end
+ local info = result[1]
+ return createCharacterTable({
+ source = src,
+ id = info.charid,
+ identifier = info.identifier,
+ name = info.name,
+ firstname = info.firstname,
+ lastname = info.lastname,
+ dob = info.dob,
+ gender = info.gender,
+ cash = info.cash,
+ bank = info.bank,
+ groups = json.decode(info.groups),
+ metadata = json.decode(info.metadata),
+ inventory = json.decode(info.inventory)
+ })
+end
+
+---@param src number
+---@return table
+function NDCore.fetchAllCharacters(src)
+ local characters = {}
+ local result = MySQL.query.await("SELECT * FROM nd_characters WHERE identifier = ?", {GetPlayerIdentifierByType(src, Config.characterIdentifier)})
+
+ for i=1, #result do
+ local info = result[i]
+ characters[info.charid] = createCharacterTable({
+ source = src,
+ id = info.charid,
+ identifier = info.identifier,
+ name = info.name,
+ firstname = info.firstname,
+ lastname = info.lastname,
+ dob = info.dob,
+ gender = info.gender,
+ cash = info.cash,
+ bank = info.bank,
+ groups = json.decode(info.groups),
+ metadata = json.decode(info.metadata),
+ inventory = json.decode(info.inventory),
+ })
+ end
+ return characters
+end
+
+---@param src number
+---@param id number
+---@return table
+function NDCore.setActiveCharacter(src, id)
+ local char = NDCore.players[src]
+ if not src or char and char.id == id then return end
+
+ local character = NDCore.fetchCharacter(id, src)
+ if not character then return end
+
+ character.name = GetPlayerName(src)
+ character.active()
+ return character
+end
diff --git a/server/vehicle.lua b/server/vehicle.lua
new file mode 100644
index 0000000..85f9fd1
--- /dev/null
+++ b/server/vehicle.lua
@@ -0,0 +1,696 @@
+local ox_inventory
+local inventoryStarted = false
+local spawnedPlayerVehicles = {}
+
+NDCore.isResourceStarted("ox_inventory", function(started)
+ inventoryStarted = started
+ if not started then return end
+ ox_inventory = exports.ox_inventory
+end)
+
+local function getVehicleType(model)
+ local tempVehicle = CreateVehicle(model, 0, 0, 0, 0, true, true)
+
+ local time = os.time()
+ while not DoesEntityExist(tempVehicle) and time-os.time() < 5 do Wait(5) end
+
+ if not DoesEntityExist(tempVehicle) then return end
+ local entityType = GetVehicleType(tempVehicle)
+ DeleteEntity(tempVehicle)
+ return entityType
+end
+
+local function generatePlate()
+ local plate = {}
+ for i=1, 8 do
+ plate[i] = math.random(0, 1) == 1 and string.char(math.random(65, 90)) or math.random(0, 9)
+ end
+ return table.concat(plate)
+end
+
+local function generateVehiclePlate(newPlate)
+ local plate = newPlate or generatePlate()
+ while MySQL.scalar.await("SELECT 1 FROM nd_vehicles WHERE plate = ?", {plate}) do
+ plate = generatePlate()
+ end
+ return plate
+end
+
+local function generateTemporaryVehicleId()
+ return ("temp_%s%d"):format(string.char(math.random(65, 90)), math.random(1, 999999))
+end
+
+local function getVehicleDatabaseInfo(vehicle)
+ if not vehicle then return end
+ local stored = vehicle.stored == 1
+ local impounded = vehicle.impounded == 1
+ return {
+ id = vehicle.id,
+ owner = vehicle.owner,
+ plate = vehicle.plate,
+ properties = json.decode(vehicle.properties) or {},
+ stored = stored,
+ impounded = impounded,
+ stolen = vehicle.stolen == 1,
+ available = not impounded and stored,
+ metadata = json.decode(vehicle.metadata) or {}
+ }
+end
+
+--- get vehicle information queried from the database by the vehicle id
+---@param vehicleId string | numb
+---@return table | nil
+function NDCore.getVehicleById(vehicleId)
+ local result = MySQL.query.await("SELECT * FROM nd_vehicles WHERE id = ?", {vehicleId})
+ return getVehicleDatabaseInfo(result?[1])
+end
+
+--- get vehicle information and functions
+---@param entity number
+---@return table
+function NDCore.getVehicle(entity)
+ if not DoesEntityExist(entity) then return end
+
+ local state = Entity(entity).state
+ local self = {
+ entity = entity,
+ id = state.id,
+ owner = state.owner,
+ keys = state.keys,
+ properties = state.props,
+ locked = state.locked,
+ hotwired = state.hotwired,
+ metadata = state.metadata or {},
+ netId = NetworkGetNetworkIdFromEntity(entity)
+ }
+
+ --- delete the vehicle
+ function self.delete(saveProperties)
+ if not DoesEntityExist(entity) then return end
+ if saveProperties and self.id and self.owner then
+ local properties = lib.callback.await("ND_Vehicles:getProps", NetworkGetEntityOwner(entity), self.netId)
+ if properties then
+ MySQL.query("UPDATE nd_vehicles SET properties = ? WHERE id = ?", {json.encode(properties), self.id})
+ end
+ end
+ DeleteEntity(entity)
+ end
+
+ --- set vehicle properties
+ ---@param props table
+ function self.setProperties(props)
+ if not DoesEntityExist(entity) then return end
+ local properties = type(props) == "string" and json.decode(props) or props
+ local state = Entity(entity).state
+ state.props = properties
+ self.properties = properties
+
+ if not self.id or not self.owner then return end
+ MySQL.query("UPDATE nd_vehicles SET properties = ? WHERE id = ?", {json.encode(properties), self.id})
+ end
+
+ --- set vehicle locked/unlocked
+ ---@param status boolean
+ function self.setLocked(status)
+ if not DoesEntityExist(entity) then return end
+ local state = Entity(entity).state
+ state.locked = status
+ self.locked = status
+ end
+
+ --- update the vehicle plate
+ ---@param plate string
+ ---@return boolean
+ function self.setPlate(plate)
+ if not DoesEntityExist(entity) or MySQL.scalar.await("SELECT 1 FROM nd_vehicles WHERE plate = ?", {plate}) then return end
+ if self.properties then
+ self.properties.plate = plate
+ local state = Entity(entity).state
+ state.props = self.properties
+ end
+
+ if not self.id or not self.owner then return end
+ MySQL.query("UPDATE nd_vehicles SET plate = ? WHERE id = ?", {plate, self.id})
+ return true
+ end
+
+ --- set the vehicle availability status
+ ---@param statusType string
+ ---@param status boolean
+ function self.setStatus(statusType, status)
+ if not lib.table.contains({"stored", "impounded", "stolen"}, statusType) then return end
+ if statusType ~= "stolen" then self.delete(true) end
+ if not self.id or not self.owner then return end
+ local query = ("UPDATE nd_vehicles SET %s = ? WHERE id = ?"):format(statusType)
+ MySQL.query(query, {status and 1 or 0, self.id})
+ return true
+ end
+
+ function self.setMetadata(key, value)
+ self.metadata[key] = value
+ if DoesEntityExist(entity) then
+ local state = Entity(self.entity).state
+ local metadata = state.metadata
+ metadata[key] = value
+ state.metadata = metadata
+ end
+ if not self.id or not self.owner then return end
+ MySQL.query("UPDATE nd_vehicles SET metadata = ? WHERE id = ?", {json.encode(self.metadata), self.id})
+ end
+
+ if self.id and self.owner then
+ spawnedPlayerVehicles[self.id] = self
+ end
+
+ return self
+end
+
+--- get a characters owned vehicles
+---@param characterId number
+---@return table
+function NDCore.getVehicles(characterId)
+ local result = MySQL.query.await("SELECT * FROM nd_vehicles WHERE owner = ?", {characterId})
+ if not result then return {} end
+
+ local vehicles = {}
+ for _, vehicle in pairs(result) do
+ local info = getVehicleDatabaseInfo(vehicle)
+ if info then
+ vehicles[#vehicles+1] = info
+ end
+ end
+ return vehicles
+end
+
+--- give a player keys/access to a vehicle
+---@param source number
+---@param vehicle number
+---@param access boolean
+---@param info table
+function NDCore.giveVehicleAccess(source, vehicle, access, info)
+ if not vehicle or not DoesEntityExist(vehicle) then return end
+ local state = Entity(vehicle).state
+ local netId = info?.netId or NetworkGetNetworkIdFromEntity(vehicle)
+ local vehicleId = info?.vehicleId or state.id or generateTemporaryVehicleId()
+
+ if not state.id then
+ state.id = vehicleId
+ end
+
+ local player = NDCore.getPlayer(source)
+ if player then
+ local keys = state.keys or {}
+ keys[player.id] = access
+ state.keys = keys
+ end
+
+ if not inventoryStarted or not Config.useInventoryForKeys then return end
+ local plate = info?.plate or GetVehicleNumberPlateText(vehicle)
+ local model = info?.model or GetEntityModel(vehicle)
+ local modelName = info?.modelName or model and lib.callback.await("ND_Vehicles:getVehicleModelMakeLabel", source, model) or ""
+ local hasKey = ox_inventory:GetSlotIdWithItem(source, "keys", {
+ vehId = vehicleId
+ })
+
+ if access and not hasKey then
+ ox_inventory:AddItem(source, "keys", 1, {
+ vehOwner = owner or state.owner,
+ vehId = vehicleId,
+ vehPlate = plate,
+ vehModel = modelName,
+ keyEnabled = true,
+ vehNetId = netId
+ })
+ elseif not access and hasKey then
+ ox_inventory:RemoveItem(source, "keys", 1, nil, hasKey)
+ end
+end
+
+--- spawn a vehicle, set info & give keys.
+---@param info table
+---@return table
+function NDCore.createVehicle(info)
+ local owner = info.owner
+ local vehicleId = info.vehicleId or generateTemporaryVehicleId()
+ local properties = info.properties or {}
+ local coords = info.coords
+ local spawnCoords = coords
+
+ local coordType = type(coords)
+ if coordType == "vector3" or coordType == "table" then
+ spawnCoords = vector4(coords.x, coords.y, coords.z, coords.w or coords.h or info.heading or 0.0)
+ end
+ if not spawnCoords then
+ return Citizen.Trace("NDCore.createVehicle", "spawnCoords not found")
+ end
+
+ local model = info.model or properties.model
+ local vehType = getVehicleType(model)
+ if not vehType then
+ return Citizen.Trace("NDCore.createVehicle", "vehType not found")
+ end
+
+ local veh = CreateVehicleServerSetter(model, vehType, spawnCoords.x, spawnCoords.y, spawnCoords.z, spawnCoords.w)
+ local time = os.time()
+ while not DoesEntityExist(veh) and time-os.time() < 5 do Wait(5) end
+ if not veh or not DoesEntityExist(veh) then
+ return Citizen.Trace("NDCore.createVehicle", "vehicle entity doesn't exist")
+ end
+
+ local netId = NetworkGetNetworkIdFromEntity(veh)
+ local state = Entity(veh).state
+ state.locked = true
+ local keys = info.keys or {}
+
+ if not properties.plate then
+ properties.plate = generateVehiclePlate()
+ end
+ if owner then
+ keys[owner] = true
+ state.owner = owner
+ end
+
+ state.keys = keys
+ state.props = properties
+ state.id = vehicleId
+ local vehicleName
+ if inventoryStarted and Config.useInventoryForKeys then
+ for charId, _ in pairs(keys) do
+ local playerSource = charId == owner and info.source
+ if not playerSource then
+ local player = NDCore.getPlayers("id", charId, true)[1]
+ playerSource = player and player.source
+ end
+ if playerSource then
+ if not vehicleName then
+ vehicleName = lib.callback.await("ND_Vehicles:getVehicleModelMakeLabel", playerSource, model) or ""
+ end
+ NDCore.giveVehicleAccess(playerSource, veh, true, {
+ vehicleId = vehicleId,
+ netId = netId,
+ plate = properties?.plate,
+ model = model,
+ vehicleName = vehicleName,
+ owner = owner
+ })
+ end
+ end
+ end
+
+ return NDCore.getVehicle(veh)
+end
+
+--- transfer ownership of a vehicle between players
+---@param vehicleId number|string
+---@param fromSource number
+---@param toSource number
+---@return boolean
+function NDCore.transferVehicleOwnership(vehicleId, fromSource, toSource)
+ local playerFrom = NDCore.getPlayer(fromSource)
+ local playerTo = NDCore.getPlayer(toSource)
+ if not playerFrom or not playerTo then return end
+
+ local vehicle = NDCore.getVehicleById(vehicleId)
+ if not vehicle or vehicle.owner ~= playerFrom.id then return end
+
+ MySQL.query.await("UPDATE nd_vehicles SET owner = ? WHERE id = ?", {playerTo.id, vehicleId})
+ local vehicleInfo = spawnedPlayerVehicles[vehicleId]
+
+ if vehicleInfo then
+ local veh, netId, model = vehicleInfo.entity, vehicleInfo.netId, GetEntityModel(veh)
+ playerFrom.triggerEvent("ND_Vehicles:blip", netId, false)
+ playerTo.triggerEvent("ND_Vehicles:blip", netId, true)
+
+ NDCore.giveVehicleAccess(fromSource, veh, false, {
+ vehicleId = vehicleId,
+ netId = netId,
+ model = model,
+ owner = playerFrom.id
+ })
+
+ local state = Entity(veh).state
+ state.owner = playerTo.id
+ NDCore.giveVehicleAccess(toSource, veh, true, {
+ vehicleId = vehicleId,
+ netId = netId,
+ model = model,
+ owner = playerTo.id
+ })
+ end
+
+ playerFrom.notify({
+ title = "Ownership transfered",
+ description = ("Vehicle ownership of %s has been transfered."):format(vehicle.plate),
+ position = "bottom-right",
+ type = "success"
+ })
+ playerTo.notify({
+ title = "Ownership received",
+ description = ("Received vehicle ownership of %s."):format(vehicle.plate),
+ position = "bottom-right"
+ })
+ return true
+end
+
+--- set vehicle as owned by player
+---@param playerId number
+---@param properties table
+---@param stored boolean
+---@return vehicleId number
+function NDCore.setVehicleOwned(playerId, properties, stored)
+ local plate = generateVehiclePlate()
+ properties.plate = plate
+ return MySQL.insert.await("INSERT INTO nd_vehicles (owner, plate, properties, stored) VALUES (?, ?, ?, ?)", {playerId, plate, json.encode(properties), stored and 1 or 0})
+end
+
+--- give a player access to another players vehicle as if they are next to eachother and hand the keys.
+---@param source number
+---@param target number
+---@param vehicle number
+---@return boolean
+function NDCore.shareVehicleKeys(source, target, vehicle)
+ if not vehicle or not DoesEntityExist(vehicle) then return end
+
+ local player = NDCore.getPlayer(source)
+ local targetPlayer = NDCore.getPlayer(target)
+ local state = Entity(vehicle).state
+ if not targetPlayer or not player or player.id ~= state.owner then return end
+
+ NDCore.giveVehicleAccess(target, vehicle, true)
+ local plate = GetVehicleNumberPlateText(vehicle)
+
+ player.notify({
+ title = "Keys shared",
+ description = ("You've shared vehicle keys to %s."):format(plate),
+ position = "bottom-right",
+ type = "success",
+ })
+ targetPlayer.notify({
+ title = "Keys received",
+ description = ("Received vehicle keys to %s."):format(plate),
+ position = "bottom-right",
+ })
+ return true
+end
+
+--- spawned a vehicle that's owned by the player and checks for availability
+---@param source number
+---@param vehicleId number
+---@param coords vector4
+---@return table
+function NDCore.spawnOwnedVehicle(source, vehicleId, coords, heading)
+ local player = NDCore.getPlayer(source)
+ if not player then return end
+
+ local vehicle = NDCore.getVehicleById(vehicleId)
+ if not vehicle or not vehicle.available or vehicle.owner ~= player.id then return end
+
+ MySQL.query.await("UPDATE nd_vehicles SET stored = ? WHERE id = ?", {0, vehicleId})
+ return NDCore.createVehicle({
+ owner = player.id,
+ model = vehicle.properties.model,
+ coords = vec4(coords.x, coords.y, coords.z, coords.w or coords.heading or heading),
+ properties = vehicle.properties,
+ vehicleId = vehicleId,
+ source = source
+ })
+end
+
+local function toggleVehicleLock(source, entity, nearby, metadata)
+ local player = NDCore.getPlayer(source)
+
+ if metadata and not metadata.keyEnabled then
+ return player.notify({
+ title = "No signal",
+ description = "Vehicle key disabled.",
+ type = "error",
+ position = "bottom-right",
+ duration = 3000
+ })
+ elseif not nearby then
+ return player.notify({
+ title = "No signal",
+ description = "Vehicle to far away.",
+ type = "error",
+ position = "bottom-right",
+ duration = 3000
+ })
+ end
+
+ local vehicle = NDCore.getVehicle(entity)
+ local locked = not vehicle.locked
+ vehicle.setLocked(locked)
+
+ player.triggerEvent("ND_Vehicles:keyFob", vehicle.netId)
+ if locked then
+ return player.notify({
+ title = "LOCKED",
+ description = "Your vehicle has now been locked.",
+ type = "success",
+ position = "bottom-right",
+ duration = 3000
+ })
+ end
+ player.notify({
+ title = "UNLOCKED",
+ description = "Your vehicle has now been unlocked.",
+ type = "inform",
+ position = "bottom-right",
+ duration = 3000
+ })
+end
+
+local function getNearbyVehicles(coords, range)
+ local nearby = {}
+ local vehicles = GetAllVehicles()
+ for i=1, #vehicles do
+ local veh = vehicles[i]
+ local vehCoords = GetEntityCoords(veh)
+ if #(coords-vehCoords) < range then
+ nearby[#nearby+1] = veh
+ end
+ end
+ return nearby
+end
+
+local function lockNearestVehicle(source, vehId, metadata)
+ local ped = GetPlayerPed(source)
+ local pedCoords = GetEntityCoords(ped)
+ local veh = spawnedPlayerVehicles[vehId]?.entity
+
+ if veh and DoesEntityExist(veh) then
+ local vehCoords = GetEntityCoords(veh)
+ if not pedCoords or not vehCoords then return end
+ return toggleVehicleLock(source, veh, #(pedCoords-vehCoords) < 25.0, metadata)
+ end
+
+ local vehicles = getNearbyVehicles(pedCoords, 25.0)
+ for i=1, #vehicles do
+ local veh = vehicles[i]
+ local state = Entity(veh).state
+ if state and state.id == vehId then
+ return toggleVehicleLock(source, veh, true, metadata)
+ end
+ end
+end
+
+--- inventory keys using item.
+exports("keys", function(event, item, inventory, slot, data)
+ if event ~= "usingItem" or not Config.useInventoryForKeys or not inventoryStarted then return end
+ local metadata
+ for i=1, #inventory.items do
+ local item = inventory.items[i]
+ if item and item.slot == slot then
+ metadata = item.metadata
+ break
+ end
+ end
+
+ if not metadata then return false end
+ lockNearestVehicle(inventory.id, metadata.vehId, metadata)
+ return false
+end)
+
+-- if using inventory and inventory keys players can spawn keys when in their vehicle with this command.
+RegisterCommand("getkeys", function(source, args, rawCommand)
+ if not Config.useInventoryForKeys or not inventoryStarted then return end
+ local veh = GetVehiclePedIsIn(GetPlayerPed(source))
+ if not veh or veh == 0 then return end
+
+ local player = NDCore.getPlayer(source)
+ local state = Entity(veh).state
+ local owner = state.owner
+ if not owner or owner ~= player.id then return end
+
+ local props = state.props
+ ox_inventory:AddItem(source, "keys", 1, {
+ vehOwner = owner,
+ vehId = state.id,
+ vehPlate = props.plate,
+ vehModel = lib.callback.await("ND_Vehicles:getVehicleModelMakeLabel", source, props.model),
+ keyEnabled = true,
+ vehNetId = NetworkGetNetworkIdFromEntity(veh)
+ })
+end, false)
+
+-- key sharing if not using inventory or inventory keys.
+RegisterCommand("givekeys", function(source, args, rawCommand)
+ if Config.useInventoryForKeys and inventoryStarted then return end
+
+ local src = source
+ if not args[1] then return end
+ local target = tonumber(args[1])
+ if not GetPlayerPing(target) then return end
+
+ local veh = GetVehiclePedIsIn(GetPlayerPed(src))
+ if veh == 0 then
+ veh = GetVehiclePedIsIn(GetPlayerPed(src), true)
+ if veh == 0 then return end
+ end
+
+ NDCore.shareVehicleKeys(src, target, veh)
+end, false)
+
+-- lock/unlock vehicles if they're within range.
+RegisterNetEvent("ND_Vehicles:toggleVehicleLock", function(netId)
+ if Config.useInventoryForKeys and inventoryStarted then return end
+
+ local src = source
+ local veh = NetworkGetEntityFromNetworkId(netId)
+ if not veh or not DoesEntityExist(veh) then return end
+
+ local ped = GetPlayerPed(src)
+ local pedCoords = GetEntityCoords(ped)
+ local vehCoords = GetEntityCoords(veh)
+ if not pedCoords or not vehCoords then return end
+ toggleVehicleLock(src, veh, #(pedCoords-vehCoords) < 25.0)
+end)
+
+-- locking of npc vehicles, if the players spawns inside a vehicle it won't be locked.
+RegisterNetEvent("entityCreated", function(entity)
+ if not DoesEntityExist(entity) or GetEntityType(entity) ~= 2 then return end
+ local state = Entity(entity).state
+ if state.owner or state.locked ~= nil then return end
+
+ local driver = GetPedInVehicleSeat(entity, -1)
+ if DoesEntityExist(driver) and IsPedAPlayer(driver) then
+ state.locked = false
+ state.hotwired = true
+ end
+
+ if math.random(1, 100) <= Config.randomUnlockedVehicleChance then return end
+ state.locked = true
+end)
+
+-- disables inventory vehicles keys, disabled vehicles keys can no longer be used. Kinda like taking the battery out.
+RegisterNetEvent("ND_Vehicles:disableKey", function(slot)
+ local src = source
+ local key = ox_inventory:GetSlot(src, slot)
+ local metadata = key.metadata
+ if not metadata.keyEnabled then return end
+ metadata.keyEnabled = false
+ ox_inventory:SetMetadata(src, slot, key.metadata)
+end)
+
+-- sync alarm when vehicle is lockpicked.
+RegisterNetEvent("ND_Vehicles:lockpick", function(netId, success)
+ local src = source
+ local veh = NetworkGetEntityFromNetworkId(netId)
+ if not veh or not DoesEntityExist(veh) then return end
+
+ local ped = GetPlayerPed(src)
+ local pedCoords = GetEntityCoords(ped)
+ local vehCoords = GetEntityCoords(veh)
+ if #(pedCoords-vehCoords) > 5.0 then return end
+
+ local owner = NetworkGetEntityOwner(veh)
+ TriggerClientEvent("ND_Vehicles:syncAlarm", owner, netId)
+
+ if not success then return end
+ local state = Entity(veh).state
+ state.locked = false
+end)
+
+-- sync alarm if vehicle gets hotwired.
+RegisterNetEvent("ND_Vehicles:hotwire", function(netId)
+ local src = source
+ local ped = GetPlayerPed(src)
+ local playerVeh = GetVehiclePedIsIn(ped)
+ local veh = NetworkGetEntityFromNetworkId(netId)
+ if not playerVeh or playerVeh == 0 or playerVeh ~= veh then return end
+ local state = Entity(veh).state
+ state.hotwired = true
+end)
+
+RegisterNetEvent("ND_Vehicles:storeVehicle", function(netId)
+ local src = source
+ local vehicle = NDCore.getVehicle(NetworkGetEntityFromNetworkId(netId))
+ if not vehicle then return end
+
+ local player = NDCore.getPlayer(src)
+ if not vehicle.setStatus("stored", true) or not player or player.id ~= vehicle.owner then
+ return player.notify({
+ title = "Garage",
+ description = "No owned vehicle found nearby.",
+ type = "error",
+ position = "bottom",
+ duration = 3000
+ })
+ end
+ player.notify({
+ title = "Garage",
+ description = "Vehicle stored in garage.",
+ type = "success",
+ position = "bottom",
+ duration = 3000
+ })
+ NDCore.giveVehicleAccess(src, vehicle.entity, false, {
+ vehicleId = vehicle.id,
+ netId = vehicle.netId,
+ owner = vehicle.owner
+ })
+end)
+
+local function isParkingAvailable(locations)
+ for i=1, #locations do
+ local loc = locations[math.random(1, #locations)]
+ if #getNearbyVehicles(vec3(loc.x, loc.y, loc.z), 2.0) == 0 then
+ return loc
+ end
+ end
+end
+
+RegisterNetEvent("ND_Vehicles:takeVehicle", function(vehId, locations)
+ local src = source
+ local vehicle = NDCore.getVehicleById(vehId)
+ local player = NDCore.getPlayer(src)
+ if not player or not vehicle or vehicle.owner ~= player.id then return end
+ if vehicle.impounded then
+ local reclaimPrice = vehicle.metadata.impoundReclaimPrice or 200
+ if not player.deductMoney("bank", reclaimPrice, "Vehicle impound reclaim") then
+ return player.notify({
+ title = "Impound",
+ description = ("Price to reclaim is $%d, you don't have enough!"):format(reclaimPrice),
+ type = "error",
+ position = "bottom"
+ })
+ end
+ player.notify({
+ title = "Impound",
+ description = ("Paid $%d to reclaim vehicle!"):format(reclaimPrice),
+ type = "success",
+ position = "bottom"
+ })
+ MySQL.query.await("UPDATE nd_vehicles SET impounded = ? WHERE id = ?", {0, vehicle.id})
+ end
+
+ local info = NDCore.spawnOwnedVehicle(src, vehicle.id, isParkingAvailable(locations))
+ if not info then return end
+ TriggerClientEvent("ND_Vehicles:blip", src, info.netId, true)
+end)
+
+lib.callback.register("ND_Vehicles:getOwnedVehicles", function(src)
+ local player = NDCore.getPlayer(src)
+ if not player then return end
+ return NDCore.getVehicles(player.id)
+end)
diff --git a/shared/functions.lua b/shared/functions.lua
new file mode 100644
index 0000000..2464b0e
--- /dev/null
+++ b/shared/functions.lua
@@ -0,0 +1,30 @@
+local startedResources = {}
+
+local function stateChanged(resourceName, state)
+ local callbacks = startedResources[resourceName]
+ if not callbacks then return end
+ for i=1, #callbacks do
+ local cb = callbacks[i]
+ cb(state)
+ end
+end
+
+AddEventHandler("onResourceStart", function(resourceName)
+ stateChanged(resourceName, true)
+end)
+
+AddEventHandler("onResourceStop", function(resourceName)
+ stateChanged(resourceName, false)
+end)
+
+function NDCore.isResourceStarted(resourceName, cb)
+ local started = GetResourceState(resourceName) == "started"
+ if cb then
+ if not startedResources[resourceName] then
+ startedResources[resourceName] = {}
+ end
+ startedResources[resourceName][#startedResources[resourceName]+1] = cb
+ cb(started)
+ end
+ return started
+end