From 178040348eaf505e388db3f83c21b90692b041dc Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 27 Sep 2025 12:34:46 +0100 Subject: [PATCH 01/13] Rework `SingleProgress` slightly Realised it wasn't doing the math to take the correct amount of required items, so this fixes it also if `SingleProgress` is enabled, it won't show the "Ingredient Progress bars" any more --- shared/crafting.lua | 77 +++++++++++++++++++++++------------------- shared/itemcontrol.lua | 18 +++++----- 2 files changed, 51 insertions(+), 44 deletions(-) diff --git a/shared/crafting.lua b/shared/crafting.lua index 0aaa24d..afb901e 100644 --- a/shared/crafting.lua +++ b/shared/crafting.lua @@ -323,6 +323,7 @@ end --- ``` function makeItem(data) + local Ped = PlayerPedId() if CraftLock then return end CraftLock = true data.stashName = data.stashTable or data.stashName @@ -339,40 +340,42 @@ function makeItem(data) local canReturn = true local crafted, crafting = true, true - local cam = createTempCam(PlayerPedId(), data.coords) + local cam = createTempCam(Ped, data.coords) startTempCam(cam) -- Calculate total bartime if SingleProgress is enabled - local totalBartime = bartime * craftAmount + local totalBartime = (bartime * craftAmount) - -- Run ingredient check and usage separately first - for i = 1, craftAmount do - for k, v in pairs(data.craft) do - if not excludeKeys[k] and type(v) == "table" then - for l, b in pairs(v) do - if isInventoryOpen() then - print("^1Error^7: ^2Inventory is open, you tried to break things") - stopTempCam() - ClearPedTasks(PlayerPedId()) - if canReturn then craftingMenu(data) end - CraftLock = false - return + if not Config.Crafting.SingleProgress then -- if SingleProgress is disabled, dont do ingredient progressbars + -- Run ingredient check and usage separately first + for i = 1, craftAmount do + for k, v in pairs(data.craft) do + if not excludeKeys[k] and type(v) == "table" then + for l, b in pairs(v) do + if isInventoryOpen() then + print("^1Error^7: ^2Inventory is open, you tried to break things") + stopTempCam() + ClearPedTasks(Ped) + if canReturn then craftingMenu(data) end + CraftLock = false + return + end + if crafting and progressBar({ + label = "Using "..b.." "..getItemLabel(l), + time = 800, + cancel = true, + dict = 'pickup_object', + anim = "putdown_low", + flag = 49, + 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 crafting and progressBar({ - label = "Using "..b.." "..getItemLabel(l), - time = 1000, - cancel = true, - dict = 'pickup_object', - anim = "putdown_low", - flag = 49, - 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 end end @@ -380,7 +383,7 @@ function makeItem(data) if not crafted then stopTempCam() - ClearPedTasks(PlayerPedId()) + ClearPedTasks(Ped) if canReturn then craftingMenu(data) end CraftLock = false return @@ -391,11 +394,11 @@ function makeItem(data) 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) + AttachEntityToEntity(craftProp, Ped, GetPedBoneIndex(Ped, 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) + PlaySoundFromEntity(s.soundId, s.audioName, Ped, s.audioRef, true, 0) end if crafting and progressBar({ label = bartext..((metadata and metadata.label) or getItemLabel(data.item)).." x"..craftAmount, @@ -408,6 +411,10 @@ function makeItem(data) request = true, }) then data.craft.amount = craftAmount + for k, v in pairs(data.craft[data.item]) do + -- multiply igredient requirement in sent crafting table for removal + data.craft[data.item][k] = (v * craftAmount) + end TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken) currentToken = nil -- clear client cached token -- handle metadata and experience in a single go @@ -422,7 +429,7 @@ function makeItem(data) 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) + PlaySoundFromEntity(breakId, "Drill_Pin_Break", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0) canReturn = false end if data.sound then @@ -459,7 +466,7 @@ function makeItem(data) 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) + PlaySoundFromEntity(breakId, "Drill_Pin_Break", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0) canReturn = false end if data.requiredItemfunc then @@ -475,7 +482,7 @@ function makeItem(data) stopTempCam() CraftLock = false if canReturn then craftingMenu(data) end - ClearPedTasks(PlayerPedId()) + ClearPedTasks(Ped) end diff --git a/shared/itemcontrol.lua b/shared/itemcontrol.lua index 698c59d..d4ae691 100644 --- a/shared/itemcontrol.lua +++ b/shared/itemcontrol.lua @@ -2390,17 +2390,17 @@ if isServer() then end RegisterNetEvent(getScript()..":openGrabBox", function(data) - if isStarted(OXInv) then - return - end - local id = "" - if data.metadata then - id = data.metadata.id - elseif data.info then - id = data.info.id - end + local Ped = PlayerPedId() + local id = data.metadata and data.metadata.id or data.info and data.info.id or "" + + if isStarted(OXInv) then + + return + end + openStash({ stash = id, + coords = GetEntityCoords(Ped) }) end) From bb176addbf1354493ca0b3bf83052cff078a7142 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 27 Sep 2025 12:35:56 +0100 Subject: [PATCH 02/13] Create `setStress` function This allows per framework stress functions to be set, including ESX For ESX the math is done in this function as their values are insanely high instead of 0 - 100 --- shared/playerfunctions.lua | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/shared/playerfunctions.lua b/shared/playerfunctions.lua index 1c50c52..8cc5615 100644 --- a/shared/playerfunctions.lua +++ b/shared/playerfunctions.lua @@ -29,6 +29,14 @@ local hungerThirstFunc = { Player.Functions.SetMetaData('hunger', newHunger) TriggerClientEvent("hud:client:UpdateNeeds", src, newHunger, Player.PlayerData.metadata.thirst) end, + setStress = + function(src, stress) + local Player = Core.Functions.GetPlayer(src) + local newStress = Player.PlayerData.metadata.stress + stress + newStress = (newStress >= 100.0 and 100.0)or (newStress <= 0 and 0) or newStress + Player.Functions.SetMetaData('stress', newStress) + TriggerClientEvent('hud:client:UpdateStress', src, newStress) + end, }, { framework = QBExport, @@ -48,6 +56,14 @@ local hungerThirstFunc = { Player.Functions.SetMetaData('hunger', newHunger) TriggerClientEvent("hud:client:UpdateNeeds", src, newHunger, Player.PlayerData.metadata.thirst) end, + setStress = + function(src, stress) + local Player = Core.Functions.GetPlayer(src) + local newStress = Player.PlayerData.metadata.stress + stress + newStress = (newStress >= 100.0 and 100.0)or (newStress <= 0 and 0) or newStress + Player.Functions.SetMetaData('stress', newStress) + TriggerClientEvent('hud:client:UpdateStress', src, newStress) + end, }, { framework = RSGExport, @@ -67,6 +83,14 @@ local hungerThirstFunc = { Player.Functions.SetMetaData('hunger', newHunger) TriggerClientEvent("hud:client:UpdateNeeds", src, newHunger, Player.PlayerData.metadata.thirst) end, + setStress = + function(src, stress) + local Player = Core.Functions.GetPlayer(src) + local newStress = Player.PlayerData.metadata.stress + stress + newStress = (newStress >= 100.0 and 100.0) or (newStress <= 0 and 0) or newStress + Player.Functions.SetMetaData('stress', newStress) + TriggerClientEvent('hud:client:UpdateStress', src, newStress) + end, }, { framework = ESXExport, @@ -78,6 +102,10 @@ local hungerThirstFunc = { function(src, hunger) TriggerClientEvent('esx_status:add', src, 'hunger', tonumber(hunger) * 10000) end, + setStress = + function(src, stress) + TriggerClientEvent("esx_status:add", src, "stress", tonumber(stress) * 10000) + end, }, } @@ -126,6 +154,26 @@ function setHunger(src, hunger) end end +--- Sets the player's stress level. +--- +--- @param src number The player's server ID. +--- @param stress number The new stress level. +--- +--- @usage +--- ```lua +--- setStress(playerId, 60) +--- ``` +function setStress(src, stress) + for i = 1, #hungerThirstFunc do + local framework = hungerThirstFunc[i] + if isStarted(framework.framework) then + debugPrint("^4Debug^7: ^2Adding ^3"..framework.framework.." ^2stress^7: ^3"..stress.." ^2to player^7: ^3"..src.."^7") + framework.setStress(src, stress) + return + end + end +end + --- Server event handler for urinal usage. --- Decreases player's thirst by a random amount. RegisterNetEvent(getScript()..":server:Urinal", function() From 145e3aa1568ed291b80009f25d6b5b7a7d6e1d07 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sat, 27 Sep 2025 13:01:09 +0100 Subject: [PATCH 03/13] add new module `ropeControl.lua` **Needs work** but adds the ability to easily create ropes/wires from one coord to another --- shared/make/ropeControl.lua | 255 ++++++++++++++++++++++++++++++++++++ starter.lua | 1 + 2 files changed, 256 insertions(+) create mode 100644 shared/make/ropeControl.lua diff --git a/shared/make/ropeControl.lua b/shared/make/ropeControl.lua new file mode 100644 index 0000000..29c3f7c --- /dev/null +++ b/shared/make/ropeControl.lua @@ -0,0 +1,255 @@ +if not isServer() then + local RopeCache = { + nextId = 1, + byId = {}, + byHandle = {}, + } + + function loadRopeTextures() + RopeLoadTextures() + local tries = 0 + while not RopeAreTexturesLoaded() and tries < 200 do + Wait(0) + tries += 1 + end + return RopeAreTexturesLoaded() + end + + function maybeUnloadTextures() + local anyLeft = false + local all = GetAllRopes() + if all and type(all) == "table" then + for _, _ in ipairs(all) do anyLeft = true break end + end + if not anyLeft then + RopeUnloadTextures() + end + end + + function registerRope(ropeHandle, startCoords, endCoords, opts) + if not ropeHandle or ropeHandle == 0 then return nil end + local id = RopeCache.nextId + RopeCache.nextId = id + 1 + + local rec = { + handle = ropeHandle, + startCoords = startCoords, + endCoords = endCoords, + type = opts.ropeType, + rate = opts.lengthChangeRate, + breakable = opts.breakable, + createdAt = GetGameTimer() + } + RopeCache.byId[id] = rec + RopeCache.byHandle[ropeHandle] = id + debugPrint("^5Debug^7: 2Registered Rope^7'^3"..id.."^7' - '^3"..ropeHandle.."^7'") + return id + end + + function resolve(id) + if type(id) ~= "number" then return nil, "rope id must be a number" end + local rec = RopeCache.byId[id] + if not rec then return nil, "rope id not found" end + if not DoesRopeExist(rec.handle) then + -- stale; clean mapping + RopeCache.byHandle[rec.handle] = nil + RopeCache.byId[id] = nil + return nil, "rope no longer exists" + end + return rec + end + + --- Create a rope pinned between two coordinates. + --- @param startCoords vector3|table start coord + --- @param endCoords vector3|table end coord + --- @param opts table|nil { ropeType=0..7, breakable=false, collision=true, lockFromFront=false, lengthChangeRate=1.0, timeMultiplier=1.0, slack=0.0, preset=nil } + --- @return integer ropeId + function ropeCreateLine(startCoords, endCoords, opts) + + startCoords = type(startCoords) == "number" and GetEntityCoords(startCoords) or startCoords + endCoords = type(endCoords) == "number" and GetEntityCoords(endCoords) or endCoords + + opts = opts or {} + + local length = #(startCoords - endCoords) + local mid = (startCoords + endCoords) * 0.5 + + local ropeType = opts.ropeType or 1 -- 0..7 (see ropedata.xml types) + local initLength = length + (opts.slack or 0.0) + local maxLength = initLength -- allow droop equal to init + local minLength = 0.0 + local lengthChangeRate = opts.lengthChangeRate or 1.0 + local collisionOn = (opts.collision ~= false) + local lockFromFront = (opts.lockFromFront == true) + local timeMultiplier = opts.timeMultiplier or 1.0 + local breakable = (opts.breakable == true) + + local rope = AddRope(mid.x, mid.y, mid.z, 0.0, 0.0, 0.0, maxLength, ropeType, initLength, minLength, lengthChangeRate, false, collisionOn, lockFromFront, timeMultiplier, breakable, 0) + + if not rope or rope == 0 or not DoesRopeExist(rope) then + debugPrint("^1AddRope failed") + return nil + end + + -- Optional preset (e.g., "ropeFamily3") if provided. + if opts.preset then + -- Best-effort; if invalid it just won't change. + pcall(function() LoadRopeData(rope, tostring(opts.preset)) end) + end + + ActivatePhysics(rope) + + -- Pin both ends to the given coords. + local count = GetRopeVertexCount(rope) + if count and count >= 2 then + PinRopeVertex(rope, 0, startCoords.x, startCoords.y, startCoords.z) + PinRopeVertex(rope, count - 1, endCoords.x, endCoords.y, endCoords.z) + end + + local id = registerRope(rope, startCoords, endCoords, { + ropeType = ropeType, + lengthChangeRate = lengthChangeRate, + breakable = breakable + }) + + return id + end + + --- Delete and unregister a rope. + --- @param id integer + function ropeDelete(id) + local rec, err = resolve(id) + if not rec then return false, err end + local rope = rec.handle + if DoesRopeExist(rope) then + DeleteRope(rope) + end + RopeCache.byHandle[rope] = nil + RopeCache.byId[id] = nil + debugPrint(("deleted rope id=%s"):format(id)) + maybeUnloadTextures() + return true + end + + --- Force rope to a specific length (instant snap). + --- @param id integer + --- @param newLength number + function ropeSetLength(id, newLength) + local rec, err = resolve(id) + if not rec then return false, err end + RopeForceLength(rec.handle, newLength) + return true + end + + --- Smoothly wind the rope (shorten over time). + --- @param id integer + function ropeStartWinding(id) + local rec, err = resolve(id) + if not rec then return false, err end + StartRopeWinding(rec.handle) + return true + end + + --- Stop winding (if winding). + function ropeStopWinding(id) + local rec, err = resolve(id) + if not rec then return false, err end + StopRopeWinding(rec.handle) + return true + end + + --- Smoothly unwind the rope from the front (lengthen over time). + function ropeStartUnwinding(id) + local rec, err = resolve(id) + if not rec then return false, err end + StartRopeUnwindingFront(rec.handle) + return true + end + + --- Stop unwinding (if unwinding). + function ropeStopUnwinding(id) + local rec, err = resolve(id) + if not rec then return false, err end + StopRopeUnwindingFront(rec.handle) + return true + end + + --- Change the wind speed (length change rate). + --- @param id integer + --- @param rate number (units per sec; try 0.5..5.0) + function ropeSetRate(id, rate) + local rec, err = resolve(id) + if not rec then return false, err end + SetRopeLengthChangeRate(rec.handle, rate) + rec.rate = rate + return true + end + + --- Move end endpoint to a new coordinate (re-pins the vertex). + --- @param id integer + --- @param pos vector3|table + function ropeMoveEnd(id, pos) + local rec, err = resolve(id) + if not rec then return false, err end + pos = type(pos) == "number" and GetEntityCoords(pos) or pos + local rope = rec.handle + local count = GetRopeVertexCount(rope) + if not count or count < 2 then return false, "invalid vertex count" end + PinRopeVertex(rope, count - 1, pos.x, pos.y, pos.z) + rec.endCoord = pos + return true + end + + --- Move start endpoint to a new coordinate (re-pins the vertex). + --- @param id integer + --- @param pos vector3|table + function ropeMoveStart(id, pos) + local rec, err = resolve(id) + if not rec then return false, err end + pos = type(pos) == "number" and GetEntityCoords(pos) or pos + local rope = rec.handle + local count = GetRopeVertexCount(rope) + if not count or count < 2 then return false, "invalid vertex count" end + PinRopeVertex(rope, 0, pos.x, pos.y, pos.z) + rec.startCoord = pos + return true + end + + --- Re-pin both endpoints in one call. + function ropeSetEnds(ropeId, startCoord, endCoord) + startCoord = type(startCoord) == "number" and GetEntityCoords(startCoord) or startCoord + endCoord = type(endCoord) == "number" and GetEntityCoords(endCoord) or endCoord + local rec, err = resolve(ropeId) + if not rec then return false, err end + local rope = rec.handle + local count = GetRopeVertexCount(rope) + if not count or count < 2 then + return false, "invalid vertex count" + end + PinRopeVertex(rope, 0, startCoord.x, startCoord.y, startCoord.z) + PinRopeVertex(rope, count - 1, endCoord.x, endCoord.y, endCoord.z) + rec.startCoord = startCoord + rec.endCoord = endCoord + return true + end + + function ropeExists(id) + local rec = RopeCache.byId[id] + return rec and DoesRopeExist(rec.handle) or false + end + + onResourceStart(function() + loadRopeTextures() + end) + + onResourceStop(function() + for id, rec in pairs(RopeCache.byId) do + if rec.handle and DoesRopeExist(rec.handle) then + DeleteRope(rec.handle) + end + RopeCache.byHandle[rec.handle] = nil + RopeCache.byId[id] = nil + end + maybeUnloadTextures() + end) +end \ No newline at end of file diff --git a/starter.lua b/starter.lua index 5a89cdd..1607f15 100644 --- a/starter.lua +++ b/starter.lua @@ -176,6 +176,7 @@ for _, v in pairs({ -- This is a specific load order 'scaleEntity.lua', 'vehicles.lua', 'effects.lua', + 'make/ropeControl.lua', --'warmenu.lua', '_authToken.lua', From 37f7eb11165e846d9f1f7213b4261216d2c686fb Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sun, 28 Sep 2025 03:22:12 +0100 Subject: [PATCH 04/13] add testing code for new crafting table style --- shared/crafting.lua | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/shared/crafting.lua b/shared/crafting.lua index afb901e..851f616 100644 --- a/shared/crafting.lua +++ b/shared/crafting.lua @@ -17,7 +17,7 @@ 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, + hasCrafted = true, exp = true, anim = true, time = true, id = true, } ------------------------------------------------------------- @@ -72,6 +72,25 @@ function craftingMenu(data) -- Normalize stash name. data.stashName = data.stashTable or data.stashName + -- Wrapper to convert new style crafting recipes to be handled by the menu properly + --if not data.craftTable.Recipes[1] then -- in theory if not a numbered table its the new style + -- local compatTable = {} + -- for k, v in pairs(data.craftTable.Recipes) do + -- for l, b in pairs(v) do + -- if not excludeKeys[l] then + -- compatTable[data.craftTable.Recipes[k].info.id or #compatTable+1] = { + -- [l] = v.ingredients, + -- amount = v.info and v.info.amount or 1, + -- metadata = v.info and v.info.metadata or nil, + -- job = v.info and v.info.job or nil, + -- gang = v.info and v.info.gang or nil + -- } + -- end + -- end + -- end + -- data.craftTable.Recipes = compatTable + --end + local Menu = {} local Recipes = data.craftable.Recipes local craftedItems = {} @@ -80,7 +99,7 @@ function craftingMenu(data) -- 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 ~= "amount" and k ~= "metadata" and k ~= "job" and k ~= "gang" then + if k ~= "amount" and k ~= "metadata" and k ~= "job" and k ~= "gang" and k == "id" then tempCarryTable[k] = Recipes[i].amount or 1 end end From 45eeaf8f0dc1040049961a9eba1cbb84132f6f06 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sun, 28 Sep 2025 13:48:17 +0100 Subject: [PATCH 05/13] update a couple debug prints --- shared/itemcontrol.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/itemcontrol.lua b/shared/itemcontrol.lua index d4ae691..eddf6ab 100644 --- a/shared/itemcontrol.lua +++ b/shared/itemcontrol.lua @@ -1568,7 +1568,7 @@ RegisterNetEvent(getScript()..":server:toggleItem", function(give, item, amount, end else - debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..getItemLabel(item).."("..item..") x"..(amount or 1)) + debugPrint("^6Bridge^7: ^3"..action.."^7[^6"..invName.."^7] Player(^3"..src.."^7) "..getItemLabel(item).."("..item..") x"..(amount or 1)) end end else @@ -1593,7 +1593,7 @@ RegisterNetEvent(getScript()..":server:toggleItem", function(give, item, amount, ESX.GetPlayerFromId(src).addInventoryItem(item, amountToAdd) end else - debugPrint("^6Bridge^7: ^3"..action.."^7["..invName.."] Player("..src..") "..getItemLabel(item).."("..item..") x"..(amount or 1)) + debugPrint("^6Bridge^7: ^3"..action.."^7[^6"..invName.."^7] Player(^3"..src.."^7) "..getItemLabel(item).."("..item..") x"..(amount or 1)) end end end) From 3ad65810c30dc56e9ad50fe6b0dfb3aa44a97c28 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Sun, 28 Sep 2025 14:03:23 +0100 Subject: [PATCH 06/13] Rework `crafting.lua` Changes: - Crafting prop not spawning - Crafting "sound" not playing - `SingleProgress` is now "one progressbar" I'm working on refactoring crafting table's layout style too I'm updating my scripts crafting recipes to a new format, this commit basically makes use of it and makes it slightly easier to edit later if needed It *should* increase readability and users ability to edit these recipes # "old style" crafting recipes should still be compatible thanks to a wrapper I added so you shouldn't notice a difference. --- shared/crafting.lua | 428 +++++++++++++++++++++++--------------------- 1 file changed, 227 insertions(+), 201 deletions(-) diff --git a/shared/crafting.lua b/shared/crafting.lua index 851f616..3a9a976 100644 --- a/shared/crafting.lua +++ b/shared/crafting.lua @@ -18,6 +18,7 @@ local excludeKeys = { job = true, gang = true, oneUse = true, slot = true, blueprintRef = true, craftingLevel = true, craftedItems = true, hasCrafted = true, exp = true, anim = true, time = true, id = true, + ingredients = true, } ------------------------------------------------------------- @@ -40,9 +41,14 @@ local excludeKeys = { --- craftable = { --- Header = "Weapon Crafting", --- Recipes = { ---- [1] = { ---- ["weapon_pistol"] = { ["steel"] = 5, ["plastic"] = 2 }, ---- amount = 1, +--- weapon_pistol = { +--- id = 1, +--- ingredients = { +--- steel = 5, plastic = 5, +--- }, +--- info = { +--- amount = 1, +--- }, --- }, --- -- More recipes... --- }, @@ -56,6 +62,7 @@ local excludeKeys = { --- job = "mechanic", --- onBack = function() print("Returning to previous menu") end, --- }) +--- ``` function craftingMenu(data) if CraftLock then return end @@ -72,35 +79,49 @@ function craftingMenu(data) -- Normalize stash name. data.stashName = data.stashTable or data.stashName - -- Wrapper to convert new style crafting recipes to be handled by the menu properly - --if not data.craftTable.Recipes[1] then -- in theory if not a numbered table its the new style - -- local compatTable = {} - -- for k, v in pairs(data.craftTable.Recipes) do - -- for l, b in pairs(v) do - -- if not excludeKeys[l] then - -- compatTable[data.craftTable.Recipes[k].info.id or #compatTable+1] = { - -- [l] = v.ingredients, - -- amount = v.info and v.info.amount or 1, - -- metadata = v.info and v.info.metadata or nil, - -- job = v.info and v.info.job or nil, - -- gang = v.info and v.info.gang or nil - -- } - -- end - -- end - -- end - -- data.craftTable.Recipes = compatTable - --end + -- Wrapper to convert old style crafting recipes to be handled by the menu properly + if data.craftable.Recipes[1] then -- assume old style crafting table + local compatTable = {} + local id = 0 + for k, v in ipairs(data.craftable.Recipes) do + local Recipe = v + for l, b in pairs(Recipe) do + id += 1 + if doesItemExist(l) then + compatTable[l] = { + ingredients = b, + id = id, + info = { + amount = Recipe.amount or 1, + metadata = Recipe.metadata or nil, + job = Recipe.job or nil, + gang = Recipe.gang or nil, + hasCrafted = Recipe.hasCrafted or nil, + }, + } + end + end + end + data.craftable.Recipes = compatTable + end + + -- Convert to array + local RecipesArray = {} + for k, v in pairs(data.craftable.Recipes) do + RecipesArray[v.id] = { [k] = v } + end 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 ~= "amount" and k ~= "metadata" and k ~= "job" and k ~= "gang" and k == "id" then - tempCarryTable[k] = Recipes[i].amount or 1 + local tempCarryTable = {} + -- Build a temporary table of all required ingredients (default quantity is 1). + for i = 1, #RecipesArray do + for k, v in pairs(RecipesArray[i]) do + if doesItemExist(k) then + if not v.info.amount then v.info.amount = 1 end + tempCarryTable[k] = tempCarryTable[k] and (tempCarryTable[k] < v.info.amount) or v.info.amount end end end @@ -116,75 +137,89 @@ function craftingMenu(data) disabled = true, } - -- 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 + for i = 1, #RecipesArray do + local item = "" + for k in pairs(RecipesArray[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 - local label = getItemLabel(l) - local hasItem = checkStashItem(data.stashName, { [l] = b }) - local missingMark = not hasItem and " ❌" or " " - settext = settext..(settext ~= "" and br or "").."[ x"..b.." ] - "..label..missingMark - - metaTable[label] = b - itemTable[l] = b - end - - while not canCarryTable do - Wait(10) - end - disable = not checkStashItem(data.stashName, itemTable) - setheader = ((metadata and metadata.label) or getItemLabel(k)) - ..(Recipes[i]["amount"] > 1 and " x"..Recipes[i]["amount"] or "") - - local statusEmoji = disable and " " or not canCarryTable[k] and " 📦" or " ✔️" - local isNew = (Recipes[i]["hasCrafted"] ~= nil and craftedItems[k] == nil) and "✨ " or "" - setheader = isNew .. setheader .. statusEmoji - - Menu[#Menu + 1] = { - arrow = isOx() and (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, - txt = 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 + item = k + break end - --Wait(0) + end + local Recipe = RecipesArray[i][item] + + -- Job Check + local hasGroup = true + if Recipe.info.job then + for l, b in pairs(Recipe.info.job) do + hasGroup = hasJob(l, nil, b) + if hasGroup then goto skipcheck end + end + end + if Recipe.info.gang then + for l, b in pairs(Recipe.info.gang) do + hasGroup = hasJob(l, nil, b) + if hasGroup then goto skipcheck end + end + end + ::skipcheck:: + + -- if has group requirement, continue + if hasGroup then + local setheader, settext, disable, metadata = "", "", false, (Recipe.info.metadata or Recipe.info.info or nil) + local itemTable = {} + local metaTable = {} + -- Build ingredient details. + + for l, b in pairs(Recipe.ingredients) do + local label = getItemLabel(l) + local hasItem = checkStashItem(data.stashName, { [l] = b }) + local missingMark = not hasItem and " ❌" or " " + settext = settext..(settext ~= "" and br or "").."[ x"..b.." ] - "..label..missingMark + + metaTable[label] = b + itemTable[l] = b + end + + -- Make sure "canCarryTable" exists + while not canCarryTable do Wait(10) end + + disable = not checkStashItem(data.stashName, itemTable) + setheader = ((metadata and metadata.label) or getItemLabel(item)) + ..(Recipe.info.amount > 1 and " x"..Recipe.info.amount or "") + + local statusEmoji = disable and " " or not canCarryTable[item] and " 📦" or " ✔️" + local isNew = (Recipe.info.hasCrafted ~= nil and craftedItems[item] == nil) and "✨ " or "" + setheader = isNew .. setheader .. statusEmoji + + -- Build menu option using info + Menu[#Menu + 1] = { + arrow = isOx() and (not disable and canCarryTable[item]), + isMenuHeader = disable or not canCarryTable[item], + icon = invImg((metadata and metadata.image) or item), + image = invImg((metadata and metadata.image) or item), + header = setheader, + txt = settext or nil, + metadata = metaTable, + onSelect = (not disable and canCarryTable[item]) and function() + local transdata = { + item = item, + craft = RecipesArray[i][item], + 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 - + -- open context menu openMenu(Menu, { header = data.craftable.Header, headertxt = data.craftable.Headertxt, @@ -235,7 +270,7 @@ function multiCraft(data) for i = 1, maxCreation do multiItemTable[i] = {} - for l, b in pairs(data.craft[data.item]) do + for l, b in pairs(data.craft.ingredients) do multiItemTable[i][l] = (b * i) end end @@ -356,112 +391,62 @@ function makeItem(data) 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 + jsonPrint(prop) local canReturn = true local crafted, crafting = true, true - local cam = createTempCam(Ped, data.coords) - startTempCam(cam) + local cam = createCam(Ped, data.coords.xyz - vec3(0,0, 0.7)) + startCam(cam, 5000) -- Calculate total bartime if SingleProgress is enabled local totalBartime = (bartime * craftAmount) + local craftProp = nil + if prop then + craftProp = makeProp({ prop = prop.model, coords = GetEntityCoords(PlayerPedId()), true, true }) + AttachEntityToEntity(craftProp, Ped, GetPedBoneIndex(Ped, 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, Ped, s.audioRef, true, 0) + end if not Config.Crafting.SingleProgress then -- if SingleProgress is disabled, dont do ingredient progressbars -- Run ingredient check and usage separately first for i = 1, craftAmount do - for k, v in pairs(data.craft) do - if not excludeKeys[k] and type(v) == "table" then - for l, b in pairs(v) do - if isInventoryOpen() then - print("^1Error^7: ^2Inventory is open, you tried to break things") - stopTempCam() - ClearPedTasks(Ped) - if canReturn then craftingMenu(data) end - CraftLock = false - return - end - if crafting and progressBar({ - label = "Using "..b.." "..getItemLabel(l), - time = 800, - cancel = true, - dict = 'pickup_object', - anim = "putdown_low", - flag = 49, - 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 + for k, v in pairs(data.craft.ingredients) do + if isInventoryOpen() then + print("^1Error^7: ^2Inventory is open, you tried to break things") + stopCam(0) + ClearPedTasks(Ped) + if canReturn then craftingMenu(data) end + CraftLock = false + return end + if crafting and progressBar({ + label = "Using "..v.." "..getItemLabel(k), + time = 800, + cancel = true, + dict = 'pickup_object', + anim = "putdown_low", + flag = 49, + icon = k, + }) then + TriggerEvent((isStarted(QBInv) and QBInvNew and "qb-" or "")..'inventory:client:ItemBox', Items[k], "use", v) + else + crafted, crafting = false, false + break + end + Wait(200) end - end - end - if not crafted then - stopTempCam() - ClearPedTasks(Ped) - if canReturn then craftingMenu(data) end - CraftLock = false - return - end + if not crafted then + stopCam(0) + ClearPedTasks(Ped) + if canReturn then craftingMenu(data) end + CraftLock = false + return + end - -- Handle SingleProgress option - if Config.Crafting.SingleProgress then - local craftProp = nil - if prop then - craftProp = makeProp({ prop = prop.model, coords = vec4(0, 0, 0, 0), true, true }) - AttachEntityToEntity(craftProp, Ped, GetPedBoneIndex(Ped, 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, Ped, s.audioRef, true, 0) - end - if crafting and progressBar({ - label = bartext..((metadata and metadata.label) or getItemLabel(data.item)).." x"..craftAmount, - time = totalBartime, - cancel = true, - dict = animDict, - anim = anim, - flag = 49, - icon = data.item, - request = true, - }) then - data.craft.amount = craftAmount - for k, v in pairs(data.craft[data.item]) do - -- multiply igredient requirement in sent crafting table for removal - data.craft[data.item][k] = (v * craftAmount) - end - TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken) - currentToken = nil -- clear client cached token - -- handle metadata and experience in a single go - if data.craft["hasCrafted"] ~= nil then - data.craftable.craftedItems[data.item] = true - triggerCallback(getScript()..":server:setPlayerMetadata", "craftedItems", data.craftable.craftedItems) - end - if data.craft["exp"] ~= nil then - craftingLevel += data.craft["exp"].give * craftAmount - triggerCallback(getScript()..":server:setPlayerMetadata", "craftingLevel", craftingLevel) - 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", Ped, "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 - end - if craftProp then destroyProp(craftProp) end - else - -- Run the original loop for multiple progress bars - for i = 1, craftAmount do if crafting and progressBar({ label = bartext..((metadata and metadata.label) or getItemLabel(data.item)), time = bartime, @@ -474,31 +459,73 @@ function makeItem(data) }) then TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken) currentToken = nil - if data.craft["hasCrafted"] ~= nil then + if data.craft.info.hasCrafted ~= nil then data.craftable.craftedItems[data.item] = true triggerCallback(getScript()..":server:setPlayerMetadata", "craftedItems", data.craftable.craftedItems) end - if data.craft["exp"] ~= nil then - craftingLevel += data.craft["exp"].give + if data.craft.info.exp ~= nil then + craftingLevel += data.craft.exp.give triggerCallback(getScript()..":server:setPlayerMetadata", "craftingLevel", craftingLevel) 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", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0) - canReturn = false - 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", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0) + -- canReturn = false + --end if data.requiredItemfunc then data.requiredItemfunc() end else break end + + end + else + if crafting and progressBar({ + label = bartext..((metadata and metadata.label) or getItemLabel(data.item)).." x"..craftAmount, + time = totalBartime, + cancel = true, + dict = animDict, + anim = anim, + flag = 49, + icon = data.item, + request = true, + }) then + data.craft.info.amount = craftAmount + for k, v in pairs(data.craft.ingredients) do + -- multiply igredient requirement in sent crafting table for removal + data.craft.ingredients[k] = (v * craftAmount) + end + TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken) + currentToken = nil -- clear client cached token + -- handle metadata and experience in a single go + if data.craft.info.hasCrafted ~= nil then + data.craftable.craftedItems[data.item] = true + triggerCallback(getScript()..":server:setPlayerMetadata", "craftedItems", data.craftable.craftedItems) + end + if data.craft.info.exp ~= nil then + craftingLevel += data.craft["exp"].give * craftAmount + triggerCallback(getScript()..":server:setPlayerMetadata", "craftingLevel", craftingLevel) + end + --if data.craft.Recipes[1].oneUse == true then + -- removeItem("craftrecipe", 1, nil, data.craftable.Recipes[1].slot) + -- local breakId = GetSoundId() + -- PlaySoundFromEntity(breakId, "Drill_Pin_Break", Ped, "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 end end + if craftProp then destroyProp(craftProp) end - Wait(500) - stopTempCam() + --Wait(500) + stopCam(0) CraftLock = false if canReturn then craftingMenu(data) end ClearPedTasks(Ped) @@ -531,14 +558,13 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, return 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 k, v in pairs(craftable.ingredients) do for _, b in pairs(stashItems or {}) do if k == b.name then itemRemove[k] = v @@ -548,7 +574,7 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, end else stashItems = getStash(stashName) - for k, v in pairs(craftable[ItemMake] or {}) do + for k, v in pairs(craftable.ingredients) do for _, b in pairs(stashItems or {}) do if k == b.name then itemRemove[k] = v @@ -559,12 +585,12 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, stashRemoveItem(stashItems, stashName, itemRemove) else if craftable then - for k, v in pairs(craftable[ItemMake] or {}) do + for k, v in pairs(craftable.ingredients or {}) do removeItem(tostring(k), v, src) end end end - addItem(ItemMake, craftable.amount or 1, metadata, src) + addItem(ItemMake, craftable.info.amount or 1, metadata, src) -- Optionally, add experience here: -- for example: -- if isStarted("core_skills") then exports["core_skills"]:AddExperience(src, 2) end From 7738bb4fca6c9038440e4d8c8ed7eea11272988b Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Tue, 30 Sep 2025 23:39:55 +0100 Subject: [PATCH 07/13] Version Bump `2.1.03` - `2.1.04` --- fxmanifest.lua | 2 +- shared/crafting.lua | 2 +- version.txt | 16 ++++++---------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/fxmanifest.lua b/fxmanifest.lua index 41fb921..15b73f0 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -1,6 +1,6 @@ name "Jim_Bridge" author "Jimathy" -version "2.1.03" +version "2.1.04" description "Framework Bridge By Jimathy" fx_version "cerulean" rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' diff --git a/shared/crafting.lua b/shared/crafting.lua index 3a9a976..e6772e0 100644 --- a/shared/crafting.lua +++ b/shared/crafting.lua @@ -395,7 +395,7 @@ function makeItem(data) local canReturn = true local crafted, crafting = true, true - local cam = createCam(Ped, data.coords.xyz - vec3(0,0, 0.7)) + local cam = createCam(Ped, data.coords.xyz) startCam(cam, 5000) -- Calculate total bartime if SingleProgress is enabled diff --git a/version.txt b/version.txt index c5bf465..481d3d1 100644 --- a/version.txt +++ b/version.txt @@ -1,13 +1,9 @@ -2.1.03 +2.1.04 -- Fix hunger and thirst math (add instead of set) -- Fix cleanup of targets qb-targets on unload -- Add support for new jim-shops exploit protection -- Add full support for esx_society (fixes esx bossmenu errors) -- Fix possible issue with qb-core /command registering -- Make ox_lib progressbars return false correctly -- Fix ox_inv players current weight checks -- User alternate function for inventory retreival for CodeM Inv -- Rework _loaders.lua tobe more organised/optimizied +- Crafting fixes (prop/sound/remove ingredients) +- "SingleProgress" is now one progressbar +- Add ropeControl.lua - to help create and edit custom ropes/wires +- Support for new crafting recipe style +- create setStress() multiframework function https://github.com/jimathy/jim_bridge/releases/latest \ No newline at end of file From d791a2e09ff056c77c4473bf4bef6ab31c6a02b8 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Wed, 1 Oct 2025 11:42:47 +0100 Subject: [PATCH 08/13] quick fix converting old crafting style --- shared/crafting.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/crafting.lua b/shared/crafting.lua index e6772e0..8ba96c0 100644 --- a/shared/crafting.lua +++ b/shared/crafting.lua @@ -86,8 +86,8 @@ function craftingMenu(data) for k, v in ipairs(data.craftable.Recipes) do local Recipe = v for l, b in pairs(Recipe) do - id += 1 if doesItemExist(l) then + id += 1 compatTable[l] = { ingredients = b, id = id, From 7e595df143091af15611b82f094dac2bef72f76a Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Wed, 1 Oct 2025 17:04:59 +0100 Subject: [PATCH 09/13] Remove gta progressbar print from testing --- ui_modules/progressbar.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui_modules/progressbar.lua b/ui_modules/progressbar.lua index 80a2947..2b8e29f 100644 --- a/ui_modules/progressbar.lua +++ b/ui_modules/progressbar.lua @@ -231,7 +231,6 @@ function gtaProgressBar(data) while result == nil do Wait(10) end inProgress = false - print(tostring(result)) -- Cleanup animations/tasks if data.dict then stopAnim(data.dict, data.anim, ped) @@ -240,7 +239,6 @@ function gtaProgressBar(data) ClearPedTasks(ped) end - -- Cleanup FreezeEntityPosition(ped, false) From d1dab48accec336d3da092c74d800573f148509f Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Thu, 2 Oct 2025 00:14:13 +0100 Subject: [PATCH 10/13] Fix singleprogress multiplying crafting recipes Adds a `cloneTable()` function to make a "deep copy" of the crafting recipes when calculating the multiple amounts to be taken when multicrafting --- shared/crafting.lua | 11 +++++++---- shared/helpers.lua | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/shared/crafting.lua b/shared/crafting.lua index 8ba96c0..f31fe90 100644 --- a/shared/crafting.lua +++ b/shared/crafting.lua @@ -376,7 +376,10 @@ end --- }) --- ``` -function makeItem(data) +function makeItem(origData) + + local data = cloneTable(origData) + local Ped = PlayerPedId() if CraftLock then return end CraftLock = true @@ -418,7 +421,7 @@ function makeItem(data) print("^1Error^7: ^2Inventory is open, you tried to break things") stopCam(0) ClearPedTasks(Ped) - if canReturn then craftingMenu(data) end + if canReturn then craftingMenu(origData) end CraftLock = false return end @@ -442,7 +445,7 @@ function makeItem(data) if not crafted then stopCam(0) ClearPedTasks(Ped) - if canReturn then craftingMenu(data) end + if canReturn then craftingMenu(origData) end CraftLock = false return end @@ -527,7 +530,7 @@ function makeItem(data) --Wait(500) stopCam(0) CraftLock = false - if canReturn then craftingMenu(data) end + if canReturn then craftingMenu(origData) end ClearPedTasks(Ped) end diff --git a/shared/helpers.lua b/shared/helpers.lua index f5c17bc..f26f4d6 100644 --- a/shared/helpers.lua +++ b/shared/helpers.lua @@ -651,6 +651,29 @@ function lookEnt(entity) end end +-- Function to clone tables, to use when referencing tables that need to be hard set +function cloneTable(obj, opts, seen) + if type(obj) ~= "table" then return obj end + seen = seen or {} + if seen[obj] then return seen[obj] end + + local copy = {} + seen[obj] = copy + + -- Copy entries + for k, v in pairs(obj) do + local k2 = (opts and opts.copy_keys) and cloneTable(k, opts, seen) or k + copy[k2] = cloneTable(v, opts, seen) + end + + -- Preserve metatable unless told not to + if not (opts and opts.strip_meta) then + local mt = getmetatable(obj) + if mt ~= nil then setmetatable(copy, mt) end + end + + return copy +end ------------------------------------------------------------- -- Material and Prop Functions From 331dc94e2ff8119a05c21c575b662941ecc02bcd Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Thu, 2 Oct 2025 12:20:58 +0100 Subject: [PATCH 11/13] Revert Crafting I realised in trying to increase readability and optimize, I broke crafting recipes that supported different multiple recipes in the same list eg. Mining has 3 recipes in the smelter for `goldingot` This reverts the system back to the previous recipe style but keeps all the fixes/features from the latest updates --- shared/crafting.lua | 166 +++++++++++++++++++++----------------------- 1 file changed, 81 insertions(+), 85 deletions(-) diff --git a/shared/crafting.lua b/shared/crafting.lua index f31fe90..e618619 100644 --- a/shared/crafting.lua +++ b/shared/crafting.lua @@ -65,7 +65,7 @@ local excludeKeys = { --- ``` function craftingMenu(data) if CraftLock then return end - + local data = cloneTable(data) -- 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 @@ -80,48 +80,48 @@ function craftingMenu(data) data.stashName = data.stashTable or data.stashName -- Wrapper to convert old style crafting recipes to be handled by the menu properly - if data.craftable.Recipes[1] then -- assume old style crafting table - local compatTable = {} - local id = 0 - for k, v in ipairs(data.craftable.Recipes) do - local Recipe = v - for l, b in pairs(Recipe) do - if doesItemExist(l) then - id += 1 - compatTable[l] = { - ingredients = b, - id = id, - info = { - amount = Recipe.amount or 1, - metadata = Recipe.metadata or nil, - job = Recipe.job or nil, - gang = Recipe.gang or nil, - hasCrafted = Recipe.hasCrafted or nil, - }, - } - end - end - end - data.craftable.Recipes = compatTable - end + --if data.craftable.Recipes[1] then -- assume old style crafting table + -- local compatTable = {} + -- local id = 0 + -- for k, v in ipairs(data.craftable.Recipes) do + -- local Recipe = v + -- for l, b in pairs(Recipe) do + -- if doesItemExist(l) then + -- id += 1 + -- compatTable[l] = { + -- ingredients = b, + -- id = id, + -- info = { + -- amount = Recipe.amount or 1, + -- metadata = Recipe.metadata or nil, + -- job = Recipe.job or nil, + -- gang = Recipe.gang or nil, + -- hasCrafted = Recipe.hasCrafted or nil, + -- }, + -- } + -- end + -- end + -- end + -- data.craftable.Recipes = compatTable + --end -- Convert to array - local RecipesArray = {} - for k, v in pairs(data.craftable.Recipes) do - RecipesArray[v.id] = { [k] = v } - end + --local RecipesArray = {} + --for k, v in pairs(data.craftable.Recipes) do + -- RecipesArray[v.id] = { [k] = v } + --end local Menu = {} - local Recipes = data.craftable.Recipes + local Recipes = cloneTable(data.craftable.Recipes) local craftedItems = {} local tempCarryTable = {} -- Build a temporary table of all required ingredients (default quantity is 1). - for i = 1, #RecipesArray do - for k, v in pairs(RecipesArray[i]) do - if doesItemExist(k) then - if not v.info.amount then v.info.amount = 1 end - tempCarryTable[k] = tempCarryTable[k] and (tempCarryTable[k] < v.info.amount) or v.info.amount + for i = 1, #Recipes do + for k, v in pairs(Recipes[i]) do + if not excludeKeys[k] then + if not Recipes[i].amount then Recipes[i].amount = 1 end + tempCarryTable[k] = tempCarryTable[k] and (tempCarryTable[k] < Recipes[i].amount) or Recipes[i].amount end end end @@ -136,27 +136,29 @@ function craftingMenu(data) header = "Material Source: "..(usingStash and "Job Stash" or "Player Inventory"), disabled = true, } - - for i = 1, #RecipesArray do + for i = 1, #Recipes do + local menuId = #Menu+1 local item = "" - for k in pairs(RecipesArray[i]) do + local Recipe = {} + for k, v in pairs(Recipes[i]) do if not excludeKeys[k] then item = k + Recipe = Recipes[i] + Recipe.amount = Recipe.amount or 1 break end end - local Recipe = RecipesArray[i][item] -- Job Check local hasGroup = true - if Recipe.info.job then - for l, b in pairs(Recipe.info.job) do + if Recipe.job then + for l, b in pairs(Recipe.job) do hasGroup = hasJob(l, nil, b) if hasGroup then goto skipcheck end end end - if Recipe.info.gang then - for l, b in pairs(Recipe.info.gang) do + if Recipe.gang then + for l, b in pairs(Recipe.gang) do hasGroup = hasJob(l, nil, b) if hasGroup then goto skipcheck end end @@ -165,12 +167,11 @@ function craftingMenu(data) -- if has group requirement, continue if hasGroup then - local setheader, settext, disable, metadata = "", "", false, (Recipe.info.metadata or Recipe.info.info or nil) + local setheader, settext, disable, metadata = "", "", false, (Recipe.metadata or Recipe.info or nil) local itemTable = {} local metaTable = {} -- Build ingredient details. - - for l, b in pairs(Recipe.ingredients) do + for l, b in pairs(Recipe[item]) do local label = getItemLabel(l) local hasItem = checkStashItem(data.stashName, { [l] = b }) local missingMark = not hasItem and " ❌" or " " @@ -182,17 +183,16 @@ function craftingMenu(data) -- Make sure "canCarryTable" exists while not canCarryTable do Wait(10) end - disable = not checkStashItem(data.stashName, itemTable) setheader = ((metadata and metadata.label) or getItemLabel(item)) - ..(Recipe.info.amount > 1 and " x"..Recipe.info.amount or "") + ..(Recipe.amount > 1 and " x"..Recipe.amount or "") local statusEmoji = disable and " " or not canCarryTable[item] and " 📦" or " ✔️" - local isNew = (Recipe.info.hasCrafted ~= nil and craftedItems[item] == nil) and "✨ " or "" + local isNew = (Recipe.hasCrafted ~= nil and craftedItems[item] == nil) and "✨ " or "" setheader = isNew .. setheader .. statusEmoji -- Build menu option using info - Menu[#Menu + 1] = { + Menu[menuId] = { arrow = isOx() and (not disable and canCarryTable[item]), isMenuHeader = disable or not canCarryTable[item], icon = invImg((metadata and metadata.image) or item), @@ -203,9 +203,10 @@ function craftingMenu(data) onSelect = (not disable and canCarryTable[item]) and function() local transdata = { item = item, - craft = RecipesArray[i][item], + craft = Recipe, craftable = data.craftable, coords = data.coords, + amount = Recipe.amount, stashName = data.stashName, onBack = data.onBack, metadata = metadata, @@ -270,7 +271,7 @@ function multiCraft(data) for i = 1, maxCreation do multiItemTable[i] = {} - for l, b in pairs(data.craft.ingredients) do + for l, b in pairs(data.craft[data.item]) do multiItemTable[i][l] = (b * i) end end @@ -394,7 +395,7 @@ function makeItem(origData) 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 - jsonPrint(prop) + local canReturn = true local crafted, crafting = true, true @@ -416,7 +417,7 @@ function makeItem(origData) if not Config.Crafting.SingleProgress then -- if SingleProgress is disabled, dont do ingredient progressbars -- Run ingredient check and usage separately first for i = 1, craftAmount do - for k, v in pairs(data.craft.ingredients) do + for k, v in pairs(data.craft[data.item]) do if isInventoryOpen() then print("^1Error^7: ^2Inventory is open, you tried to break things") stopCam(0) @@ -443,11 +444,7 @@ function makeItem(origData) end if not crafted then - stopCam(0) - ClearPedTasks(Ped) - if canReturn then craftingMenu(origData) end - CraftLock = false - return + goto finishEarly end if crafting and progressBar({ @@ -462,22 +459,19 @@ function makeItem(origData) }) then TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken) currentToken = nil - if data.craft.info.hasCrafted ~= nil then + if data.craft.hasCrafted ~= nil then data.craftable.craftedItems[data.item] = true triggerCallback(getScript()..":server:setPlayerMetadata", "craftedItems", data.craftable.craftedItems) end - if data.craft.info.exp ~= nil then + if data.craft.exp ~= nil then craftingLevel += data.craft.exp.give triggerCallback(getScript()..":server:setPlayerMetadata", "craftingLevel", craftingLevel) 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", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0) - -- canReturn = false - --end - if data.requiredItemfunc then - data.requiredItemfunc() + 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", Ped, "DLC_HEIST_FLEECA_SOUNDSET", 1, 0) + canReturn = false end else break @@ -495,19 +489,19 @@ function makeItem(origData) icon = data.item, request = true, }) then - data.craft.info.amount = craftAmount - for k, v in pairs(data.craft.ingredients) do + data.craft.amount *= craftAmount + for k, v in pairs(data.craft[data.item]) do -- multiply igredient requirement in sent crafting table for removal - data.craft.ingredients[k] = (v * craftAmount) + data.craft[data.item][k] = (v * craftAmount) end TriggerServerEvent(getScript()..":Crafting:GetItem", data.item, data.craft, data.stashName, metadata, currentToken) currentToken = nil -- clear client cached token -- handle metadata and experience in a single go - if data.craft.info.hasCrafted ~= nil then + if data.craft.hasCrafted ~= nil then data.craftable.craftedItems[data.item] = true triggerCallback(getScript()..":server:setPlayerMetadata", "craftedItems", data.craftable.craftedItems) end - if data.craft.info.exp ~= nil then + if data.craft.exp ~= nil then craftingLevel += data.craft["exp"].give * craftAmount triggerCallback(getScript()..":server:setPlayerMetadata", "craftingLevel", craftingLevel) end @@ -517,16 +511,18 @@ function makeItem(origData) -- PlaySoundFromEntity(breakId, "Drill_Pin_Break", Ped, "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 end end + ::finishEarly:: if craftProp then destroyProp(craftProp) end + if data.sound then + StopSound(data.sound.soundId) + end + if data.requiredItemfunc then + data.requiredItemfunc() + end + --Wait(500) stopCam(0) CraftLock = false @@ -567,7 +563,7 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, if type(stashName) == "table" then for _, name in pairs(stashName) do stashItems = getStash(name) - for k, v in pairs(craftable.ingredients) do + for k, v in pairs(craftable[ItemMake]) do for _, b in pairs(stashItems or {}) do if k == b.name then itemRemove[k] = v @@ -577,7 +573,7 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, end else stashItems = getStash(stashName) - for k, v in pairs(craftable.ingredients) do + for k, v in pairs(craftable[ItemMake]) do for _, b in pairs(stashItems or {}) do if k == b.name then itemRemove[k] = v @@ -587,13 +583,13 @@ RegisterNetEvent(getScript()..":Crafting:GetItem", function(ItemMake, craftable, end stashRemoveItem(stashItems, stashName, itemRemove) else - if craftable then - for k, v in pairs(craftable.ingredients or {}) do + if craftable[ItemMake] then + for k, v in pairs(craftable[ItemMake]) do removeItem(tostring(k), v, src) end end end - addItem(ItemMake, craftable.info.amount or 1, metadata, src) + 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 From 26b1f360296b0f54d27a46ca39cef510bdbc7a87 Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Thu, 2 Oct 2025 12:24:27 +0100 Subject: [PATCH 12/13] Version Bump `2.1.04` - `2.1.05` --- fxmanifest.lua | 2 +- version.txt | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/fxmanifest.lua b/fxmanifest.lua index 15b73f0..74bbf3a 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -1,6 +1,6 @@ name "Jim_Bridge" author "Jimathy" -version "2.1.04" +version "2.1.05" description "Framework Bridge By Jimathy" fx_version "cerulean" rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.' diff --git a/version.txt b/version.txt index 481d3d1..a3e3140 100644 --- a/version.txt +++ b/version.txt @@ -1,9 +1,7 @@ -2.1.04 +2.1.05 -- Crafting fixes (prop/sound/remove ingredients) -- "SingleProgress" is now one progressbar -- Add ropeControl.lua - to help create and edit custom ropes/wires -- Support for new crafting recipe style -- create setStress() multiframework function +- Fix "true/false" print from built-in gta progressbars +- Revert Crafting system to previous style (I was too quick to do this) +- Fix singleprogress multiplying crafting recipes https://github.com/jimathy/jim_bridge/releases/latest \ No newline at end of file From c040390d1f0ea314db2f31ef9ebcf90749085acd Mon Sep 17 00:00:00 2001 From: Jim Shield Date: Fri, 3 Oct 2025 12:22:05 +0100 Subject: [PATCH 13/13] Start working on inventory fallback functions NEEDS TESTING A lot of ESX stuff not done yet --- shared/itemcontrol.lua | 148 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 136 insertions(+), 12 deletions(-) diff --git a/shared/itemcontrol.lua b/shared/itemcontrol.lua index eddf6ab..85a2c41 100644 --- a/shared/itemcontrol.lua +++ b/shared/itemcontrol.lua @@ -1147,7 +1147,7 @@ local InvFunc = { function(itemTable, src) local resultTable = {} for k, v in pairs(itemTable) do - resultTable[k] = exports[OXInv]:CanCarryItem(src, k, v) + resultTable[k] = exports[RSGInv]:CanCarryItem(src, k, v) end return resultTable end, @@ -1301,6 +1301,20 @@ function hasItem(items, amount, src) end end end + + -- Fallback to default hasItem function + if not hasTable or not next(hasTable) then + for item, amt in pairs(items) do + local count = 0 + for _, itemData in pairs(grabInv) do + if itemData and itemData.name == item then + count += (itemData.amount or itemData.count or 1) + end + end + hasTable[item] = { hasItem = count >= amt, count = count } + end + end + for _, v in pairs(hasTable) do if not v.hasItem then return false, hasTable @@ -1324,17 +1338,35 @@ end --- ``` function getPlayerInv(src) local grabInv = nil - local foundInv = "" for i = 1, #InvFunc do local inv = InvFunc[i] if isStarted(inv.invName) then - foundInv = inv.invName grabInv = inv.getPlayerInv(src) break end end - if foundInv == "" then + + -- Fallback to framework functions + if not grabInv then + if isStarted(QBExport) then + if src then + grabInv = Core.Functions.GetPlayer(src).PlayerData.items + else + grabInv = Core.Functions.GetPlayerData().items + end + elseif isStarted(ESXExport) then + if src then + local xPlayer = ESX.GetPlayerFromId(src) + grabInv = xPlayer and xPlayer.getInventory() or {} + else + local xPlayer = ESX.GetPlayerData() + grabInv = xPlayer and xPlayer.inventory or {} + end + end + end + + if grabInv == nil then print("^4ERROR^7: ^2No Supported Inventory detected ^7- ^2Check ^3starter^1.^2lua^7") end return grabInv, foundInv @@ -1423,6 +1455,17 @@ function createUseableItem(item, funct) return end end + + -- Fallback to framework functiosn + if isStarted(QBExport) then + debugPrint("^6Bridge^7: ^2Registering ^3UsableItem^2 with ^4"..QBExport.."^7:", item) + Core.Functions.CreateUseableItem(item, funct) + elseif isStarted(ESXExport) then + debugPrint("^6Bridge^7: ^2Registering ^3UsableItem^2 with ^4"..ESXExport.."^7:", item) + while not ESX do Wait(0) end + ESX.RegisterUsableItem(item, funct) + end + debugPrint("^4ERROR^7: No supported framework detected for registering usable item: ^3"..item.."^7") else print("^1ERROR^7: ^1Tried to make item usable but it didn't exist^7: "..item) @@ -1448,7 +1491,7 @@ end --- addItem("health_potion", 2, { quality = "high" }) --- ``` function addItem(item, amount, info, src) - if not Items[item] then + if not doesItemExist(item) then print("^6Bridge^7: ^1Error^7 - ^2Tried to give ^7'^3"..item.."^7'^2 but it doesn't exist") return end @@ -1477,7 +1520,7 @@ end --- removeItem("health_potion", 1) --- ``` function removeItem(item, amount, src, slot) - if not Items[item] then + if not doesItemExist(item) then print("^6Bridge^7: ^1Error^7 - ^2Tried to remove ^7'^3"..item.."^7'^2 but it doesn't exist") return end @@ -1725,9 +1768,22 @@ RegisterNetEvent(getScript()..":server:setItemMetaData", function(data, src) local inv = InvFunc[i] if isStarted(inv.invName) then inv.setItemMetadata(data, src) - break + return end end + + -- Fallback functions + if isStarted(QBExport) or isStarted(QBXExport) then -- if qbcore or qbxcore, just use core functions + local Player = Core.Functions.GetPlayer(src) + Player.PlayerData.items[data.slot].info = data.metadata + if data.metadata.durability then + Player.PlayerData.items[data.slot].description = "HP : "..data.metadata.durability + end + Player.Functions.SetInventory(Player.PlayerData.items) + + elseif ESX and isStarted(ESXExport) then -- if esx then use core functions + --? + end end) ------------------------------------------------------------- @@ -1804,6 +1860,22 @@ function canCarry(itemTable, src) return inv.canCarry(itemTable, src) end end + + -- Fallback to default canCarry function + local items = getPlayerInv(src) + local totalWeight = 0 + if not items then return false end + for _, item in pairs(items) do + totalWeight += (item.weight * item.amount) + end + for k, v in pairs(itemTable) do + local itemInfo = Items[k] + if not itemInfo then + resultTable[k] = true + else + resultTable[k] = (totalWeight + (itemInfo.weight * v)) <= InventoryWeight + end + end return resultTable end @@ -1855,6 +1927,11 @@ function getMaxInvWeight() break end end + + -- Fallback function + if weight == 0 then + weight = InventoryWeight + end return weight end @@ -1877,6 +1954,13 @@ function getCurrentInvWeight(src) end end + -- fallback function + if weight == 0 then + local itemcheck = getPlayerInv(src) + for _, v in pairs(itemcheck) do + weight += ((v.weight * v.amount) or 0) + end + end return weight end @@ -2185,7 +2269,6 @@ end --- local items = getStash("playerStash") --- ``` function getStash(stashName) - local stashResource = "" if stashName == "" or type(stashName) ~= "string" then print("^6Bridge^7: ^2Stash name was not a string ^3"..stashName.."^7(^3"..type(stashName).."^7)") return {} @@ -2201,6 +2284,19 @@ function getStash(stashName) end end + -- Fallback to sql checks if no supported inventory found + if not stashItems or not next(stashItems) then + if isStarted(QBExport) or isStarted(QBXExport) then + local result = MySQL.scalar.await('SELECT items FROM stashitems WHERE stash = ?', { stashName }) + if result then + stashItems = json.decode(result) + end + elseif ESX and isStarted(ESXExport) then -- if esx then use core functions + --? + end + + end + if stashItems then for _, item in pairs(stashItems) do local itemInfo = Items[item.name:lower()] @@ -2224,7 +2320,7 @@ function getStash(stashName) end debugPrint("^6Bridge^7: ^3GetStashItems^7: ^2Stash information for '^6"..stashName.."^7' retrieved") end - jsonPrint(items) + --jsonPrint(items) return items end @@ -2260,7 +2356,28 @@ function stashRemoveItem(stashItems, stashName, items) return end end - + if isStarted(QBExport) then + local stashItems = getStash(stashName[1]) + for k, v in pairs(items) do + for l in pairs(stashItems) do + if stashItems[l].name == k then + if (stashItems[l].amount - v) <= 0 then + stashItems[l] = nil + else + debugPrint("^6Bridge^7: ^2Removing ^3"..PSInv.." ^2Stash item^7:", k, v) + stashItems[l].amount -= v + end + end + end + end + debugPrint("^6Bridge^7: ^3saveStash^7: ^2Saving ^3QBCORE^2 stash ^7'^6"..stashName[1].."^7'") + MySQL.Async.insert('INSERT INTO stashitems (stash, items) VALUES (:stash, :items) ON DUPLICATE KEY UPDATE items = :items', { + ['stash'] = stashName[1], + ['items'] = json.encode(stashItems) + }) + elseif isStarted(ESXExport) then + --? + end print("^4ERROR^7: ^2No supported Inventory detected ^7- ^2Check ^3starter^1.^2lua^7") end RegisterNetEvent(getScript()..":server:stashRemoveItem", stashRemoveItem) @@ -2394,7 +2511,7 @@ RegisterNetEvent(getScript()..":openGrabBox", function(data) local id = data.metadata and data.metadata.id or data.info and data.info.id or "" if isStarted(OXInv) then - + -- this SHOLD be handled within ox_inventory if set up right return end @@ -2646,10 +2763,17 @@ function openShop(data) if isStarted(inv.invName) then inv.openShop(data.shop, data.items.label, data.items) lookEnt(data.coords) - break + return end end + if isStarted(QBExport) then + TriggerServerEvent(getScript()..':server:openServerShop', data.shop) + TriggerServerEvent("inventory:server:OpenInventory", "shop", data.items.label, data.items) + elseif isStarted(ESXExport) then + --? + end + end RegisterNetEvent(getScript()..':server:openServerShop', function(shopName)