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 ') else TriggerClientEvent('mercy-coinshop:notify', source, 'Verwendung: /' .. Config.Admin.commandName .. ' add|remove ', '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)