├── README.md ├── client └── client.lua ├── config └── config.lua ├── donator.sql ├── fxmanifest.lua └── server └── server.lua /README.md: -------------------------------------------------------------------------------- 1 | ![image](https://user-images.githubusercontent.com/82112471/195740699-7fe040c6-bd35-4376-85c0-b045aa8ff4e4.png) 2 | 3 | # Requirements 4 | [ox_inventory](https://github.com/overextended/ox_inventory) 5 | [ox_lib](https://github.com/overextended/ox_lib) 6 | [oxmysql](https://github.com/overextended/oxmysql) 7 | 8 | # Instructions 9 | Run the sql code in donator.sql to your database. 10 | 11 | Follow the Tebex instructions to get Tebex installed on your server. 12 | In your Tebex store on the packages you want coins to be added to do the following. 13 | At the bottom of the package select "Add Game Server Command" 14 | ![image](https://user-images.githubusercontent.com/7463741/193162239-df5c838a-63f4-4ac0-816f-0e783275026a.png) 15 | 16 | In the "When the package is purchased" section paste the following 17 | ``` 18 | donatorPurchase {"transactionId":"{transaction}", "package":"{packageName}"} 19 | ``` 20 | ![image](https://user-images.githubusercontent.com/7463741/193162202-93c9245d-c49e-4837-922c-53fe3a273c63.png) 21 | 22 | IMPORTANT 23 | The packagename in the config needs to MATCH the package name in tebex. 24 | So if you have a package named "coinpack1" in tebex then you need to have in the config. 25 | ``` 26 | ["coinpack1"] = CoinAmountHere, 27 | ``` 28 | 29 | Players can use the /redeem transactionId in game to have their coins added. 30 | -------------------------------------------------------------------------------- /client/client.lua: -------------------------------------------------------------------------------- 1 | local NPC = nil 2 | 3 | RegisterNetEvent("donator:createMenu", function() 4 | local options = {} 5 | 6 | local coins = lib.callback.await('donator:GetCoins', false) or 0 7 | 8 | options[#options + 1] = { 9 | title = ('Coins : %s'):format(coins), 10 | icon = 'star', 11 | iconColor = '#ffd43b', 12 | colorScheme = '#ffd43b', 13 | readOnly = true, 14 | } 15 | 16 | for id, data in pairs(config.shop) do 17 | 18 | options[#options + 1] = { 19 | title = data.title, 20 | description = data.description, 21 | serverEvent = 'donator:purchase', 22 | args = { 23 | id = id 24 | } 25 | } 26 | end 27 | 28 | 29 | lib.registerContext({ 30 | id = 'donator_store_menu', 31 | title = 'Donator Store', 32 | options = options, 33 | }) 34 | 35 | lib.showContext('donator_store_menu') 36 | end) 37 | 38 | local function onEnter() 39 | lib.requestModel(config.model) 40 | NPC = CreatePed(4, config.model, config.npc.x, config.npc.y, config.npc.z, config.npc.w, false, false) 41 | FreezeEntityPosition(NPC, true) 42 | SetEntityInvincible(NPC, true) 43 | SetBlockingOfNonTemporaryEvents(NPC, true) 44 | 45 | if config.anim then 46 | lib.requestAnimDict(config.anim.dict) 47 | TaskPlayAnim(NPC, config.anim.dict, config.anim.name, 8.0, 8.0, -1, 1, 0, false, false, false) 48 | end 49 | 50 | if config.scenario then 51 | TaskStartScenarioInPlace(NPC, config.scenario, 0, true) 52 | end 53 | 54 | exports.ox_target:addLocalEntity(NPC, { 55 | { 56 | label = 'Donator Store', 57 | icon = 'fa-solid fa-comment-dots', 58 | event = 'donator:createMenu', 59 | } 60 | }) 61 | end 62 | 63 | local function onExit() 64 | DeleteEntity(NPC) 65 | end 66 | 67 | lib.zones.sphere({ 68 | coords = config.npc, 69 | radius = 50.0, 70 | debug = false, 71 | onEnter = onEnter, 72 | onExit = onExit 73 | }) 74 | 75 | CreateThread(function() 76 | 77 | if config.blip.enabled then 78 | local blip = AddBlipForCoord(config.npc) 79 | SetBlipSprite(blip, config.blip.sprite) 80 | SetBlipScale(blip, config.blip.scale) 81 | SetBlipColour(blip, config.blip.color) 82 | BeginTextCommandSetBlipName("STRING") 83 | AddTextComponentSubstringPlayerName(config.blip.name) 84 | EndTextCommandSetBlipName(blip) 85 | end 86 | end) -------------------------------------------------------------------------------- /config/config.lua: -------------------------------------------------------------------------------- 1 | config = { 2 | 3 | framework = 'QBCore', -- Framework to use. (QBCore/QBX/SA) 4 | giveKeysEvent = 'qb-vehiclekeys:client:GiveKeys', -- Vehicle Keys Give Keys event Name. 5 | 6 | npc = vector4(185.29, -916.61, 29.69, 148.52), -- Location for the NPC to spawn. 7 | model = "cs_fbisuit_01", -- NPC Model. 8 | vehicleSpawn = vector4(180.45, -923.15, 30.10, 230.33), -- Location for car to spawn. 9 | garage = "garage1", -- Default garage for the car to be tagged too. 10 | 11 | blip = { 12 | enabled = true, 13 | sprite = 351, 14 | scale = 1.0, 15 | color = 50, 16 | name = "Donator Store", 17 | }, 18 | 19 | shop = { 20 | { 21 | type = "item", -- Item/Car 22 | name = "weapon_pistol", -- Name of Item. 23 | amount = 1, -- amount of items to give. 24 | cost = 1, -- Coin Cost to purchase item. 25 | title = "Buy Pistol", -- Title Text. 26 | description = "Buy pistol for 1 coin.", -- description Text. 27 | }, 28 | { 29 | type = "car", -- Item/Car 30 | name = "sultan", -- Model of the vehicle to spawn. 31 | cost = 1, -- Coin Cost to purchase item. 32 | title = "Buy Sultan", -- Title Text. 33 | description = "Buy a Sultan for 1 coin.", -- description Text. 34 | }, 35 | }, 36 | 37 | defaultVehicleMods = { 38 | engineHealth = 1000.0, 39 | bodyHealth = 1000.0, 40 | fuelLevel = 100.0, 41 | tankHealth = 1000.0, 42 | }, 43 | 44 | packages = { 45 | ["coinpack1"] = 100, -- Number of coins given on redemption. 46 | ["coinpack2"] = 200, -- Number of coins given on redemption. 47 | }, 48 | } -------------------------------------------------------------------------------- /donator.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS `donator`; 2 | CREATE TABLE `donator` ( 3 | `license` VARCHAR(255) NOT NULL COLLATE 'utf8_general_ci', 4 | `coins` INT(11) NOT NULL DEFAULT 0, 5 | UNIQUE INDEX `license` (`license`) USING BTREE 6 | ); 7 | 8 | DROP TABLE IF EXISTS `donator_pending`; 9 | CREATE TABLE `donator_pending` ( 10 | `transactionId` VARCHAR(50) NOT NULL COLLATE 'utf8_general_ci', 11 | `package` LONGTEXT NOT NULL COLLATE 'utf8_general_ci', 12 | `redeemed` INT(11) NOT NULL DEFAULT '0', 13 | PRIMARY KEY (`transactionId`) USING BTREE 14 | ); -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | fx_version 'cerulean' 2 | games { 'gta5' } 3 | lua54 'yes' 4 | version '2.0.0' 5 | 6 | author 'devyn' 7 | 8 | client_script 'client/client.lua' 9 | 10 | server_scripts { 11 | '@oxmysql/lib/MySQL.lua', 12 | "server/server.lua", 13 | } 14 | 15 | shared_scripts { 16 | '@ox_lib/init.lua', 17 | 'config/config.lua', 18 | } 19 | 20 | -------------------------------------------------------------------------------- /server/server.lua: -------------------------------------------------------------------------------- 1 | local string = lib.string 2 | 3 | local function GeneratePlate() 4 | local plate = lib.string.random('........'):upper() 5 | local result = MySQL.Sync.fetchScalar('SELECT plate FROM player_vehicles WHERE plate = ?', {plate}) 6 | if result then 7 | return GeneratePlate() 8 | else 9 | return plate:upper() 10 | end 11 | end 12 | 13 | local function GetCoins(license) 14 | local coins = MySQL.Sync.fetchScalar('SELECT coins FROM donator WHERE license = ?', { license }) 15 | if coins ~= nil then 16 | return coins 17 | else 18 | MySQL.Async.insert('INSERT INTO donator (license, coins) VALUES (?, 0)', { license }) 19 | return 0 20 | end 21 | end 22 | 23 | local function SetCoins(license, amount) 24 | local affectedRows = MySQL.update.await('UPDATE donator SET coins = ? WHERE license = ?', { amount, license }) 25 | if affectedRows then 26 | lib.logger(-1, 'set_coins', string.format("Set %s coins to %s", license, amount)) 27 | else 28 | MySQL.Async.insert('INSERT INTO donator (license, coins) VALUES (?, ?)', { license, amount }) 29 | lib.logger(-1, 'set_coins', string.format("Set %s coins to %s", license, amount)) 30 | end 31 | end 32 | 33 | local function AddCoins(license, amount) 34 | local coins = GetCoins(license) 35 | coins += amount 36 | 37 | local affectedRows = MySQL.update.await('UPDATE donator SET coins = ? WHERE license = ?', { coins, license }) 38 | if affectedRows then 39 | lib.logger(-1, 'add_coins', string.format("Added %s to %s", coins, license)) 40 | else 41 | MySQL.Async.insert('INSERT INTO donator (license, coins) VALUES (?, ?)', { license, coins }) 42 | lib.logger(-1, 'add_coins', string.format("Added %s to %s", coins, license)) 43 | end 44 | end 45 | 46 | local function RemoveCoins(license, amount) 47 | local coins = GetCoins(license) 48 | local total = (coins - amount) 49 | if total < 0 then 50 | return false 51 | else 52 | SetCoins(license, total) 53 | return true 54 | end 55 | end 56 | 57 | local function spawnVehicle(playerId, model, coords, props) 58 | 59 | local tempVehicle = CreateVehicle(model, 0, 0, -100.0, 0, true, true) 60 | while not DoesEntityExist(tempVehicle) do Wait(0) end 61 | 62 | local vehicleType = GetVehicleType(tempVehicle) 63 | DeleteEntity(tempVehicle) 64 | 65 | local veh = CreateVehicleServerSetter(model, vehicleType, coords.x, coords.y, coords.z, coords.w) 66 | while not DoesEntityExist(veh) do Wait(0) end 67 | while GetVehicleNumberPlateText(veh) == '' do Wait(0) end 68 | 69 | local state = Entity(veh).state 70 | state:set('initVehicle', true, true) 71 | state:set('setVehicleProperties', props, true) 72 | 73 | lib.waitFor(function() 74 | if state.setVehicleProperties then return false end 75 | return true 76 | end, 'Failed to set vehicle properties', 5000) 77 | 78 | SetPedIntoVehicle(playerId, veh, -1) 79 | 80 | local netId = NetworkGetNetworkIdFromEntity(veh) 81 | 82 | return netId, veh 83 | end 84 | 85 | lib.callback.register('donator:GetCoins', function(source) 86 | local license = GetPlayerIdentifierByType(source, 'license') 87 | local coins = GetCoins(license) 88 | return coins 89 | end) 90 | 91 | RegisterNetEvent("donator:purchase", function(data) 92 | local ID = data.id 93 | local src = source 94 | local coords = GetEntityCoords(GetPlayerPed(src)) 95 | 96 | if #(coords - vec(config.npc.x, config.npc.y, config.npc.z)) > 6.0 then -- Check to see if player is close to NPC 97 | print("Cheater tried to access the donator store from too far away! ID: " .. src) 98 | return 99 | end 100 | 101 | if ID == nil or config.shop[ID] == nil then -- Check to see if passed ID is actually a valid ID 102 | print("Cheater tried to access an invalid donator item! ID: " .. src) 103 | return 104 | end 105 | 106 | local license = GetPlayerIdentifierByType(src, 'license') 107 | 108 | if RemoveCoins(license, config.shop[ID].cost) then 109 | 110 | if config.shop[ID].type == "item" then 111 | exports.ox_inventory:AddItem(src, config.shop[ID].name, config.shop[ID].amount) 112 | elseif config.shop[ID].type == "car" then 113 | 114 | local cid = nil 115 | 116 | if config.framework == 'QBCore' then 117 | local QBCore = exports['qb-core']:GetCoreObject() 118 | local player = QBCore.Functions.GetPlayer(src) 119 | cid = player.PlayerData.citizenid 120 | elseif config.framework == 'QBX' then 121 | local player = exports.qbox_core:GetPlayer(src) 122 | cid = player.PlayerData.citizenid 123 | elseif config.framework == 'SA' then 124 | -- Custom get player here. 125 | end 126 | 127 | if cid == nil then 128 | print("Invalid Framework for Donator Store") 129 | return 130 | end 131 | 132 | 133 | local plate = GeneratePlate() 134 | MySQL.Async.insert('INSERT INTO player_vehicles (license, citizenid, vehicle, hash, mods, plate, state, garage) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', { 135 | license, 136 | cid, 137 | config.shop[ID].name, 138 | GetHashKey(config.shop[ID].name), 139 | json.encode(config.defaultVehicleMods), 140 | plate, 141 | 1, 142 | config.Garage, 143 | }) 144 | 145 | local netId, veh = spawnVehicle(src, GetHashKey(config.shop[ID].name), config.vehicleSpawn, config.defaultVehicleMods) 146 | SetVehicleNumberPlateText(veh, plate) 147 | 148 | TriggerClientEvent('qb-vehiclekeys:client:GiveKeys', src, plate) 149 | 150 | end 151 | else 152 | TriggerClientEvent("ox_lib:notify", src, { 153 | title = "Donator Store", 154 | description = "You do not have enough coins to purchase this item!", 155 | type = "error", 156 | }) 157 | end 158 | end) 159 | 160 | 161 | lib.addCommand('addcoins', { 162 | help = 'Give Player Coins (Admin Only)', 163 | params = { 164 | { 165 | name = 'target', 166 | type = 'playerId', 167 | help = 'Target player\'s server id', 168 | }, 169 | { 170 | name = 'amount', 171 | type = 'number', 172 | help = 'Number of the coins to give, or blank to give 1', 173 | }, 174 | }, 175 | restricted = 'group.admin' 176 | }, function(source, args, raw) 177 | 178 | local license = GetPlayerIdentifierByType(source, 'license') 179 | if license then 180 | AddCoins(license, tonumber(args.amount) or 1) 181 | else 182 | TriggerClientEvent("ox_lib:notify", source, { 183 | title = "Donator Store", 184 | description = "Invalid Player", 185 | type = "error", 186 | }) 187 | end 188 | 189 | end) 190 | 191 | lib.addCommand('setcoins', { 192 | help = 'Set Player Coins (Admin Only)', 193 | params = { 194 | { 195 | name = 'target', 196 | type = 'playerId', 197 | help = 'Target player\'s server id', 198 | }, 199 | { 200 | name = 'amount', 201 | type = 'number', 202 | help = 'Number of the coins to set', 203 | }, 204 | }, 205 | restricted = 'group.admin' 206 | }, function(source, args, raw) 207 | 208 | local license = GetPlayerIdentifierByType(source, 'license') 209 | if license then 210 | SetCoins(license, tonumber(args.amount)) 211 | else 212 | TriggerClientEvent("ox_lib:notify", source, { 213 | title = "Donator Store", 214 | description = "Invalid Player", 215 | type = "error", 216 | }) 217 | end 218 | 219 | end) 220 | 221 | -- REDEMPTION 222 | 223 | lib.addCommand('redeem', { 224 | help = 'Redeem tebex store purchase', 225 | params = { 226 | { 227 | name = 'id', 228 | type = 'string', 229 | help = 'tebex transaction id', 230 | }, 231 | }, 232 | restricted = 'group.user' 233 | }, function(source, args, raw) 234 | 235 | local transactionId = args.id 236 | local pending = MySQL.query.await('SELECT * FROM donator_pending WHERE transactionId = ? LIMIT 1', { transactionId }) 237 | if pending[1] then 238 | if pending[1].redeemed == 0 then 239 | local license = GetPlayerIdentifierByType(source, 'license') 240 | 241 | AddCoins(license, config.packages[pending[1].package]) 242 | lib.logger(-1, 'donator_redeem', string.format("License: %s redeemed %s", license, transactionId)) 243 | 244 | TriggerClientEvent("ox_lib:notify", source, { 245 | title = "Donator Store", 246 | description = "Purchase Redeemed", 247 | type = "success", 248 | }) 249 | 250 | MySQL.update.await('UPDATE donator_pending SET redeemed = 1 WHERE transactionId = ?', { transactionId }) 251 | else 252 | TriggerClientEvent("ox_lib:notify", source, { 253 | title = "Donator Store", 254 | description = "This package has already been redeemed", 255 | type = "error", 256 | }) 257 | end 258 | else 259 | TriggerClientEvent("ox_lib:notify", source, { 260 | title = "Donator Store", 261 | description = "Invalid transaction id", 262 | type = "error", 263 | }) 264 | end 265 | 266 | end) 267 | 268 | -- Console Tebex Command 269 | 270 | RegisterCommand("donatorPurchase", function(source, args) 271 | -- Only allow console to run this command. 272 | if source ~= 0 then return end 273 | print(string.format("New Donator Purchase Details: %s", json.encode(args))) 274 | local data = json.decode(args[1]) or {} 275 | 276 | if not data.transactionId then 277 | data.transactionId = args[1] 278 | data.package = args[2] 279 | end 280 | 281 | if not data.transactionId then 282 | print("Invalid purchase") 283 | return 284 | end 285 | 286 | local pending = MySQL.query.await('SELECT transactionId, redeemed FROM donator_pending WHERE transactionId = ?', { data.transactionId }) 287 | if pending[1] and pending[1].redeemed == 0 then 288 | local affectedRows = MySQL.update.await('UPDATE donator_pending SET package = ? WHERE transactionId = ?', { data.package, data.transactionId }) 289 | if affectedRows then 290 | lib.logger(-1, 'donatorPurchase', string.format("Added new pending redeem ID: %s Package: %s", data.transactionId, data.package)) 291 | else 292 | lib.logger(-1, 'donatorPurchase', string.format("Error adding redeem ID: %s Package: %s", data.transactionId, data.package)) 293 | end 294 | else 295 | MySQL.Async.insert('INSERT INTO donator_pending (transactionId, package) VALUES (?, ?)', { data.transactionId, data.package }) 296 | lib.logger(-1, 'donatorPurchase', string.format("Added ID: %s Package: %s", data.transactionId, data.package)) 297 | end 298 | end, false) --------------------------------------------------------------------------------