├── README.md └── esx_potjob ├── __resource.lua ├── client.lua ├── options.lua └── server.lua /README.md: -------------------------------------------------------------------------------- 1 | # Secure Resource Examples 2 | 3 | This is a bunch of FiveM example resources, they are not meant to have any fancy animations or be advanced they are meant to be an example how to implement your events more secure 4 | 5 | ## esx_potjob 6 | 7 | This is a simple job, it's just driving to different locations but instead of most jobs keep all calculations of the money at the client we are storing a lot on the server check out this bad example how some resources might do it serverside 8 | 9 | ```lua 10 | -- bad example 11 | AddEventHandler("esx_potjob:pay", function(money) 12 | local xPlayer = ESX.GetPlayerFromId(source) 13 | 14 | -- we are literally blindlessly accepting the money input, every attacker could trigger this event with any amount 15 | xPlayer.addMoney(money) 16 | end) 17 | ``` 18 | 19 | Instead we are sending every of our deliveries and when we start to the server so we can do some time checks and calculating the money itself -------------------------------------------------------------------------------- /esx_potjob/__resource.lua: -------------------------------------------------------------------------------- 1 | resource_manifest_version "44febabe-d386-4d18-afbe-5e627f4af937" 2 | 3 | shared_script "options.lua" 4 | client_script "client.lua" 5 | server_script "server.lua" -------------------------------------------------------------------------------- /esx_potjob/client.lua: -------------------------------------------------------------------------------- 1 | local onDelivery = false 2 | local closeToShop = false 3 | 4 | local closeLocations = {} 5 | local deliveredLocations = {} 6 | local shouldDrawQuitMarker = false 7 | local deliverVan = nil 8 | 9 | function DrawMissionMarker(pos, scale, r, g, b) 10 | DrawMarker(1, pos, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, scale, r, g, b, 100, false, false, 0, false, nil, nil, false) 11 | end 12 | 13 | function AddBlip(pos, name, sprite, shortRange) 14 | local blip = AddBlipForCoord(pos) 15 | if sprite then 16 | SetBlipSprite(blip, sprite) 17 | end 18 | SetBlipAsShortRange(blip, shortRange) 19 | 20 | BeginTextCommandSetBlipName("STRING") 21 | AddTextComponentSubstringPlayerName(name) 22 | EndTextCommandSetBlipName(blip) 23 | 24 | return blip 25 | end 26 | 27 | function SetupDeliveryLocations() 28 | for i, location in pairs(options.deliveryLocations) do 29 | local blip = AddBlip(location.position, "Delivery location " .. i) 30 | options.deliveryLocations[i].blip = blip 31 | end 32 | end 33 | 34 | function AlreadyDelivered(index) 35 | for _, location in pairs(deliveredLocations) do 36 | if index == location then 37 | return true 38 | end 39 | end 40 | 41 | return false 42 | end 43 | 44 | function GetCloseLocations() 45 | local locations = {} 46 | 47 | for k, location in pairs(options.deliveryLocations) do 48 | -- only draw close markers 49 | if #(location.position - GetEntityCoords(PlayerPedId())) < 100.0 and not AlreadyDelivered(k) then 50 | table.insert(locations, k) 51 | end 52 | end 53 | 54 | return locations 55 | end 56 | 57 | function ShowSubtitle(text, time) 58 | BeginTextCommandPrint(text) 59 | EndTextCommandPrint(time, 1); 60 | end 61 | 62 | function CleanUp() 63 | onDelivery = false 64 | shouldDrawQuitMarker = false 65 | 66 | deliveredLocations = {} 67 | for _, v in pairs(options.deliveryLocations) do 68 | if DoesBlipExist(v.blip) then 69 | RemoveBlip(v.blip) 70 | end 71 | end 72 | 73 | DeleteVehicle(deliverVan) 74 | end 75 | 76 | CreateThread(function() 77 | AddTextEntry("potshop_start_text", "Press ~INPUT_CONTEXT~ to start the ~g~pot delivery~s~.") 78 | AddTextEntry("potshop_start_subtitle", "Deliver the pot to the ~y~locations~s~, return to the potshop after.") 79 | AddTextEntry("potshop_deliver_text", "Press ~INPUT_CONTEXT~ to deliver this guy's ~g~pot~s~.") 80 | AddTextEntry("potshop_delivered_subtitle", "Thanks for the delivery, go back to the potshop or deliver some more.") 81 | AddTextEntry("potshop_stop_text", "Press ~INPUT_CONTEXT~ to end the delivery") 82 | 83 | -- https://docs.fivem.net/game-references/blips/ 84 | AddBlip(options.shop, "Potshop Delivery", 140, true) 85 | 86 | -- request vehicle model 87 | RequestModel(options.car.model) 88 | while not HasModelLoaded(options.car.model) do 89 | Wait(0) 90 | end 91 | 92 | while true do 93 | Wait(0) 94 | 95 | if not onDelivery and closeToShop then 96 | DrawMissionMarker(options.shop, vec3(1.3, 1.3, 1.0), 0, 179, 60) 97 | 98 | -- show message if close 99 | if #(options.shop - GetEntityCoords(PlayerPedId())) < 1.5 then 100 | DisplayHelpTextThisFrame("potshop_start_text") 101 | 102 | -- start delivery if key is pressed 103 | -- https://docs.fivem.net/game-references/controls/#controls 104 | if IsControlJustPressed(0, 51) then 105 | onDelivery = true 106 | 107 | SetupDeliveryLocations() 108 | 109 | deliverVan = CreateVehicle(options.car.model, options.car.spawn, options.car.heading, true, false) 110 | SetEntityAsMissionEntity(deliverVan) 111 | SetPedIntoVehicle(PlayerPedId(), deliverVan, -1) 112 | 113 | ShowSubtitle("potshop_start_subtitle", 4000) 114 | 115 | -- tell server we are starting our job 116 | TriggerServerEvent("esx_potjob:start") 117 | end 118 | end 119 | end 120 | end 121 | end) 122 | 123 | -- slower thread to not flood natives 124 | CreateThread(function() 125 | while true do 126 | Wait(1000) 127 | 128 | if onDelivery then 129 | closeLocations = GetCloseLocations() 130 | 131 | shouldDrawQuitMarker = (#deliveredLocations >= 1 and #(options.car.spawn - GetEntityCoords(PlayerPedId())) < 100.0) 132 | end 133 | 134 | if not onDelivery then 135 | closeToShop = (#(options.shop - GetEntityCoords(PlayerPedId())) < 100.0) 136 | end 137 | end 138 | end) 139 | 140 | CreateThread(function() 141 | while true do 142 | Wait(0) 143 | 144 | if onDelivery then 145 | for _, i in pairs(closeLocations) do 146 | local location = options.deliveryLocations[i] 147 | 148 | DrawMissionMarker(location.position, vec3(1.3, 1.3, 1.0), 0, 179, 60) 149 | 150 | if #(location.position - GetEntityCoords(PlayerPedId())) < 1.5 then 151 | DisplayHelpTextThisFrame("potshop_deliver_text") 152 | 153 | if IsControlJustPressed(0, 51) then 154 | ShowSubtitle("potshop_delivered_subtitle", 3000) 155 | 156 | -- mark as done and reparse location list 157 | table.insert(deliveredLocations, i) 158 | closeLocations = GetCloseLocations() 159 | 160 | RemoveBlip(location.blip) 161 | 162 | -- tell server we delivered something 163 | TriggerServerEvent("esx_potjob:delivered", i) 164 | end 165 | end 166 | end 167 | 168 | if shouldDrawQuitMarker then 169 | DrawMissionMarker(options.car.spawn, vec3(3.0, 3.0, 1.0), 0, 179, 60) 170 | 171 | if #(options.car.spawn - GetEntityCoords(PlayerPedId())) < 3.0 then 172 | DisplayHelpTextThisFrame("potshop_stop_text") 173 | 174 | if IsControlJustPressed(0, 51) then 175 | CleanUp() 176 | 177 | -- tell server we are done and to give our money 178 | TriggerServerEvent("esx_potjob:finished") 179 | end 180 | end 181 | end 182 | end 183 | end 184 | end) -------------------------------------------------------------------------------- /esx_potjob/options.lua: -------------------------------------------------------------------------------- 1 | options = { 2 | shop = vec3(-1172.23, -1572.25, 3.4), 3 | 4 | deliveryLocations = { 5 | { 6 | position = vec3(-1058.02, -1540.34, 4.0), 7 | pay = 50.0 8 | }, 9 | { 10 | position = vec3(-1064.41, -1158.9, 1.0), 11 | pay = 100.0 12 | }, 13 | { 14 | position = vec3(-1076.2, -1620.49, 3.2), 15 | pay = 65.0 16 | } 17 | }, 18 | 19 | car = { 20 | spawn = vec3(-1178.68, -1575.91, 3.0), 21 | heading = 215.80, 22 | -- make sure you use `` and not "" or '' for hash 23 | model = `Speedo2` 24 | }, 25 | 26 | -- a legit time in seconds which player shouldn't be able to finish/deliver new location after an action 27 | -- dont make this too strict like for example when you have houses really close to eachother 28 | -- if you have a big distance between houses you can really higher this value which prevents attackers calling it every 10 seconds 29 | cooldown = 10, 30 | 31 | -- If you are **not** running onesync disable this, even though you probably should :p 32 | -- This allows more serverside checks for example coordinates 33 | onesync_checks = true 34 | } -------------------------------------------------------------------------------- /esx_potjob/server.lua: -------------------------------------------------------------------------------- 1 | RegisterNetEvent("esx_potjob:start") 2 | RegisterNetEvent("esx_potjob:delivered") 3 | RegisterNetEvent("esx_potjob:finished") 4 | 5 | ESX = nil 6 | TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end) 7 | 8 | local sessions = {} 9 | 10 | function IsClientTooFast(source) 11 | -- calculate if client isn't doing stuff too fast for a time which can't be legit 12 | local tooFast = (sessions[source].last + (options.cooldown * 1000) > GetGameTimer()) 13 | 14 | if tooFast then 15 | print(string.format("%s %s delivered too fast, is they hacking or did you configure cooldown wrong?", 16 | GetPlayerName(source), GetPlayerIdentifier(source, 0))) 17 | end 18 | 19 | return tooFast 20 | end 21 | 22 | function IsLegitLocation(source, location) 23 | -- checks if location hasn't already been delivered and exists 24 | return (not IsLocationAlreadyDelivered(source, location) and options.deliveryLocations[location] ~= nil) 25 | end 26 | 27 | function IsLocationAlreadyDelivered(source, location) 28 | for _, loc in pairs(sessions[source].visited) do 29 | if loc == location then 30 | return true 31 | end 32 | end 33 | 34 | return false 35 | end 36 | 37 | AddEventHandler("esx_potjob:start", function() 38 | sessions[source] = { 39 | visited = {}, 40 | -- save when this was triggered to check if times are legit 41 | last = GetGameTimer() 42 | } 43 | end) 44 | 45 | -- we are not accepting an amount of money but the location id which server can then lookup later 46 | AddEventHandler("esx_potjob:delivered", function(id) 47 | -- only continue if player has already started job 48 | if sessions[source] and IsLegitLocation(source, id) and not IsClientTooFast(source) then 49 | local clean = true 50 | local distance 51 | 52 | -- do checks if onesync 53 | if options.onesync_checks then 54 | distance = #(GetEntityCoords(GetPlayerPed(source)) - options.deliveryLocations[id].position) 55 | 56 | -- checking from a distance of 15 because it might not be 100% correct 57 | if distance > 15 then 58 | clean = false 59 | end 60 | end 61 | 62 | if clean then 63 | table.insert(sessions[source].visited, id) 64 | sessions[source].last = GetGameTimer() 65 | else 66 | print(string.format("%s %s delivered from a too big distance (%s), is they hacking?", GetPlayerName(source), GetPlayerIdentifier(source, 0), math.floor(distance))) 67 | end 68 | end 69 | end) 70 | 71 | AddEventHandler("esx_potjob:finished", function() 72 | if sessions[source] then 73 | -- client isn't sending anything only that he's done, we already know what he did so we can calculate the payment 74 | local xPlayer = ESX.GetPlayerFromId(source) 75 | local money = 0 76 | 77 | for _, i in pairs(sessions[xPlayer.source].visited) do 78 | -- the client send us what he did so now we can see what money that location has 79 | local location = options.deliveryLocations[i] 80 | 81 | money = money + location.pay 82 | end 83 | 84 | -- give player money 85 | xPlayer.addMoney(money) 86 | 87 | -- clean up 88 | sessions[xPlayer.source] = nil 89 | end 90 | end) 91 | 92 | AddEventHandler("playerDropped", function (source, reason) 93 | if sessions[source] then 94 | sessions[source] = nil 95 | end 96 | end) --------------------------------------------------------------------------------