.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ultimate system empowering you to effortlessly create and manage a multitude of personalized vending machines whenever you desire.
6 |
7 | # Dependency
8 |
9 | - **[oxmysql](https://github.com/overextended/oxmysql)**
10 | - **[ox_lib](https://github.com/overextended/ox_lib)**
11 | - **[ox_inventory](https://github.com/overextended/ox_inventory)**
12 | - **[ESX Legacy](https://github.com/esx-framework/esx-legacy) / [qb-core](https://github.com/qbcore-framework/qb-core)**
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Visitor count
23 |
--------------------------------------------------------------------------------
/bridge/esx/client.lua:
--------------------------------------------------------------------------------
1 | if not IsESX() then return end
2 |
3 | local ESX = exports['es_extended']:getSharedObject()
4 |
5 | RegisterNetEvent('esx:playerLoaded')
6 | AddEventHandler('esx:playerLoaded', function(xPlayer)
7 | PlayerData = xPlayer
8 | PlayerLoaded = true
9 | SetupVendings()
10 | end)
11 |
12 |
13 | RegisterNetEvent('esx:setJob')
14 | AddEventHandler('esx:setJob', function(job)
15 | PlayerData.job = job
16 | end)
17 |
18 |
19 | RegisterNetEvent('esx:onPlayerLogout', function()
20 | RemovePoints()
21 | table.wipe(PlayerData)
22 | PlayerLoaded = false
23 | end)
24 |
25 |
26 |
27 | function GetIdentifier()
28 | return PlayerData.identifier
29 | end
30 |
31 |
32 | function GetJob()
33 | return PlayerData.job.name, PlayerData.job.grade
34 | end
35 |
36 | AddEventHandler('onResourceStart', function(resource)
37 | if cache.resource == resource then
38 | Wait(1500)
39 | PlayerData = ESX.GetPlayerData()
40 | PlayerLoaded = true
41 | SetupVendings()
42 | end
43 | end)
--------------------------------------------------------------------------------
/bridge/esx/server.lua:
--------------------------------------------------------------------------------
1 | if not IsESX() then return end
2 |
3 | local ESX = exports['es_extended']:getSharedObject()
4 |
5 | function GetAllPlayers()
6 | return ESX.GetExtendedPlayers()
7 | end
8 |
9 | function GetPlayerFromId(id)
10 | return ESX.GetPlayerFromId(id)
11 | end
12 |
13 | function GetJobs()
14 | return ESX.GetJobs()
15 | end
16 |
17 | function GetJob(id)
18 | local xPlayer = ESX.GetPlayerFromId(id)
19 |
20 | if xPlayer then
21 | return xPlayer.job.name, xPlayer.job.grade
22 | end
23 | end
24 |
25 | function GetIdentifier(id)
26 | local xPlayer = ESX.GetPlayerFromId(id)
27 |
28 | if xPlayer then
29 | return xPlayer.identifier
30 | end
31 |
32 | return false
33 | end
--------------------------------------------------------------------------------
/bridge/framework.lua:
--------------------------------------------------------------------------------
1 | PlayerData = {}
2 | PlayerLoaded = false
3 |
4 | function IsESX()
5 | return GetResourceState("es_extended") ~= "missing"
6 | end
7 |
8 | function IsQBCore()
9 | return GetResourceState("qb-core") ~= "missing"
10 | end
--------------------------------------------------------------------------------
/bridge/qb/client.lua:
--------------------------------------------------------------------------------
1 | if not IsQBCore() then return end
2 |
3 | local QBCore = exports['qb-core']:GetCoreObject()
4 |
5 | AddEventHandler('QBCore:Client:OnPlayerLoaded', function()
6 | PlayerData = QBCore.Functions.GetPlayerData()
7 | PlayerLoaded = true
8 | SetupVendings()
9 | end)
10 |
11 |
12 | RegisterNetEvent('QBCore:Client:OnPlayerUnload', function()
13 | RemovePoints()
14 | table.wipe(PlayerData)
15 | PlayerLoaded = false
16 | end)
17 |
18 |
19 | RegisterNetEvent('QBCore:Player:SetPlayerData', function(val)
20 | PlayerData = val
21 | end)
22 |
23 |
24 | function GetIdentifier()
25 | return PlayerData.citizenid
26 | end
27 |
28 | function GetJob()
29 | return PlayerData.job.name, PlayerData.job.grade.level
30 | end
31 |
32 | AddEventHandler('onResourceStart', function(resource)
33 | if resource == cache.resource then
34 | Wait(1500)
35 | PlayerData = QBCore.Functions.GetPlayerData()
36 | PlayerLoaded = true
37 | SetupVendings()
38 | end
39 | end)
40 |
--------------------------------------------------------------------------------
/bridge/qb/server.lua:
--------------------------------------------------------------------------------
1 | if not IsQBCore() then return end
2 |
3 | local QBCore = exports['qb-core']:GetCoreObject()
4 |
5 | function GetAllPlayers()
6 | return QBCore.Functions.GetQBPlayers()
7 | end
8 |
9 | function GetPlayerFromId(id)
10 | return QBCore.Functions.GetPlayer(id)
11 | end
12 |
13 | function GetJobs()
14 | return QBCore.Shared.Jobs
15 | end
16 |
17 | function GetJob(id)
18 | local Player = QBCore.Functions.GetPlayer(id)
19 |
20 | if Player then
21 | return Player.PlayerData.job.name, Player.PlayerData.job.grade.level
22 | end
23 | end
24 |
25 | function GetIdentifier(id)
26 | local Player = QBCore.Functions.GetPlayer(id)
27 |
28 | if Player then
29 | return Player.PlayerData.citizenid
30 | end
31 |
32 | return false
33 | end
--------------------------------------------------------------------------------
/client/main.lua:
--------------------------------------------------------------------------------
1 | local cfg = lib.require('config.config')
2 | local raycast = lib.require('client.raycast')
3 | local Points, Blips = {}, {}
4 |
5 |
6 | function RemovePoints()
7 | for k,v in pairs(Points) do
8 | if v.entity then
9 | if DoesEntityExist(v.entity) then
10 | SetEntityAsMissionEntity(v.entity, false, true)
11 | DeleteEntity(v.entity)
12 | end
13 | end
14 |
15 | v:remove()
16 | Points[k] = nil
17 | end
18 | end
19 |
20 | local function onEnter(point)
21 | if not point.entity then
22 | local model = lib.requestModel(point.model)
23 | if not model then return end
24 |
25 | local entity = CreateObject(model, point.coords.x, point.coords.y, point.coords.z, false, true, true)
26 |
27 | SetModelAsNoLongerNeeded(model)
28 | SetEntityHeading(entity, point.heading)
29 | PlaceObjectOnGroundProperly(entity)
30 | FreezeEntityPosition(entity, true)
31 |
32 | point.entity = entity
33 | end
34 | end
35 |
36 | local function onExit(point)
37 | local entity = point.entity
38 |
39 | if entity then
40 | if DoesEntityExist(entity) then
41 | SetEntityAsMissionEntity(entity, false, true)
42 | DeleteEntity(entity)
43 | end
44 |
45 | point.entity = nil
46 | end
47 | end
48 |
49 | local menu = {
50 | id = 'uniq_vending:main',
51 | title = L('context.vending_title'),
52 | options = {}
53 | }
54 |
55 | function GenerateMenu(point)
56 | local options = {
57 | {
58 | icon = 'fas fa-shopping-basket',
59 | title = L('context.access_vending'),
60 | onSelect = function()
61 | exports.ox_inventory:openInventory('shop', { type = point.label, id = 1})
62 | end,
63 | distance = 2.0
64 | }
65 | }
66 |
67 | if not point.owner or point.owner == false then
68 | options[#options + 1] = {
69 | title = L('context.buy_vending'),
70 | icon = 'fa-solid fa-dollar-sign',
71 | onSelect = function ()
72 | local alert = lib.alertDialog({
73 | header = L('context.buy_vending'),
74 | content = L('alert.buy_vending_confirm'):format(point.price),
75 | centered = true,
76 | cancel = true
77 | })
78 |
79 | if alert == 'confirm' then
80 | TriggerServerEvent('uniq_vending:buyVending', point.label)
81 | end
82 | end
83 | }
84 | end
85 |
86 | -- owned by player
87 | if type(point.owner) == 'string' then
88 | if point.owner == GetIdentifier() then
89 | options[#options+1] = {
90 | title = L('context.sell_vending'),
91 | icon = 'fa-solid fa-money-bill-trend-up',
92 | onSelect = function()
93 | local alert = lib.alertDialog({
94 | header = L('context.sell_vending'),
95 | content = L('alert.sell_vending_confirm'):format(math.floor(point.price * cfg.SellPertencage)),
96 | centered = true,
97 | cancel = true
98 | })
99 |
100 | if alert == 'confirm' then
101 | TriggerServerEvent('uniq_vending:sellVending', point.label)
102 | end
103 | end
104 | }
105 |
106 | options[#options + 1] = {
107 | title = L('context.stock'),
108 | icon = 'fa-solid fa-box',
109 | onSelect = function()
110 | exports.ox_inventory:openInventory('stash', ('%s'):format(point.label))
111 | end
112 | }
113 |
114 | options[#options + 1] = {
115 | title = L('context.money'),
116 | icon = 'fa-solid fa-sack-dollar',
117 | onSelect = function()
118 | exports.ox_inventory:openInventory('stash', ('stash-money-%s'):format(point.label))
119 | end
120 | }
121 | end
122 | -- owned by job
123 | elseif type(point.owner) == 'table' then
124 | local job, grade = GetJob()
125 |
126 | if point.owner[job] and grade >= point.owner[job] then
127 | options[#options+1] = {
128 | title = L('context.sell_vending'),
129 | icon = 'fa-solid fa-money-bill-trend-up',
130 | onSelect = function()
131 | local alert = lib.alertDialog({
132 | header = L('context.sell_vending'),
133 | content = L('alert.sell_vending_confirm'):format(math.floor(point.price * cfg.SellPertencage)),
134 | centered = true,
135 | cancel = true
136 | })
137 |
138 | if alert == 'confirm' then
139 | TriggerServerEvent('uniq_vending:sellVending', point.label)
140 | end
141 | end
142 | }
143 |
144 | options[#options + 1] = {
145 | title = L('context.stock'),
146 | icon = 'fa-solid fa-box',
147 | onSelect = function()
148 | exports.ox_inventory:openInventory('stash', ('%s'):format(point.label))
149 | end
150 | }
151 |
152 | options[#options + 1] = {
153 | title = L('context.money'),
154 | icon = 'fa-solid fa-sack-dollar',
155 | onSelect = function()
156 | exports.ox_inventory:openInventory('stash', ('stash-money-%s'):format(point.label))
157 | end
158 | }
159 | end
160 | end
161 |
162 | menu.options = options
163 |
164 | lib.registerContext(menu)
165 |
166 | return lib.showContext(menu.id)
167 | end
168 |
169 | local function nearby(point)
170 | if point.currentDistance < 1.4 then
171 | if IsControlJustPressed(0, 38) then
172 | GenerateMenu(point)
173 | end
174 | end
175 | end
176 |
177 | local textUI = false
178 | CreateThread(function()
179 | while true do
180 | local point = lib.points.getClosestPoint()
181 |
182 | if point then
183 | if point.currentDistance < 1.4 then
184 | if not textUI then
185 | textUI = true
186 | lib.showTextUI(L('text_ui.open_vending'))
187 | end
188 | else
189 | if textUI then
190 | textUI = false
191 | lib.hideTextUI()
192 | end
193 | end
194 | end
195 |
196 | Wait(300)
197 | end
198 | end)
199 |
200 |
201 | local function createBlip(data, coords)
202 | local blip = AddBlipForCoord(coords.x, coords.y, coords.z)
203 |
204 | SetBlipSprite (blip, data.sprite)
205 | SetBlipDisplay(blip, 4)
206 | SetBlipScale (blip, data.scale)
207 | SetBlipColour (blip, data.colour)
208 | SetBlipAsShortRange(blip, true)
209 |
210 | BeginTextCommandSetBlipName('STRING')
211 | AddTextComponentSubstringPlayerName(data.name)
212 | EndTextCommandSetBlipName(blip)
213 |
214 | Blips[#Blips + 1] = blip
215 | end
216 |
217 | function SetupVendings()
218 | for k,v in pairs(Blips) do
219 | RemoveBlip(v)
220 | Blips[k] = nil
221 | end
222 | local data = lib.callback.await('uniq_vending:fetchVendings', false)
223 |
224 | if data then
225 | for k,v in pairs(data) do
226 | Points[#Points + 1] = lib.points.new({
227 | coords = v.coords,
228 | distance = 15.0,
229 | onEnter = onEnter,
230 | nearby = nearby,
231 | onExit = onExit,
232 | label = v.name,
233 | owner = v.owner,
234 | model = v.obj,
235 | price = v.price,
236 | heading = v.heading
237 | })
238 |
239 | if v.blip then
240 | createBlip(v.blip, v.coords)
241 | end
242 | end
243 | end
244 | end
245 |
246 | RegisterNetEvent('uniq_vending:sync', function(data, clear)
247 | if source == '' then return end
248 |
249 | for k,v in pairs(Blips) do
250 | RemoveBlip(v)
251 | Blips[k] = nil
252 | end
253 |
254 | if clear then RemovePoints() end
255 |
256 | Wait(200)
257 |
258 | for k,v in pairs(data) do
259 | Points[#Points + 1] = lib.points.new({
260 | coords = v.coords,
261 | distance = 15.0,
262 | onEnter = onEnter,
263 | nearby = nearby,
264 | onExit = onExit,
265 | label = v.name,
266 | owner = v.owner,
267 | model = v.obj,
268 | price = v.price,
269 | heading = v.heading
270 | })
271 |
272 | if v.blip then
273 | createBlip(v.blip, v.coords)
274 | end
275 | end
276 | end)
277 |
278 | RegisterNetEvent('uniq_vending:startCreating', function(players)
279 | if source == '' then return end
280 | local vending = {}
281 |
282 | local input = lib.inputDialog(L('input.vending_creator'), {
283 | { type = 'input', label = L('input.vending_label'), required = true },
284 | { type = 'number', label = L('input.vending_price'), required = true, min = 1 },
285 | { type = 'select', label = L('input.select_object'), required = true, options = cfg.Machines, clearable = true },
286 | { type = 'select', label = L('input.owned_type.title'), options = {
287 | { label = L('input.owned_type.a'), value = 'a' },
288 | { label = L('input.owned_type.b'), value = 'b' },
289 | }, clearable = true, required = true },
290 | { type = 'checkbox', label = L('input.blip'), checked = true }
291 | })
292 |
293 | if not input then return end
294 |
295 | if input[5] then
296 | local blip = lib.inputDialog('', {
297 | { type = 'number', label = L('input.blipInput.a'), required = true },
298 | { type = 'number', label = L('input.blipInput.b'), required = true, precision = true, min = 0 },
299 | { type = 'number', label = L('input.blipInput.c'), required = true },
300 | { type = 'input', label = L('input.blipInput.d'), required = true, default = input[1] },
301 | })
302 |
303 | if not blip then return end
304 |
305 | vending.blip = {
306 | sprite = blip[1],
307 | scale = blip[2],
308 | colour = blip[3],
309 | name = blip[4],
310 | }
311 | end
312 |
313 | vending.name = input[1]
314 | vending.price = input[2]
315 | vending.obj = input[3]
316 |
317 | if input[4] == 'a' then
318 | table.sort(players, function (a, b)
319 | return a.id < b.id
320 | end)
321 |
322 | local owner = lib.inputDialog(L('input.vending_creator'), {
323 | { type = 'select', label = L('input.player_owned_label'), description = L('input.player_owned_desc'), options = players, clearable = true }
324 | })
325 |
326 | if not owner then
327 | vending.owner = false
328 | else
329 | vending.owner = owner[1]
330 | end
331 | vending.type = 'player'
332 | elseif input[4] == 'b' then
333 | local jobs = lib.callback.await('uniq_vending:getJobs', 100)
334 |
335 | table.sort(jobs, function (a, b)
336 | return a.label < b.label
337 | end)
338 |
339 | local owner = lib.inputDialog(L('input.vending_creator'), {
340 | { type = 'select', label = L('input.job_owned_label'), description = L('input.job_owned_desc'), options = jobs, clearable = true }
341 | }, {allowCancel = false})
342 |
343 | if not owner[1] then
344 | vending.owner = false
345 | else
346 | local grades = lib.callback.await('uniq_vending:getGrades', 100, owner[1])
347 |
348 | table.sort(grades, function (a, b)
349 | return a.value < b.value
350 | end)
351 |
352 | local grade = lib.inputDialog(L('input.vending_creator'), {
353 | { type = 'select', label = L('input.chose_grade'), description = L('input.chose_grade_desc'), required = true, options = grades, clearable = true }
354 | })
355 |
356 | if not grade then return end
357 |
358 | vending.owner = { [owner[1]] = grade[1] }
359 | end
360 | vending.type = 'job'
361 | end
362 |
363 | lib.showTextUI(table.concat(L('text_ui.help')))
364 | local heading = 0
365 | local obj
366 | local created = false
367 |
368 | lib.requestModel(vending.obj)
369 |
370 | CreateThread(function ()
371 | while true do
372 | ---@diagnostic disable-next-line: need-check-nil
373 | local hit, coords, entity = raycast(100.0)
374 |
375 | if not created then
376 | created = true
377 | obj = CreateObject(vending.obj , coords.x, coords.y, coords.z, false, false, false)
378 | end
379 |
380 | if IsControlPressed(0, 174) then
381 | heading += 1.5
382 | end
383 |
384 | if IsControlPressed(0, 175) then
385 | heading -= 1.5
386 | end
387 |
388 | if IsDisabledControlPressed(0, 176) then
389 | lib.hideTextUI()
390 | vending.coords = coords
391 | vending.heading = GetEntityHeading(obj)
392 | DeleteObject(obj)
393 | TriggerServerEvent('uniq_vending:createVending', vending)
394 |
395 | break
396 | end
397 |
398 | local pedPos = GetEntityCoords(cache.ped)
399 | local distance = #(coords - pedPos)
400 |
401 | if distance >= 3.5 then
402 | SetEntityCoords(obj, coords.x, coords.y, coords.z)
403 | SetEntityHeading(obj, heading)
404 | end
405 |
406 |
407 | Wait(0)
408 | end
409 | end)
410 |
411 | collectgarbage("collect")
412 | end)
413 |
414 |
415 | RegisterNetEvent('uniq_vending:client:dellvending', function(data)
416 | if source == '' then return end
417 |
418 | table.sort(data, function (a, b)
419 | return a.value < b.value
420 | end)
421 |
422 | local input = lib.inputDialog('Delete Vending', {
423 | { type = 'select', label = L('input.job_owned_label'), required = true, clearable = true, options = data }
424 | })
425 |
426 | if not input then return end
427 |
428 | TriggerServerEvent('uniq_vending:server:dellvending', input[1])
429 | end)
430 |
431 |
432 | RegisterNetEvent('uniq_vending:selectCurrency', function(payload)
433 | if source == '' then return end
434 | local itemNames = {}
435 |
436 | for item, data in pairs(exports.ox_inventory:Items()) do
437 | if not cfg.CantBeCurrency[data.name] then
438 | itemNames[#itemNames + 1] = { label = data.label, value = data.name }
439 | end
440 | end
441 |
442 | table.sort(itemNames, function (a, b)
443 | return a.label < b.label
444 | end)
445 |
446 | local input = lib.inputDialog(payload.fromSlot.label, {
447 | { type = 'number', label = L('context.item_price_per_one'), required = true, min = 1 },
448 | { type = 'select', label = L('context.currency'), description = L('context.currency_desc'), clearable = true, options = itemNames }
449 | }, { allowCancel = false })
450 |
451 | if not input[2] then
452 | input[2] = 'money'
453 | end
454 |
455 | TriggerServerEvent('uniq_vending:setData', input[1], input[2], payload)
456 | end)
457 |
458 | lib.callback.register('uniq_vending:choseVending', function(options)
459 | local input = lib.inputDialog('', { { type = 'select', label = 'Chose Vending', required = true, options = options} })
460 | if not input then return false end
461 |
462 | return input[1]
463 | end)
464 |
465 | AddEventHandler('onResourceStop', function(name)
466 | if name == cache.resource then
467 | RemovePoints()
468 | if textUI then
469 | lib.hideTextUI()
470 | end
471 | end
472 | end)
473 |
--------------------------------------------------------------------------------
/client/raycast.lua:
--------------------------------------------------------------------------------
1 | -- https://github.com/Risky-Shot/new_banking/blob/main/new_banking/client/client.lua
2 |
3 | local function RotationToDirection(rotation)
4 | local adjustedRotation =
5 | {
6 | x = (math.pi / 180) * rotation.x,
7 | y = (math.pi / 180) * rotation.y,
8 | z = (math.pi / 180) * rotation.z
9 | }
10 | local direction =
11 | {
12 | x = -math.sin(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)),
13 | y = math.cos(adjustedRotation.z) * math.abs(math.cos(adjustedRotation.x)),
14 | z = math.sin(adjustedRotation.x)
15 | }
16 |
17 | return direction
18 | end
19 |
20 | local function RayCastGamePlayCamera(distance)
21 | local cameraRotation = GetGameplayCamRot(0)
22 | local cameraCoord = GetGameplayCamCoord()
23 | local direction = RotationToDirection(cameraRotation)
24 | local destination =
25 | {
26 | x = cameraCoord.x + direction.x * distance,
27 | y = cameraCoord.y + direction.y * distance,
28 | z = cameraCoord.z + direction.z * distance
29 | }
30 | local a, b, c, d, e = GetShapeTestResult(StartShapeTestRay(cameraCoord.x, cameraCoord.y, cameraCoord.z, destination.x, destination.y, destination.z, -1, cache.ped, 0))
31 |
32 | return b, c, e
33 | end
34 |
35 | return RayCastGamePlayCamera
--------------------------------------------------------------------------------
/config/config.lua:
--------------------------------------------------------------------------------
1 | return {
2 | Locale = 'en',
3 |
4 | Machines = {
5 | { label = 'prop_vend_coffe_01', value = 'prop_vend_coffe_01' },
6 | { label = 'prop_vend_water_01', value = 'prop_vend_water_01' },
7 | -- you can add more
8 | },
9 |
10 |
11 | -- jobs that cant buy vending, if chosen type is "owned by job"
12 | BlacklsitedJobs = {
13 | ['unemployed'] = true,
14 | },
15 |
16 | -- items that cant be put for sale
17 | BlacklistedItems = {
18 | ['money'] = true,
19 | ['black_money'] = true,
20 | },
21 |
22 | -- items that cant be used as currency
23 | CantBeCurrency = {
24 | --['water'] = true
25 | },
26 |
27 | SellPertencage = 0.70 -- 70% of original price
28 | }
--------------------------------------------------------------------------------
/fxmanifest.lua:
--------------------------------------------------------------------------------
1 | --[[ FX Information ]] --
2 | fx_version 'cerulean'
3 | use_experimental_fxv2_oal 'yes'
4 | lua54 'yes'
5 | game 'gta5'
6 | author 'uniq-team'
7 | repository 'https://github.com/uniqscripts/uniq_vendingmachine'
8 | version '1.3.1'
9 |
10 |
11 | dependencies {
12 | '/server:6116',
13 | '/onesync',
14 | 'oxmysql',
15 | 'ox_lib',
16 | 'ox_inventory'
17 | }
18 |
19 | shared_scripts {
20 | '@ox_lib/init.lua',
21 | 'config/config.lua',
22 | 'locales.lua',
23 | 'locales/*.lua',
24 | 'bridge/framework.lua',
25 | }
26 | server_scripts {
27 | '@oxmysql/lib/MySQL.lua',
28 | 'bridge/**/server.lua',
29 | 'server/*'
30 | }
31 | client_scripts {
32 | 'bridge/**/client.lua',
33 | 'client/*'
34 | }
--------------------------------------------------------------------------------
/locales.lua:
--------------------------------------------------------------------------------
1 | Locales = {}
2 | local lang = lib.require('config.config').Locale
3 |
4 | function L(key)
5 | local value = Locales[lang]
6 |
7 | for k in key:gmatch("[^.]+") do
8 | value = value[k]
9 |
10 | if not value then
11 | print("Missing locale for: " .. key)
12 | return ""
13 | end
14 | end
15 |
16 | return value
17 | end
--------------------------------------------------------------------------------
/locales/en.lua:
--------------------------------------------------------------------------------
1 | Locales['en'] = {
2 | commands = {
3 | addvending = 'Command that helps you create ownable vending machine',
4 | dellvending = 'Command that helps you to delete vendings',
5 | findvending = 'Command that teleports you to desired vending'
6 | },
7 | notify = {
8 | not_enough_money = 'You don\'t have enough money to buy this Vending Machine ($%s)',
9 | vending_bought = 'You have bought Vending Machine "%s" for $%s',
10 | vending_sold = 'You have sold Vending Machine "%s" for $%s',
11 | no_targeted_owner = 'Could not find targeted owner',
12 | vending_created = 'You have created Vending Machine "%s". Price: $%s',
13 | cant_put = 'You cant\'t put this item for sale',
14 | no_vendings = 'There is no created vendings'
15 | },
16 | context = {
17 | vending_title = 'Vending Machine',
18 | vending_settings = 'Vending Settings',
19 | money = 'Money',
20 | update_stock = 'Update Stock',
21 | stock_price = 'Stock: %s | Price: $%s',
22 | item_price = 'Item Price',
23 | item_stock = 'Item Stock',
24 | item_currency = 'Item to be used as currency',
25 | items = 'Items',
26 | buy_vending = 'Buy Vending Machine',
27 | sell_vending = 'Sell Vending Machine',
28 | manage_vending = 'Manage Vending Machine',
29 | access_vending = 'Access Vending',
30 | item_price_per_one = 'Price per item',
31 | currency = 'Currency',
32 | currency_desc = 'If you leave empty then currency will be money',
33 | stock = 'Vending Stock'
34 | },
35 | alert = {
36 | buy_vending_confirm = 'Would you like to buy this Vending Machine for $%s?',
37 | sell_vending_confirm = 'Would you like to send this Vending Machine for $%s?',
38 | },
39 | input = {
40 | vending_creator = 'Vending Machine Creator',
41 | vending_label = 'Label',
42 | vending_price = 'Price',
43 | select_object = 'Select Object',
44 | blip = 'Add Blip?',
45 | blipInput = {
46 | a = 'Sprite',
47 | b = 'Scale',
48 | c = 'Colour',
49 | d = 'Blip Name',
50 | },
51 | owned_type = {
52 | title = 'Chose Type',
53 | desc = 'If you leave input empty then vending will go for sale',
54 | a = 'Owned by player',
55 | b = 'Owned by job'
56 | },
57 | player_owned_label = 'Chose Player',
58 | player_owned_desc = 'If you leave input empty then vending will go for sale',
59 | job_owned_label = 'Chose Job',
60 | job_owned_desc = 'If you leave input empty then vending will go for sale, if type is owned by job then when player buys vending it will apply to his job',
61 | chose_grade = 'Chose Grade',
62 | chose_grade_desc = 'Minimal grade that will have access vending managment'
63 | },
64 | text_ui = {
65 | help = {
66 | ('By moving your mouse you are moving the object \n'),
67 | ('[←] - Rotate the object left \n'),
68 | ('[→] - Rotate the object right \n'),
69 | ('[ENTER] - Finish \n'),
70 | },
71 | open_vending = '[E] - Open Vending',
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/server/main.lua:
--------------------------------------------------------------------------------
1 | local success, msg = lib.checkDependency('oxmysql', '2.7.3')
2 |
3 | if success then
4 | success, msg = lib.checkDependency('ox_lib', '3.10.0')
5 | end
6 |
7 | ---@diagnostic disable-next-line: param-type-mismatch
8 | if not success then return warn(msg) end
9 |
10 |
11 | local cfg = lib.require('config.config')
12 | local Vending = {}
13 | local buyHook, swapHook
14 |
15 |
16 | RegisterNetEvent('uniq_vending:setData', function(price, currency, payload)
17 | payload.fromSlot.metadata.price = price
18 | payload.fromSlot.metadata.currency = currency
19 |
20 | exports.ox_inventory:SetMetadata(payload.toInventory, payload.toSlot, payload.fromSlot.metadata)
21 | Wait(200)
22 | local items = exports.ox_inventory:GetInventoryItems(payload.toInventory, false)
23 | local inventory = {}
24 |
25 | if items then
26 | for k,v in pairs(items) do
27 | inventory[#inventory + 1] = { name = v.name, metadata = v.metadata, price = v.metadata.price or 999999, currency = v.metadata.currency, count = v.count }
28 | end
29 |
30 | exports.ox_inventory:RegisterShop(payload.toInventory, {
31 | name = payload.toInventory,
32 | inventory = inventory,
33 | })
34 | end
35 | end)
36 |
37 | RegisterNetEvent('uniq_vending:SetUpStore', function(store)
38 | Wait(300)
39 | local items = exports.ox_inventory:GetInventoryItems(store, false)
40 | local inventory = {}
41 |
42 | for k,v in pairs(items) do
43 | inventory[#inventory + 1] = { name = v.name, metadata = v.metadata, price = v.metadata.price or 999999, currency = v.metadata.currency, count = v.count }
44 | end
45 |
46 | exports.ox_inventory:RegisterShop(store, {
47 | name = store,
48 | inventory = inventory,
49 | })
50 | end)
51 |
52 | local function SetUpHooks(inventoryFilter)
53 | if buyHook then exports.ox_inventory:removeHooks(buyHook) end
54 | if swapHook then exports.ox_inventory:removeHooks(swapHook) end
55 |
56 | buyHook = exports.ox_inventory:registerHook('buyItem', function(payload)
57 | payload.metadata.price = nil
58 | payload.metadata.currency = nil
59 |
60 | exports.ox_inventory:RemoveItem(payload.shopType, payload.itemName, payload.count)
61 | exports.ox_inventory:AddItem(('stash-money-%s'):format(payload.shopType), payload.currency, payload.totalPrice)
62 |
63 | return true
64 | end, { inventoryFilter = inventoryFilter })
65 |
66 | swapHook = exports.ox_inventory:registerHook('swapItems', function(payload)
67 | if payload.toType == 'stash' then
68 | if cfg.BlacklistedItems[payload.fromSlot.name] then
69 | lib.notify(payload.source, { description = L('notify.cant_put'), type = 'error' })
70 |
71 | return false
72 | end
73 |
74 | TriggerClientEvent('uniq_vending:selectCurrency', payload.source, payload)
75 | else
76 | TriggerEvent('uniq_vending:SetUpStore', payload.fromInventory)
77 | end
78 |
79 | return true
80 | end, { inventoryFilter = inventoryFilter })
81 | end
82 |
83 | local function RegisterStash(name)
84 | exports.ox_inventory:RegisterStash(('stash-money-%s'):format(name), name, 100, 100000)
85 | exports.ox_inventory:RegisterStash(('%s'):format(name), name, 100, 100000)
86 | end
87 |
88 | MySQL.ready(function()
89 | Wait(1000) -- to not fetch old data from sql
90 | local success, error = pcall(MySQL.scalar.await, 'SELECT 1 FROM `uniq_vending`')
91 |
92 | if not success then
93 | MySQL.query([[
94 | CREATE TABLE IF NOT EXISTS `uniq_vending` (
95 | `name` varchar(50) DEFAULT NULL,
96 | `data` longtext DEFAULT NULL
97 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
98 | ]])
99 | end
100 |
101 | Wait(100)
102 |
103 | local result = MySQL.query.await('SELECT * FROM `uniq_vending`')
104 |
105 | if result[1] then
106 | local inventoryFilter = {}
107 |
108 | for k,v in pairs(result) do
109 | local data = json.decode(v.data)
110 |
111 | RegisterStash(data.name)
112 |
113 | Vending[v.name] = data
114 |
115 | table.insert(inventoryFilter, v.name)
116 |
117 | TriggerEvent('uniq_vending:SetUpStore', v.name)
118 | end
119 |
120 | SetUpHooks(inventoryFilter)
121 | end
122 | end)
123 |
124 | lib.callback.register('uniq_vending:fetchVendings', function(source)
125 | return Vending
126 | end)
127 |
128 | lib.addCommand('addvending', {
129 | help = L('commands.addvending'),
130 | restricted = 'group.admin'
131 | }, function(source, args, raw)
132 | if source == 0 then return end
133 | local options = {}
134 |
135 | local players = GetAllPlayers()
136 |
137 | if IsQBCore() then
138 | for k,v in pairs(players) do
139 | options[#options + 1] = { label = ('%s | %s'):format(v.PlayerData.name, v.PlayerData.source), value = v.PlayerData.citizenid, id = v.PlayerData.source}
140 | end
141 | elseif IsESX() then
142 | for k,v in pairs(players) do
143 | options[#options + 1] = { label = ('%s | %s'):format(v.getName(), v.source), value = v.identifier, id = v.source }
144 | end
145 | end
146 |
147 | TriggerClientEvent('uniq_vending:startCreating', source, options)
148 | end)
149 |
150 |
151 | lib.addCommand('dellvending', {
152 | help = L('commands.dellvending'),
153 | restricted = 'group.admin'
154 | }, function(source, args, raw)
155 | if source == 0 then return end
156 | local options = {}
157 | local count = 0
158 |
159 | for k,v in pairs(Vending) do
160 | count += 1
161 | end
162 |
163 | if count == 0 then
164 | return lib.notify(source, { description = L('notify.no_vendings'), type = 'error' })
165 | end
166 |
167 | for k,v in pairs(Vending) do
168 | options[#options + 1] = { label = v.name, value = v.name }
169 | end
170 |
171 | TriggerClientEvent('uniq_vending:client:dellvending', source, options)
172 | end)
173 |
174 |
175 | lib.addCommand('findvending', {
176 | help = L('commands.findvending'),
177 | restricted = 'group.admin'
178 | }, function(source, args, raw)
179 | if source == 0 then return end
180 | local options = {}
181 | local count = 0
182 |
183 | for k,v in pairs(Vending) do
184 | count += 1
185 | end
186 |
187 | if count == 0 then
188 | return lib.notify(source, { description = L('notify.no_vendings'), type = 'error' })
189 | end
190 |
191 | for k,v in pairs(Vending) do
192 | options[#options + 1] = { label = v.name, value = v.name }
193 | end
194 |
195 | local cb = lib.callback.await('uniq_vending:choseVending', source, options)
196 |
197 | if cb then
198 | if Vending[cb] then
199 | local coords = Vending[cb].coords
200 | local ped = GetPlayerPed(source)
201 | SetEntityCoords(ped, coords.x, coords.y + 1, coords.z, false, false , false, false)
202 | end
203 | end
204 | end)
205 |
206 | RegisterNetEvent('uniq_vending:server:dellvending', function(shop)
207 | if Vending[shop] then
208 | MySQL.query('DELETE FROM `uniq_vending` WHERE `name` = ?', { shop })
209 |
210 | exports.ox_inventory:ClearInventory(('stash-money-%s'):format(shop))
211 | exports.ox_inventory:ClearInventory(('%s'):format(shop))
212 |
213 | Vending[shop] = nil
214 | TriggerClientEvent('uniq_vending:sync', -1, Vending, true)
215 | end
216 | end)
217 |
218 | RegisterNetEvent('uniq_vending:buyVending', function(name)
219 | local src = source
220 |
221 | if Vending[name] then
222 | if exports.ox_inventory:Search(src, 'count', 'money') >= Vending[name].price then
223 | exports.ox_inventory:RemoveItem(src, 'money', Vending[name].price)
224 |
225 | if Vending[name].type == 'player' then
226 | local identifier = GetIdentifier(src)
227 | Vending[name].owner = identifier
228 | elseif Vending[name].type == 'job' then
229 | local job, grade = GetJob(src)
230 |
231 | Vending[name].owner = { [job] = grade }
232 | end
233 | MySQL.update('UPDATE `uniq_vending` SET `data` = ? WHERE `name` = ?', {json.encode(Vending[name], {sort_keys = true}), name})
234 | TriggerClientEvent('uniq_vending:sync', -1, Vending, true)
235 | lib.notify(src, { description = L('notify.vending_bought'):format(Vending[name].name, Vending[name].price), type = 'success' })
236 | else
237 | lib.notify(src, { description = L('notify.not_enough_money'):format(Vending[name].price), type = 'error' })
238 | end
239 | end
240 | end)
241 |
242 | RegisterNetEvent('uniq_vending:sellVending', function(name)
243 | local src = source
244 |
245 | if Vending[name] then
246 | local price = math.floor(Vending[name].price * cfg.SellPertencage)
247 |
248 | exports.ox_inventory:AddItem(src, 'money', price)
249 | Vending[name].owner = false
250 |
251 | MySQL.update('UPDATE `uniq_vending` SET `data` = ? WHERE `name` = ?', {json.encode(Vending[name], {sort_keys = true}), name})
252 | TriggerClientEvent('uniq_vending:sync', -1, Vending, true)
253 | lib.notify(src, { description = L('notify.vending_sold'):format(Vending[name].name, price), type = 'success' })
254 | end
255 | end)
256 |
257 | RegisterNetEvent('uniq_vending:createVending', function(data)
258 | local src = source
259 |
260 | MySQL.insert('INSERT INTO `uniq_vending` (name, data) VALUES (?, ?)', {data.name, json.encode(data, {sort_keys = true})})
261 |
262 | RegisterStash(data.name)
263 | lib.notify(src, { description = L('notify.vending_created'):format(data.name, data.price), type = 'success' })
264 |
265 | Vending[data.name] = data
266 |
267 | local inventoryFilter = {}
268 |
269 | for k,v in pairs(Vending) do
270 | table.insert(inventoryFilter, v.name)
271 | end
272 |
273 |
274 | TriggerEvent('uniq_vending:SetUpStore', data.name)
275 |
276 | SetUpHooks(inventoryFilter)
277 |
278 | TriggerClientEvent('uniq_vending:sync', -1, Vending, false)
279 | end)
280 |
281 | lib.callback.register('uniq_vending:getJobs', function(source)
282 | local jobs = GetJobs()
283 | local options = {}
284 |
285 | if IsESX() then
286 | for k,v in pairs(jobs) do
287 | if not cfg.BlacklsitedJobs[k] then
288 | options[#options + 1] = { label = v.label, value = k }
289 | end
290 | end
291 | elseif IsQBCore() then
292 | for k,v in pairs(jobs) do
293 | if not cfg.BlacklsitedJobs[k] then
294 | options[#options + 1] = { label = v.label, value = k }
295 | end
296 | end
297 | end
298 |
299 | return options
300 | end)
301 |
302 | lib.callback.register('uniq_vending:getGrades', function(source, job)
303 | local jobs = GetJobs()
304 | local options = {}
305 |
306 | if IsESX() then
307 | for k,v in pairs(jobs[job].grades) do
308 | options[#options + 1] = { label = v.label, value = v.grade }
309 | end
310 | elseif IsQBCore() then
311 | for k,v in pairs(jobs[job].grades) do
312 | options[#options + 1] = { label = v.name, value = tonumber(k) }
313 | end
314 | end
315 |
316 | return options
317 | end)
318 |
319 | local function saveDB()
320 | local insertTable = {}
321 | if table.type(Vending) == 'empty' then return end
322 |
323 | for k,v in pairs(Vending) do
324 | insertTable[#insertTable + 1] = { query = 'UPDATE `uniq_vending` SET `data` = ? WHERE `name` = ?', values = { json.encode(v, {sort_keys = true} ), v.name } }
325 | end
326 |
327 | MySQL.transaction(insertTable)
328 | end
329 |
330 | AddEventHandler('onResourceStop', function(name)
331 | if name == cache.resource then
332 | exports.ox_inventory:removeHooks(buyHook)
333 | exports.ox_inventory:removeHooks(swapHook)
334 | saveDB()
335 | end
336 | end)
337 |
338 |
339 | lib.versionCheck('uniqscripts/uniq_vendingmachine')
--------------------------------------------------------------------------------