445 lines
18 KiB
Lua
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)
|