mirror of
https://github.com/jimathy/jim_bridge.git
synced 2026-08-17 14:06:02 +01:00
I unfortunately forgot to add this before when adding the extra exploit protection. Now demands an auth token before allowing crafting results to help reduce exploiters calling it externally
490 lines
21 KiB
Lua
490 lines
21 KiB
Lua
--[[
|
|
Crafting, Selling, and Shop Module
|
|
-----------------------------------
|
|
This module provides functions for opening crafting menus, handling multi-crafting,
|
|
performing the crafting process (with animations and progress bars), selling items,
|
|
and opening shop interfaces. It integrates with various inventory and menu systems,
|
|
and uses server callbacks to check item carry capacity.
|
|
]]
|
|
|
|
-------------------------------------------------------------
|
|
-- Global Variables
|
|
-------------------------------------------------------------
|
|
CraftLock = false
|
|
|
|
-- helper filter table for crafting menus
|
|
local excludeKeys = {
|
|
amount = true, metadata = true, description = true, info = true,
|
|
job = true, gang = true, oneUse = true, slot = true,
|
|
blueprintRef = true, craftingLevel = true, craftedItems = true,
|
|
hasCrafted = true, exp = true, anim = true, time = true,
|
|
}
|
|
|
|
-------------------------------------------------------------
|
|
-- Crafting Menu
|
|
-------------------------------------------------------------
|
|
|
|
--- Opens the crafting menu based on provided data.
|
|
--- Checks job restrictions, builds the recipe menu, and opens the menu.
|
|
---
|
|
--- @param data table Crafting menu configuration containing:
|
|
--- - craftable (`table`) Table with Header, Recipes, Anims, and (optionally) craftedItems.
|
|
--- - coords (`vector3`) The coordinates where the crafting menu is being opened.
|
|
--- - stashTable|stashName (`string\table`) Name(s) of the stash for checking item availability.
|
|
--- - job|gang (`string`) Job or gang requirements.
|
|
--- - onBack (optional): Function to call when returning.
|
|
---
|
|
--- @usage
|
|
--- ```lua
|
|
--- craftingMenu({
|
|
--- craftable = {
|
|
--- Header = "Weapon Crafting",
|
|
--- Recipes = {
|
|
--- [1] = {
|
|
--- ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 },
|
|
--- amount = 1,
|
|
--- },
|
|
--- -- More recipes...
|
|
--- },
|
|
--- Anims = {
|
|
--- animDict = "amb@prop_human_parking_meter@male@idle_a",
|
|
--- anim = "idle_a",
|
|
--- },
|
|
--- },
|
|
--- coords = vector3(100.0, 200.0, 300.0),
|
|
--- stashTable = "crafting_stash",
|
|
--- job = "mechanic",
|
|
--- onBack = function() print("Returning to previous menu") end,
|
|
--- })
|
|
function craftingMenu(data)
|
|
if CraftLock then return end
|
|
|
|
-- Job or gang check; exit if not authorized.
|
|
if (data.job or data.gang) and not jobCheck(data.job or data.gang) then return end
|
|
|
|
-- Display a temporary "thinking" notification.
|
|
if Config.System.Menu == "jim" then
|
|
triggerNotify(nil, "Thinking", "info")
|
|
else
|
|
openMenu({ { header = "Thinking...", icon = "fas fa-hourglass-end", isMenuHeader = true } }, { header = "Crafting Menu" } )
|
|
end
|
|
|
|
-- Normalize stash name.
|
|
data.stashName = data.stashTable or data.stashName
|
|
|
|
local Menu = {}
|
|
local Recipes = data.craftable.Recipes
|
|
local craftedItems = {}
|
|
local tempCarryTable = {}
|
|
|
|
-- Build a table of all required ingredients (default quantity is 1).
|
|
for i = 1, #Recipes do
|
|
for k in pairs(Recipes[i]) do
|
|
if k == "hasCrafted" and not data.craftable.craftedItems then
|
|
-- Retreive list of already crafted items from playermetadata to see if we should class this recipe as "new"
|
|
craftedItems = GetMetadata(nil, "craftedItems") or {}
|
|
data.craftable.craftedItems = craftedItems
|
|
end
|
|
if k ~= "amount" and k ~= "metadata" and k ~= "job" and k ~= "gang" then
|
|
tempCarryTable[k] = Recipes[i].amount or 1
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check if the player can carry the required items (server callback).
|
|
local canCarryTable = triggerCallback(getScript()..':server:canCarry', tempCarryTable)
|
|
-- Process each recipe to create menu entries.
|
|
for i = 1, #Recipes do
|
|
if not Recipes[i]["amount"] then Recipes[i]["amount"] = 1 end
|
|
for k, _ in pairs(Recipes[i]) do
|
|
if not excludeKeys[k] then
|
|
local hasjob = true
|
|
if Recipes[i].job then
|
|
for l, b in pairs(Recipes[i].job) do
|
|
hasjob = hasJob(l, nil, b)
|
|
if hasjob then break end
|
|
end
|
|
end
|
|
if hasjob then
|
|
local setheader, settext, disable, metadata = "", "", false, (Recipes[i]["metadata"] or Recipes[i]["info"] or nil)
|
|
local itemTable = {}
|
|
local metaTable = {}
|
|
-- Build ingredient details.
|
|
for l, b in pairs(Recipes[i][tostring(k)]) do
|
|
settext = settext..(settext ~= "" and br or "")..(Items[l] and Items[l].label or "error - "..l)..(b > 1 and " x"..b or "")
|
|
metaTable[Items[l] and Items[l].label or "error - "..l] = b
|
|
itemTable[l] = b
|
|
Wait(0)
|
|
end
|
|
|
|
while not canCarryTable do Wait(0) end
|
|
disable = not checkStashItem(data.stashName, itemTable)
|
|
setheader = ((metadata and metadata.label) or (Items[tostring(k)] and Items[tostring(k)].label) or "error - "..tostring(k))
|
|
..(Recipes[i]["amount"] > 1 and " x"..Recipes[i]["amount"] or "")
|
|
|
|
if not disable then
|
|
if not canCarryTable[k] then
|
|
setheader = setheader.." 📦"
|
|
else
|
|
setheader = setheader.." ✔️"
|
|
end
|
|
elseif not canCarryTable[k] then
|
|
setheader = setheader.." 📦"
|
|
end
|
|
if Recipes[i]["hasCrafted"] ~= nil and craftedItems[k] == nil then
|
|
setheader = "✨ "..setheader
|
|
end
|
|
|
|
Menu[#Menu + 1] = {
|
|
arrow = not disable and canCarryTable[k],
|
|
isMenuHeader = disable or not canCarryTable[k],
|
|
icon = invImg((metadata and metadata.image) or tostring(k)),
|
|
image = invImg((metadata and metadata.image) or tostring(k)),
|
|
header = setheader..((disable or not canCarryTable[k]) and " ❌" or ""),
|
|
txt = (isStarted(QBMenuExport) or disable) and settext or nil,
|
|
metadata = metaTable,
|
|
onSelect = (not disable and canCarryTable[k]) and function()
|
|
local transdata = {
|
|
item = k,
|
|
craft = data.craftable.Recipes[i],
|
|
craftable = data.craftable,
|
|
coords = data.coords,
|
|
stashName = data.stashName,
|
|
onBack = data.onBack,
|
|
metadata = metadata,
|
|
}
|
|
if Config.Crafting.MultiCraft then
|
|
multiCraft(transdata)
|
|
else
|
|
makeItem(transdata)
|
|
end
|
|
end or nil,
|
|
}
|
|
end
|
|
end
|
|
Wait(0)
|
|
end
|
|
end
|
|
|
|
openMenu(Menu, {
|
|
header = data.craftable.Header,
|
|
headertxt = data.craftable.Headertxt,
|
|
onBack = data.onBack or nil,
|
|
canClose = true,
|
|
onExit = data.onExit or (function() end),
|
|
})
|
|
lookEnt(data.coords)
|
|
end
|
|
|
|
-------------------------------------------------------------
|
|
-- Multi-Craft Menu
|
|
-------------------------------------------------------------
|
|
|
|
--- Opens a menu for selecting the quantity to craft.
|
|
---
|
|
--- Presents the player with multiple crafting quantities based on Config.Crafting.MultiCraftAmounts.
|
|
---
|
|
--- @param data table Crafting configuration containing:
|
|
--- - item `string`) The item to craft.
|
|
--- - craft (`table`) The crafting recipe.
|
|
--- - craftable (`table`) Crafting options.
|
|
--- - coords (`vector3`) where crafting occurs.
|
|
--- - stashName (`string`) The stash name(s) for item availability.
|
|
--- - onBack (`function`) Callback when returning.
|
|
--- - metadata (`table`) (optional): Metadata for the crafted item.
|
|
---
|
|
--- @usage
|
|
--- ```lua
|
|
--- multiCraft({
|
|
--- item = "weapon_pistol",
|
|
--- craft = { ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 }, amount = 1 },
|
|
--- craftable = craftingOptions,
|
|
--- coords = vector3(100,200,300),
|
|
--- stashName = "crafting_stash",
|
|
--- onBack = function() craftingMenu(data) end,
|
|
--- metadata = { label = "Custom Pistol", image = "custom_pistol.png" },
|
|
--- })
|
|
--- ```
|
|
function multiCraft(data)
|
|
local max = 0
|
|
local stashName = nil
|
|
for i = 1, 100 do
|
|
local itemTable = {}
|
|
for l, b in pairs(data.craft[data.item]) do
|
|
debugPrint("")
|
|
itemTable[l] = (b * i)
|
|
end
|
|
|
|
if data.stashName then
|
|
debugPrint("")
|
|
local hasItems, stashname = checkStashItem(data.stashName, itemTable)
|
|
if hasItems == true then
|
|
max += 1
|
|
stashName = stashname
|
|
else
|
|
break
|
|
end
|
|
else
|
|
debugPrint("")
|
|
local has, _ = hasItem(itemTable, nil, nil)
|
|
if has then
|
|
max += 1
|
|
else
|
|
break
|
|
end
|
|
end
|
|
Wait(10)
|
|
end
|
|
|
|
local dialog = createInput(data.craftable.Header, {
|
|
((Config.System.Menu == "ox") and {
|
|
type = "slider",
|
|
label = "How many to craft?",
|
|
required = true,
|
|
default = 1,
|
|
min = 1,
|
|
max = max
|
|
}) or nil,
|
|
((Config.System.Menu == "qb") and {
|
|
type = "number",
|
|
label = "How many to craft?"..br.."Max: "..max,
|
|
name = "amount",
|
|
isRecuired = true,
|
|
default = 1,
|
|
}) or nil,
|
|
})
|
|
|
|
if dialog then
|
|
if Config.System.Menu == "ox" then
|
|
|
|
end
|
|
if Config.System.Menu == "qb" then
|
|
if dialog["amount"] > max or dialog["amount"] < 1 or dialog["amount"] == nil or dialog["amount"] == "" then
|
|
triggerNotify(nil, "Invalid Amount", "error")
|
|
craftingMenu(data)
|
|
return
|
|
end
|
|
end
|
|
|
|
|
|
makeItem({
|
|
item = data.item,
|
|
craft = data.craft,
|
|
craftable = data.craftable,
|
|
amount = dialog["amount"] or dialog[1],
|
|
coords = data.coords,
|
|
stashName = stashName or nil,
|
|
--stashTable = data.stashName,
|
|
onBack = data.onBack,
|
|
metadata = data.metadata,
|
|
})
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------
|
|
-- Crafting Process
|
|
-------------------------------------------------------------
|
|
|
|
--- Initiates the crafting process for a specified item.
|
|
---
|
|
--- Plays crafting animations, shows progress bars, removes ingredients, and triggers item creation.
|
|
---
|
|
--- @param data table Crafting configuration containing:
|
|
--- - item `string`) The item to craft.
|
|
--- - craft (`table`) The crafting recipe.
|
|
--- - craftable (`table`) Crafting options.
|
|
--- - amount (`number`) (optional): Quantity to craft (default 1).
|
|
--- - coords (`vector3`) where crafting occurs.
|
|
--- - stashName (`string`) The stash name(s) for item availability.
|
|
--- - onBack (`function`) Callback when returning.
|
|
--- - metadata (`table`) (optional): Metadata for the crafted item.
|
|
---
|
|
--- @usage
|
|
--- ```lua
|
|
--- makeItem({
|
|
--- item = "weapon_pistol",
|
|
--- craft = { ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 }, amount = 1 },
|
|
--- craftable = craftingOptions,
|
|
--- amount = 2,
|
|
--- coords = vector3(100,200,300),
|
|
--- stashName = "crafting_stash",
|
|
--- onBack = function() craftingMenu(data) end,
|
|
--- metadata = { label = "Custom Pistol", image = "custom_pistol.png" },
|
|
--- })
|
|
--- ```
|
|
function makeItem(data)
|
|
if CraftLock then return end
|
|
CraftLock = true
|
|
data.stashName = data.stashTable or data.stashName
|
|
|
|
local bartime = (data.craftable.progressBar and data.craftable.progressBar.time) or 5000
|
|
local bartext = (data.craftable.progressBar and data.craftable.progressBar.label)
|
|
or (Loc[Config.Lan].progressbar and Loc[Config.Lan].progressbar["progress_make"])
|
|
or "Making "
|
|
local animDict = (data.craftable.Anims and data.craftable.Anims.animDict) or "amb@prop_human_parking_meter@male@idle_a"
|
|
local anim = (data.craftable.Anims and data.craftable.Anims.anim) or "idle_a"
|
|
local craftAmount = (data.amount and data.amount ~= 1) and data.amount or 1
|
|
local metadata = data.metadata or nil
|
|
local prop = data.craftable.Anims and data.craftable.Anims.prop or nil
|
|
local canReturn = true
|
|
|
|
local crafted, crafting = true, true
|
|
local cam = createTempCam(PlayerPedId(), data.coords)
|
|
startTempCam(cam)
|
|
|
|
for i = 1, craftAmount do
|
|
for k, v in pairs(data.craft) do
|
|
if not excludeKeys[k] then
|
|
if type(v) == "table" then
|
|
for l, b in pairs(v) do
|
|
if crafting and progressBar({
|
|
label = "Using "..b.." "..Items[l].label,
|
|
time = 1000,
|
|
cancel = true,
|
|
dict = 'pickup_object',
|
|
anim = "putdown_low",
|
|
flag = 48,
|
|
icon = l,
|
|
}) then
|
|
TriggerEvent((isStarted(QBInv) and QBInvNew and "qb-" or "")..'inventory:client:ItemBox', Items[l], "use", b)
|
|
else
|
|
crafted, crafting = false, false
|
|
break
|
|
end
|
|
Wait(200)
|
|
end
|
|
if crafted then
|
|
local craftProp = nil
|
|
if prop then
|
|
craftProp = makeProp({ prop = prop.model, coords = vec4(0, 0, 0, 0), true, true })
|
|
AttachEntityToEntity(craftProp, PlayerPedId(), GetPedBoneIndex(PlayerPedId(), prop.bone), prop.pos.x, prop.pos.y, prop.pos.z, prop.rot.x, prop.rot.y, prop.rot.z, true, true, false, true, 1, true)
|
|
end
|
|
if data.sound then
|
|
local s = data.sound
|
|
PlaySoundFromEntity(s.soundId, s.audioName, PlayerPedId(), s.audioRef, true, 0)
|
|
end
|
|
if crafting and progressBar({
|
|
label = bartext..((metadata and metadata.label) or Items[data.item].label),
|
|
time = bartime,
|
|
cancel = true,
|
|
dict = animDict,
|
|
anim = anim,
|
|
flag = 49,
|
|
icon = data.item,
|
|
request = true,
|
|
}) then
|
|
TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken)
|
|
currentToken = nil -- clear client cached token
|
|
CreateThread(function()
|
|
if data.craft["hasCrafted"] ~= nil then
|
|
debugPrint("hasCrafted Found, marking '"..data.item.."' as crafted for player")
|
|
data.craftable.craftedItems[data.item] = true
|
|
triggerCallback(getScript()..":server:SetMetadata", "craftedItems", data.craftable.craftedItems)
|
|
end
|
|
Wait(100)
|
|
if data.craft["exp"] ~= nil then
|
|
craftingLevel += data.craft["exp"].give
|
|
jsonPrint(data.craft["exp"])
|
|
debugPrint("exp found, giving exp for '"..data.item.."'")
|
|
triggerCallback(getScript()..":server:SetMetadata", "craftingLevel", craftingLevel)
|
|
end
|
|
end)
|
|
if data.craftable.Recipes[1].oneUse == true then
|
|
removeItem("craftrecipe", 1, nil, data.craftable.Recipes[1].slot)
|
|
local breakId = GetSoundId()
|
|
PlaySoundFromEntity(breakId, "Drill_Pin_Break", PlayerPedId(), "DLC_HEIST_FLEECA_SOUNDSET", 1, 0)
|
|
canReturn = false
|
|
end
|
|
if data.sound then
|
|
StopSound(data.sound.soundId)
|
|
end
|
|
if data.requiredItemfunc then
|
|
data.requiredItemfunc()
|
|
end
|
|
else
|
|
crafting = false
|
|
break
|
|
end
|
|
if craftProp then destroyProp(craftProp) end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
Wait(500)
|
|
end
|
|
stopTempCam()
|
|
CraftLock = false
|
|
lockInv(false)
|
|
if canReturn then craftingMenu(data) end
|
|
ClearPedTasks(PlayerPedId())
|
|
end
|
|
|
|
-------------------------------------------------------------
|
|
-- Server Event Handler: Crafted Item
|
|
-------------------------------------------------------------
|
|
|
|
--- Server event handler for giving the crafted item to the player.
|
|
---
|
|
--- Removes required ingredients from the player's inventory or stash,
|
|
--- then adds the crafted item to their inventory.
|
|
---
|
|
--- @param ItemMake string The item being crafted.
|
|
--- @param craftable table The crafting recipe and details.
|
|
--- @param stashName string|table The stash name(s) to remove ingredients from.
|
|
--- @param metadata table (optional) Metadata for the crafted item.
|
|
--- @usage
|
|
RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, stashName, metadata, token)
|
|
local src = source
|
|
debugPrint(GetInvokingResource())
|
|
if GetInvokingResource() and GetInvokingResource() ~= getScript() and GetInvokingResource() ~= "qb-core" then
|
|
debugPrint("^1Error^7: ^1Possible exploit^7, ^1vital function was called from an external resource^7")
|
|
return
|
|
end
|
|
|
|
if token == nil then
|
|
debugPrint("^1Auth^7: ^1No token recieved^7")
|
|
dupeWarn(src, item, "Auth: Player "..src.." attempted to spawn "..item.." without an auth token")
|
|
else
|
|
if token ~= validTokens[src] then
|
|
debugPrint("^1Auth^7: ^1Tokens don't match! ^7", token, validTokens[src])
|
|
dupeWarn(src, item, "Auth: "..src.." attempted to trigger server only events with an incorrect auth token")
|
|
else
|
|
debugPrint("^1Auth^7: ^2Client and Server Auth tokens match^7!", token, validTokens[src])
|
|
validTokens[src] = nil
|
|
end
|
|
end
|
|
|
|
|
|
local hasItems, hasTable = hasItem(ItemMake, 1, src)
|
|
if stashName then
|
|
local itemRemove = {}
|
|
if type(stashName) == "table" then
|
|
for _, name in pairs(stashName) do
|
|
stashItems = getStash(name)
|
|
for k, v in pairs(craftable[ItemMake] or {}) do
|
|
for _, b in pairs(stashItems or {}) do
|
|
if k == b.name then itemRemove[k] = v end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
stashItems = getStash(stashName)
|
|
for k, v in pairs(craftable[ItemMake] or {}) do
|
|
for _, b in pairs(stashItems or {}) do
|
|
if k == b.name then itemRemove[k] = v end
|
|
end
|
|
end
|
|
end
|
|
stashRemoveItem(stashItems, stashName, itemRemove)
|
|
else
|
|
if craftable then
|
|
for k, v in pairs(craftable[ItemMake] or {}) do
|
|
removeItem(tostring(k), v, src)
|
|
end
|
|
end
|
|
end
|
|
addItem(ItemMake, craftable.amount or 1, metadata, src)
|
|
-- Optionally, add experience here:
|
|
-- for example:
|
|
-- if isStarted("core_skills") then exports["core_skills"]:AddExperience(src, 2) end
|
|
end) |