ESX = exports['es_extended']:getSharedObject() -- In-memory caches local shops = {} -- shop_id -> shop data local shelves = {} -- shop_id -> { item_name -> {id, item_name, item_label, price, quantity} } local storage = {} -- shop_id -> { item_name -> {id, item_name, item_label, quantity} } local employees = {} -- shop_id -> { identifier -> {id, name} } -- ===================== -- DEFAULT ITEMS HELPERS -- ===================== local function FindDefaultItem(itemName, shopId) local shopConfig = shopId and Config.Shops[shopId] local items = Config.DefaultItems if shopConfig and shopConfig.shopType == 'weapon' then items = Config.WeaponDefaultItems or {} end for _, item in ipairs(items) do if item.name == itemName then return item end end return nil end local itemLabelCache = {} local function GetItemLabel(itemName) if itemLabelCache[itemName] then return itemLabelCache[itemName] end -- Versuche codem-inventory Export 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 -- Fallback: ESX Items 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 local function GetDefaultItemsForNUI(shopId) local shopConfig = Config.Shops[shopId] local items = Config.DefaultItems if shopConfig and shopConfig.shopType == 'weapon' then items = Config.WeaponDefaultItems or {} end local result = {} for _, item in ipairs(items) do result[#result + 1] = { item_name = item.name, item_label = GetItemLabel(item.name), image = item.image or item.name, price = item.price, quantity = -1, -- unlimited category = item.category or '', } end return result end local function GetCategoriesForNUI() if not Config.Categories then return {} end local result = {} for _, cat in ipairs(Config.Categories) do result[#result + 1] = { id = cat.id, label = cat.label, icon = cat.icon or 'circle', } end return result end -- In-memory Lizenz-Cache (identifier -> { licenseName -> true }) local licenseCache = {} local function GetThemeForNUI() if not Config.Theme then return {} end return Config.Theme end -- ===================== -- WEBHOOK HELPER -- ===================== local function SendWebhook(title, message, color) if not Config.WebhookURL or Config.WebhookURL == "" then return end local embed = { { ["color"] = color or 3447003, ["title"] = "**" .. title .. "**", ["description"] = message, ["footer"] = { ["text"] = os.date("%d.%m.%Y | %H:%M:%S") }, } } PerformHttpRequest(Config.WebhookURL, function() end, 'POST', json.encode({username = "Shop Log", embeds = embed}), { ['Content-Type'] = 'application/json' }) end -- ===================== -- HELPERS -- ===================== local function GetIdentifier(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return nil end return xPlayer.getIdentifier() end local function LoadPlayerLicenses(identifier) if licenseCache[identifier] then return end licenseCache[identifier] = {} local result = MySQL.query.await('SELECT license_name FROM mercyv_shop_licenses WHERE identifier = ?', { identifier }) if result then for _, row in ipairs(result) do licenseCache[identifier][row.license_name] = true end end end local function PlayerHasLicense(source, licenseName) local identifier = GetIdentifier(source) if not identifier then return false end LoadPlayerLicenses(identifier) return licenseCache[identifier][licenseName] == true end local function GrantLicense(source, licenseName) local identifier = GetIdentifier(source) if not identifier then return false end LoadPlayerLicenses(identifier) if licenseCache[identifier][licenseName] then return true end MySQL.insert.await('INSERT IGNORE INTO mercyv_shop_licenses (identifier, license_name) VALUES (?, ?)', { identifier, licenseName }) licenseCache[identifier][licenseName] = true return true end local function GetLockedCategories(source, shopId) local shopConfig = Config.Shops[shopId] if not shopConfig then return {} end local categories = Config.Categories if shopConfig.shopType == 'weapon' then categories = Config.WeaponCategories or {} end local locked = {} for _, cat in ipairs(categories) do if cat.requiresLicense and cat.requiresLicense ~= '' then if not PlayerHasLicense(source, cat.requiresLicense) then locked[#locked + 1] = cat.id end end end return locked end local function GetPlayerName(source) local xPlayer = ESX.GetPlayerFromId(source) if not xPlayer then return 'Unbekannt' end return xPlayer.getName() end local function IsOwner(shopId, identifier) local shop = shops[shopId] return shop and shop.owner_identifier == identifier end local function IsEmployee(shopId, identifier) return employees[shopId] and employees[shopId][identifier] ~= nil end local function CountPlayerShops(identifier) local count = 0 for _, shop in pairs(shops) do if shop.owner_identifier == identifier then count = count + 1 end end return count end local function GetItemCategory(itemName) for _, item in ipairs(Config.DefaultItems) do if item.name == itemName then return item.category or '' end end if Config.WeaponDefaultItems then for _, item in ipairs(Config.WeaponDefaultItems) do if item.name == itemName then return item.category or '' end end end return '' end local function GetShelfData(shopId) local result = {} if shelves[shopId] then for _, item in pairs(shelves[shopId]) do if item.quantity > 0 then result[#result + 1] = { item_name = item.item_name, item_label = item.item_label, price = item.price, quantity = item.quantity, category = GetItemCategory(item.item_name), } end end end return result end local function GetStorageData(shopId) local result = {} if storage[shopId] then for _, item in pairs(storage[shopId]) do if item.quantity > 0 then result[#result + 1] = { item_name = item.item_name, item_label = item.item_label, quantity = item.quantity, } end end end return result end local function GetEmployeeData(shopId) local result = {} if employees[shopId] then for identifier, data in pairs(employees[shopId]) do result[#result + 1] = { identifier = identifier, name = data.name, } end end return result end local function GetShopPublicData(shopId) local shop = shops[shopId] if not shop then return nil end local configShop = Config.Shops[shopId] return { shop_id = shopId, shop_name = shop.shop_name, owner_name = shop.owner_name, owner_identifier = shop.owner_identifier, vault_money = shop.vault_money, is_for_sale = shop.is_for_sale, sale_price = shop.sale_price, config_price = configShop and configShop.price or 0, config_label = configShop and configShop.label or '', sell_back_percentage = Config.SellBackPercentage or 0.70, } end -- Nur öffentliche Daten fürClient-Sync (ohne vault_money, owner_identifier etc.) local function GetSyncData() local syncData = {} for shopId, shop in pairs(shops) do syncData[shopId] = { owner_identifier = shop.owner_identifier, -- benötigt fürBlip-Farbe owner_name = shop.owner_name, shop_name = shop.shop_name, } end return syncData end local function CountShelfSlots(shopId) local count = 0 if shelves[shopId] then for _, item in pairs(shelves[shopId]) do if item.quantity and item.quantity > 0 then count = count + 1 end end end return count end local function CountStorageSlots(shopId) local count = 0 if storage[shopId] then for _, item in pairs(storage[shopId]) do if item.quantity and item.quantity > 0 then count = count + 1 end end end return count end local function ClearShopData(shopId) shelves[shopId] = {} storage[shopId] = {} employees[shopId] = {} MySQL.query('DELETE FROM mercyv_shop_shelves WHERE shop_id = ?', { shopId }) MySQL.query('DELETE FROM mercyv_shop_storage WHERE shop_id = ?', { shopId }) MySQL.query('DELETE FROM mercyv_shop_employees WHERE shop_id = ?', { shopId }) end -- ===================== -- SQL SETUP & CACHE LOAD -- ===================== MySQL.ready(function() -- Create tables MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shops ( id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL UNIQUE, owner_identifier VARCHAR(60) DEFAULT NULL, owner_name VARCHAR(100) DEFAULT NULL, shop_name VARCHAR(100) NOT NULL, vault_money INT NOT NULL DEFAULT 0, is_for_sale TINYINT(1) NOT NULL DEFAULT 0, sale_price INT NOT NULL DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_owner (owner_identifier) )]]) MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_employees ( id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL, employee_identifier VARCHAR(60) NOT NULL, employee_name VARCHAR(100) NOT NULL, added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_shop (shop_id), INDEX idx_employee (employee_identifier), UNIQUE KEY uq_shop_employee (shop_id, employee_identifier) )]]) MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_shelves ( id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL, item_name VARCHAR(50) NOT NULL, item_label VARCHAR(100) NOT NULL, price INT NOT NULL, quantity INT NOT NULL DEFAULT 0, INDEX idx_shop_shelves (shop_id), UNIQUE KEY uq_shop_item (shop_id, item_name) )]]) MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_storage ( id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL, item_name VARCHAR(50) NOT NULL, item_label VARCHAR(100) NOT NULL, quantity INT NOT NULL DEFAULT 0, INDEX idx_shop_storage (shop_id), UNIQUE KEY uq_shop_storage_item (shop_id, item_name) )]]) MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_sales_history ( id INT AUTO_INCREMENT PRIMARY KEY, shop_id VARCHAR(50) NOT NULL, buyer_identifier VARCHAR(60) NOT NULL, buyer_name VARCHAR(100) NOT NULL, item_name VARCHAR(50) NOT NULL, item_label VARCHAR(100) NOT NULL, price INT NOT NULL, quantity INT NOT NULL, total INT NOT NULL, sold_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_shop_sales (shop_id), INDEX idx_sold_at (sold_at) )]]) MySQL.query.await([[CREATE TABLE IF NOT EXISTS mercyv_shop_licenses ( id INT AUTO_INCREMENT PRIMARY KEY, identifier VARCHAR(60) NOT NULL, license_name VARCHAR(50) NOT NULL, purchased_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_player_license (identifier, license_name), INDEX idx_identifier (identifier) )]]) -- Ensure all config shops exist in DB for shopId, configShop in pairs(Config.Shops) do MySQL.query.await('INSERT IGNORE INTO mercyv_shops (shop_id, shop_name) VALUES (?, ?)', { shopId, configShop.label }) end -- Load shops local shopRows = MySQL.query.await('SELECT * FROM mercyv_shops') if shopRows then for _, row in ipairs(shopRows) do shops[row.shop_id] = { id = row.id, shop_id = row.shop_id, owner_identifier = row.owner_identifier, owner_name = row.owner_name, shop_name = row.shop_name, vault_money = row.vault_money, is_for_sale = row.is_for_sale == 1, sale_price = row.sale_price, } shelves[row.shop_id] = {} storage[row.shop_id] = {} employees[row.shop_id] = {} end end -- Load shelves local shelfRows = MySQL.query.await('SELECT * FROM mercyv_shop_shelves WHERE quantity > 0') if shelfRows then for _, row in ipairs(shelfRows) do if shelves[row.shop_id] then shelves[row.shop_id][row.item_name] = { id = row.id, item_name = row.item_name, item_label = row.item_label, price = row.price, quantity = row.quantity, } end end end -- Load storage local storageRows = MySQL.query.await('SELECT * FROM mercyv_shop_storage WHERE quantity > 0') if storageRows then for _, row in ipairs(storageRows) do if storage[row.shop_id] then storage[row.shop_id][row.item_name] = { id = row.id, item_name = row.item_name, item_label = row.item_label, quantity = row.quantity, } end end end -- Load employees local empRows = MySQL.query.await('SELECT * FROM mercyv_shop_employees') if empRows then for _, row in ipairs(empRows) do if employees[row.shop_id] then employees[row.shop_id][row.employee_identifier] = { id = row.id, name = row.employee_name, } end end end print('[mercyv-shops] Loaded ' .. #(shopRows or {}) .. ' shops') -- Sync shops to all clients TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData()) end) -- ===================== -- PLAYER ITEMS CALLBACK -- ===================== ESX.RegisterServerCallback('mercyv-shops: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']:GetInventory(xPlayer.getIdentifier(), source) 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) -- ===================== -- ACCESS LEVEL CALLBACK -- ===================== ESX.RegisterServerCallback('mercyv-shops:getAccessLevel', function(source, cb, shopId) -- Waffen-Shops haben keinen Management-Zugang local shopConfig = Config.Shops[shopId] if shopConfig and shopConfig.shopType == 'weapon' then return cb('none', nil) end local identifier = GetIdentifier(source) if not identifier then return cb('none', nil) end local shop = shops[shopId] if not shop then return cb('none', nil) end if not shop.owner_identifier then return cb('unowned', GetShopPublicData(shopId)) end if shop.owner_identifier == identifier then return cb('owner', { shop = GetShopPublicData(shopId), shelves = GetShelfData(shopId), storage = GetStorageData(shopId), employees = GetEmployeeData(shopId), defaultItems = GetDefaultItemsForNUI(shopId), }) end if IsEmployee(shopId, identifier) then return cb('employee', { shop = GetShopPublicData(shopId), shelves = GetShelfData(shopId), storage = GetStorageData(shopId), }) end return cb('none', nil) end) -- ===================== -- SHOP DATA CALLBACKS -- ===================== ESX.RegisterServerCallback('mercyv-shops:getShelfItems', function(source, cb, shopId) local shopConfig = Config.Shops[shopId] local locked = GetLockedCategories(source, shopId) -- Waffen-Shops sind immer NPC-Shops if shopConfig and shopConfig.shopType == 'weapon' then return cb(GetDefaultItemsForNUI(shopId), locked) end local shop = shops[shopId] if shop and not shop.owner_identifier then -- NPC-Shop: return default items cb(GetDefaultItemsForNUI(shopId), locked) else cb(GetShelfData(shopId), locked) end end) ESX.RegisterServerCallback('mercyv-shops:getSalesHistory', function(source, cb, shopId) local identifier = GetIdentifier(source) if not identifier or not IsOwner(shopId, identifier) then return cb({}) end local rows = MySQL.query.await('SELECT * FROM mercyv_shop_sales_history WHERE shop_id = ? ORDER BY sold_at DESC LIMIT 50', { shopId }) cb(rows or {}) end) -- ===================== -- PURCHASE SHOP -- ===================== RegisterNetEvent('mercyv-shops:purchaseShop') AddEventHandler('mercyv-shops:purchaseShop', function(shopId) local source = source local xPlayer = ESX.GetPlayerFromId(source) local identifier = GetIdentifier(source) if not xPlayer or not identifier then return end local shop = shops[shopId] local configShop = Config.Shops[shopId] if not shop or not configShop then TriggerClientEvent('mercyv-shops:notify', source, 'Shop nicht gefunden', 'error') TriggerClientEvent('mercyv-shops:forceClose', source) return end if shop.owner_identifier then TriggerClientEvent('mercyv-shops:notify', source, 'Dieser Shop ist bereits vergeben', 'error') TriggerClientEvent('mercyv-shops:forceClose', source) return end if CountPlayerShops(identifier) >= Config.MaxShopsPerPlayer then TriggerClientEvent('mercyv-shops:notify', source, 'Du besitzt bereits die maximale Anzahl an Shops (' .. Config.MaxShopsPerPlayer .. ')', 'error') TriggerClientEvent('mercyv-shops:forceClose', source) return end local price = configShop.price if xPlayer.getAccount('money').money < price then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug Geld ($' .. price .. ' benötigt)', 'error') TriggerClientEvent('mercyv-shops:forceClose', source) return end xPlayer.removeAccountMoney('money', price) shop.owner_identifier = identifier shop.owner_name = GetPlayerName(source) shop.shop_name = configShop.label MySQL.query('UPDATE mercyv_shops SET owner_identifier = ?, owner_name = ?, shop_name = ? WHERE shop_id = ?', { identifier, shop.owner_name, shop.shop_name, shopId }) -- Add start stock (DefaultItems) directly to shelves so customers can buy immediately if Config.StartStock and Config.StartStock > 0 and Config.DefaultItems then print('[mercyv-shops] Adding start stock for shop ' .. shopId .. ' (' .. #Config.DefaultItems .. ' items, ' .. Config.StartStock .. ' each)') if not shelves[shopId] then shelves[shopId] = {} end for _, defaultItem in ipairs(Config.DefaultItems) do local itemLabel = GetItemLabel(defaultItem.name) shelves[shopId][defaultItem.name] = { item_name = defaultItem.name, item_label = itemLabel, price = defaultItem.price, quantity = Config.StartStock, } MySQL.query('INSERT INTO mercyv_shop_shelves (shop_id, item_name, item_label, price, quantity) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', { shopId, defaultItem.name, itemLabel, defaultItem.price, Config.StartStock }) end print('[mercyv-shops] Start stock added to shelves successfully') end TriggerClientEvent('mercyv-shops:notify', source, 'Shop "' .. shop.shop_name .. '" gekauft!', 'success') TriggerClientEvent('mercyv-shops:forceClose', source) TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData()) SendWebhook("Shop Gekauft", string.format("Spieler: **%s** (%s)\nShop: **%s**\nPreis: **$%d**", shop.owner_name, identifier, shop.shop_name, price), 3066993) end) -- ===================== -- CUSTOMER BUY -- ===================== RegisterNetEvent('mercyv-shops:buyProduct') AddEventHandler('mercyv-shops:buyProduct', function(shopId, 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 shopConfig = Config.Shops[shopId] if not shopConfig then TriggerClientEvent('mercyv-shops:notify', source, 'Shop nicht gefunden', 'error') return end -- ===== WAFFEN-SHOP oder NPC-SHOP (kein Owner) ===== local shop = shops[shopId] local isWeaponShop = shopConfig.shopType == 'weapon' local isNpcShop = isWeaponShop or (shop and not shop.owner_identifier) if isNpcShop then local defaultItem = FindDefaultItem(itemName, shopId) if not defaultItem then TriggerClientEvent('mercyv-shops:notify', source, 'Item nicht verfügbar', 'error') return end -- Prüfe ob das Item selbst eine Lizenz ist local isLicenseItem = false local categories = isWeaponShop and (Config.WeaponCategories or {}) or (Config.Categories or {}) for _, cat in ipairs(categories) do if cat.requiresLicense == itemName then isLicenseItem = true break end end -- Lizenz-Check: Prüfe ob Kategorie eine Lizenz erfordert if not isLicenseItem then local itemCategory = defaultItem.category or '' for _, cat in ipairs(categories) do if cat.id == itemCategory and cat.requiresLicense and cat.requiresLicense ~= '' then if not PlayerHasLicense(source, cat.requiresLicense) then TriggerClientEvent('mercyv-shops:notify', source, 'Du benötigst eine Lizenz für dieses Item', 'error') return end end end end -- Lizenz: bereits vorhanden? if isLicenseItem and PlayerHasLicense(source, itemName) then TriggerClientEvent('mercyv-shops:notify', source, 'Du besitzt diese Lizenz bereits', 'error') return end local total = defaultItem.price * quantity if xPlayer.getAccount('money').money < total then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug Geld ($' .. total .. ' benötigt)', 'error') return end if isLicenseItem then -- Lizenz in Datenbank speichern (permanent) GrantLicense(source, itemName) else -- Normales Item ins Inventar local addOk = pcall(function() exports['codem-inventory']:AddItem(source, itemName, quantity) end) if not addOk then TriggerClientEvent('mercyv-shops:notify', source, 'Item konnte nicht hinzugefuegt werden', 'error') return end end xPlayer.removeAccountMoney('money', total) TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x ' .. GetItemLabel(itemName) .. ' gekauft für$' .. total, 'success') TriggerClientEvent('mercyv-shops:buyResult', source, true, shopId, GetLockedCategories(source, shopId)) return end -- ===== OWNER-SHOP (Shelves-basiert) ===== local shelfItem = shelves[shopId] and shelves[shopId][itemName] if not shelfItem or shelfItem.quantity < quantity then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug auf Lager', 'error') return end local total = shelfItem.price * quantity if xPlayer.getAccount('money').money < total then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug Geld ($' .. total .. ' benötigt)', 'error') return end -- Race condition protection local affected = MySQL.update.await('UPDATE mercyv_shop_shelves SET quantity = quantity - ? WHERE shop_id = ? AND item_name = ? AND quantity >= ?', { quantity, shopId, itemName, quantity }) if not affected or affected == 0 then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht mehr verfügbar', 'error') return end -- Item zuerst hinzufuegen, dann Geld abziehen local addOk = pcall(function() exports['codem-inventory']:AddItem(source, itemName, quantity) end) if not addOk then -- Shelf-Quantity in DB zurücksetzen MySQL.query('UPDATE mercyv_shop_shelves SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { quantity, shopId, itemName }) TriggerClientEvent('mercyv-shops:notify', source, 'Item konnte nicht hinzugefuegt werden', 'error') return end xPlayer.removeAccountMoney('money', total) -- Update cache shelfItem.quantity = shelfItem.quantity - quantity if shelfItem.quantity <= 0 then shelves[shopId][itemName] = nil MySQL.query('DELETE FROM mercyv_shop_shelves WHERE shop_id = ? AND item_name = ?', { shopId, itemName }) end -- Add to vault (minus tax) local tax = math.floor(total * Config.SalesTax) local revenue = total - tax shop.vault_money = shop.vault_money + revenue MySQL.query('UPDATE mercyv_shops SET vault_money = vault_money + ? WHERE shop_id = ?', { revenue, shopId }) -- Record sale MySQL.query('INSERT INTO mercyv_shop_sales_history (shop_id, buyer_identifier, buyer_name, item_name, item_label, price, quantity, total) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', { shopId, identifier, GetPlayerName(source), itemName, shelfItem.item_label, shelfItem.price, quantity, total }) TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x ' .. shelfItem.item_label .. ' gekauft für$' .. total, 'success') TriggerClientEvent('mercyv-shops:buyResult', source, true, shopId) SendWebhook("Produkt Verkauft", string.format("Shop: **%s**\nKäufer: **%s**\nItem: **%s** x%d\nUmsatz: **$%d**", shop.shop_name, GetPlayerName(source), shelfItem.item_label, quantity, total), 5763719) end) -- ===================== -- STORAGE MANAGEMENT -- ===================== RegisterNetEvent('mercyv-shops:addToStorage') AddEventHandler('mercyv-shops:addToStorage', function(shopId, itemName, itemLabel, quantity) local source = source local xPlayer = ESX.GetPlayerFromId(source) local identifier = GetIdentifier(source) if not xPlayer or not identifier then return end if not IsOwner(shopId, identifier) then TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error') return end quantity = math.max(1, math.floor(tonumber(quantity) or 1)) -- itemLabel serverseitig ermitteln (Client-Daten nicht vertrauen) itemLabel = GetItemLabel(itemName) -- Check storage slot limit if not storage[shopId][itemName] and CountStorageSlots(shopId) >= Config.MaxStorageSlots then TriggerClientEvent('mercyv-shops:notify', source, 'Lager voll (max ' .. Config.MaxStorageSlots .. ' verschiedene Items)', 'error') return end -- Check player has items local totalAmount = exports['codem-inventory']:GetItemsTotalAmount(source, itemName) if not totalAmount or tonumber(totalAmount) < quantity then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug Items im Inventar', 'error') return end exports['codem-inventory']:RemoveItem(source, itemName, quantity) if storage[shopId][itemName] then storage[shopId][itemName].quantity = storage[shopId][itemName].quantity + quantity MySQL.query('UPDATE mercyv_shop_storage SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { quantity, shopId, itemName }) else storage[shopId][itemName] = { item_name = itemName, item_label = itemLabel, quantity = quantity } MySQL.query('INSERT INTO mercyv_shop_storage (shop_id, item_name, item_label, quantity) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', { shopId, itemName, itemLabel, quantity }) end TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x ' .. itemLabel .. ' eingelagert', 'success') TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId) end) -- ===================== -- SHELF MANAGEMENT -- ===================== RegisterNetEvent('mercyv-shops:moveToShelves') AddEventHandler('mercyv-shops:moveToShelves', function(shopId, itemName, quantity, price) local source = source local identifier = GetIdentifier(source) if not identifier then return end if not IsOwner(shopId, identifier) and not IsEmployee(shopId, identifier) then TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error') return end quantity = math.max(1, math.floor(tonumber(quantity) or 1)) price = math.max(1, math.floor(tonumber(price) or 1)) if price > Config.MaxPrice then TriggerClientEvent('mercyv-shops:notify', source, 'Maximaler Preis: $' .. Config.MaxPrice, 'error') return end local storageItem = storage[shopId] and storage[shopId][itemName] if not storageItem or storageItem.quantity < quantity then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug im Lager', 'error') return end -- Check shelf slot limit if not shelves[shopId][itemName] and CountShelfSlots(shopId) >= Config.MaxShelfSlots then TriggerClientEvent('mercyv-shops:notify', source, 'Regale voll (max ' .. Config.MaxShelfSlots .. ' verschiedene Items)', 'error') return end -- Save label before potentially removing from storage local itemLabel = storageItem.item_label -- Remove from storage storageItem.quantity = storageItem.quantity - quantity if storageItem.quantity <= 0 then storage[shopId][itemName] = nil MySQL.query('DELETE FROM mercyv_shop_storage WHERE shop_id = ? AND item_name = ?', { shopId, itemName }) else MySQL.query('UPDATE mercyv_shop_storage SET quantity = ? WHERE shop_id = ? AND item_name = ?', { storageItem.quantity, shopId, itemName }) end -- Add to shelves if shelves[shopId][itemName] then shelves[shopId][itemName].quantity = shelves[shopId][itemName].quantity + quantity shelves[shopId][itemName].price = price MySQL.query('UPDATE mercyv_shop_shelves SET quantity = quantity + ?, price = ? WHERE shop_id = ? AND item_name = ?', { quantity, price, shopId, itemName }) else shelves[shopId][itemName] = { item_name = itemName, item_label = itemLabel, price = price, quantity = quantity } MySQL.query('INSERT INTO mercyv_shop_shelves (shop_id, item_name, item_label, price, quantity) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity), price = VALUES(price)', { shopId, itemName, itemLabel, price, quantity }) end TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x ins Regal gestellt für$' .. price, 'success') TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId) end) RegisterNetEvent('mercyv-shops:moveToStorage') AddEventHandler('mercyv-shops:moveToStorage', function(shopId, itemName, quantity) local source = source local identifier = GetIdentifier(source) if not identifier or (not IsOwner(shopId, identifier) and not IsEmployee(shopId, identifier)) then TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error') return end quantity = math.max(1, math.floor(tonumber(quantity) or 1)) local shelfItem = shelves[shopId] and shelves[shopId][itemName] if not shelfItem or shelfItem.quantity < quantity then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug im Regal', 'error') return end -- Check storage slot limit if not storage[shopId][itemName] and CountStorageSlots(shopId) >= Config.MaxStorageSlots then TriggerClientEvent('mercyv-shops:notify', source, 'Lager voll', 'error') return end -- Save label before potentially removing from shelves local itemLabel = shelfItem.item_label -- Remove from shelves shelfItem.quantity = shelfItem.quantity - quantity if shelfItem.quantity <= 0 then shelves[shopId][itemName] = nil MySQL.query('DELETE FROM mercyv_shop_shelves WHERE shop_id = ? AND item_name = ?', { shopId, itemName }) else MySQL.query('UPDATE mercyv_shop_shelves SET quantity = ? WHERE shop_id = ? AND item_name = ?', { shelfItem.quantity, shopId, itemName }) end -- Add to storage if storage[shopId][itemName] then storage[shopId][itemName].quantity = storage[shopId][itemName].quantity + quantity MySQL.query('UPDATE mercyv_shop_storage SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { quantity, shopId, itemName }) else storage[shopId][itemName] = { item_name = itemName, item_label = itemLabel, quantity = quantity } MySQL.query('INSERT INTO mercyv_shop_storage (shop_id, item_name, item_label, quantity) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', { shopId, itemName, itemLabel, quantity }) end TriggerClientEvent('mercyv-shops:notify', source, quantity .. 'x zurück ins Lager', 'success') TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId) end) RegisterNetEvent('mercyv-shops:updateShelfPrice') AddEventHandler('mercyv-shops:updateShelfPrice', function(shopId, itemName, newPrice) local source = source local identifier = GetIdentifier(source) if not identifier or not IsOwner(shopId, identifier) then TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error') return end newPrice = math.max(1, math.floor(tonumber(newPrice) or 1)) if newPrice > Config.MaxPrice then TriggerClientEvent('mercyv-shops:notify', source, 'Maximaler Preis: $' .. Config.MaxPrice, 'error') return end local shelfItem = shelves[shopId] and shelves[shopId][itemName] if not shelfItem then TriggerClientEvent('mercyv-shops:notify', source, 'Item nicht im Regal', 'error') return end shelfItem.price = newPrice MySQL.query('UPDATE mercyv_shop_shelves SET price = ? WHERE shop_id = ? AND item_name = ?', { newPrice, shopId, itemName }) TriggerClientEvent('mercyv-shops:notify', source, 'Preis aktualisiert auf $' .. newPrice, 'success') TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId) end) RegisterNetEvent('mercyv-shops:removeFromShelves') AddEventHandler('mercyv-shops:removeFromShelves', function(shopId, itemName) local source = source local identifier = GetIdentifier(source) if not identifier or not IsOwner(shopId, identifier) then TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error') return end local shelfItem = shelves[shopId] and shelves[shopId][itemName] if not shelfItem then return end -- Save data before removing local itemLabel = shelfItem.item_label local itemQty = shelfItem.quantity -- Check storage slot limit if not storage[shopId][itemName] and CountStorageSlots(shopId) >= Config.MaxStorageSlots then TriggerClientEvent('mercyv-shops:notify', source, 'Lager voll (max ' .. Config.MaxStorageSlots .. ' verschiedene Items) — Item kann nicht verschoben werden', 'error') return end -- Move to storage if storage[shopId][itemName] then storage[shopId][itemName].quantity = storage[shopId][itemName].quantity + itemQty MySQL.query('UPDATE mercyv_shop_storage SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { itemQty, shopId, itemName }) else storage[shopId][itemName] = { item_name = itemName, item_label = itemLabel, quantity = itemQty } MySQL.query('INSERT INTO mercyv_shop_storage (shop_id, item_name, item_label, quantity) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', { shopId, itemName, itemLabel, itemQty }) end shelves[shopId][itemName] = nil MySQL.query('DELETE FROM mercyv_shop_shelves WHERE shop_id = ? AND item_name = ?', { shopId, itemName }) TriggerClientEvent('mercyv-shops:notify', source, itemLabel .. ' vom Regal entfernt', 'success') TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId) end) -- ===================== -- VAULT -- ===================== RegisterNetEvent('mercyv-shops:withdrawVault') AddEventHandler('mercyv-shops:withdrawVault', function(shopId, amount) local source = source local xPlayer = ESX.GetPlayerFromId(source) local identifier = GetIdentifier(source) if not xPlayer or not identifier or not IsOwner(shopId, identifier) then TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error') return end amount = math.max(1, math.floor(tonumber(amount) or 0)) local shop = shops[shopId] if amount > shop.vault_money then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug im Tresor', 'error') return end -- Zuerst DB aktualisieren, dann Cache local affected = MySQL.update.await('UPDATE mercyv_shops SET vault_money = vault_money - ? WHERE shop_id = ? AND vault_money >= ?', { amount, shopId, amount }) if not affected or affected == 0 then TriggerClientEvent('mercyv-shops:notify', source, 'Abhebung fehlgeschlagen', 'error') return end shop.vault_money = shop.vault_money - amount xPlayer.addAccountMoney('money', amount) TriggerClientEvent('mercyv-shops:notify', source, '$' .. amount .. ' aus dem Tresor abgehoben', 'success') TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId) SendWebhook("Tresor Abhebung", string.format("Spieler: **%s**\nShop: **%s**\nBetrag: **$%d**", GetPlayerName(source), shop.shop_name, amount), 16766720) end) -- ===================== -- EMPLOYEES -- ===================== RegisterNetEvent('mercyv-shops:addEmployee') AddEventHandler('mercyv-shops:addEmployee', function(shopId, targetServerId) local source = source local identifier = GetIdentifier(source) if not identifier or not IsOwner(shopId, identifier) then TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error') return end local targetPlayer = ESX.GetPlayerFromId(tonumber(targetServerId)) if not targetPlayer then TriggerClientEvent('mercyv-shops:notify', source, 'Spieler nicht gefunden (ID: ' .. tostring(targetServerId) .. ')', 'error') return end local targetIdentifier = targetPlayer.getIdentifier() local targetName = targetPlayer.getName() if targetIdentifier == identifier then TriggerClientEvent('mercyv-shops:notify', source, 'Du kannst dich nicht selbst einstellen', 'error') return end if IsEmployee(shopId, targetIdentifier) then TriggerClientEvent('mercyv-shops:notify', source, targetName .. ' ist bereits Mitarbeiter', 'error') return end local empCount = 0 if employees[shopId] then for _ in pairs(employees[shopId]) do empCount = empCount + 1 end end if empCount >= Config.MaxEmployeesPerShop then TriggerClientEvent('mercyv-shops:notify', source, 'Maximale Mitarbeiter erreicht (' .. Config.MaxEmployeesPerShop .. ')', 'error') return end employees[shopId][targetIdentifier] = { name = targetName } MySQL.query('INSERT IGNORE INTO mercyv_shop_employees (shop_id, employee_identifier, employee_name) VALUES (?, ?, ?)', { shopId, targetIdentifier, targetName }) TriggerClientEvent('mercyv-shops:notify', source, targetName .. ' als Mitarbeiter eingestellt', 'success') TriggerClientEvent('mercyv-shops:notify', targetPlayer.source, 'Du wurdest als Mitarbeiter bei "' .. shops[shopId].shop_name .. '" eingestellt!', 'success') TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId) end) RegisterNetEvent('mercyv-shops:removeEmployee') AddEventHandler('mercyv-shops:removeEmployee', function(shopId, empIdentifier) local source = source local identifier = GetIdentifier(source) if not identifier or not IsOwner(shopId, identifier) then TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error') return end if not employees[shopId] or not employees[shopId][empIdentifier] then TriggerClientEvent('mercyv-shops:notify', source, 'Mitarbeiter nicht gefunden', 'error') return end local empName = employees[shopId][empIdentifier].name employees[shopId][empIdentifier] = nil MySQL.query('DELETE FROM mercyv_shop_employees WHERE shop_id = ? AND employee_identifier = ?', { shopId, empIdentifier }) TriggerClientEvent('mercyv-shops:notify', source, empName .. ' entlassen', 'success') TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId) end) -- ===================== -- SETTINGS -- ===================== RegisterNetEvent('mercyv-shops:renameShop') AddEventHandler('mercyv-shops:renameShop', function(shopId, newName) local source = source local identifier = GetIdentifier(source) if not identifier or not IsOwner(shopId, identifier) then return end newName = tostring(newName or ''):sub(1, 50) if #newName < 2 then TriggerClientEvent('mercyv-shops:notify', source, 'Name zu kurz (min 2 Zeichen)', 'error') return end shops[shopId].shop_name = newName MySQL.query('UPDATE mercyv_shops SET shop_name = ? WHERE shop_id = ?', { newName, shopId }) TriggerClientEvent('mercyv-shops:notify', source, 'Shop umbenannt zu "' .. newName .. '"', 'success') TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData()) end) RegisterNetEvent('mercyv-shops:sellToPlayer') AddEventHandler('mercyv-shops:sellToPlayer', function(shopId, targetServerId, salePrice) local source = source local xPlayer = ESX.GetPlayerFromId(source) local identifier = GetIdentifier(source) if not xPlayer or not identifier or not IsOwner(shopId, identifier) then return end salePrice = math.max(1, math.floor(tonumber(salePrice) or 0)) local targetPlayer = ESX.GetPlayerFromId(tonumber(targetServerId)) if not targetPlayer then TriggerClientEvent('mercyv-shops:notify', source, 'Spieler nicht gefunden', 'error') return end local targetIdentifier = targetPlayer.getIdentifier() if targetIdentifier == identifier then TriggerClientEvent('mercyv-shops:notify', source, 'Du kannst den Shop nicht an dich selbst verkaufen', 'error') return end if CountPlayerShops(targetIdentifier) >= Config.MaxShopsPerPlayer then TriggerClientEvent('mercyv-shops:notify', source, 'Spieler besitzt bereits die maximale Anzahl an Shops', 'error') return end if targetPlayer.getAccount('money').money < salePrice then TriggerClientEvent('mercyv-shops:notify', source, 'Spieler hat nicht genug Geld', 'error') return end -- Transfer targetPlayer.removeAccountMoney('money', salePrice) xPlayer.addAccountMoney('money', salePrice) -- Pay out vault to old owner if shops[shopId].vault_money > 0 then xPlayer.addAccountMoney('money', shops[shopId].vault_money) shops[shopId].vault_money = 0 end -- Transfer ownership local shop = shops[shopId] shop.owner_identifier = targetIdentifier shop.owner_name = targetPlayer.getName() -- Clear employees employees[shopId] = {} MySQL.query('DELETE FROM mercyv_shop_employees WHERE shop_id = ?', { shopId }) MySQL.query('UPDATE mercyv_shops SET owner_identifier = ?, owner_name = ?, vault_money = 0 WHERE shop_id = ?', { targetIdentifier, shop.owner_name, shopId }) TriggerClientEvent('mercyv-shops:notify', source, 'Shop verkauft an ' .. shop.owner_name .. ' für$' .. salePrice, 'success') TriggerClientEvent('mercyv-shops:notify', targetPlayer.source, 'Du hast "' .. shop.shop_name .. '" gekauft für$' .. salePrice .. '!', 'success') TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData()) SendWebhook("Shop Verkauft (Spieler)", string.format("Von: **%s**\nAn: **%s**\nShop: **%s**\nPreis: **$%d**", GetPlayerName(source), shop.owner_name, shop.shop_name, salePrice), 16766720) end) RegisterNetEvent('mercyv-shops:sellToServer') AddEventHandler('mercyv-shops:sellToServer', function(shopId) local source = source local xPlayer = ESX.GetPlayerFromId(source) local identifier = GetIdentifier(source) if not xPlayer or not identifier or not IsOwner(shopId, identifier) then return end local configShop = Config.Shops[shopId] if not configShop then return end local payout = math.floor(configShop.price * Config.SellBackPercentage) local shop = shops[shopId] -- Pay owner: sell price + vault money local totalPayout = payout + shop.vault_money xPlayer.addAccountMoney('money', totalPayout) -- Return storage + shelf items to owner inventory local failedItems = {} if storage[shopId] then for _, item in pairs(storage[shopId]) do if item.quantity > 0 then local ok = pcall(function() exports['codem-inventory']:AddItem(source, item.item_name, item.quantity) end) if not ok then failedItems[#failedItems + 1] = item.quantity .. 'x ' .. item.item_label end end end end if shelves[shopId] then for _, item in pairs(shelves[shopId]) do if item.quantity > 0 then local ok = pcall(function() exports['codem-inventory']:AddItem(source, item.item_name, item.quantity) end) if not ok then failedItems[#failedItems + 1] = item.quantity .. 'x ' .. item.item_label end end end end if #failedItems > 0 then TriggerClientEvent('mercyv-shops:notify', source, 'Warnung: Einige Items konnten nicht zurückgegeben werden: ' .. table.concat(failedItems, ', '), 'error') end -- Reset shop shop.owner_identifier = nil shop.owner_name = nil shop.shop_name = configShop.label shop.vault_money = 0 shop.is_for_sale = false shop.sale_price = 0 ClearShopData(shopId) MySQL.query('UPDATE mercyv_shops SET owner_identifier = NULL, owner_name = NULL, shop_name = ?, vault_money = 0, is_for_sale = 0, sale_price = 0 WHERE shop_id = ?', { configShop.label, shopId }) TriggerClientEvent('mercyv-shops:notify', source, 'Shop an Server verkauft! $' .. totalPayout .. ' erhalten', 'success') TriggerClientEvent('mercyv-shops:syncShops', -1, GetSyncData()) -- Close NUI for the seller TriggerClientEvent('mercyv-shops:forceClose', source) SendWebhook("Shop an Server Verkauft", string.format("Spieler: **%s**\nShop: **%s**\nAuszahlung: **$%d**", GetPlayerName(source), configShop.label, totalPayout), 15158332) end) -- ===================== -- DATA REFRESH -- ===================== RegisterNetEvent('mercyv-shops:requestRefresh') AddEventHandler('mercyv-shops:requestRefresh', function(shopId) local source = source local identifier = GetIdentifier(source) if not identifier then return end local shop = shops[shopId] if not shop then return end -- Nur Owner und Employees duerfen Daten abrufen if not IsOwner(shopId, identifier) and not IsEmployee(shopId, identifier) then return end local data = { shop = GetShopPublicData(shopId), shelves = GetShelfData(shopId), storage = GetStorageData(shopId), } -- Employees erhalten keine Employee-Liste if IsOwner(shopId, identifier) then data.employees = GetEmployeeData(shopId) end TriggerClientEvent('mercyv-shops:refreshData', source, data) end) -- ===================== -- ORDER ITEMS (Nachbestellen aus Tresor) -- ===================== RegisterNetEvent('mercyv-shops:orderItems') AddEventHandler('mercyv-shops:orderItems', function(shopId, items) local source = source local identifier = GetIdentifier(source) if not identifier then return end if not IsOwner(shopId, identifier) then TriggerClientEvent('mercyv-shops:notify', source, 'Kein Zugriff', 'error') return end local shop = shops[shopId] if not shop then return end if not items or type(items) ~= 'table' or #items == 0 then TriggerClientEvent('mercyv-shops:notify', source, 'Keine Items ausgewaehlt', 'error') return end -- Validate items, calculate total cost, and check storage capacity upfront local totalCost = 0 local validItems = {} local newSlots = 0 for _, orderItem in ipairs(items) do local defaultItem = FindDefaultItem(orderItem.name) if not defaultItem then TriggerClientEvent('mercyv-shops:notify', source, 'Item "' .. tostring(orderItem.name) .. '" nicht verfügbar', 'error') return end local qty = math.max(1, math.floor(tonumber(orderItem.quantity) or 1)) totalCost = totalCost + (defaultItem.price * qty) -- Zähle neue Storage-Slots die benötigt werden if not storage[shopId][defaultItem.name] then newSlots = newSlots + 1 end validItems[#validItems + 1] = { name = defaultItem.name, quantity = qty, price = defaultItem.price } end -- Check storage capacity BEFORE payment local currentSlots = CountStorageSlots(shopId) if currentSlots + newSlots > Config.MaxStorageSlots then TriggerClientEvent('mercyv-shops:notify', source, 'Lager hat nicht genug Platz (braucht ' .. newSlots .. ' neue Slots, ' .. (Config.MaxStorageSlots - currentSlots) .. ' frei)', 'error') return end -- Check vault if shop.vault_money < totalCost then TriggerClientEvent('mercyv-shops:notify', source, 'Nicht genug im Tresor ($' .. totalCost .. ' benötigt, $' .. shop.vault_money .. ' vorhanden)', 'error') return end -- Deduct from vault (now safe since we checked capacity) shop.vault_money = shop.vault_money - totalCost MySQL.query('UPDATE mercyv_shops SET vault_money = vault_money - ? WHERE shop_id = ? AND vault_money >= ?', { totalCost, shopId, totalCost }) -- Add items to storage for _, item in ipairs(validItems) do local itemLabel = GetItemLabel(item.name) if storage[shopId][item.name] then storage[shopId][item.name].quantity = storage[shopId][item.name].quantity + item.quantity MySQL.query('UPDATE mercyv_shop_storage SET quantity = quantity + ? WHERE shop_id = ? AND item_name = ?', { item.quantity, shopId, item.name }) else storage[shopId][item.name] = { item_name = item.name, item_label = itemLabel, quantity = item.quantity } MySQL.query('INSERT INTO mercyv_shop_storage (shop_id, item_name, item_label, quantity) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)', { shopId, item.name, itemLabel, item.quantity }) end end TriggerClientEvent('mercyv-shops:notify', source, 'Bestellung aufgegeben! $' .. totalCost .. ' vom Tresor abgezogen', 'success') TriggerClientEvent('mercyv-shops:dataRefresh', source, shopId) SendWebhook("Nachbestellung", string.format("Owner: **%s**\nShop: **%s**\nKosten: **$%d**\nItems: **%d**", GetPlayerName(source), shop.shop_name, totalCost, #validItems), 3447003) end) -- ===================== -- SYNC ON JOIN -- ===================== RegisterNetEvent('mercyv-shops:requestSync') AddEventHandler('mercyv-shops:requestSync', function() TriggerClientEvent('mercyv-shops:syncShops', source, GetSyncData()) end)