368 lines
12 KiB
Lua
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)
|