2026-04-14 15:54:53 +02:00

445 lines
18 KiB
Lua

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)