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)