⚙️Concepts & How It Works
This guide shows how to create and reuse shop themes in MarketV2. Define visuals (NPC/blip/marker/UI), payments, access rules, and full item catalogs, then style the UI via CSS theme tokens.......
Risk Market V2 — Shops & Themes
1) Overview
Shops are world instances you place (in
Config.MarketShops
,WeaponShops
,BlackMarkets
,JobShops
).Each shop points to a theme:
Config.MarketThemes[THEME_KEY]
.A theme defines visuals (NPC/blip/marker/UI colors), economy (payments), access, and the catalog (categories + items).
2) Theme Anatomy
Define under Config.MarketThemes
:
Config.MarketThemes = Config.MarketThemes or {}
Config.MarketThemes.mytheme = {
distance = 3.0,
npc = { enabled = true, model = 'a_m_y_business_01' },
blip = { enabled = true, sprite = 52, color = 48, scale = 0.8, name = 'My Themed Shop' },
marker= { enabled = true, type = 29, scale = 0.5, color = { r=255,g=255,b=0,a=150 } },
ui = {
title = 'MY THEME',
description = 'Welcome!',
showStock = true -- show stock to customers on owned shops
},
payment = {
cash = true, bank = true, blackmoney = false
-- itemCurrency = { enabled = true, item = 'gold_token', per = 100 }
},
-- Optional access control:
-- access = {
-- jobs = 'all' or { 'police','ambulance' },
-- minGrade = 0,
-- minGrades = { police = 3 },
-- useLicense = true,
-- licenseMode = 'item' or 'esx',
-- licenseItem = 'weapon_license',
-- esxLicenseType = 'weapon'
-- },
weaponsAsItems = false, -- false=give to loadout, true=inventory item
-- Optional per-job category gates:
-- categoryAccess = {
-- police = { { minGrade = 2, categories = { 'Restricted' } } }
-- },
categories = {
{ name = 'Snacks', icon = 'img/items/hamburger.png' },
-- { name = 'Restricted', icon = 'img/categories/crown.png' }
},
items = {
Snacks = {
{ name='bread', label='Bread', price={min=10,max=15}, image='img/items/bread.png', description='Fresh loaf.' },
{ name='hamburger', label='Hamburger', price={min=25,max=25}, image='img/items/hamburger.png', description='Juicy & quick.' }
}
-- Restricted = { ... }
}
}
Rules
Category names in
items
must matchcategories
.price
can be a number (price=100
) or{min,max}
.image
can beimg/...
(NUI) orhttps://...
.
3) Attach Theme to a Shop
Config.MarketShops = Config.MarketShops or {}
table.insert(Config.MarketShops, {
coords = {
{
id='myshop_1', theme='mytheme',
pos=vector4(24.50,-1347.06,29.50,264.45),
ownable=true,
mgmt=vector4(30.03,-1339.62,29.49,7.72),
price=200000
}
}
})
IDs must be unique across all shops.
Switch look/catalog by changing
theme
.
4) UI Styling (CSS)
Frontend adds theme-<key>
on <body>
. Define colors in html/css/config.css
:
:root { --main-color:#FDFF18; --main-color-2:#FDFF1836; }
.theme-market { --main-color:#FDFF18; --main-color-2:#FDFF1836; }
/* Your theme */
.theme-mytheme { --main-color:#2EE6A8; --main-color-2:#2EE6A836; }
5) Payments
Toggle
cash
,bank
,blackmoney
inpayment
.Item currency:
payment = { itemCurrency = { enabled=true, item='gold_token', per=100 } } -- each token = $100
QBCore black money as item:
Config.BlackMoneyAsItem = { name='black_money', per=1 }
6) Ownership, Pricing & Stock
Unowned: prices read from theme each open (min..max random if range). No DB stock.
Owned:
Prices freeze to DB on purchase (owner can edit).
Stock tracked per item (show in UI if
ui.showStock=true
).Sales credit shop balance; purchases decrement stock.
7) Access Control & Weapons
Restrict by job/grade via
access
(and optionallycategoryAccess
).Licenses:
ESX:
useLicense=true, licenseMode='esx', esxLicenseType='weapon'
Item:
useLicense=true, licenseMode='item', licenseItem='weapon_license'
Weapons:
weaponsAsItems=false
→ given to loadout (duplicates blocked).weaponsAsItems=true
→ added as inventory items.
8) Delivery Missions (Owner Restock)
Start from owner UI → server calls:
exports['risk-marketv2']:StartDelivery(source, shopId)
Truck spawns at
Config.Delivery.Spawns[shopId].spawn
.Load at
loadMarker
(progress), return to shop, unload → stock increases.Configure per shop:
Config.Delivery.Spawns = { ['247_1'] = { spawn=vector4(...), loadMarker=vector3(...) }, ['myshop_1']= { spawn=vector4(...), loadMarker=vector3(...) } }
9) Owner Flow (Bank & Procurement)
Buy: charge bank; create
risk_mv2_balances
row; freeze prices; init stock rows.Fund/Withdraw: bank ↔ shop balance (fee via
Config.FundFeePercent
).Set price: DB update + live NUI refresh.
Procure: buy stock from balance at wholesale =
min(price) * (1 - Discount%)
.
10) Exports & Events
Server exports
exports('StartDelivery', function(src, shopId) end)
exports('GetShopSnapshot', function(shopId) -> table end) -- { id, balance, items[{name,label,stock,price,image,min}], recentSales, popular }
exports('CanManage', function(src, shopId) -> bool end)
Core events
risk_markets:open
→ open UI for shop id (server).risk_markets:purchase
→ process cart/payment (server).Owner actions:
risk-mv2a:setItemPrice
,risk-mv2a:fundAccount
,risk-mv2a:withdrawAccount
,risk-mv2a:procureItem
.Delivery flow:
risk-markets:delivery:*
(spawn/load/unload/done).
11) Images & Paths
Prefer local
img/...
under the resource’shtml
folder.Remote images: full
https://...
.If you rename the resource, update any
nui://<resource>/...
paths used elsewhere.
12) ox_target vs. Markers
If
Config.UseOxTarget = true
andox_target
is running → NPC/box-zone context options.Otherwise → 3D marker + “E” prompt.
13) Best Practices
Keep catalog & visuals in themes; reuse themes across many shops.
Ensure unique shop IDs.
Show stock to players only where it’s meaningful (
ui.showStock=true
).Start access simple (
jobs='all'
), addminGrade
/categoryAccess
later.
14) Gotchas & Troubleshooting
Duplicate IDs: don’t reuse (e.g.,
247_2
twice) — rename one (e.g.,247_4
).UI won’t open: check
ox_target
state or stand in marker and press E.Images missing: bad path; verify relative to
html
or usehttps://
.Prices not persistent: only persist after ownership (unowned uses theme config).
Stock not changing: tracked only for owned shops.
15) Minimal Example Theme + Shop
Theme
Config.MarketThemes.neon = {
distance = 3.0,
npc = { enabled = true, model = 'a_f_y_hipster_02' },
blip = { enabled = true, sprite = 52, color = 27, scale = 0.8, name = 'Neon Store' },
marker = { enabled = true, type = 29, scale = 0.5, color = { r=0,g=200,b=255,a=140 } },
ui = { title = 'NEON MART', description = 'Open late. Shine bright.', showStock = true },
payment = { cash = true, bank = true, blackmoney = false },
weaponsAsItems = false,
categories = {
{ name='Drinks', icon='img/items/water.png' },
{ name='Gadgets', icon='img/items/usb.png' }
},
items = {
Drinks = {
{ name='water', label='Water Bottle', price={min=5,max=5}, image='img/items/water.png', description='Stay hydrated.' },
{ name='energy_drink', label='Energy Drink', price={min=18,max=22}, image='img/items/energy.png', description='Quick boost.' }
},
Gadgets = {
{ name='usb_stick', label='USB Stick', price={min=18,max=22}, image='img/items/usb.png', description='Portable storage.' },
{ name='radio', label='Radio', price={min=95,max=110},image='img/items/radio.png', description='Stay connected.' }
}
}
}
CSS
.theme-neon{ --main-color:#00E5FF; --main-color-2:#00E5FF36; }
Shop
table.insert(Config.MarketShops, {
coords = {
{
id='neon_1', theme='neon',
pos=vector4(1160.0, -324.0, 69.2, 90.0),
ownable=true, mgmt=vector4(1159.2, -319.7, 69.2, 178.0), price=240000
}
}
})
Last updated