1014 lines
38 KiB
Lua
1014 lines
38 KiB
Lua
ESX = exports['es_extended']:getSharedObject()
|
|
|
|
-- In-memory caches
|
|
local coinCache = {} -- identifier -> coin_count
|
|
local stockCache = {} -- item_model -> sold_count
|
|
local redeemedCodes = {} -- code -> true (to prevent double-redeem)
|
|
local imageCache = {} -- model -> base64 data url
|
|
|
|
-- =====================
|
|
-- 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 IsAdmin(source)
|
|
return IsPlayerAceAllowed(source, Config.Admin.acePermission)
|
|
end
|
|
|
|
local function GetPlayerCoins(identifier)
|
|
return coinCache[identifier] or 0
|
|
end
|
|
|
|
local function SetPlayerCoins(identifier, amount)
|
|
coinCache[identifier] = amount
|
|
MySQL.query('UPDATE mercy_coinshop_coins SET coins = ? WHERE identifier = ?', { amount, identifier })
|
|
end
|
|
|
|
local function AddPlayerCoins(identifier, amount)
|
|
local current = GetPlayerCoins(identifier)
|
|
local newAmount = current + amount
|
|
coinCache[identifier] = newAmount
|
|
MySQL.query('INSERT INTO mercy_coinshop_coins (identifier, coins) VALUES (?, ?) ON DUPLICATE KEY UPDATE coins = ?', { identifier, newAmount, newAmount })
|
|
end
|
|
|
|
local function DeductPlayerCoins(identifier, amount)
|
|
local current = GetPlayerCoins(identifier)
|
|
local newAmount = math.max(0, current - amount)
|
|
coinCache[identifier] = newAmount
|
|
MySQL.query('UPDATE mercy_coinshop_coins SET coins = ? WHERE identifier = ?', { newAmount, identifier })
|
|
return newAmount
|
|
end
|
|
|
|
local function LogTransaction(identifier, playerName, txType, itemName, price)
|
|
MySQL.query('INSERT INTO mercy_coinshop_transactions (identifier, player_name, type, item_name, price) VALUES (?, ?, ?, ?, ?)', {
|
|
identifier, playerName, txType, itemName, price
|
|
})
|
|
end
|
|
|
|
local function GetStockCount(model)
|
|
return stockCache[model] or 0
|
|
end
|
|
|
|
local function IncrementStock(model)
|
|
local current = GetStockCount(model)
|
|
stockCache[model] = current + 1
|
|
MySQL.query('INSERT INTO mercy_coinshop_stock (item_model, sold_count) VALUES (?, 1) ON DUPLICATE KEY UPDATE sold_count = sold_count + 1', { model })
|
|
end
|
|
|
|
-- =====================
|
|
-- WEBHOOK
|
|
-- =====================
|
|
local function SendWebhook(title, message, color)
|
|
if not Config.WebhookURL or Config.WebhookURL == "" then return end
|
|
local embed = {
|
|
{
|
|
["color"] = color or 16761600,
|
|
["title"] = "**" .. title .. "**",
|
|
["description"] = message,
|
|
["footer"] = { ["text"] = os.date("%d.%m.%Y | %H:%M:%S") },
|
|
}
|
|
}
|
|
PerformHttpRequest(Config.WebhookURL, function() end, 'POST', json.encode({username = "Coinshop Log", embeds = embed}), { ['Content-Type'] = 'application/json' })
|
|
end
|
|
|
|
-- =====================
|
|
-- WEEKLY VEHICLE
|
|
-- =====================
|
|
local function GetCurrentWeeklyVehicles()
|
|
if not Config.WeeklyVehicles.enabled or not Config.WeeklyVehicles.pool or #Config.WeeklyVehicles.pool == 0 then
|
|
return {}
|
|
end
|
|
local weekNum = tonumber(os.date('%W')) or 0
|
|
local amount = Config.WeeklyVehicles.amountPerWeek or 3
|
|
local pool = Config.WeeklyVehicles.pool
|
|
local result = {}
|
|
|
|
for i = 1, math.min(amount, #pool) do
|
|
local poolIndex = ((weekNum + i - 1) % #pool) + 1
|
|
result[#result + 1] = pool[poolIndex]
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- Legacy single vehicle (for FindWeeklyVehicle)
|
|
local function GetCurrentWeeklyVehicle()
|
|
local vehicles = GetCurrentWeeklyVehicles()
|
|
if #vehicles > 0 then return vehicles[1] end
|
|
return nil
|
|
end
|
|
|
|
local function GetWeeklyPrice(vehicle)
|
|
if not vehicle then return 0 end
|
|
local discount = Config.WeeklyVehicles.discount or 0
|
|
if discount <= 0 then return vehicle.price end
|
|
return math.floor(vehicle.price * (1 - discount / 100))
|
|
end
|
|
|
|
-- =====================
|
|
-- FIND ITEM IN CONFIG
|
|
-- =====================
|
|
local function FindVehicleInConfig(model)
|
|
for _, cat in ipairs(Config.Categories) do
|
|
if cat.type == 'vehicle' and cat.items then
|
|
for _, item in ipairs(cat.items) do
|
|
if item.model == model then
|
|
return item, cat
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
local function FindWeeklyVehicle(model)
|
|
local weeklies = GetCurrentWeeklyVehicles()
|
|
for _, weekly in ipairs(weeklies) do
|
|
if weekly.model == model then
|
|
return weekly
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function FindItemInConfig(categoryId, itemIndex)
|
|
for _, cat in ipairs(Config.Categories) do
|
|
if cat.id == categoryId and cat.type == 'item' and cat.items then
|
|
if cat.items[itemIndex] then
|
|
return cat.items[itemIndex], cat
|
|
end
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
local function FindPackInConfig(categoryId, itemIndex)
|
|
for _, cat in ipairs(Config.Categories) do
|
|
if cat.id == categoryId and cat.type == 'pack' and cat.items then
|
|
if cat.items[itemIndex] then
|
|
return cat.items[itemIndex], cat
|
|
end
|
|
end
|
|
end
|
|
return nil, nil
|
|
end
|
|
|
|
-- =====================
|
|
-- DATABASE SETUP
|
|
-- =====================
|
|
MySQL.ready(function()
|
|
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercy_coinshop_coins (
|
|
identifier VARCHAR(60) NOT NULL PRIMARY KEY,
|
|
coins INT NOT NULL DEFAULT 0,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)]])
|
|
|
|
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercy_coinshop_transactions (
|
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
identifier VARCHAR(60) NOT NULL,
|
|
player_name VARCHAR(100) NOT NULL,
|
|
type VARCHAR(30) NOT NULL,
|
|
item_name VARCHAR(100) NOT NULL,
|
|
price INT NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_identifier (identifier),
|
|
INDEX idx_created (created_at)
|
|
)]])
|
|
|
|
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercy_coinshop_stock (
|
|
item_model VARCHAR(60) NOT NULL PRIMARY KEY,
|
|
sold_count INT NOT NULL DEFAULT 0
|
|
)]])
|
|
|
|
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercy_coinshop_redeemed (
|
|
code VARCHAR(100) NOT NULL PRIMARY KEY,
|
|
identifier VARCHAR(60) NOT NULL,
|
|
redeemed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)]])
|
|
|
|
MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercy_coinshop_images (
|
|
model VARCHAR(60) NOT NULL PRIMARY KEY,
|
|
image LONGTEXT NOT NULL,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
)]])
|
|
|
|
-- Load coin cache
|
|
local coinRows = MySQL.query.await('SELECT * FROM mercy_coinshop_coins')
|
|
if coinRows then
|
|
for _, row in ipairs(coinRows) do
|
|
coinCache[row.identifier] = row.coins
|
|
end
|
|
end
|
|
|
|
-- Load stock cache
|
|
local stockRows = MySQL.query.await('SELECT * FROM mercy_coinshop_stock')
|
|
if stockRows then
|
|
for _, row in ipairs(stockRows) do
|
|
stockCache[row.item_model] = row.sold_count
|
|
end
|
|
end
|
|
|
|
-- Load redeemed codes
|
|
local redeemRows = MySQL.query.await('SELECT code FROM mercy_coinshop_redeemed')
|
|
if redeemRows then
|
|
for _, row in ipairs(redeemRows) do
|
|
redeemedCodes[row.code] = true
|
|
end
|
|
end
|
|
|
|
-- Load image cache
|
|
local imageRows = MySQL.query.await('SELECT model, image FROM mercy_coinshop_images')
|
|
if imageRows then
|
|
for _, row in ipairs(imageRows) do
|
|
imageCache[row.model] = row.image
|
|
end
|
|
end
|
|
|
|
print('[mercy-coinshop] System gestartet (' .. #(coinRows or {}) .. ' Spieler-Coins, ' .. #(imageRows or {}) .. ' Fahrzeugbilder geladen)')
|
|
end)
|
|
|
|
-- =====================
|
|
-- CALLBACKS
|
|
-- =====================
|
|
ESX.RegisterServerCallback('mercy-coinshop:getShopData', function(source, cb)
|
|
local identifier = GetIdentifier(source)
|
|
if not identifier then return cb(nil) end
|
|
|
|
-- Ensure player has a coin entry
|
|
if coinCache[identifier] == nil then
|
|
coinCache[identifier] = 0
|
|
MySQL.query('INSERT IGNORE INTO mercy_coinshop_coins (identifier, coins) VALUES (?, 0)', { identifier })
|
|
end
|
|
|
|
local coins = GetPlayerCoins(identifier)
|
|
local admin = IsAdmin(source)
|
|
|
|
-- No ownership tracking - players can buy same vehicle multiple times
|
|
|
|
-- Build categories with stock + owned info + tile data
|
|
local categories = {}
|
|
for _, cat in ipairs(Config.Categories) do
|
|
local catData = {
|
|
id = cat.id,
|
|
label = cat.label,
|
|
icon = cat.icon,
|
|
type = cat.type,
|
|
image = cat.image or '',
|
|
color = cat.color or '',
|
|
gridSize = cat.gridSize or 'small',
|
|
}
|
|
|
|
if cat.type == 'vehicle' and cat.items then
|
|
catData.items = {}
|
|
for _, item in ipairs(cat.items) do
|
|
local modelHash = tostring(GetHashKey(item.model))
|
|
local itemData = {
|
|
model = item.model,
|
|
label = item.label,
|
|
price = item.price,
|
|
image = (item.image ~= '' and item.image) or imageCache[item.model] or '',
|
|
description = item.description or '',
|
|
limited = item.limited or false,
|
|
maxStock = item.maxStock or 0,
|
|
soldCount = item.limited and GetStockCount(item.model) or 0,
|
|
owned = false,
|
|
}
|
|
catData.items[#catData.items + 1] = itemData
|
|
end
|
|
|
|
elseif cat.type == 'item' and cat.items then
|
|
catData.items = {}
|
|
for i, item in ipairs(cat.items) do
|
|
local itemData = {
|
|
index = i,
|
|
item = item.item,
|
|
label = item.label,
|
|
price = item.price,
|
|
image = item.image or '',
|
|
description = item.description or '',
|
|
amount = item.amount or 1,
|
|
limited = item.limited or false,
|
|
maxStock = item.maxStock or 0,
|
|
soldCount = item.limited and GetStockCount(item.item) or 0,
|
|
}
|
|
catData.items[#catData.items + 1] = itemData
|
|
end
|
|
|
|
elseif cat.type == 'pack' and cat.items then
|
|
catData.items = {}
|
|
for i, item in ipairs(cat.items) do
|
|
local itemData = {
|
|
index = i,
|
|
label = item.label,
|
|
price = item.price,
|
|
image = item.image or '',
|
|
description = item.description or '',
|
|
limited = item.limited or false,
|
|
maxStock = item.maxStock or 0,
|
|
soldCount = item.limited and GetStockCount('pack_' .. cat.id .. '_' .. i) or 0,
|
|
contents = item.contents or {},
|
|
}
|
|
catData.items[#catData.items + 1] = itemData
|
|
end
|
|
|
|
elseif cat.type == 'plate' then
|
|
catData.price = cat.price or 200
|
|
catData.maxLength = cat.maxLength or 8
|
|
catData.allowedPattern = cat.allowedPattern or '^[A-Z0-9 ]+$'
|
|
|
|
elseif cat.type == 'weekly' then
|
|
local weeklies = GetCurrentWeeklyVehicles()
|
|
catData.items = {}
|
|
for _, weekly in ipairs(weeklies) do
|
|
local modelHash = tostring(GetHashKey(weekly.model))
|
|
catData.items[#catData.items + 1] = {
|
|
model = weekly.model,
|
|
label = weekly.label,
|
|
price = weekly.price,
|
|
discountedPrice = GetWeeklyPrice(weekly),
|
|
discount = Config.WeeklyVehicles.discount or 0,
|
|
image = (weekly.image ~= '' and weekly.image) or imageCache[weekly.model] or '',
|
|
description = weekly.description or '',
|
|
limited = false,
|
|
maxStock = 0,
|
|
soldCount = 0,
|
|
owned = false,
|
|
isWeekly = true,
|
|
}
|
|
end
|
|
end
|
|
|
|
categories[#categories + 1] = catData
|
|
end
|
|
|
|
-- Playtime bonus info
|
|
local playtimeBonus = nil
|
|
if Config.PlaytimeBonus and Config.PlaytimeBonus.enabled then
|
|
playtimeBonus = {
|
|
coins = Config.PlaytimeBonus.coins,
|
|
intervalMinutes = Config.PlaytimeBonus.intervalMinutes,
|
|
}
|
|
end
|
|
|
|
cb({
|
|
coins = coins,
|
|
categories = categories,
|
|
isAdmin = admin,
|
|
playtimeBonus = playtimeBonus,
|
|
tebexEnabled = Config.Tebex and Config.Tebex.enabled or false,
|
|
})
|
|
end)
|
|
|
|
ESX.RegisterServerCallback('mercy-coinshop:getHistory', function(source, cb)
|
|
local identifier = GetIdentifier(source)
|
|
if not identifier then return cb({}) end
|
|
local rows = MySQL.query.await(
|
|
'SELECT type, item_name, price, created_at FROM mercy_coinshop_transactions WHERE identifier = ? ORDER BY created_at DESC LIMIT ?',
|
|
{ identifier, Config.HistoryLimit or 50 }
|
|
)
|
|
cb(rows or {})
|
|
end)
|
|
|
|
ESX.RegisterServerCallback('mercy-coinshop:getPlayerVehicles', function(source, cb)
|
|
local identifier = GetIdentifier(source)
|
|
if not identifier then return cb({}) end
|
|
local rows = MySQL.query.await('SELECT plate, vehicle FROM owned_vehicles WHERE owner = ?', { identifier })
|
|
local vehicles = {}
|
|
if rows then
|
|
for _, row in ipairs(rows) do
|
|
local label = 'Unbekannt'
|
|
local modelHash = nil
|
|
if row.vehicle then
|
|
local vehData = json.decode(row.vehicle)
|
|
if vehData and vehData.model then
|
|
modelHash = vehData.model
|
|
-- Try to find a readable name from config
|
|
local found = false
|
|
for _, cat in ipairs(Config.Categories) do
|
|
if cat.type == 'vehicle' and cat.items then
|
|
for _, item in ipairs(cat.items) do
|
|
if GetHashKey(item.model) == vehData.model then
|
|
label = item.label
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
if found then break end
|
|
end
|
|
-- Also check weekly pool
|
|
if not found and Config.WeeklyVehicles and Config.WeeklyVehicles.pool then
|
|
for _, item in ipairs(Config.WeeklyVehicles.pool) do
|
|
if GetHashKey(item.model) == vehData.model then
|
|
label = item.label
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
vehicles[#vehicles + 1] = {
|
|
plate = row.plate,
|
|
label = label,
|
|
}
|
|
end
|
|
end
|
|
cb(vehicles)
|
|
end)
|
|
|
|
-- =====================
|
|
-- TEBEX REDEEM (manual)
|
|
-- =====================
|
|
ESX.RegisterServerCallback('mercy-coinshop:redeemCode', function(source, cb, code)
|
|
local identifier = GetIdentifier(source)
|
|
local playerName = GetPlayerName(source)
|
|
if not identifier then return cb({ success = false, message = 'Spieler nicht gefunden' }) end
|
|
|
|
if not Config.Tebex or not Config.Tebex.enabled or not Config.Tebex.apiKey or Config.Tebex.apiKey == '' then
|
|
return cb({ success = false, message = 'Tebex ist nicht konfiguriert' })
|
|
end
|
|
|
|
code = tostring(code or ''):gsub("^%s+", ""):gsub("%s+$", "")
|
|
if code == '' then
|
|
return cb({ success = false, message = 'Bitte Code eingeben' })
|
|
end
|
|
|
|
-- Check if already redeemed locally
|
|
if redeemedCodes[code] then
|
|
return cb({ success = false, message = Config.Locale.redeemInvalid or 'Code bereits eingeloest!' })
|
|
end
|
|
|
|
-- Look up payment on Tebex
|
|
PerformHttpRequest('https://plugin.tebex.io/payments/' .. code, function(statusCode, responseBody, headers)
|
|
if statusCode ~= 200 then
|
|
return cb({ success = false, message = Config.Locale.redeemInvalid or 'Ungueltiger Code!' })
|
|
end
|
|
|
|
local ok, data = pcall(json.decode, responseBody)
|
|
if not ok or not data then
|
|
return cb({ success = false, message = Config.Locale.redeemError or 'Fehler beim Einloesen' })
|
|
end
|
|
|
|
-- Check payment status
|
|
if data.status ~= 'Complete' then
|
|
return cb({ success = false, message = Config.Locale.redeemInvalid or 'Zahlung nicht abgeschlossen!' })
|
|
end
|
|
|
|
-- Parse coin amount from packages
|
|
local totalCoins = 0
|
|
if data.packages then
|
|
for _, pkg in ipairs(data.packages) do
|
|
-- Try to find coin amount in package commands
|
|
if pkg.commands then
|
|
for _, cmd in ipairs(pkg.commands) do
|
|
local cmdStr = cmd.command or cmd
|
|
if type(cmdStr) == 'string' then
|
|
local amount = string.match(cmdStr, 'coinshop_add%s+%S+%s+(%d+)')
|
|
if amount then
|
|
totalCoins = totalCoins + (tonumber(amount) or 0)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Fallback: try amount field
|
|
if totalCoins == 0 and data.amount then
|
|
totalCoins = tonumber(data.amount) or 0
|
|
end
|
|
|
|
if totalCoins <= 0 then
|
|
return cb({ success = false, message = 'Keine Coins in diesem Paket gefunden' })
|
|
end
|
|
|
|
-- Mark as redeemed
|
|
redeemedCodes[code] = true
|
|
MySQL.query('INSERT IGNORE INTO mercy_coinshop_redeemed (code, identifier) VALUES (?, ?)', { code, identifier })
|
|
|
|
-- Credit coins
|
|
AddPlayerCoins(identifier, totalCoins)
|
|
LogTransaction(identifier, playerName, 'tebex_credit', totalCoins .. ' Coins (Tebex Redeem: ' .. code .. ')', 0)
|
|
|
|
local newCoins = GetPlayerCoins(identifier)
|
|
TriggerClientEvent('mercy-coinshop:updateCoins', source, newCoins)
|
|
|
|
SendWebhook("Tebex Redeem", string.format(
|
|
"Spieler: **%s** (%s)\nCode: **%s**\nCoins: **+%d**\nNeuer Stand: **%d**",
|
|
playerName, identifier, code, totalCoins, newCoins
|
|
), 3066993)
|
|
|
|
cb({
|
|
success = true,
|
|
message = string.format(Config.Locale.redeemSuccess or 'Code eingeloest! +%d Coins', totalCoins),
|
|
coins = newCoins,
|
|
})
|
|
end, 'GET', '', {
|
|
['X-Tebex-Secret'] = Config.Tebex.apiKey,
|
|
})
|
|
end)
|
|
|
|
-- =====================
|
|
-- BUY VEHICLE
|
|
-- =====================
|
|
RegisterNetEvent('mercy-coinshop:buyVehicle')
|
|
AddEventHandler('mercy-coinshop:buyVehicle', function(model, isWeekly)
|
|
local source = source
|
|
local identifier = GetIdentifier(source)
|
|
local playerName = GetPlayerName(source)
|
|
if not identifier then return end
|
|
|
|
local admin = IsAdmin(source)
|
|
local item, price
|
|
|
|
if isWeekly then
|
|
local weekly = FindWeeklyVehicle(model)
|
|
if not weekly then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, 'Fahrzeug nicht gefunden', 'error')
|
|
return
|
|
end
|
|
item = weekly
|
|
price = GetWeeklyPrice(weekly)
|
|
else
|
|
local configItem = FindVehicleInConfig(model)
|
|
if not configItem then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, 'Fahrzeug nicht gefunden', 'error')
|
|
return
|
|
end
|
|
item = configItem
|
|
price = configItem.price
|
|
end
|
|
|
|
-- No ownership check - players can buy the same vehicle multiple times
|
|
|
|
-- Check stock for limited items
|
|
if item.limited and item.maxStock and item.maxStock > 0 then
|
|
local sold = GetStockCount(model)
|
|
if sold >= item.maxStock then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.soldOut or 'Ausverkauft!', 'error')
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Check coins
|
|
if not admin then
|
|
local coins = GetPlayerCoins(identifier)
|
|
if coins < price then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.notEnoughCoins or 'Nicht genug Coins!', 'error')
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Generate plate
|
|
local plate = string.char(math.random(65,90)) .. string.char(math.random(65,90)) .. string.char(math.random(65,90)) .. ' ' .. tostring(math.random(100, 999))
|
|
|
|
-- Insert vehicle
|
|
local vehicleData = json.encode({ model = tonumber(GetHashKey(model)), plate = plate })
|
|
MySQL.query('INSERT INTO owned_vehicles (owner, plate, vehicle, type, stored, parking) VALUES (?, ?, ?, ?, 1, ?)', { identifier, plate, vehicleData, 'car', Config.DefaultGarage or 'Garage A' })
|
|
|
|
-- Deduct coins
|
|
local newCoins
|
|
if admin then
|
|
newCoins = GetPlayerCoins(identifier)
|
|
else
|
|
newCoins = DeductPlayerCoins(identifier, price)
|
|
end
|
|
|
|
-- Increment stock
|
|
if item.limited then
|
|
IncrementStock(model)
|
|
end
|
|
|
|
-- Log
|
|
LogTransaction(identifier, playerName, 'vehicle', item.label, admin and 0 or price)
|
|
|
|
-- Notify
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.purchaseSuccess or 'Erfolgreich gekauft!', 'success')
|
|
TriggerClientEvent('mercy-coinshop:updateCoins', source, newCoins)
|
|
TriggerClientEvent('mercy-coinshop:purchaseSuccess', source, model)
|
|
|
|
SendWebhook("Fahrzeug Gekauft", string.format(
|
|
"Spieler: **%s** (%s)\nFahrzeug: **%s**\nPreis: **%d Coins**%s\nKennzeichen: **%s**",
|
|
playerName, identifier, item.label, admin and 0 or price,
|
|
admin and ' (Admin-Bypass)' or '', plate
|
|
), admin and 10181046 or 3066993)
|
|
end)
|
|
|
|
-- =====================
|
|
-- BUY ITEM (Inventar)
|
|
-- =====================
|
|
RegisterNetEvent('mercy-coinshop:buyItem')
|
|
AddEventHandler('mercy-coinshop:buyItem', function(categoryId, itemIndex)
|
|
local source = source
|
|
local identifier = GetIdentifier(source)
|
|
local playerName = GetPlayerName(source)
|
|
if not identifier then return end
|
|
|
|
local admin = IsAdmin(source)
|
|
local xPlayer = ESX.GetPlayerFromId(source)
|
|
if not xPlayer then return end
|
|
|
|
local item, cat = FindItemInConfig(categoryId, itemIndex)
|
|
if not item then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, 'Item nicht gefunden', 'error')
|
|
return
|
|
end
|
|
|
|
local price = item.price or 0
|
|
|
|
-- Check stock
|
|
if item.limited and item.maxStock and item.maxStock > 0 then
|
|
local sold = GetStockCount(item.item)
|
|
if sold >= item.maxStock then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.soldOut or 'Ausverkauft!', 'error')
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Check coins
|
|
if not admin then
|
|
local coins = GetPlayerCoins(identifier)
|
|
if coins < price then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.notEnoughCoins or 'Nicht genug Coins!', 'error')
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Give item
|
|
local amount = item.amount or 1
|
|
xPlayer.addInventoryItem(item.item, amount)
|
|
|
|
-- Deduct coins
|
|
local newCoins
|
|
if admin then
|
|
newCoins = GetPlayerCoins(identifier)
|
|
else
|
|
newCoins = DeductPlayerCoins(identifier, price)
|
|
end
|
|
|
|
-- Increment stock
|
|
if item.limited then
|
|
IncrementStock(item.item)
|
|
end
|
|
|
|
-- Log
|
|
LogTransaction(identifier, playerName, 'item', item.label .. ' x' .. amount, admin and 0 or price)
|
|
|
|
-- Notify
|
|
TriggerClientEvent('mercy-coinshop:notify', source, string.format(Config.Locale.itemReceived or '%dx %s erhalten!', amount, item.label), 'success')
|
|
TriggerClientEvent('mercy-coinshop:updateCoins', source, newCoins)
|
|
TriggerClientEvent('mercy-coinshop:purchaseSuccess', source, item.item)
|
|
|
|
SendWebhook("Item Gekauft", string.format(
|
|
"Spieler: **%s** (%s)\nItem: **%s** x%d\nPreis: **%d Coins**%s",
|
|
playerName, identifier, item.label, amount, admin and 0 or price,
|
|
admin and ' (Admin-Bypass)' or ''
|
|
), admin and 10181046 or 5763719)
|
|
end)
|
|
|
|
-- =====================
|
|
-- BUY PACK (Bundle)
|
|
-- =====================
|
|
RegisterNetEvent('mercy-coinshop:buyPack')
|
|
AddEventHandler('mercy-coinshop:buyPack', function(categoryId, itemIndex)
|
|
local source = source
|
|
local identifier = GetIdentifier(source)
|
|
local playerName = GetPlayerName(source)
|
|
if not identifier then return end
|
|
|
|
local admin = IsAdmin(source)
|
|
local xPlayer = ESX.GetPlayerFromId(source)
|
|
if not xPlayer then return end
|
|
|
|
local pack, cat = FindPackInConfig(categoryId, itemIndex)
|
|
if not pack then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, 'Pack nicht gefunden', 'error')
|
|
return
|
|
end
|
|
|
|
local price = pack.price or 0
|
|
local stockKey = 'pack_' .. categoryId .. '_' .. itemIndex
|
|
|
|
-- Check stock
|
|
if pack.limited and pack.maxStock and pack.maxStock > 0 then
|
|
local sold = GetStockCount(stockKey)
|
|
if sold >= pack.maxStock then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.soldOut or 'Ausverkauft!', 'error')
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Check coins
|
|
if not admin then
|
|
local coins = GetPlayerCoins(identifier)
|
|
if coins < price then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.notEnoughCoins or 'Nicht genug Coins!', 'error')
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Give all contents
|
|
if pack.contents then
|
|
for _, content in ipairs(pack.contents) do
|
|
if content.type == 'vehicle' and content.model then
|
|
local plate = string.char(math.random(65,90)) .. string.char(math.random(65,90)) .. string.char(math.random(65,90)) .. ' ' .. tostring(math.random(100, 999))
|
|
local vehicleData = json.encode({ model = tonumber(GetHashKey(content.model)), plate = plate })
|
|
MySQL.query('INSERT INTO owned_vehicles (owner, plate, vehicle, type, stored, parking) VALUES (?, ?, ?, ?, 1, ?)', { identifier, plate, vehicleData, 'car', Config.DefaultGarage or 'Garage A' })
|
|
elseif content.type == 'item' and content.item then
|
|
local amount = content.amount or 1
|
|
xPlayer.addInventoryItem(content.item, amount)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Deduct coins
|
|
local newCoins
|
|
if admin then
|
|
newCoins = GetPlayerCoins(identifier)
|
|
else
|
|
newCoins = DeductPlayerCoins(identifier, price)
|
|
end
|
|
|
|
-- Increment stock
|
|
if pack.limited then
|
|
IncrementStock(stockKey)
|
|
end
|
|
|
|
-- Log
|
|
LogTransaction(identifier, playerName, 'pack', pack.label, admin and 0 or price)
|
|
|
|
-- Notify
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.purchaseSuccess or 'Erfolgreich gekauft!', 'success')
|
|
TriggerClientEvent('mercy-coinshop:updateCoins', source, newCoins)
|
|
|
|
SendWebhook("Pack Gekauft", string.format(
|
|
"Spieler: **%s** (%s)\nPack: **%s**\nPreis: **%d Coins**%s",
|
|
playerName, identifier, pack.label, admin and 0 or price,
|
|
admin and ' (Admin-Bypass)' or ''
|
|
), admin and 10181046 or 15844367)
|
|
end)
|
|
|
|
-- =====================
|
|
-- BUY PLATE
|
|
-- =====================
|
|
RegisterNetEvent('mercy-coinshop:buyPlate')
|
|
AddEventHandler('mercy-coinshop:buyPlate', function(vehiclePlate, newPlate)
|
|
local source = source
|
|
local identifier = GetIdentifier(source)
|
|
local playerName = GetPlayerName(source)
|
|
if not identifier then return end
|
|
|
|
local admin = IsAdmin(source)
|
|
|
|
-- Find plate config
|
|
local plateConfig = nil
|
|
for _, cat in ipairs(Config.Categories) do
|
|
if cat.type == 'plate' then
|
|
plateConfig = cat
|
|
break
|
|
end
|
|
end
|
|
|
|
if not plateConfig then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, 'Kennzeichen-Kauf nicht verfuegbar', 'error')
|
|
return
|
|
end
|
|
|
|
local price = plateConfig.price or 200
|
|
local maxLength = plateConfig.maxLength or 8
|
|
local allowedPattern = plateConfig.allowedPattern or '^[A-Z0-9 ]+$'
|
|
|
|
newPlate = string.upper(tostring(newPlate or ''))
|
|
newPlate = newPlate:gsub("^%s+", ""):gsub("%s+$", "")
|
|
|
|
if newPlate == '' then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.plateEmpty or 'Bitte Kennzeichen eingeben!', 'error')
|
|
return
|
|
end
|
|
|
|
if #newPlate > maxLength then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, string.format(Config.Locale.plateTooLong or 'Kennzeichen zu lang (max %d Zeichen)!', maxLength), 'error')
|
|
return
|
|
end
|
|
|
|
if not string.match(newPlate, allowedPattern) then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.plateInvalid or 'Ungueltige Zeichen im Kennzeichen!', 'error')
|
|
return
|
|
end
|
|
|
|
local vehCheck = MySQL.query.await('SELECT 1 FROM owned_vehicles WHERE owner = ? AND plate = ?', { identifier, vehiclePlate })
|
|
if not vehCheck or #vehCheck == 0 then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, 'Fahrzeug nicht gefunden', 'error')
|
|
return
|
|
end
|
|
|
|
local plateCheck = MySQL.query.await('SELECT 1 FROM owned_vehicles WHERE plate = ?', { newPlate })
|
|
if plateCheck and #plateCheck > 0 then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.plateInUse or 'Kennzeichen bereits vergeben!', 'error')
|
|
return
|
|
end
|
|
|
|
if not admin then
|
|
local coins = GetPlayerCoins(identifier)
|
|
if coins < price then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.notEnoughCoins or 'Nicht genug Coins!', 'error')
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Update plate column AND plate inside vehicle JSON
|
|
MySQL.query('UPDATE owned_vehicles SET plate = ?, vehicle = JSON_SET(vehicle, "$.plate", ?) WHERE owner = ? AND plate = ?', { newPlate, newPlate, identifier, vehiclePlate })
|
|
|
|
local newCoins
|
|
if admin then
|
|
newCoins = GetPlayerCoins(identifier)
|
|
else
|
|
newCoins = DeductPlayerCoins(identifier, price)
|
|
end
|
|
|
|
LogTransaction(identifier, playerName, 'plate', vehiclePlate .. ' -> ' .. newPlate, admin and 0 or price)
|
|
|
|
TriggerClientEvent('mercy-coinshop:notify', source, Config.Locale.purchaseSuccess or 'Erfolgreich gekauft!', 'success')
|
|
TriggerClientEvent('mercy-coinshop:updateCoins', source, newCoins)
|
|
|
|
SendWebhook("Kennzeichen Gekauft", string.format(
|
|
"Spieler: **%s** (%s)\nAltes Kennzeichen: **%s**\nNeues Kennzeichen: **%s**\nPreis: **%d Coins**%s",
|
|
playerName, identifier, vehiclePlate, newPlate, admin and 0 or price,
|
|
admin and ' (Admin-Bypass)' or ''
|
|
), admin and 10181046 or 3447003)
|
|
end)
|
|
|
|
-- =====================
|
|
-- PLAYTIME BONUS
|
|
-- =====================
|
|
if Config.PlaytimeBonus and Config.PlaytimeBonus.enabled then
|
|
Citizen.CreateThread(function()
|
|
local intervalMs = (Config.PlaytimeBonus.intervalMinutes or 15) * 60 * 1000
|
|
local bonusCoins = Config.PlaytimeBonus.coins or 10
|
|
|
|
Citizen.Wait(10000) -- Initial wait for server startup
|
|
print('[mercy-coinshop] Playtime Bonus gestartet (' .. bonusCoins .. ' Coins alle ' .. Config.PlaytimeBonus.intervalMinutes .. ' Min)')
|
|
|
|
while true do
|
|
Citizen.Wait(intervalMs)
|
|
|
|
-- Give coins to all online players
|
|
for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do
|
|
local identifier = xPlayer.getIdentifier()
|
|
if identifier then
|
|
AddPlayerCoins(identifier, bonusCoins)
|
|
local newCoins = GetPlayerCoins(identifier)
|
|
TriggerClientEvent('mercy-coinshop:updateCoins', xPlayer.source, newCoins)
|
|
TriggerClientEvent('mercy-coinshop:notify', xPlayer.source, '+' .. bonusCoins .. ' Coins (Spielzeit-Bonus)', 'success')
|
|
TriggerClientEvent('mercy-coinshop:playtimeReset', xPlayer.source)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- =====================
|
|
-- TEBEX POLLING (background, for auto-credit)
|
|
-- =====================
|
|
Citizen.CreateThread(function()
|
|
if not Config.Tebex or not Config.Tebex.apiKey or Config.Tebex.apiKey == '' then
|
|
print('[mercy-coinshop] Tebex API Key nicht gesetzt - Polling deaktiviert')
|
|
return
|
|
end
|
|
|
|
Citizen.Wait(10000)
|
|
print('[mercy-coinshop] Tebex Polling gestartet')
|
|
|
|
while true do
|
|
PerformHttpRequest('https://plugin.tebex.io/queue', function(statusCode, responseBody, headers)
|
|
if statusCode ~= 200 then return end
|
|
|
|
local ok, data = pcall(json.decode, responseBody)
|
|
if not ok or not data then return end
|
|
|
|
local commands = data.commands or {}
|
|
if #commands == 0 then return end
|
|
|
|
local processedIds = {}
|
|
|
|
for _, cmd in ipairs(commands) do
|
|
local playerName = cmd.player and cmd.player.name or 'Unbekannt'
|
|
local command = cmd.command or ''
|
|
|
|
local targetIdentifier, coinAmount = string.match(command, 'coinshop_add%s+(%S+)%s+(%d+)')
|
|
|
|
if targetIdentifier and coinAmount then
|
|
coinAmount = tonumber(coinAmount) or 0
|
|
if coinAmount > 0 then
|
|
AddPlayerCoins(targetIdentifier, coinAmount)
|
|
LogTransaction(targetIdentifier, playerName, 'tebex_credit', coinAmount .. ' Coins (Tebex)', 0)
|
|
|
|
for _, xPlayer in pairs(ESX.GetExtendedPlayers()) do
|
|
if xPlayer.getIdentifier() == targetIdentifier then
|
|
TriggerClientEvent('mercy-coinshop:notify', xPlayer.source, coinAmount .. ' Coins gutgeschrieben!', 'success')
|
|
TriggerClientEvent('mercy-coinshop:updateCoins', xPlayer.source, GetPlayerCoins(targetIdentifier))
|
|
break
|
|
end
|
|
end
|
|
|
|
SendWebhook("Tebex Coins", string.format(
|
|
"Spieler: **%s** (%s)\nCoins: **+%d**\nNeuer Stand: **%d**",
|
|
playerName, targetIdentifier, coinAmount, GetPlayerCoins(targetIdentifier)
|
|
), 3066993)
|
|
end
|
|
end
|
|
|
|
processedIds[#processedIds + 1] = cmd.id
|
|
end
|
|
|
|
if #processedIds > 0 then
|
|
PerformHttpRequest('https://plugin.tebex.io/queue', function(delStatus)
|
|
if delStatus == 204 then
|
|
print('[mercy-coinshop] ' .. #processedIds .. ' Tebex Commands verarbeitet')
|
|
end
|
|
end, 'DELETE', json.encode({ ids = processedIds }), {
|
|
['X-Tebex-Secret'] = Config.Tebex.apiKey,
|
|
['Content-Type'] = 'application/json',
|
|
})
|
|
end
|
|
end, 'GET', '', {
|
|
['X-Tebex-Secret'] = Config.Tebex.apiKey,
|
|
})
|
|
|
|
Citizen.Wait(5 * 60 * 1000) -- Poll every 5 minutes
|
|
end
|
|
end)
|
|
|
|
-- =====================
|
|
-- ADMIN COMMAND
|
|
-- =====================
|
|
RegisterCommand(Config.Admin.commandName, function(source, args)
|
|
if source > 0 and not IsAdmin(source) then
|
|
TriggerClientEvent('mercy-coinshop:notify', source, 'Keine Berechtigung!', 'error')
|
|
return
|
|
end
|
|
|
|
local action = args[1]
|
|
local targetId = tonumber(args[2])
|
|
local amount = tonumber(args[3])
|
|
|
|
if not action or not targetId or not amount or amount <= 0 then
|
|
if source == 0 then
|
|
print('Verwendung: ' .. Config.Admin.commandName .. ' add|remove <server_id> <amount>')
|
|
else
|
|
TriggerClientEvent('mercy-coinshop:notify', source, 'Verwendung: /' .. Config.Admin.commandName .. ' add|remove <id> <amount>', 'error')
|
|
end
|
|
return
|
|
end
|
|
|
|
local targetIdentifier = GetIdentifier(targetId)
|
|
local targetName = GetPlayerName(targetId)
|
|
if not targetIdentifier then
|
|
local msg = 'Spieler nicht gefunden (ID: ' .. targetId .. ')'
|
|
if source == 0 then print(msg) else TriggerClientEvent('mercy-coinshop:notify', source, msg, 'error') end
|
|
return
|
|
end
|
|
|
|
if action == 'add' then
|
|
AddPlayerCoins(targetIdentifier, amount)
|
|
LogTransaction(targetIdentifier, targetName, 'admin_add', amount .. ' Coins (Admin)', 0)
|
|
TriggerClientEvent('mercy-coinshop:updateCoins', targetId, GetPlayerCoins(targetIdentifier))
|
|
TriggerClientEvent('mercy-coinshop:notify', targetId, amount .. ' Coins erhalten!', 'success')
|
|
|
|
local msg = amount .. ' Coins an ' .. targetName .. ' vergeben'
|
|
if source == 0 then print(msg) else TriggerClientEvent('mercy-coinshop:notify', source, msg, 'success') end
|
|
|
|
SendWebhook("Admin: Coins Vergeben", string.format(
|
|
"Admin: **%s**\nSpieler: **%s** (%s)\nCoins: **+%d**\nNeuer Stand: **%d**",
|
|
source == 0 and 'Console' or GetPlayerName(source), targetName, targetIdentifier, amount, GetPlayerCoins(targetIdentifier)
|
|
), 10181046)
|
|
|
|
elseif action == 'remove' then
|
|
DeductPlayerCoins(targetIdentifier, amount)
|
|
LogTransaction(targetIdentifier, targetName, 'admin_remove', amount .. ' Coins (Admin)', 0)
|
|
TriggerClientEvent('mercy-coinshop:updateCoins', targetId, GetPlayerCoins(targetIdentifier))
|
|
TriggerClientEvent('mercy-coinshop:notify', targetId, amount .. ' Coins abgezogen!', 'error')
|
|
|
|
local msg = amount .. ' Coins von ' .. targetName .. ' abgezogen'
|
|
if source == 0 then print(msg) else TriggerClientEvent('mercy-coinshop:notify', source, msg, 'success') end
|
|
|
|
SendWebhook("Admin: Coins Entfernt", string.format(
|
|
"Admin: **%s**\nSpieler: **%s** (%s)\nCoins: **-%d**\nNeuer Stand: **%d**",
|
|
source == 0 and 'Console' or GetPlayerName(source), targetName, targetIdentifier, amount, GetPlayerCoins(targetIdentifier)
|
|
), 15158332)
|
|
else
|
|
local msg = 'Unbekannte Aktion: ' .. action .. ' (add|remove)'
|
|
if source == 0 then print(msg) else TriggerClientEvent('mercy-coinshop:notify', source, msg, 'error') end
|
|
end
|
|
end, true)
|
|
|