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

368 lines
12 KiB
Lua

ESX = exports['es_extended']:getSharedObject()
-- In-memory state
local activeLocationIds = {} -- array of active location_id strings
local purchaseLimits = {} -- identifier -> { item_name -> count }
local lastLimitReset = os.time()
local itemLabelCache = {}
local itemLookup = {} -- item_name -> { price, limit, image, categoryId }
-- =====================
-- ITEM LOOKUP TABLE
-- =====================
local function BuildItemLookup()
for _, category in ipairs(Config.Categories) do
for _, item in ipairs(category.items) do
itemLookup[item.name] = {
price = item.price,
limit = item.limit,
image = item.image,
categoryId = category.id,
}
end
end
end
-- =====================
-- HELPERS
-- =====================
local function GetIdentifier(source)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return nil end
return xPlayer.getIdentifier()
end
local function GetPlayerName(source)
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer then return 'Unbekannt' end
return xPlayer.getName()
end
local function GetItemLabel(itemName, configLabel)
if configLabel and configLabel ~= '' then return configLabel end
if itemLabelCache[itemName] then return itemLabelCache[itemName] end
local ok, label = pcall(function() return exports['codem-inventory']:GetItemLabel(itemName) end)
if ok and label and label ~= '' then
itemLabelCache[itemName] = label
return label
end
local ok2, esxItems = pcall(function() return ESX.GetItems() end)
if ok2 and esxItems and esxItems[itemName] then
itemLabelCache[itemName] = esxItems[itemName].label or itemName
return itemLabelCache[itemName]
end
return itemName
end
-- =====================
-- WEBHOOK
-- =====================
local function SendWebhook(title, message, color)
if not Config.WebhookURL or Config.WebhookURL == "" then return end
local embed = {
{
["color"] = color or 10038562,
["title"] = "**" .. title .. "**",
["description"] = message,
["footer"] = { ["text"] = os.date("%d.%m.%Y | %H:%M:%S") },
}
}
PerformHttpRequest(Config.WebhookURL, function() end, 'POST', json.encode({username = "Schwarzmarkt Log", embeds = embed}), { ['Content-Type'] = 'application/json' })
end
-- =====================
-- PAYMENT PROCESSING
-- =====================
local function ProcessPayment(xPlayer, amount)
if Config.PaymentMethod == 'money' then
if xPlayer.getAccount('money').money >= amount then
xPlayer.removeAccountMoney('money', amount)
return true
end
return false
elseif Config.PaymentMethod == 'black_money' then
if xPlayer.getAccount('black_money').money >= amount then
xPlayer.removeAccountMoney('black_money', amount)
return true
end
return false
elseif Config.PaymentMethod == 'both' then
local blackMoney = xPlayer.getAccount('black_money').money
local cash = xPlayer.getAccount('money').money
if blackMoney + cash >= amount then
local fromBlack = math.min(blackMoney, amount)
local fromCash = amount - fromBlack
if fromBlack > 0 then xPlayer.removeAccountMoney('black_money', fromBlack) end
if fromCash > 0 then xPlayer.removeAccountMoney('money', fromCash) end
return true
end
return false
end
return false
end
local function RefundPayment(xPlayer, amount)
if Config.PaymentMethod == 'black_money' or Config.PaymentMethod == 'both' then
xPlayer.addAccountMoney('black_money', amount)
else
xPlayer.addAccountMoney('money', amount)
end
end
-- =====================
-- PURCHASE LIMIT TRACKING
-- =====================
local function GetPlayerPurchases(identifier, itemName)
if not purchaseLimits[identifier] then return 0 end
return purchaseLimits[identifier][itemName] or 0
end
local function AddPlayerPurchase(identifier, itemName, quantity)
if not purchaseLimits[identifier] then purchaseLimits[identifier] = {} end
purchaseLimits[identifier][itemName] = (purchaseLimits[identifier][itemName] or 0) + quantity
if Config.LimitResetMode == 'daily' then
MySQL.query('INSERT INTO mercyv_blackmarket_limits (identifier, item_name, amount, reset_date) VALUES (?, ?, ?, CURDATE()) ON DUPLICATE KEY UPDATE amount = amount + ?', {
identifier, itemName, quantity, quantity
})
end
end
local function ResetAllLimits()
purchaseLimits = {}
if Config.LimitResetMode == 'daily' then
MySQL.query('DELETE FROM mercyv_blackmarket_limits WHERE reset_date < CURDATE()')
end
print('[mercyv-blackmarket] Kauflimits zurückgesetzt')
end
-- =====================
-- LOCATION SYSTEM
-- =====================
local function GetActiveLocationsData()
local data = {}
for _, id in ipairs(activeLocationIds) do
data[id] = Config.Locations[id]
end
return data
end
local function BroadcastLocations()
TriggerClientEvent('mercyv-blackmarket:syncLocations', -1, GetActiveLocationsData())
end
local function RotateLocations()
local pool = {}
for id, _ in pairs(Config.Locations) do
pool[#pool + 1] = id
end
-- Fisher-Yates shuffle
for i = #pool, 2, -1 do
local j = math.random(1, i)
pool[i], pool[j] = pool[j], pool[i]
end
activeLocationIds = {}
local maxActive = math.min(Config.Rotation.maxActive or 1, #pool)
for i = 1, maxActive do
activeLocationIds[i] = pool[i]
end
BroadcastLocations()
end
local function InitLocations()
if Config.LocationMode == 'fixed' then
activeLocationIds = {}
for id, _ in pairs(Config.Locations) do
activeLocationIds[#activeLocationIds + 1] = id
end
else
RotateLocations()
end
end
-- =====================
-- DATABASE SETUP (nur fürdaily mode)
-- =====================
MySQL.ready(function()
if Config.LimitResetMode == 'daily' then
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_blackmarket_limits (
id INT AUTO_INCREMENT PRIMARY KEY,
identifier VARCHAR(60) NOT NULL,
item_name VARCHAR(50) NOT NULL,
amount INT NOT NULL DEFAULT 0,
reset_date DATE NOT NULL,
UNIQUE KEY uq_player_item_date (identifier, item_name, reset_date),
INDEX idx_reset_date (reset_date)
)]])
-- Lade heutige Limits in Memory
local rows = MySQL.query.await('SELECT identifier, item_name, amount FROM mercyv_blackmarket_limits WHERE reset_date = CURDATE()')
if rows then
for _, row in ipairs(rows) do
if not purchaseLimits[row.identifier] then purchaseLimits[row.identifier] = {} end
purchaseLimits[row.identifier][row.item_name] = row.amount
end
end
print('[mercyv-blackmarket] Daily limits geladen')
end
-- Item Lookup aufbauen
BuildItemLookup()
-- Standorte initialisieren
InitLocations()
print('[mercyv-blackmarket] System gestartet (' .. #activeLocationIds .. ' Standorte aktiv)')
end)
-- =====================
-- ROTATION TIMER
-- =====================
Citizen.CreateThread(function()
if Config.LocationMode ~= 'rotating' then return end
while true do
Citizen.Wait(Config.Rotation.intervalMinutes * 60 * 1000)
print('[mercyv-blackmarket] Standorte rotieren...')
TriggerClientEvent('mercyv-blackmarket:forceClose', -1)
Citizen.Wait(500)
RotateLocations()
end
end)
-- =====================
-- LIMIT RESET TIMERS
-- =====================
Citizen.CreateThread(function()
if Config.LimitResetMode == 'daily' then
while true do
Citizen.Wait(60000) -- Check jede Minute
local now = os.date("*t")
if now.hour == 0 and now.min == 0 then
ResetAllLimits()
Citizen.Wait(61000) -- Verhindere doppelten Reset
end
end
elseif Config.LimitResetMode == 'interval' then
while true do
Citizen.Wait(Config.LimitResetIntervalMinutes * 60 * 1000)
ResetAllLimits()
end
end
end)
-- =====================
-- SERVER CALLBACK: MARKET DATA
-- =====================
ESX.RegisterServerCallback('mercyv-blackmarket:getMarketData', function(source, cb)
local identifier = GetIdentifier(source)
if not identifier then return cb({ categories = {} }) end
local categories = {}
for _, cat in ipairs(Config.Categories) do
local items = {}
for _, item in ipairs(cat.items) do
local purchased = GetPlayerPurchases(identifier, item.name)
local remaining = math.max(0, item.limit - purchased)
items[#items + 1] = {
item_name = item.name,
item_label = GetItemLabel(item.name, item.label),
image = item.image or item.name,
price = item.price,
limit = item.limit,
remaining = remaining,
}
end
categories[#categories + 1] = {
id = cat.id,
label = cat.label,
icon = cat.icon,
image = cat.image or '',
color = cat.color or '',
gridSize = cat.gridSize or 'small',
height = cat.height or nil,
items = items,
}
end
cb({ categories = categories })
end)
-- =====================
-- BUY ITEM
-- =====================
RegisterNetEvent('mercyv-blackmarket:buyItem')
AddEventHandler('mercyv-blackmarket:buyItem', function(itemName, quantity)
local source = source
local xPlayer = ESX.GetPlayerFromId(source)
local identifier = GetIdentifier(source)
if not xPlayer or not identifier then return end
quantity = math.max(1, math.floor(tonumber(quantity) or 1))
local itemConfig = itemLookup[itemName]
if not itemConfig then
TriggerClientEvent('mercyv-blackmarket:notify', source, 'Item nicht verfügbar', 'error')
return
end
-- Kauflimit pruefen
local purchased = GetPlayerPurchases(identifier, itemName)
local remaining = itemConfig.limit - purchased
if quantity > remaining then
TriggerClientEvent('mercyv-blackmarket:notify', source, 'Kauflimit erreicht (' .. remaining .. ' übrig)', 'error')
return
end
local total = itemConfig.price * quantity
-- Bezahlung
local paid = ProcessPayment(xPlayer, total)
if not paid then
TriggerClientEvent('mercyv-blackmarket:notify', source, 'Nicht genug Geld ($' .. total .. ' benötigt)', 'error')
return
end
-- Item hinzufuegen via codem-inventory
local addOk = pcall(function()
exports['codem-inventory']:AddItem(source, itemName, quantity)
end)
if not addOk then
RefundPayment(xPlayer, total)
TriggerClientEvent('mercyv-blackmarket:notify', source, 'Item konnte nicht hinzugefügt werden', 'error')
return
end
-- Kauf tracken
AddPlayerPurchase(identifier, itemName, quantity)
-- Aktualisierte Limits senden
local updatedLimits = {}
updatedLimits[itemName] = math.max(0, itemConfig.limit - GetPlayerPurchases(identifier, itemName))
TriggerClientEvent('mercyv-blackmarket:notify', source, quantity .. 'x ' .. GetItemLabel(itemName) .. ' gekauft', 'success')
TriggerClientEvent('mercyv-blackmarket:buyResult', source, true, updatedLimits)
-- Webhook
SendWebhook("Schwarzmarkt Kauf", string.format(
"Spieler: **%s** (%s)\nItem: **%s** x%d\nPreis: **$%d**",
GetPlayerName(source), identifier, GetItemLabel(itemName), quantity, total
), 10038562)
end)
-- =====================
-- PLAYER SYNC
-- =====================
RegisterNetEvent('mercyv-blackmarket:requestSync')
AddEventHandler('mercyv-blackmarket:requestSync', function()
TriggerClientEvent('mercyv-blackmarket:syncLocations', source, GetActiveLocationsData())
end)