Developer

7 min readUpdated 2 weeks ago

Overview

Other resources open shops with a client export and read or change shop data with server exports. Pricing, access and spawn rules are changed in open/handlers_server.lua and open/handlers_client.lua, which stay unencrypted. Every handler returns the default value, so change only the ones you need. An error inside a handler is logged and never stops a purchase.

Client Exports

lua
1exports['ml_shops']:openShop('general_store_1')
  • openShop(shopId): opens a shop for the local player. With no trader of that shop nearby, the shop must be listed in Config.AllowVirtualShop

Server Exports

Shops

lua
1local shop = exports['ml_shops']:GetShopData(shopId)
2local trader = exports['ml_shops']:GetTraderData(traderId)
3local shops = exports['ml_shops']:GetAllShops()
4local traders = exports['ml_shops']:GetAllTraders()
5local categories = exports['ml_shops']:GetShopCategories(shopId)
  • GetShopData(shopId): full shop data with its items, or nil
  • GetTraderData(traderId): full trader data, or nil
  • GetAllShops(): list of shops as { id, label, itemCount }
  • GetAllTraders(): list of traders as { id, shopId, name, isActive }
  • GetShopCategories(shopId): categories used by the items of a shop

Pricing and Access

lua
1local discount = exports['ml_shops']:GetPlayerDiscount(source, shopId, itemId)
2local cost, percent = exports['ml_shops']:CalculateFinalPrice(source, shopId, itemId)
3local open, reason = exports['ml_shops']:IsShopOpen(source, shopId)
4local ok, reason = exports['ml_shops']:IsItemPurchasable(source, shopId, itemId, traderId)
  • GetPlayerDiscount(source, shopId, itemId): discount percent the player gets on that item
  • CalculateFinalPrice(source, shopId, itemId): cost table after discounts and custom pricing, and the discount percent
  • IsShopOpen(source, shopId): true, or false and a reason
  • IsItemPurchasable(source, shopId, itemId, traderId?): checks stock, shop access and the job and skill requirements of the item. With a trader ID it also checks that the trader is open

itemId is the ID of the item inside the shop, the key of the item in GetShopData(shopId).items. Each entry also holds the inventory item name in itemId.

Skills

lua
1local level = exports['ml_shops']:GetPlayerSkillLevel(source, categoryUid)
  • GetPlayerSkillLevel(source, categoryUid): skill level from ml_skills
  • GetPlayerSkillXp(source, categoryUid): skill XP
  • GetPlayerSkillPoints(source, categoryUid): skill points
  • IsSkillRequirementMet(source, categoryUid, minPoints): true when the player has at least that many points
  • HasPlayerUnlockedSkill(source, categoryUid, skillUid): true when the skill is unlocked
  • GetPlayerReputation(source, factionId): value returned by the GetPlayerReputation handler

Stock

Only resources listed in Config.AdminExportResources can call these.

lua
1exports['ml_shops']:SetItemStock(shopId, itemId, 25)
2exports['ml_shops']:AddItemStock(shopId, itemId, 5)
3exports['ml_shops']:TriggerShopRefresh(shopId)
  • SetItemStock(shopId, itemId, stock): sets the stock. A number from 0 to 100000, or 'infinite'
  • AddItemStock(shopId, itemId, amount): adds stock, or removes it with a negative amount. Never goes below 0
  • TriggerShopRefresh(shopId): sends the shop again to the players who have it open

Each one returns true on success. Stock set by export is not saved to the database, so it resets at the next restart.

Server Events

lua
1AddEventHandler('ml_shops:purchase', function(source, shopId, items, costs) end)
2AddEventHandler('ml_shops:stockChanged', function(shopId, itemId, oldStock, newStock, reason) end)
  • ml_shops:purchase: fires after a purchase. costs is { money = number, items = table }
  • ml_shops:stockChanged: fires when an export changes the stock. reason is 'external' for SetItemStock and 'restock' for AddItemStock

Server Handlers

Located in open/handlers_server.lua.

Shop Lifecycle

open/handlers_server.lua
1Handlers.OnShopOpen = function(source, shopId, shopData, traderData)
2end
3
4Handlers.OnShopLoaded = function(shopId, shopData)
5end
  • OnShopOpen: a player opens a shop. traderData is nil for shops opened by export
  • OnShopLoaded: a shop is loaded at server start

Purchase

open/handlers_server.lua
1Handlers.OnBeforePurchase = function(source, shopId, cart, shopData)
2    return true
3end
4
5Handlers.OnPurchaseStarted = function(source, shopId, cart)
6end
7
8Handlers.BeforeGiveItem = function(source, item, amount)
9    return true
10end
11
12Handlers.OnAfterPurchase = function(source, shopId, purchasedItems, totalCosts)
13end
14
15Handlers.OnActionComplete = function(source, actionType, data)
16end
  • OnBeforePurchase: return false, 'reason' to cancel the purchase
  • OnPurchaseStarted: runs after the access checks, before money and items move
  • BeforeGiveItem: runs for each item. Return false, 'reason' to cancel and refund. The player sees the reason
  • OnAfterPurchase: runs after a purchase. totalCosts is { money = number, items = table }
  • OnActionComplete: runs after OnAfterPurchase with actionType = 'purchase'

Pricing

open/handlers_server.lua
1Handlers.CalculatePlayerDiscount = function(source, shopId, itemId, itemData, traderData)
2    return 0
3end
4
5Handlers.GetCustomItemPrice = function(source, shopId, itemId, itemData, originalCost)
6    return nil
7end
  • CalculatePlayerDiscount: discount percent from 0 to 100. The shipped version adds up the job and ml_skills discounts set on the item, then applies Config.MaxDiscountPercent
  • GetCustomItemPrice: return a new cost table to replace the price, or nil to keep it. A replaced price gets no discount. It runs once per cart entry: read outside values into locals first, never wait inside it, and build a new table instead of editing itemData or originalCost

Access

open/handlers_server.lua
1Handlers.CanPlayerAccessShop = function(source, shopId, shopData, traderData)
2    return true, nil
3end
4
5Handlers.CanPlayerBuyItem = function(source, shopId, itemId, itemData, amount)
6    return true, nil
7end
8
9Handlers.ValidateInventorySpace = function(source, item, amount)
10    return true, nil
11end
12
13Handlers.GetPlayerReputation = function(source, factionId)
14    return 0
15end
  • CanPlayerAccessShop: return false, 'reason' to refuse the whole shop
  • CanPlayerBuyItem: the shipped version checks the job and skill requirements of the item
  • ValidateInventorySpace: the shipped version asks the inventory whether the player can carry the item
  • GetPlayerReputation: return a reputation value from your own faction system

Traders and Stock

open/handlers_server.lua
1Handlers.BeforeTraderSpawn = function(traderId, traderData)
2    return true
3end
4
5Handlers.AfterTraderSpawn = function(traderId, traderData)
6end
7
8Handlers.OnTraderRoutineChange = function(traderId, oldState, newState)
9end
10
11Handlers.OnStockChanged = function(shopId, itemId, oldStock, newStock, changeReason)
12end
  • BeforeTraderSpawn: return false to keep a trader hidden, for event or holiday traders
  • AfterTraderSpawn: the trader is sent to the players
  • OnTraderRoutineChange: a trader opens or closes. States are 'open' and 'closed'
  • OnStockChanged: stock changes from a purchase or an export. changeReason is 'purchase', 'external' or 'restock'

Client Handlers

Located in open/handlers_client.lua.

Target

open/handlers_client.lua
1Handlers.CanInteractWithTrader = function(traderData)
2    return true
3end
4
5Handlers.CanInteractWithAdmin = function(traderData)
6    return AdminClient.IsAdmin()
7end
8
9Handlers.GetCustomTargetLabel = function(traderData)
10    return nil
11end
12
13Handlers.GetCustomTargetIcon = function(traderData)
14    return nil
15end
16
17Handlers.GetCustomTargetDistance = function(traderData)
18    return nil
19end
20
21Handlers.AddCustomTargetOptions = function(traderData, entity)
22    return {}
23end
  • CanInteractWithTrader: return false to hide the shop option on that trader
  • CanInteractWithAdmin: shows the admin option on the trader to admins. The server checks permission again on every admin action
  • GetCustomTargetLabel, GetCustomTargetIcon: return a string to replace the label or the icon. The default icon is 'fas fa-store'
  • GetCustomTargetDistance: return a number to replace the default range of 2.0
  • AddCustomTargetOptions: extra target options on the trader, such as a rob option

Opening the Shop

open/handlers_client.lua
1Handlers.BeforeOpenUI = function(data)
2    return true
3end
4
5Handlers.CanOpenShop = function(shopId, traderData)
6    return true
7end
8
9Handlers.OnShopOpened = function(shopId, traderData)
10end
11
12Handlers.OnShopClosed = function(shopId)
13end
  • BeforeOpenUI, CanOpenShop: return false to block, or false, 'reason' to block and notify. The server checks access again
  • OnShopOpened, OnShopClosed: run when the shop window opens and closes

Items and Cart

open/handlers_client.lua
1Handlers.FilterShopItems = function(shopId, items)
2    return items
3end
4
5Handlers.ShouldShowItem = function(shopId, itemData)
6    return true
7end
8
9Handlers.CanPurchaseItem = function(shopId, itemData, amount)
10    return true, nil
11end
12
13Handlers.OnPurchaseComplete = function(shopId, purchasedItems)
14end
  • FilterShopItems: returns the list of items to show
  • ShouldShowItem: return false to hide one item
  • CanPurchaseItem: return false, 'reason' to block the purchase before it is sent
  • OnPurchaseComplete: a purchase succeeds

Blips and Traders

open/handlers_client.lua
1Handlers.OnBlipCreated = function(traderData, blip)
2end
3
4Handlers.GetCustomBlipSprite = function(traderData)
5    return nil
6end
7
8Handlers.GetCustomBlipColor = function(traderData)
9    return nil
10end
11
12Handlers.OnTraderSpawned = function(traderData, entity)
13end
14
15Handlers.OnTraderDespawned = function(traderId, entity)
16end
  • OnBlipCreated: change the blip after it is created
  • GetCustomBlipSprite, GetCustomBlipColor: return a number to replace the sprite or color set in the panel
  • OnTraderSpawned: the trader NPC spawns. Attach props or effects here
  • OnTraderDespawned: the trader NPC is removed

Examples

open/handlers_server.lua
1Handlers.GetCustomItemPrice = function(source, shopId, itemId, itemData, originalCost)
2    local hour = tonumber(os.date('%H'))
3    if hour >= 6 then return nil end
4    local cost = {}
5    for i, entry in ipairs(originalCost) do
6        local copy = {}
7        for k, v in pairs(entry) do copy[k] = v end
8        copy.amount = math.ceil(entry.amount * 1.2)
9        cost[i] = copy
10    end
11    return cost
12end
open/handlers_client.lua
1Handlers.ShouldShowItem = function(shopId, itemData)
2    local job = Bridge.GetJob()
3    if job and job.name == 'police' and itemData.itemId == 'lockpick' then
4        return false
5    end
6    return true
7end
server.lua
1local shop = exports['ml_shops']:GetShopData('general_store_1')
2for id, item in pairs(shop.items) do
3    if item.itemId == 'water' then
4        exports['ml_shops']:AddItemStock('general_store_1', id, 20)
5    end
6end

Add the delivery resource to Config.AdminExportResources first.