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 +

+ + Andyyy Development Server + +
-## 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? -[![Discord](https://discordapp.com/api/guilds/857672921912836116/widget.png?style=banner3)](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