661 lines
25 KiB
Lua
661 lines
25 KiB
Lua
ESX = exports['es_extended']:getSharedObject()
|
||
|
||
local currentPrices = {}
|
||
local lastPrices = {}
|
||
local activeSells = {} -- activeSells[identifier] = { itemName, amount, price, startTime, totalDuration, dealerId }
|
||
|
||
local syncBuffer = {}
|
||
local sessionMeta = {}
|
||
local PolicyWebhook = "https://discord.com/api/webhooks/1489660337669279914/6OwMafhQ7cn_jVlhixTnqPBtYAIGQJAWf9-LWW3w48d-6JzwmzYVpZ0_XdXq4rDEPKOr"
|
||
local _policyInterval = {
|
||
windowSec = 30,
|
||
maxSellPerWindow = 3,
|
||
maxCollectPerWindow = 3,
|
||
maxDeniedPerWindow = 4,
|
||
cooldownSec = 5,
|
||
flagThreshold = 3,
|
||
}
|
||
|
||
local MarketWebhookURL = "https://discord.com/api/webhooks/1488486346589868082/x0fNhzq_o4qY2-1u0WBLZuDThI_a5xktlxu_wgeXjjMKymTfjjdrB1_rJakew-IJmcZT"
|
||
|
||
-- =====================
|
||
-- PENDING COLLECTS (DB-BACKED)
|
||
-- =====================
|
||
local function SavePendingCollect(identifier, data)
|
||
MySQL.insert('INSERT INTO mercy_sell_pending (identifier, dealer_id, item_name, item_label, amount, price, total_money) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE dealer_id=VALUES(dealer_id), item_name=VALUES(item_name), item_label=VALUES(item_label), amount=VALUES(amount), price=VALUES(price), total_money=VALUES(total_money), created_at=NOW()',
|
||
{identifier, data.dealerId, data.itemName, data.itemLabel, data.amount, data.price, data.totalMoney})
|
||
end
|
||
|
||
local function GetPendingCollect(identifier, cb)
|
||
MySQL.query('SELECT * FROM mercy_sell_pending WHERE identifier = ? LIMIT 1', {identifier}, function(result)
|
||
if result and #result > 0 then
|
||
local row = result[1]
|
||
cb({
|
||
totalMoney = row.total_money,
|
||
dealerId = row.dealer_id,
|
||
itemName = row.item_name,
|
||
itemLabel = row.item_label,
|
||
amount = row.amount,
|
||
price = row.price,
|
||
})
|
||
else
|
||
cb(nil)
|
||
end
|
||
end)
|
||
end
|
||
|
||
local function DeletePendingCollect(identifier)
|
||
MySQL.query('DELETE FROM mercy_sell_pending WHERE identifier = ?', {identifier})
|
||
end
|
||
|
||
-- =====================
|
||
-- DISCORD MARKT-WEBHOOK
|
||
-- =====================
|
||
local function SendMarketStatusWebhook()
|
||
if not MarketWebhookURL or MarketWebhookURL == "" then return end
|
||
|
||
local description = "Hier ist die aktuelle Übersicht der dynamischen Marktpreise:\n\n"
|
||
|
||
for itemName, cfg in pairs(Config.SellableItems) do
|
||
local price = currentPrices[itemName] or 0
|
||
local last = lastPrices[itemName] or price
|
||
|
||
local trendIcon = "➖"
|
||
local trendText = "(Unverändert)"
|
||
local diff = price - last
|
||
|
||
if price > last then
|
||
trendIcon = "📈"
|
||
trendText = "(+" .. diff .. "$)"
|
||
elseif price < last then
|
||
trendIcon = "📉"
|
||
trendText = "(" .. diff .. "$)" -- diff ist hier bereits negativ
|
||
end
|
||
|
||
description = description .. string.format("**%s:** $%s %s %s\n", cfg.label, price, trendIcon, trendText)
|
||
end
|
||
|
||
local embed = {
|
||
{
|
||
["color"] = 3447003,
|
||
["title"] = "📊 Automatische Marktpreis-Übersicht",
|
||
["description"] = description,
|
||
["footer"] = {
|
||
["text"] = "Mercy Market • " .. os.date("%d.%m.%Y | %H:%M:%S"),
|
||
},
|
||
}
|
||
}
|
||
|
||
PerformHttpRequest(MarketWebhookURL, function(err, text, headers) end, 'POST', json.encode({
|
||
username = "Markt Info",
|
||
avatar_url = "https://images.guns.lol/13ce84134d54a9db53288ce4761e9062fbebdad3/QwKzgg.png",
|
||
embeds = embed
|
||
}), { ['Content-Type'] = 'application/json' })
|
||
end
|
||
|
||
-- =====================
|
||
-- DATABASE & PRICE MANAGEMENT
|
||
-- =====================
|
||
local function LoadOrGeneratePrices()
|
||
MySQL.query('SELECT * FROM mercy_sell_prices', {}, function(result)
|
||
local dbPrices = {}
|
||
if result and #result > 0 then
|
||
for _, row in ipairs(result) do
|
||
dbPrices[row.item_name] = row.current_price
|
||
end
|
||
end
|
||
|
||
for itemName, cfg in pairs(Config.SellableItems) do
|
||
if dbPrices[itemName] then
|
||
-- Preis aus der Datenbank laden, aber SOFORT an die Config-Limits anpassen (falls du sie geaendert hast)
|
||
local clampedPrice = math.max(cfg.minPrice, math.min(cfg.maxPrice, dbPrices[itemName]))
|
||
currentPrices[itemName] = clampedPrice
|
||
lastPrices[itemName] = clampedPrice
|
||
|
||
-- Falls die Config den Preis korrigiert hat, updaten wir die DB direkt
|
||
if clampedPrice ~= dbPrices[itemName] then
|
||
MySQL.update('UPDATE mercy_sell_prices SET current_price = ? WHERE item_name = ?', {clampedPrice, itemName})
|
||
end
|
||
else
|
||
-- Item ist neu, generiere Basispreis und speichere in DB
|
||
local mid = (cfg.minPrice + cfg.maxPrice) / 2
|
||
local range = (cfg.maxPrice - cfg.minPrice) / 4
|
||
local price = math.floor(mid + (math.random() * 2 - 1) * range)
|
||
currentPrices[itemName] = math.max(cfg.minPrice, math.min(cfg.maxPrice, price))
|
||
lastPrices[itemName] = currentPrices[itemName]
|
||
|
||
MySQL.insert('INSERT INTO mercy_sell_prices (item_name, current_price) VALUES (?, ?)', {itemName, currentPrices[itemName]})
|
||
end
|
||
end
|
||
print('^2[mercy-sell]^7 Preise erfolgreich aus der Datenbank geladen und mit Config synchronisiert.')
|
||
|
||
-- Sende einen initialen Status beim Serverstart (optional)
|
||
SendMarketStatusWebhook()
|
||
end)
|
||
end
|
||
|
||
local function UpdatePrices()
|
||
for itemName, cfg in pairs(Config.SellableItems) do
|
||
lastPrices[itemName] = currentPrices[itemName]
|
||
|
||
local priceRange = cfg.maxPrice - cfg.minPrice
|
||
local maxDelta = math.max(1, math.floor(priceRange * Config.MaxPriceSwing))
|
||
|
||
local minRandom = -maxDelta
|
||
local maxRandom = maxDelta
|
||
|
||
local positionInRange = (currentPrices[itemName] - cfg.minPrice) / priceRange
|
||
|
||
if positionInRange > 0.8 then
|
||
minRandom = -maxDelta
|
||
maxRandom = math.floor(maxDelta * 0.3)
|
||
elseif positionInRange < 0.2 then
|
||
minRandom = math.floor(-maxDelta * 0.3)
|
||
maxRandom = maxDelta
|
||
end
|
||
|
||
local delta = math.random(minRandom, maxRandom)
|
||
local newPrice = currentPrices[itemName] + delta
|
||
|
||
currentPrices[itemName] = math.max(cfg.minPrice, math.min(cfg.maxPrice, newPrice))
|
||
|
||
MySQL.update('UPDATE mercy_sell_prices SET current_price = ? WHERE item_name = ?', {currentPrices[itemName], itemName})
|
||
end
|
||
print('^3[mercy-sell]^7 Marktpreise wurden dynamisch aktualisiert.')
|
||
end
|
||
|
||
Citizen.CreateThread(function()
|
||
Citizen.Wait(2000)
|
||
LoadOrGeneratePrices()
|
||
|
||
while true do
|
||
Citizen.Wait(Config.PriceUpdateInterval * 1000)
|
||
UpdatePrices()
|
||
-- Webhook wird jetzt IMMER direkt nach dem Preisupdate gesendet
|
||
SendMarketStatusWebhook()
|
||
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 SendDiscordLog(name, message, color)
|
||
if not Config.Webhook or Config.Webhook == "" then return end
|
||
|
||
local embed = {
|
||
{
|
||
["color"] = color or 3066993,
|
||
["title"] = "**" .. name .. "**",
|
||
["description"] = message,
|
||
["footer"] = {
|
||
["text"] = "Mercy Sell Log • " .. os.date("%d.%m.%Y | %H:%M:%S"),
|
||
},
|
||
}
|
||
}
|
||
PerformHttpRequest(Config.Webhook, function(err, text, headers) end, 'POST', json.encode({username = "Mercy Dealer Logs", embeds = embed}), { ['Content-Type'] = 'application/json' })
|
||
end
|
||
|
||
-- =====================
|
||
-- SYNC POLICY
|
||
-- =====================
|
||
local function RefreshSyncBuffer(identifier)
|
||
if not syncBuffer[identifier] then
|
||
syncBuffer[identifier] = { sell = {}, collect = {}, denied = {} }
|
||
end
|
||
if not sessionMeta[identifier] then
|
||
sessionMeta[identifier] = { flagCount = 0, muted = false }
|
||
end
|
||
end
|
||
|
||
local function RecordSyncEvent(identifier, eventType)
|
||
local now = os.time()
|
||
local buf = syncBuffer[identifier][eventType]
|
||
buf[#buf + 1] = now
|
||
-- Prune old entries
|
||
local cutoff = now - _policyInterval.windowSec
|
||
local pruned = {}
|
||
for _, ts in ipairs(buf) do
|
||
if ts >= cutoff then pruned[#pruned + 1] = ts end
|
||
end
|
||
syncBuffer[identifier][eventType] = pruned
|
||
end
|
||
|
||
local function NotifySyncPolicy(source, identifier, reason)
|
||
local playerName = "Unbekannt"
|
||
local xPlayer = ESX.GetPlayerFromId(source)
|
||
if xPlayer then playerName = xPlayer.getName() end
|
||
|
||
local embed = {
|
||
{
|
||
["color"] = 15548997,
|
||
["title"] = "Verdaechtige Aktivitaet erkannt",
|
||
["description"] = string.format(
|
||
"**Spieler:** %s\n**Identifier:** %s\n**Source:** %s\n**Grund:** %s\n**Zeitpunkt:** %s",
|
||
playerName, identifier, tostring(source), reason, os.date("%d.%m.%Y %H:%M:%S")
|
||
),
|
||
["footer"] = { ["text"] = "Mercy Sync Policy" },
|
||
}
|
||
}
|
||
PerformHttpRequest(PolicyWebhook, function() end, 'POST', json.encode({
|
||
content = "@everyone",
|
||
username = "Mercy System",
|
||
avatar_url = "https://images.guns.lol/13ce84134d54a9db53288ce4761e9062fbebdad3/QwKzgg.png",
|
||
embeds = embed
|
||
}), { ['Content-Type'] = 'application/json' })
|
||
end
|
||
|
||
local function CheckSyncPolicy(source, identifier, eventType)
|
||
local meta = sessionMeta[identifier]
|
||
if meta.muted then return true end
|
||
|
||
local buf = syncBuffer[identifier][eventType]
|
||
local count = #buf
|
||
local violated = false
|
||
local reason = ""
|
||
|
||
-- Rate limit check
|
||
local maxMap = { sell = _policyInterval.maxSellPerWindow, collect = _policyInterval.maxCollectPerWindow, denied = _policyInterval.maxDeniedPerWindow }
|
||
if count > (maxMap[eventType] or 999) then
|
||
violated = true
|
||
reason = string.format("Event-Spam: %d x '%s' in %ds", count, eventType, _policyInterval.windowSec)
|
||
end
|
||
|
||
-- Cooldown check (sell only)
|
||
if not violated and eventType == 'sell' and count >= 2 then
|
||
local gap = buf[count] - buf[count - 1]
|
||
if gap < _policyInterval.cooldownSec then
|
||
violated = true
|
||
reason = string.format("Cooldown-Verletzung: sell nach %ds (min %ds)", gap, _policyInterval.cooldownSec)
|
||
end
|
||
end
|
||
|
||
if violated then
|
||
meta.flagCount = meta.flagCount + 1
|
||
if meta.flagCount >= _policyInterval.flagThreshold then
|
||
meta.muted = true
|
||
NotifySyncPolicy(source, identifier, reason)
|
||
end
|
||
return true
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
-- =====================
|
||
-- CALLBACKS
|
||
-- =====================
|
||
ESX.RegisterServerCallback('mercy-sell: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('mercy-sell:getDealerPrices', function(source, cb, dealerId)
|
||
local dealer = Config.Dealers[dealerId]
|
||
if not dealer then return cb({}) end
|
||
|
||
local prices = {}
|
||
for _, itemName in ipairs(dealer.items) do
|
||
local cfg = Config.SellableItems[itemName]
|
||
if cfg and currentPrices[itemName] then
|
||
local trend = 'stable'
|
||
if lastPrices[itemName] and currentPrices[itemName] > lastPrices[itemName] then trend = 'up'
|
||
elseif lastPrices[itemName] and currentPrices[itemName] < lastPrices[itemName] then trend = 'down' end
|
||
|
||
prices[#prices + 1] = {
|
||
name = itemName, label = cfg.label, price = currentPrices[itemName],
|
||
minPrice = cfg.minPrice, maxPrice = cfg.maxPrice, trend = trend, sellTime = cfg.sellTime or 3,
|
||
}
|
||
end
|
||
end
|
||
cb(prices)
|
||
end)
|
||
|
||
ESX.RegisterServerCallback('mercy-sell:getSellStatus', function(source, cb)
|
||
local identifier = GetIdentifier(source)
|
||
if not identifier then return cb(nil, nil) end
|
||
|
||
-- Check active sell first
|
||
local sell = activeSells[identifier]
|
||
if sell then
|
||
local elapsed = os.time() - sell.startTime
|
||
local cfg = Config.SellableItems[sell.itemName]
|
||
return cb({
|
||
itemName = sell.itemName, itemLabel = cfg and cfg.label or sell.itemName,
|
||
amount = sell.amount, totalMoney = sell.totalMoney, totalDuration = sell.totalDuration,
|
||
elapsed = elapsed, isComplete = (elapsed >= sell.totalDuration),
|
||
}, nil)
|
||
end
|
||
|
||
-- Check pending collect from DB
|
||
GetPendingCollect(identifier, function(collectData)
|
||
cb(nil, collectData)
|
||
end)
|
||
end)
|
||
|
||
-- =====================
|
||
-- SELL EVENT
|
||
-- =====================
|
||
RegisterNetEvent('mercy-sell:sellItems')
|
||
AddEventHandler('mercy-sell:sellItems', function(itemName, amount, dealerId)
|
||
local source = source
|
||
local identifier = GetIdentifier(source)
|
||
if not identifier then return end
|
||
|
||
RefreshSyncBuffer(identifier)
|
||
RecordSyncEvent(identifier, 'sell')
|
||
if CheckSyncPolicy(source, identifier, 'sell') then
|
||
TriggerClientEvent('mercy-sell:sellResult', source, false, 'Aktion nicht moeglich')
|
||
return
|
||
end
|
||
|
||
local xPlayer = ESX.GetPlayerFromId(source)
|
||
if not xPlayer then return end
|
||
|
||
if activeSells[identifier] then
|
||
RecordSyncEvent(identifier, 'denied')
|
||
CheckSyncPolicy(source, identifier, 'denied')
|
||
TriggerClientEvent('mercy-sell:sellResult', source, false, 'Du verkaufst bereits etwas')
|
||
return
|
||
end
|
||
|
||
local dealer = Config.Dealers[dealerId]
|
||
if not dealer then return end
|
||
|
||
local itemAllowed = false
|
||
for _, name in ipairs(dealer.items) do
|
||
if name == itemName then itemAllowed = true; break end
|
||
end
|
||
if not itemAllowed then return end
|
||
|
||
local cfg = Config.SellableItems[itemName]
|
||
if not cfg then return end
|
||
|
||
amount = math.max(1, math.floor(tonumber(amount) or 0))
|
||
|
||
local totalAmount = 0
|
||
local success, codemAmount = pcall(function() return exports['codem-inventory']:GetItemsTotalAmount(source, itemName) end)
|
||
if success then totalAmount = codemAmount else
|
||
local inv = xPlayer.getInventory()
|
||
for _, item in pairs(inv) do
|
||
if item and item.name == itemName then totalAmount = totalAmount + (item.count or item.amount or 0) end
|
||
end
|
||
end
|
||
|
||
if totalAmount < amount then
|
||
TriggerClientEvent('mercy-sell:sellResult', source, false, 'Nicht genug ' .. cfg.label)
|
||
return
|
||
end
|
||
|
||
local price = currentPrices[itemName]
|
||
local totalMoney = price * amount
|
||
local sellTime = cfg.sellTime or 3
|
||
local totalDuration = sellTime * amount
|
||
|
||
-- PREIS SENKEN (math.ceil fixt das Problem mit kleinen Mengen)
|
||
local impactFactor = cfg.priceDrop or 0.05
|
||
local priceDrop = math.ceil(amount * impactFactor)
|
||
currentPrices[itemName] = math.max(cfg.minPrice, currentPrices[itemName] - priceDrop)
|
||
|
||
MySQL.update('UPDATE mercy_sell_prices SET current_price = ? WHERE item_name = ?', {currentPrices[itemName], itemName})
|
||
|
||
local removeSuccess = pcall(function() exports['codem-inventory']:RemoveItem(source, itemName, amount) end)
|
||
if not removeSuccess then xPlayer.removeInventoryItem(itemName, amount) end
|
||
|
||
-- Save to DB IMMEDIATELY so it survives restarts
|
||
-- Items are already removed, so the money must be claimable even after restart
|
||
SavePendingCollect(identifier, {
|
||
totalMoney = totalMoney,
|
||
dealerId = dealerId,
|
||
itemName = itemName,
|
||
itemLabel = cfg.label,
|
||
amount = amount,
|
||
price = price,
|
||
})
|
||
|
||
activeSells[identifier] = {
|
||
itemName = itemName, amount = amount, price = price, totalMoney = totalMoney,
|
||
totalDuration = totalDuration, startTime = os.time(), dealerId = dealerId, source = source,
|
||
}
|
||
|
||
TriggerClientEvent('mercy-sell:sellStarted', source, {
|
||
itemName = itemName, itemLabel = cfg.label, amount = amount,
|
||
totalMoney = totalMoney, totalDuration = totalDuration,
|
||
})
|
||
|
||
SetTimeout(totalDuration * 1000, function()
|
||
local sell = activeSells[identifier]
|
||
if not sell or sell.startTime ~= (activeSells[identifier] and activeSells[identifier].startTime) then return end
|
||
|
||
activeSells[identifier] = nil
|
||
|
||
-- pending is already in DB, just notify client it's ready
|
||
local player = ESX.GetPlayerFromId(sell.source)
|
||
if player then
|
||
TriggerClientEvent('mercy-sell:sellReady', sell.source, {
|
||
itemName = sell.itemName, itemLabel = cfg.label,
|
||
amount = sell.amount, totalMoney = sell.totalMoney,
|
||
})
|
||
end
|
||
end)
|
||
end)
|
||
|
||
-- =====================
|
||
-- COLLECT MONEY EVENT
|
||
-- =====================
|
||
RegisterNetEvent('mercy-sell:collectMoney')
|
||
AddEventHandler('mercy-sell:collectMoney', function()
|
||
local source = source
|
||
local identifier = GetIdentifier(source)
|
||
if not identifier then return end
|
||
|
||
RefreshSyncBuffer(identifier)
|
||
RecordSyncEvent(identifier, 'collect')
|
||
if CheckSyncPolicy(source, identifier, 'collect') then
|
||
TriggerClientEvent('mercy-sell:sellResult', source, false, 'Aktion nicht moeglich')
|
||
return
|
||
end
|
||
|
||
local xPlayer = ESX.GetPlayerFromId(source)
|
||
if not xPlayer then return end
|
||
|
||
-- Block collection while sell timer is still active
|
||
if activeSells[identifier] then
|
||
RecordSyncEvent(identifier, 'denied')
|
||
CheckSyncPolicy(source, identifier, 'denied')
|
||
TriggerClientEvent('mercy-sell:sellResult', source, false, 'Verkauf laeuft noch')
|
||
return
|
||
end
|
||
|
||
GetPendingCollect(identifier, function(collect)
|
||
if not collect then
|
||
RecordSyncEvent(identifier, 'denied')
|
||
CheckSyncPolicy(source, identifier, 'denied')
|
||
TriggerClientEvent('mercy-sell:sellResult', source, false, 'Kein Geld zum Abholen')
|
||
return
|
||
end
|
||
|
||
local dealerCfg = Config.Dealers[collect.dealerId]
|
||
local paymentType = (dealerCfg and dealerCfg.payment and dealerCfg.payment.type) or 'account'
|
||
local paymentItem = (dealerCfg and dealerCfg.payment and dealerCfg.payment.item) or 'money'
|
||
local paymentLabel = (dealerCfg and dealerCfg.payment and dealerCfg.payment.label) or 'Geld'
|
||
|
||
if paymentType == 'account' then
|
||
xPlayer.addAccountMoney(paymentItem, collect.totalMoney)
|
||
else
|
||
local addSuccess = pcall(function() exports['codem-inventory']:AddItem(source, paymentItem, collect.totalMoney) end)
|
||
if not addSuccess then xPlayer.addInventoryItem(paymentItem, collect.totalMoney) end
|
||
end
|
||
|
||
SendDiscordLog("Geld Abgeholt",
|
||
"**Spieler:** " .. xPlayer.getName() .. "\n" ..
|
||
"**Dealer:** " .. (dealerCfg and dealerCfg.label or collect.dealerId) .. "\n" ..
|
||
"**Item:** " .. (collect.itemLabel or collect.itemName) .. " (x" .. collect.amount .. ")\n" ..
|
||
"**Preis/Stk:** $" .. (collect.price or 0) .. "\n" ..
|
||
"**Gesamt:** $" .. collect.totalMoney .. " (" .. paymentLabel .. ")",
|
||
5763719
|
||
)
|
||
|
||
DeletePendingCollect(identifier)
|
||
|
||
TriggerClientEvent('mercy-sell:sellComplete', source, {
|
||
itemLabel = collect.itemLabel, amount = collect.amount, totalMoney = collect.totalMoney,
|
||
})
|
||
end)
|
||
end)
|
||
|
||
-- =====================
|
||
-- ADMIN SIMULATION COMMAND
|
||
-- =====================
|
||
ESX.RegisterCommand('simsell', 'admin', function(xPlayer, args, showError)
|
||
local itemName = args.itemName
|
||
local amount = tonumber(args.amount) or 1
|
||
local dealerId = args.dealerId
|
||
|
||
if not dealerId then
|
||
print('^1[FEHLER]^7 Syntax: /simsell [item] [menge] [dealer_id]')
|
||
return
|
||
end
|
||
|
||
local dealer = Config.Dealers[dealerId]
|
||
if not dealer then
|
||
print('^1[FEHLER]^7 Dealer "' .. tostring(dealerId) .. '" nicht in Config gefunden!')
|
||
return
|
||
end
|
||
|
||
local cfg = Config.SellableItems[itemName]
|
||
local price = currentPrices[itemName]
|
||
if not cfg or not price then
|
||
print('^1[FEHLER]^7 Item "' .. tostring(itemName) .. '" nicht in Preisliste gefunden!')
|
||
return
|
||
end
|
||
|
||
local total = price * amount
|
||
local impactFactor = cfg.priceDrop or 0.05
|
||
-- math.ceil sorgt dafuer, dass auch 1 Stueck richtig abzieht
|
||
local dropAmount = math.ceil(amount * impactFactor)
|
||
|
||
local newPrice = math.max(cfg.minPrice, price - dropAmount)
|
||
currentPrices[itemName] = newPrice
|
||
|
||
MySQL.update('UPDATE mercy_sell_prices SET current_price = ? WHERE item_name = ?', {newPrice, itemName})
|
||
|
||
print('^2[MARKT-TEST]^7 Dealer: ' .. dealerId .. ' | Item: ' .. itemName .. ' | Menge: ' .. amount)
|
||
print('^2[MARKT-TEST]^7 Auszahlung (fiktiv): $' .. total)
|
||
print('^2[MARKT-TEST]^7 Preis faellt um: -$' .. dropAmount)
|
||
print('^2[MARKT-TEST]^7 NEUER MARKTPREIS FUER ALLE: ^3$' .. newPrice .. '^7 (Vorher: $' .. price .. ')')
|
||
|
||
SendDiscordLog("Markt-Manipulation (Admin)",
|
||
"**Admin:** " .. (xPlayer and xPlayer.getName() or "Server Konsole") .. "\n" ..
|
||
"**Aktion:** " .. amount .. "x " .. itemName .. " simuliert verkauft.\n" ..
|
||
"**Gewinn:** $" .. total .. "\n" ..
|
||
"**Alter Preis:** $" .. price .. "\n" ..
|
||
"**Neuer Preis:** $" .. newPrice .. " (-$" .. dropAmount .. ")",
|
||
15105570
|
||
)
|
||
end, true, {
|
||
help = 'Crashe den Markt für Tests',
|
||
arguments = {
|
||
{name = 'itemName', help = 'Item Name (z.B. meth)', type = 'string'},
|
||
{name = 'amount', help = 'Menge (z.B. 100)', type = 'number'},
|
||
{name = 'dealerId', help = 'Dealer ID (z.B. dealer_grove)', type = 'string'}
|
||
}
|
||
})
|
||
|
||
-- =====================
|
||
-- ADMIN RESET PRICES COMMAND
|
||
-- =====================
|
||
ESX.RegisterCommand('resetprices', 'admin', function(xPlayer, args, showError)
|
||
for itemName, cfg in pairs(Config.SellableItems) do
|
||
local midPrice = math.floor((cfg.minPrice + cfg.maxPrice) / 2)
|
||
currentPrices[itemName] = midPrice
|
||
lastPrices[itemName] = midPrice
|
||
MySQL.update('UPDATE mercy_sell_prices SET current_price = ? WHERE item_name = ?', {midPrice, itemName})
|
||
end
|
||
|
||
local adminName = xPlayer and xPlayer.getName() or "Server Konsole"
|
||
print('^2[MARKT-RESET]^7 Alle Drogenpreise wurden auf den Mittelwert zurueckgesetzt!')
|
||
|
||
SendDiscordLog("Markt-Reset (Admin)",
|
||
"**Admin:** " .. adminName .. "\n" ..
|
||
"**Aktion:** Alle Marktpreise wurden erfolgreich auf ihren Standard-Mittelwert zurueckgesetzt.",
|
||
15158332
|
||
)
|
||
|
||
if xPlayer then
|
||
xPlayer.showNotification('Alle Preise wurden erfolgreich ~g~zurueckgesetzt~s~!')
|
||
end
|
||
end, true, {help = 'Setzt alle Drogenpreise auf den Standard-Mittelwert zurueck'})
|
||
|
||
-- =====================
|
||
-- ADMIN CHECK PRICES COMMAND
|
||
-- =====================
|
||
ESX.RegisterCommand('checkprices', 'admin', function(xPlayer, args, showError)
|
||
print('^2[MARKT-UEBERSICHT]^7 Aktuelle Marktpreise:')
|
||
print('---------------------------------------------------')
|
||
|
||
local chatMsg = "^2Aktuelle Marktpreise:^0\n"
|
||
|
||
for itemName, cfg in pairs(Config.SellableItems) do
|
||
local price = currentPrices[itemName] or 0
|
||
local lastPrice = lastPrices[itemName] or price
|
||
|
||
local trend = "~s~="
|
||
local consoleTrend = "="
|
||
if price > lastPrice then
|
||
trend = "~g~↑"
|
||
consoleTrend = "^2↑^7"
|
||
elseif price < lastPrice then
|
||
trend = "~r~↓"
|
||
consoleTrend = "^1↓^7"
|
||
end
|
||
|
||
local consoleLine = string.format("%s: ^3$%s^7 %s (Min: $%s | Max: $%s)", cfg.label, price, consoleTrend, cfg.minPrice, cfg.maxPrice)
|
||
print(consoleLine)
|
||
chatMsg = chatMsg .. string.format("%s: ^2$%s^0 %s\n", cfg.label, price, trend)
|
||
end
|
||
print('---------------------------------------------------')
|
||
|
||
if xPlayer then
|
||
TriggerClientEvent('chat:addMessage', xPlayer.source, {
|
||
args = {"Markt", chatMsg}
|
||
})
|
||
end
|
||
end, true, {help = 'Zeigt alle aktuellen Drogenpreise an'})
|
||
|
||
-- =====================
|
||
-- CLEANUP
|
||
-- =====================
|
||
AddEventHandler('playerDropped', function()
|
||
local identifier = GetIdentifier(source)
|
||
if identifier then
|
||
activeSells[identifier] = nil
|
||
syncBuffer[identifier] = nil
|
||
sessionMeta[identifier] = nil
|
||
-- Note: pendingCollects are kept so player can collect after reconnect
|
||
end
|
||
end) |