Skip to content

🏬 BLN Player Stores

bln player stores

📺 Preview

Video Preview

💰 Buy Now

Get it now

Let players run real businesses on your server. BLN Player Stores turns any spot on the map into a shop owned by a character, backed by a full BLN Society — takings, staff, ranks, and permissions live in the society menu, not in a separate system.

Customers browse a beautiful book-style catalog and sell items on a clean receipt slip. Owners and staff manage categories, stock, prices, buy orders, coupons, and sales from one manage panel. Server admins create and place stores in-game with /playerstores.

Not sure which shop script you need?

  • BLN Stores — config-defined NPC shops (general store, trapper, gunsmith). Prices and items live in Lua config files.
  • BLN Player Stores (this resource) — database-driven shops run by players. Stock comes from real inventory deposits; every store has its own society account and staff.

✨ What can you do with it?

For players & business owners

  • 🏬 Player-owned shops — each store is a real business with its own society, balance, and staff
  • 📖 Book UI — customers browse categories, fill a cart, apply coupons, and check out
  • 🧾 Sell slip — players sell items the store has buy orders for, paid from the store's account
  • 📦 Real stock — deposit goods from your satchel onto shelves; stock 0 shows as sold out
  • 🏷️ Categories & pricing — create shelves, set buy/sell prices per item, optional custom labels
  • 💵 Buy orders — define what the store pays players for (out of its own money)
  • 🎟️ Coupons — discount codes customers type at the till
  • 🏷️ Sales — automatic percentage discounts on selected items
  • 🔓 Open / closed — toggle whether customers can browse and buy
  • 👥 Staff via society — hire, rank, and permission staff through the linked society menu
  • 💰 Cash & gold — per-item currency support
  • 🗺️ Map blips & markers — each store can have its own world point and blip icon

For server admins

  • 🛠️ Admin menu/playerstores to create, move, teleport to, and manage every store
  • 📍 Place anywhere — stand where the counter should be and create a store on the spot
  • 🔐 Access controlConfig.admin.allowIf checked on every admin action
  • 💸 Tax routing — percent or flat tax on purchases, sent to treasury, burned, or custom logic
  • 🌍 Translations — all player-facing text in one locale file

For developers

  • 🔌 Server & client exports — create stores, read catalog/stock, open the book, check permissions
  • 📡 Events — react to store lifecycle, purchases, and sales
  • 📦 Statebags — client reads store directory and revision without polling
  • 🔗 Society integration — one society per store; dissolve is blocked while the store is active

💻 Framework compatibility

✅ VORP ✅ RSG ✅ Custom

📦 Dependencies

Make sure these are started before bln_player_stores:

⚙️ Installation

  1. Download and extract bln_player_stores into your resources folder
  2. Add to your server.cfg after bln_society:
cfg
ensure bln_player_stores
  1. Edit the config files in bln_player_stores/config/ (see below)
  2. Restart the server — tables are created automatically on first boot

🚀 Getting started

1. Create a store (admin)

  1. Run /playerstores (or whatever you set in Config.admin.command)
  2. Stand where the shop counter should be
  3. Press Create Store, enter a name, and assign the owner's server id
  4. A society is created from your configured preset and linked to the store

2. Stock the shelves (owner / staff)

  1. Walk up to the store and press Manage Store (staff only)
  2. Open Categories — create a shelf and pick an icon
  3. Deposit items from inventory onto the shelf and set prices
  4. Toggle the store open from Settings when ready for customers

3. Customers shop

  • Browse — open the book, add items to cart, apply a coupon, pay
  • Sell — appears only when the store has at least one buy order configured

📋 Manage panel tabs

Staff see tabs based on their society permissions:

TabWhat it does
OverviewBalance, stock summary, members — plus a button into the society menu
CategoriesShelves, deposit/withdraw stock, item order, custom labels
BuyingBuy orders — what the store pays players, capped by store balance
CouponsCreate and manage discount codes
SalesRunning percentage discounts
SettingsStore name, notice text, open/closed

Important: depositing stock is a real inventory move — goods leave the player's satchel. Taking stock back requires finances.withdraw because it removes value from the business.

🏛️ Society integration

Every store is backed by exactly one BLN Society:

  • Money — sales credit the store's society account; buy orders debit it
  • Staff — members, ranks, and permissions are managed in the society menu
  • Wages — the store society pays wages from its own takings (no society action fees on store societies)
  • Delete protection — while a store is active, its society cannot be dissolved from the society UI

Give someone society.manage and they can run shelves, categories, coupons, and sales. Give them finances.withdraw as well to set prices, manage buy orders, and withdraw stock.

📁 Config files

FileWhat it's for
main.cfg.luaAdmin command, tax, trade limits, world points, security
permissions.cfg.luaMaps store actions → society permissions
locale.cfg.luaAll UI and notification text

Admin access (main.cfg.lua)

lua
Config.admin = {
    command = 'playerstores',   -- false to disable the chat command
    allowIf = function(source)
        -- Example: return IsPlayerAceAllowed(source, 'bln.stores')
        return true
    end,
},

allowIf is re-checked on every admin request — never trusted from the UI.

Society preset

lua
Config.society = {
    preset = 'business',   -- must exist in bln_society/config/presets.cfg.lua
},

Tax

Tax is added on top of what the customer pays and is never credited to the store.

lua
Config.tax = {
    enabled = true,
    mode = 'percent',      -- 'percent' | 'flat'
    amount = 0.05,         -- 5% when mode is 'percent'
    max = false,           -- cap per transaction, false = no cap
    applyToGold = false,

    -- false             burn the tax (leave the economy)
    -- '<societyId>'     credit a treasury society
    -- function(ctx)     custom routing; return true when handled
    destination = false,
}

Custom destination example:

lua
Config.tax.destination = function(ctx)
    -- ctx = { storeId, storeName, societyId, characterId, source,
    --         amount, currency, kind = 'buy'|'sell', ref }
    exports.my_treasury:Deposit(ctx.amount, ctx.currency, ctx.ref)
    return true
end

Trade limits

lua
Config.trade = {
    maxCartLines = 20,
    maxItemQuantity = 99,
    minPrice = 0.01,
    maxPrice = 100000.0,
    maxStockPerItem = 5000,
    sellReserve = 0.0,              -- minimum balance kept when paying buy orders
    closeAfterTransaction = false,
},

World points & blips

lua
Config.points = {
    prompts = {
        buy = { key = Keys.E },
        sell = { key = Keys.R },
        manage = { key = Keys.G },
    },
    defaults = {
        marker = { type = 'default', distance = 12.0, ... },
        prompt = { enabled = true, distance = 2.0 },
        blip = {
            enabled = true,
            sprite = 'blip_shop_store',
            color = false,
            closedColor = 'BLIP_MODIFIER_TOD_DAYTIME_ONLY',
        },
    },
},

Blip icons are served from bln_society — this resource ships none of its own.

🔐 Permissions (permissions.cfg.lua)

The store does not invent permissions — it maps actions onto keys from bln_society:

Store actionDefault society permission
Open manage panel, society menu, dutyMember / duty.use
View balancefinances.view
Categories, stock deposit, coupons, sales, settingssociety.manage
Prices, buy orders, stock withdrawfinances.withdraw

Use Permissions.MEMBER to open an action to anyone on the books, or Permissions.PUBLIC for every customer. The society owner always passes every check.

💰 Pricing order

Applied in this order (same on server and in the UI preview):

  1. Sale — percentage off shelf price (largest sale wins if two overlap; they do not stack)
  2. Coupon — discount off the subtotal
  3. Tax — added last on what remains

Cash and gold are kept separate throughout. The server always re-resolves prices from the database at checkout — the UI maths is preview only.

🔌 Developer API

All calls: exports.bln_player_stores:ExportName(...)

Wait for bln_player_stores:ready before write exports. Writes return not_ready until boot finishes.

Response shape

Writes return:

lua
{ success = true, code = 'store_created', message = '...', args = {}, data = {} }

Reads return the value directly, or { success = false, code = 'store_not_found' } when missing.

Optional actor on writes: { charId = '42', name = 'Arthur Morgan' }


Server exports

Meta

ExportArgumentsReturns
IsReady()boolean
GetConstants()Const table
GetSettings(){ currencies, tax, trade, limits, points }

Stores

ExportArgumentsReturns
CreateStore(data)see belowresponse, data = { storeId, societyId }
GetStore(storeId)response, data = store
GetStoreBySociety(societyId)response, data = store
GetStores()response, data = store[]
CountStores()integer
UpdateStore(storeId, data, actor?){ name?, description?, paragraphTitle?, paragraphText?, status? }response
SetStorePoint(storeId, point, actor?)see Pointresponse
ClearStorePoint(storeId)response
DeleteStore(storeId, opts?){ dissolveSociety?, actorName?, actorCharId? }response
GetStoreSociety(storeId)societyId|nil

CreateStore data:

lua
{
    name = 'Valentine Apothecary',
    description = 'Herbs and tonics.',
    ownerCharId = '42',
    ownerName = 'Arthur Morgan',
    preset = 'business',
    point = { ... },
}

Point

Used by CreateStore and SetStorePoint:

lua
{
    coords = { x = -324.0, y = 804.0, z = 117.8, h = 90.0 },
    blip = { sprite = 'blip_shop_store', name = 'Apothecary', color = false },
    marker = { type = 'default', scale = 1.0, distance = 12.0 },
    prompt = { enabled = true, distance = 2.0 },
}

Catalogue & stock

ExportArgumentsReturns
GetCatalog(storeId)response, data = { categories = [...] }
GetStock(storeId, itemName)integer
AddStock(storeId, itemName, quantity)response, data = { itemId, accepted }
SetOffer(storeId, data, actor?)see belowresponse

SetOffer data:

lua
{
    name = 'deer_pelt',
    label = 'Deer Pelt',
    sellPrice = 6.50,
    sellCurrency = 0,
    maxStock = 100,
    categoryId = 1,
}

Coupons & sales

ExportArgumentsReturns
CreateCoupon(storeId, data, actor?)see belowresponse
GetCoupons(storeId)response, data = coupon[]
DeleteCoupon(storeId, couponId, actor?)response
CreateSale(storeId, data, actor?)see belowresponse
GetSales(storeId)response, data = sale[]
DeleteSale(storeId, saleId, actor?)response

CreateCoupon data:

lua
{
    code = 'WELCOME10',
    kind = 'percent',       -- 'percent' | 'flat'
    value = 10,
    scope = 'all',          -- 'all' | 'category'
    categoryId = 1,
    minTotal = 0,
    maxUses = 50,
    perCharacterLimit = 1,
    expiresInDays = 7,
}

CreateSale data:

lua
{
    percent = 15,
    scope = 'all',          -- 'all' | 'category'
    categoryId = 1,
    durationHours = 48,
}

Permissions

ExportArgumentsReturns
HasStorePermission(storeId, characterId, action)action from permissions.cfg.luaboolean
GetStorePermissions(storeId, characterId)table

Directory & UI

ExportArgumentsReturns
GetDirectory()table keyed by store id
GetRevision()integer
OpenStore(storeId, source, mode)mode: 'buy' | 'sell' | 'manage'response

Server events

EventArguments
bln_player_stores:ready
bln_player_stores:storeCreatedstoreId, societyId
bln_player_stores:storeDeletedstoreId, societyId
bln_player_stores:storeMovedstoreId, point
bln_player_stores:purchasestoreId, characterId, lines, total
bln_player_stores:salestoreId, characterId, lines, total

Client exports

Statebag reads. Public directory only — no prices, stock, or balance.

ExportArgumentsReturns
IsReady()boolean
GetDirectory()table
GetStore(storeId)table|nil
GetRevision()integer
IsStaff(storeId)boolean
HasPermission(storeId, action)boolean
OpenStore(storeId, mode)'buy' | 'sell' | 'manage'
CloseUi()
IsUiOpen()boolean

Client events

EventArguments
bln_player_stores:c:ready
bln_player_stores:c:directoryChangeddirectory

❓ FAQ

🤝 Support

Need help? Join our Discord: