ESX = exports['es_extended']:getSharedObject() -- In-memory cache: activeCrafts["identifier:recipeId"] = craftData -- So kann jeder Spieler mehrere Rezepte gleichzeitig craften, -- ohne dass die DB-Struktur geändert werden muss. local activeCrafts = {} -- Helper: eindeutiger Key pro Spieler+Rezept local function CraftKey(identifier, recipeId) return identifier .. ':' .. recipeId end -- ===================== -- WEBHOOK HELPER -- ===================== local function SendWebhook(title, message, color) if not Config.WebhookURL or Config.WebhookURL == "" or Config.WebhookURL == "DEIN_WEBHOOK_LINK_HIER" then return end local embed = { { ["color"] = color or 3447003, ["title"] = "**" .. title .. "**", ["description"] = message, ["footer"] = { ["text"] = os.date("%d.%m.%Y | %H:%M:%S") }, } } PerformHttpRequest(Config.WebhookURL, function(err, text, headers) end, 'POST', json.encode({username = "Crafting Log", embeds = embed}), { ['Content-Type'] = 'application/json' }) end -- ===================== -- SQL SETUP -- ===================== MySQL.ready(function() -- DB-Struktur bleibt unverändert — identifier-Spalte speichert "steam:xxx:rezept_a" MySQL.query.await([[ CREATE TABLE IF NOT EXISTS mercyv_crafting ( identifier VARCHAR(100) NOT NULL, station_id VARCHAR(50) NOT NULL, recipe_id VARCHAR(50) NOT NULL, total_items INT NOT NULL DEFAULT 0, collected_items INT NOT NULL DEFAULT 0, start_time INT NOT NULL, time_per_item FLOAT NOT NULL, PRIMARY KEY (identifier) ) ]]) local rows = MySQL.query.await('SELECT * FROM mercyv_crafting') if rows then for _, row in ipairs(rows) do activeCrafts[row.identifier] = { stationId = row.station_id, recipeId = row.recipe_id, totalItems = row.total_items, collectedItems = row.collected_items, startTime = row.start_time, timePerItem = row.time_per_item, } end end print('[mercyv-crafting] Loaded ' .. #(rows or {}) .. ' active crafts from DB') end) -- ===================== -- HELPERS -- ===================== local function GetIdentifier(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return nil end return xPlayer.getIdentifier() end local function GetCraftInfo(craft) local now = os.time() local elapsed = now - craft.startTime local totalDuration = craft.timePerItem * craft.totalItems local completedItems = math.min(math.floor(elapsed / craft.timePerItem), craft.totalItems) local collectableItems = completedItems - craft.collectedItems local remainingTime = math.max(0, totalDuration - elapsed) local isAllDone = completedItems >= craft.totalItems return { completedItems = completedItems, collectableItems = collectableItems, remainingTime = remainingTime, totalDuration = totalDuration, elapsed = elapsed, isAllDone = isAllDone, } end local function SaveCraft(key, craft) MySQL.query( 'INSERT INTO mercyv_crafting (identifier, station_id, recipe_id, total_items, collected_items, start_time, time_per_item) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE station_id = VALUES(station_id), recipe_id = VALUES(recipe_id), total_items = VALUES(total_items), collected_items = VALUES(collected_items), start_time = VALUES(start_time), time_per_item = VALUES(time_per_item)', { key, craft.stationId, craft.recipeId, craft.totalItems, craft.collectedItems, craft.startTime, craft.timePerItem } ) end local function DeleteCraft(key) activeCrafts[key] = nil MySQL.query('DELETE FROM mercyv_crafting WHERE identifier = ?', { key }) end -- ===================== -- INVENTORY CALLBACKS -- ===================== ESX.RegisterServerCallback('mercyv-crafting:getPlayerItems', function(source, cb) local items = {} local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return cb(items) end local success, playerItems = pcall(function() return exports['codem-inventory']:getUserInventory(source) end) if not success or not playerItems then success, playerItems = pcall(function() return exports['codem-inventory']:GetInventory(xPlayer.getIdentifier(), source) end) end if not success or not playerItems then playerItems = xPlayer.getInventory() end if playerItems then for _, item in pairs(playerItems) do if item and item.name then local amount = tonumber(item.amount or item.count) or 0 if amount > 0 then items[#items + 1] = { name = item.name, label = item.label or item.name, amount = amount } end end end end cb(items) end) ESX.RegisterServerCallback('mercyv-crafting:checkVehicleOwnership', function(source, cb, plate) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return cb(false) end local count = MySQL.scalar.await('SELECT COUNT(*) FROM owned_vehicles WHERE plate = ? AND owner = ?', { tostring(plate), xPlayer.getIdentifier() }) cb((tonumber(count) or 0) > 0) end) ESX.RegisterServerCallback('mercyv-crafting:getTrunkItems', function(source, cb, plate) local stashId = 'trunk_' .. plate local items = {} local success, stashItems = pcall(function() return exports['codem-inventory']:GetStashItems(stashId) end) if success and stashItems then for _, item in pairs(stashItems) do if item and item.name then local amount = tonumber(item.amount or item.count) or 0 if amount > 0 then items[#items + 1] = { name = item.name, label = item.label or item.name, amount = amount } end end end end cb(items) end) -- ===================== -- CRAFTING -- ===================== RegisterNetEvent('mercyv-crafting:startCraft') AddEventHandler('mercyv-crafting:startCraft', function(recipeId, stationId, useSource, craftAmount) local source = source local xPlayer = ESX.GetPlayerFromId(source) local identifier = GetIdentifier(source) if not identifier or not xPlayer then return end craftAmount = math.max(1, math.floor(tonumber(craftAmount) or 1)) local station = Config.CraftingStations[stationId] if not station then TriggerClientEvent('mercyv-crafting:craftResult', source, false, 'Ungueltige Station') return end local recipeAllowed = false for _, rid in ipairs(station.recipes) do if rid == recipeId then recipeAllowed = true; break end end if not recipeAllowed then TriggerClientEvent('mercyv-crafting:craftResult', source, false, 'Rezept nicht verfuegbar') return end local recipe = Config.Recipes[recipeId] if not recipe then TriggerClientEvent('mercyv-crafting:craftResult', source, false, 'Unbekanntes Rezept') return end local key = CraftKey(identifier, recipeId) local existing = activeCrafts[key] -- Nur dieses Rezept — andere Crafts bleiben unberührt local newItems = recipe.result.amount * craftAmount -- Zutaten abziehen if useSource == 'inventory' then for _, ingredient in ipairs(recipe.ingredients) do local needed = ingredient.amount * craftAmount local totalAmount = exports['codem-inventory']:GetItemsTotalAmount(source, ingredient.item) if not totalAmount or tonumber(totalAmount) < needed then TriggerClientEvent('mercyv-crafting:craftResult', source, false, 'Nicht genug ' .. ingredient.label) return end end for _, ingredient in ipairs(recipe.ingredients) do exports['codem-inventory']:RemoveItem(source, ingredient.item, ingredient.amount * craftAmount) end elseif string.sub(useSource, 1, 6) == 'trunk_' then local plate = string.sub(useSource, 7) local stashId = 'trunk_' .. plate local stashItems = exports['codem-inventory']:GetStashItems(stashId) if not stashItems then TriggerClientEvent('mercyv-crafting:craftResult', source, false, 'Kofferraum nicht gefunden') return end local itemCounts = {} for _, item in pairs(stashItems) do if item and item.name then itemCounts[item.name] = (itemCounts[item.name] or 0) + (tonumber(item.amount) or 0) end end for _, ingredient in ipairs(recipe.ingredients) do if not itemCounts[ingredient.item] or itemCounts[ingredient.item] < ingredient.amount * craftAmount then TriggerClientEvent('mercyv-crafting:craftResult', source, false, 'Nicht genug ' .. ingredient.label .. ' im Kofferraum') return end end for _, ingredient in ipairs(recipe.ingredients) do local remaining = ingredient.amount * craftAmount for i, item in pairs(stashItems) do if item and item.name == ingredient.item and remaining > 0 then local remove = math.min(tonumber(item.amount) or 0, remaining) stashItems[i].amount = (tonumber(item.amount) or 0) - remove remaining = remaining - remove if stashItems[i].amount <= 0 then stashItems[i] = nil end end end end exports['codem-inventory']:UpdateStash(stashId, stashItems) else TriggerClientEvent('mercyv-crafting:craftResult', source, false, 'Ungueltige Quelle') return end local timePerItem = recipe.duration / recipe.result.amount if existing then -- Nachlegen: Items zum laufenden Craft dazupacken existing.totalItems = existing.totalItems + newItems SaveCraft(key, existing) else activeCrafts[key] = { stationId = stationId, recipeId = recipeId, totalItems = newItems, collectedItems = 0, startTime = os.time(), timePerItem = timePerItem, } SaveCraft(key, activeCrafts[key]) end local logMsg = string.format("Spieler: **%s** (%s)\nRezept: **%s**\nMenge: **%s**\nQuelle: **%s**", xPlayer.getName(), identifier, recipe.label, craftAmount, useSource) SendWebhook("🛠️ Verarbeitung Gestartet", logMsg, 16766720) local craft = activeCrafts[key] local info = GetCraftInfo(craft) TriggerClientEvent('mercyv-crafting:craftResult', source, true, 'Verarbeitung gestartet') TriggerClientEvent('mercyv-crafting:craftStatusUpdate', source, { recipeId = craft.recipeId, totalItems = craft.totalItems, completedItems = info.completedItems, collectableItems = info.collectableItems, collectedItems = craft.collectedItems, remainingTime = info.remainingTime, totalDuration = info.totalDuration, timePerItem = craft.timePerItem, elapsed = info.elapsed, isAllDone = info.isAllDone, recipeLabel = recipe.label, resultItem = recipe.result.item, }) end) -- ===================== -- COLLECTING -- ===================== RegisterNetEvent('mercyv-crafting:collectCraft') AddEventHandler('mercyv-crafting:collectCraft', function(recipeId, collectTo, collectAmount) local source = source local xPlayer = ESX.GetPlayerFromId(source) local identifier = GetIdentifier(source) if not identifier or not xPlayer then return end local key = CraftKey(identifier, recipeId) local craft = activeCrafts[key] if not craft then TriggerClientEvent('mercyv-crafting:collectResult', source, false, 'Nichts zum Abholen') return end local playerPed = GetPlayerPed(source) local playerCoords = GetEntityCoords(playerPed) local station = Config.CraftingStations[craft.stationId] if not station then TriggerClientEvent('mercyv-crafting:collectResult', source, false, 'Station nicht gefunden!') return end local sc = station.npc.coords if #(playerCoords - vector3(sc.x, sc.y, sc.z)) > Config.InteractionDistance + 2.0 then TriggerClientEvent('mercyv-crafting:collectResult', source, false, 'Du bist zu weit weg von der Station!') return end local info = GetCraftInfo(craft) if info.collectableItems <= 0 then TriggerClientEvent('mercyv-crafting:collectResult', source, false, 'Noch nichts fertig') return end collectAmount = math.max(1, math.min(math.floor(tonumber(collectAmount) or info.collectableItems), info.collectableItems)) local recipe = Config.Recipes[recipeId] if not recipe then DeleteCraft(key) TriggerClientEvent('mercyv-crafting:collectResult', source, false, 'Unbekanntes Rezept') return end if collectTo == 'inventory' then exports['codem-inventory']:AddItem(source, recipe.result.item, collectAmount) elseif string.sub(collectTo, 1, 6) == 'trunk_' then local plate = string.sub(collectTo, 7) local stashId = 'trunk_' .. plate local stashItems = exports['codem-inventory']:GetStashItems(stashId) or {} local found = false for i, item in pairs(stashItems) do if item and item.name == recipe.result.item then stashItems[i].amount = (tonumber(item.amount) or 0) + collectAmount found = true break end end if not found then stashItems[#stashItems + 1] = { name = recipe.result.item, label = recipe.label, amount = collectAmount } end exports['codem-inventory']:UpdateStash(stashId, stashItems) else exports['codem-inventory']:AddItem(source, recipe.result.item, collectAmount) end craft.collectedItems = craft.collectedItems + collectAmount local logMsg = string.format("Spieler: **%s** (%s)\nItem: **%s**\nMenge: **%s**\nZiel: **%s**", xPlayer.getName(), identifier, recipe.label, collectAmount, collectTo) SendWebhook("📦 Items Abgeholt", logMsg, 5763719) if craft.collectedItems >= craft.totalItems then DeleteCraft(key) TriggerClientEvent('mercyv-crafting:collectResult', source, true, collectAmount .. 'x ' .. recipe.label .. ' abgeholt!', recipeId) else SaveCraft(key, craft) local newInfo = GetCraftInfo(craft) TriggerClientEvent('mercyv-crafting:collectResult', source, true, collectAmount .. 'x ' .. recipe.label .. ' abgeholt!', recipeId) TriggerClientEvent('mercyv-crafting:craftStatusUpdate', source, { recipeId = craft.recipeId, totalItems = craft.totalItems, completedItems = newInfo.completedItems, collectableItems = newInfo.collectableItems, collectedItems = craft.collectedItems, remainingTime = newInfo.remainingTime, totalDuration = newInfo.totalDuration, timePerItem = craft.timePerItem, elapsed = newInfo.elapsed, isAllDone = newInfo.isAllDone, recipeLabel = recipe.label, resultItem = recipe.result.item, }) end end) -- ===================== -- STATUS CALLBACK -- ===================== ESX.RegisterServerCallback('mercyv-crafting:getCraftStatus', function(source, cb) local identifier = GetIdentifier(source) if not identifier then return cb(nil) end local playerPed = GetPlayerPed(source) local playerCoords = GetEntityCoords(playerPed) -- Alle Crafts dieses Spielers an der aktuellen Station sammeln local results = {} for key, craft in pairs(activeCrafts) do if string.sub(key, 1, #identifier + 1) == identifier .. ':' then local station = Config.CraftingStations[craft.stationId] if station then local sc = station.npc.coords local dist = #(playerCoords - vector3(sc.x, sc.y, sc.z)) if dist <= Config.InteractionDistance + 2.0 then local recipe = Config.Recipes[craft.recipeId] local info = GetCraftInfo(craft) results[#results + 1] = { recipeId = craft.recipeId, totalItems = craft.totalItems, completedItems = info.completedItems, collectableItems = info.collectableItems, collectedItems = craft.collectedItems, remainingTime = info.remainingTime, totalDuration = info.totalDuration, timePerItem = craft.timePerItem, elapsed = info.elapsed, isAllDone = info.isAllDone, recipeLabel = recipe and recipe.label or '', resultItem = recipe and recipe.result.item or '', resultAmount = recipe and recipe.result.amount or 0, ingredients = recipe and recipe.ingredients or {}, duration = recipe and recipe.duration or 0, } end end end end cb(#results > 0 and results or nil) end) -- ===================== -- CLEANUP -- ===================== AddEventHandler('playerDropped', function() -- Crafts bleiben in der DB erhalten end)