Developer
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
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 inConfig.AllowVirtualShop
Server Exports
Shops
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, ornilGetTraderData(traderId): full trader data, ornilGetAllShops(): 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
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 itemCalculateFinalPrice(source, shopId, itemId): cost table after discounts and custom pricing, and the discount percentIsShopOpen(source, shopId):true, orfalseand a reasonIsItemPurchasable(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
1local level = exports['ml_shops']:GetPlayerSkillLevel(source, categoryUid)GetPlayerSkillLevel(source, categoryUid): skill level fromml_skillsGetPlayerSkillXp(source, categoryUid): skill XPGetPlayerSkillPoints(source, categoryUid): skill pointsIsSkillRequirementMet(source, categoryUid, minPoints):truewhen the player has at least that many pointsHasPlayerUnlockedSkill(source, categoryUid, skillUid):truewhen the skill is unlockedGetPlayerReputation(source, factionId): value returned by theGetPlayerReputationhandler
Stock
Only resources listed in Config.AdminExportResources can call these.
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 from0to100000, or'infinite'AddItemStock(shopId, itemId, amount): adds stock, or removes it with a negative amount. Never goes below0TriggerShopRefresh(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
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.costsis{ money = number, items = table }ml_shops:stockChanged: fires when an export changes the stock.reasonis'external'forSetItemStockand'restock'forAddItemStock
Server Handlers
Located in open/handlers_server.lua.
Shop Lifecycle
1Handlers.OnShopOpen = function(source, shopId, shopData, traderData)
2end
3
4Handlers.OnShopLoaded = function(shopId, shopData)
5endOnShopOpen: a player opens a shop.traderDataisnilfor shops opened by exportOnShopLoaded: a shop is loaded at server start
Purchase
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)
16endOnBeforePurchase: returnfalse, 'reason'to cancel the purchaseOnPurchaseStarted: runs after the access checks, before money and items moveBeforeGiveItem: runs for each item. Returnfalse, 'reason'to cancel and refund. The player sees the reasonOnAfterPurchase: runs after a purchase.totalCostsis{ money = number, items = table }OnActionComplete: runs afterOnAfterPurchasewithactionType = 'purchase'
Pricing
1Handlers.CalculatePlayerDiscount = function(source, shopId, itemId, itemData, traderData)
2 return 0
3end
4
5Handlers.GetCustomItemPrice = function(source, shopId, itemId, itemData, originalCost)
6 return nil
7endCalculatePlayerDiscount: discount percent from0to100. The shipped version adds up the job andml_skillsdiscounts set on the item, then appliesConfig.MaxDiscountPercentGetCustomItemPrice: return a new cost table to replace the price, ornilto 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 editingitemDataororiginalCost
Access
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
15endCanPlayerAccessShop: returnfalse, 'reason'to refuse the whole shopCanPlayerBuyItem: the shipped version checks the job and skill requirements of the itemValidateInventorySpace: the shipped version asks the inventory whether the player can carry the itemGetPlayerReputation: return a reputation value from your own faction system
Traders and Stock
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)
12endBeforeTraderSpawn: returnfalseto keep a trader hidden, for event or holiday tradersAfterTraderSpawn: the trader is sent to the playersOnTraderRoutineChange: a trader opens or closes. States are'open'and'closed'OnStockChanged: stock changes from a purchase or an export.changeReasonis'purchase','external'or'restock'
Client Handlers
Located in open/handlers_client.lua.
Target
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 {}
23endCanInteractWithTrader: returnfalseto hide the shop option on that traderCanInteractWithAdmin: shows the admin option on the trader to admins. The server checks permission again on every admin actionGetCustomTargetLabel,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 of2.0AddCustomTargetOptions: extra target options on the trader, such as a rob option
Opening the Shop
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)
13endBeforeOpenUI,CanOpenShop: returnfalseto block, orfalse, 'reason'to block and notify. The server checks access againOnShopOpened,OnShopClosed: run when the shop window opens and closes
Items and Cart
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)
14endFilterShopItems: returns the list of items to showShouldShowItem: returnfalseto hide one itemCanPurchaseItem: returnfalse, 'reason'to block the purchase before it is sentOnPurchaseComplete: a purchase succeeds
Blips and Traders
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)
16endOnBlipCreated: change the blip after it is createdGetCustomBlipSprite,GetCustomBlipColor: return a number to replace the sprite or color set in the panelOnTraderSpawned: the trader NPC spawns. Attach props or effects hereOnTraderDespawned: the trader NPC is removed
Examples
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
12end1Handlers.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
7end1local 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
6endAdd the delivery resource to Config.AdminExportResources first.