24 KiB
TraderX – Server Configuration Guide
Written from a read-through of the TraderX source (TheDmitri/TraderX, main, mod version string 1.0.1). Everything below describes what the code actually does, not what the README promises. Where the code is inconsistent or a feature is only partially implemented, it is called out.
1. Directory layout
All paths are under the server profile folder ($profile:), i.e. wherever your -profiles= startup parameter points. TraderX creates the folders it needs on first start.
<profiles>/TraderX/
├── TraderXConfig/
│ ├── TraderXGeneralSettings.json ← traders (NPCs), licenses, accepted item states
│ ├── TraderXCurrencySettings.json ← currency types and denominations
│ ├── TraderXDynamicTranslationSettings.json ← notification / error strings
│ ├── TraderXDebugSettings.json ← debug mode + test suites
│ ├── TraderXTXSafeZoneSettings.json ← safe zone module
│ ├── SourceConfig.json ← which product/category format to load
│ ├── Products/ prod_<name>_NNN.json ← LEGACY format (one file per product)
│ ├── Categories/ cat_<name>_NNN.json ← LEGACY format (one file per category)
│ ├── Source/ products.csv, categories.csv, presets.csv ← CSV format
│ ├── Compiled/ Products_compiled.json, Categories_compiled.json ← COMPILED (auto-generated)
│ ├── Backup/ *.backup ← last successful compile
│ ├── TraderXVehicleParking/
│ │ ├── parking_config.json
│ │ └── parkings_<npcId>.json
│ └── DebugTestCases/ *.json
├── TraderXDatabase/ ← runtime state, do NOT hand-edit while server is up
│ ├── Stock/ <productId>.json ← current stock per product
│ └── PlayerLicenses/ playerLicense_<steam64>.json
├── TraderXPresets/ presets_<id>.json ← player-saved attachment presets
└── TraderXLogs/
├── Config/LoggingSettings.json
├── Config/ConfigReport.log ← CSV validation report
└── Logs/TraderX_<date>.log
2. Startup / load order (important for "my edit did nothing")
On server start, TraderXModule.InitializeModule() does, in order:
- Converts old TraderPlus V1 configs if found (
TraderPlusPriceConfig.json,TraderPlusGeneralConfig.json). - Loads
SourceConfig.json→ decides LEGACY / CSV / COMPILED. - Loads products, then categories, then vehicle parkings.
- Applies "stock behavior at restart" to every limited-stock product.
- Loads
TraderXGeneralSettings.jsonand spawns NPCs.
⚠ The compiled files always win
TraderXProductRepository.LoadAllProducts() and LoadAllCategories() check for Compiled/Products_compiled.json / Categories_compiled.json first, regardless of preferredFormat. If those files exist and are non-empty, the Products/ and Categories/ folders are ignored completely.
So: if you edit legacy JSON files and nothing changes, check whether a Compiled/ folder exists. Either delete it (it will be regenerated only in CSV/COMPILED mode) or switch to editing the CSV source.
Fallback chain
Compiled JSON → (empty/corrupt) → Legacy Products/ + Categories/
In CSV mode, Source/*.csv is compiled to Compiled/*.json before this chain runs. If validation fails, the previous Backup/ copy is restored.
3. SourceConfig.json
{
"preferredFormat": "LEGACY",
"autoMigrate": false,
"allowCompilation": true
}
| Field | Values | Meaning |
|---|---|---|
preferredFormat |
"LEGACY", "CSV", "COMPILED" (case-insensitive) |
Which editing workflow you use. Default LEGACY. |
autoMigrate |
bool | One-shot conversion on next start. CSV mode: legacy JSON → CSV. COMPILED mode: legacy → CSV → compiled. The mod sets this back to false automatically after a successful migration. Original files are not deleted. |
allowCompilation |
bool | CSV mode only. false skips CSV → compiled (effectively disables CSV loading; not recommended). |
Recommended workflows
- Small server / few items: stay on
LEGACY, edit one JSON per product. Make sure noCompiled/folder exists. - Large item lists / spreadsheet editing: set
"CSV"+"autoMigrate": trueonce, then editSource/products.csvandSource/categories.csvin Excel/LibreOffice. Every restart recompiles. CheckTraderXLogs/Config/ConfigReport.logafter each restart. - Web editor: https://traderpluseditor.dmitri-dev-area.dev exports the legacy per-file format.
4. Products
4.1 Legacy file: TraderXConfig/Products/<productId>.json
{
"className": "SNAFU_MKahles",
"coefficient": 0.66874,
"maxStock": 5,
"tradeQuantity": 18,
"buyPrice": 110,
"sellPrice": 30,
"stockSettings": 0,
"attachments": [],
"variants": []
}
⚠ File name is the product ID and it is validated
TraderXProductId.IsValidUUID() rejects (and skips, with an error in the log) any file that does not match:
prod_<name>_NNN.json
- must start with
prod_ <name>= letters, digits, underscores (other characters are sanitized to_)- must end with
_+ exactly three digits,001–999 - minimum total length 9 chars
Valid: prod_akm_001.json, prod_snafu_mkahles_001.json
Invalid (silently skipped): prod_snafu_mkahles.json, akm.json, prod_akm_1.json, prod_akm_1000.json
The productId inside the file, if present, is ignored in legacy mode; the ID always comes from the filename. (In Compiled/ files the productId field is used.)
Log lines to grep for: Skipping invalid UUID filename and Invalid product ID.
Field reference
| Field | Type | Meaning |
|---|---|---|
className |
string | DayZ class name. Compared case-insensitively. |
buyPrice |
int | Price the player pays. -1 = cannot be bought (item is sell-only). |
sellPrice |
int | Price the player receives. -1 = cannot be sold (buy-only). |
maxStock |
int | -1 = unlimited. 0 or more = limited stock, tracked in TraderXDatabase/Stock/<productId>.json. |
coefficient |
float | Dynamic price factor. Only used when maxStock != -1 and coefficient != 1.0. See §4.2. |
tradeQuantity |
int (bitfield) | How full the item is when bought / must be when sold. See §4.3. |
stockSettings |
int (bitfield) | De-stock rate + restart behavior. See §4.4. |
attachments |
string[] | Product IDs (prod_…) that can be attached at purchase (customize page / presets). Each must be its own product file. |
variants |
string[] | Product IDs shown as colour/skin variants of this product. |
4.2 Dynamic pricing (how coefficient actually works)
From TraderXPriceCalculation:
if maxStock == -1 or coefficient == 1.0:
price = round(basePrice × quantity × stateMultiplier)
else, per unit, with s = current trader stock of this product:
unitPrice = coefficient ^ (s − 1) × basePrice × stateMultiplier
(s is clamped to 1 if 0)
buy: s increases by 1 for each additional unit in the cart
sell: s increases by 1 for each additional unit sold
total = round(Σ unitPrice), minimum 1
Consequences:
coefficientdoes nothing ifmaxStockis-1. Unlimited stock ⇒ flat price. If you want dynamic prices you need a finitemaxStock.- With stock
s = 1the price equalsbasePrice. Every additional unit in stock multiplies bycoefficient. Example:buyPrice 100,coefficient 0.9: stock 1 → 100, stock 2 → 90, stock 5 → 65.6, stock 10 → 38.7. - Lower coefficient = steeper price drop as the trader fills up.
0.9–0.98is a sensible range;0.66(as in the sample above) collapses the price to ~5 % by stock 8. - The same formula is used for buy and sell, so a trader that is out of stock (s = 0 → treated as 1) sells at full
sellPrice. stateMultiplier(item condition) for sales: pristine 1.0, worn 0.8, damaged 0.6, badly damaged 0.4, ruined 0 (refused). Whether worn/damaged/badly-damaged items are accepted at all is set inTraderXGeneralSettings.json → acceptedStates(§6). Note:TraderXStates.coefficientWorn/Damaged/BadlyDamagedexist in the settings file but the pricing code uses the hard-codedTraderXItemStatemultipliers above.
4.3 tradeQuantity bitfield
Decoded at trade time by TraderXTradeQuantity:
bits 0–2 sellMode
bits 3–5 buyMode
bits 6–18 sellValue (13 bits, 0–8191)
bits 19–31 buyValue
| Mode value | Buy meaning | Sell meaning |
|---|---|---|
| 0 | BUY_EMPTY – item spawns empty |
SELL_NO_MATTER – any fill level accepted, paid pro-rata |
| 1 | – | SELL_EMPTY – must be empty |
| 2 | BUY_FULL – item spawns full |
SELL_FULL – must be full |
| 3 | BUY_COEFFICIENT – full × (buyValue/100) |
SELL_COEFFICIENT – max × (sellValue/100) |
| 4 | BUY_STATIC – exactly buyValue units |
SELL_STATIC – exactly sellValue units |
tradeQuantity = sellMode | (buyMode << 3) | (sellValue << 6) | (buyValue << 19)
Common values
| Value | Meaning |
|---|---|
0 |
buy empty, sell any |
2 |
buy empty, sell only full |
16 |
buy full, sell any |
18 |
buy full, sell full (most common for mags, food, etc.) |
78644644 |
(README example) buy static 150, sell static 22 – for stackables like mushrooms |
Quick decode in bash:
tq=18; echo "sell=$((tq&7)) buy=$(((tq>>3)&7)) sellVal=$(((tq>>6)&0x1FFF)) buyVal=$((tq>>19))"
⚠ Known inconsistency: the CSV compiler (
TraderXBitfieldPacker) packs values with a different layout (sell value in bits 6–15 ×1000, buy value in bits 16–31 ×1000) than the runtime decoder above. ForFULL/EMPTY/NO_MATTERmodes this doesn't matter (values are 0). ForCOEFFICIENT/STATICproducts created via CSV, the resulting quantities may be wrong. If you need STATIC/COEFFICIENT quantities, verify in-game, or settradeQuantitydirectly in legacy JSON using the runtime layout.
4.4 stockSettings bitfield
bits 0–6 destockCoefficient × 100 (0–100)
bits 7–8 stockBehaviorAtRestart (0, 1, 2)
stockSettings = round(destock × 100) | (behavior << 7)
stockBehaviorAtRestart |
Effect on server start |
|---|---|
| 0 | keep stock as saved |
| 1 | reset stock to maxStock |
| 2 | random stock between 0 and maxStock |
destockCoefficient (0.0–1.0): additionally, on every restart, stock = round(stock × (1 − destock)). Applied after the behavior above.
| Example | Value |
|---|---|
| keep, no destock | 0 |
| reset to max every restart | 128 |
| random every restart | 256 |
| keep, lose 25 % per restart | 25 |
| reset to max + destock 10 % (nets 90 %) | 138 |
Only applies when maxStock != -1.
4.5 Stock database
TraderXDatabase/Stock/<productId>.json:
{ "productId": "prod_akm_001", "stock": 3 }
Created automatically the first time a limited-stock product is touched. Buying decreases stock, selling increases it (capped at maxStock – trader refuses with "Trader stock is full"). Safe to delete the whole Stock/ folder while the server is down to reset everything to 0 (or to maxStock if stockBehaviorAtRestart = 1).
4.6 Attachments and variants
- Every entry in
attachments/variantsis a product ID, not a class name.["prod_mag_akm_30rnd_001", "prod_pso1optic_001"] - The referenced product must exist (be a valid file). Missing references are simply dropped at runtime; the CSV validator reports them as errors.
- Attachment products need their own
buyPrice; the customize page adds their price to the base item. - Attachments are also stock-checked: an out-of-stock attachment blocks the purchase ("Attachment out of stock").
- Variants are shown as selectable alternatives on the purchase page; each variant is a full product with its own price/stock.
5. Categories
5.1 Legacy file: TraderXConfig/Categories/<categoryId>.json
File name rule is the same as products, but with the cat_ prefix and lowercase letters only in the middle part:
cat_<name>_NNN.json e.g. cat_assault_rifles_001.json
{
"isVisible": true,
"icon": "",
"categoryName": "Assault Rifles",
"licensesRequired": [],
"productIds": [
"prod_akm_001",
"prod_m4a1_001"
]
}
| Field | Meaning |
|---|---|
isVisible |
false hides the category from the catalog (products stay usable as attachments/variants). |
icon |
Path to an image passed to ImageWidget.LoadImageFile(), e.g. "TraderX/datasets/images/car.edds" or a set:… image:… string. Empty = no icon. |
categoryName |
Display name. Also used for GetCategoryIdByName lookups (case-insensitive). |
licensesRequired |
Array of license IDs (the licenseId UUID from TraderXGeneralSettings.json, not the name). Player must hold all of them or the card is greyed out. |
productIds |
Ordered list of product IDs shown in this category. A product can appear in multiple categories. |
Categories are assigned to traders in TraderXGeneralSettings.json → traders[].categoriesId.
6. TraderXGeneralSettings.json
Generated with defaults on first start (a Chernarus example trader camp). Structure:
{
"version": "1.0.1",
"serverID": "<auto-generated uuid>",
"licenses": [
{
"licenseId": "<uuid>",
"licenseName": "Car License",
"description": "Allows you to drive a car."
}
],
"acceptedStates": {
"acceptWorn": true,
"acceptDamaged": true,
"acceptBadlyDamaged": true,
"coefficientWorn": 0.75,
"coefficientDamaged": 0.5,
"coefficientBadlyDamaged": 0.25
},
"traders": [
{
"npcId": 0,
"className": "SurvivorM_Boris",
"givenName": "Boris",
"role": "Weapons",
"position": [11596.6, 58.7, 14630.3],
"orientation": [180.0, 0.0, 0.0],
"categoriesId": ["cat_assault_rifles_001", "cat_pistols_001"],
"currenciesAccepted": ["EUR"],
"loadouts": [
{ "className": "TTSKOJacket_Camo", "quantity": -1, "slotName": "Body", "attachments": [] },
{ "className": "TTSKOPants", "quantity": -1, "slotName": "Legs", "attachments": [] },
{
"className": "AKM", "quantity": -1, "slotName": "Hand",
"attachments": [ { "className": "Mag_AKM_30Rnd", "quantity": 30 } ]
}
]
}
],
"traderObjects": [
{ "className": "bldr_Misc_RoadBarrier", "position": [11537.2, 59.0, 14726.1], "orientation": [55.0, 0.0, 0.0] }
]
}
licenses[]
Define the license catalogue here. licenseId must be unique; generate any UUID. Reference the ID from categories[].licensesRequired.
There is no in-game UI or admin command to grant licenses in this codebase. Licenses are granted by editing TraderXDatabase/PlayerLicenses/playerLicense_<steam64>.json:
{
"steamId": "76561198000000000",
"playerName": "Someone",
"licenses": [
{ "licenseId": "<same uuid as in GeneralSettings>", "licenseName": "Car License", "description": "" }
]
}
The file is read when the player joins; edit while they're offline.
acceptedStates
Controls which conditions the trader will buy. Ruined is never accepted. The coefficient* values are stored but not used by the current pricing code (see §4.2) – condition discounts are 0.8 / 0.6 / 0.4.
traders[]
| Field | Meaning |
|---|---|
npcId |
Integer, must be unique. Used for parking file names (parkings_<npcId>.json) and session tracking. |
className |
Any PlayerBase-derived class (e.g. SurvivorM_Boris, SurvivorF_Linda) or a static object that inherits BuildingBase. Anything else logs "obj was not created" and is skipped. |
givenName, role |
Shown in the action prompt (Trade – Boris (Weapons)) and in the menu header. |
position, orientation |
World coordinates. orientation is [yaw, pitch, roll]. |
categoriesId |
Category IDs this trader sells. Order = display order. |
currenciesAccepted |
Names from TraderXCurrencySettings.json → currencyTypes[].currencyName. Empty = all currencies. |
loadouts |
Clothing/items for the NPC. slotName = DayZ inventory slot (Body, Legs, Feet, Headgear, Vest, Back, Gloves, Mask, Eyewear, Shoulder, Melee…), "Hand" = in hands, empty string = into cargo. quantity -1 = don't touch. Attachments spawn onto the item (magazines are loaded into weapons). |
NPCs are invulnerable (SetAllowDamage(false)).
traderObjects[]
Kept for TraderPlus V1 compatibility and written by the converter, but no code in this version spawns them. Use your map's object spawner (init.c / DayZ Editor loader) for decoration.
7. TraderXCurrencySettings.json
{
"version": "1.0.1",
"currencyTypes": [
{
"currencyName": "EUR",
"currencies": [
{ "className": "TraderX_Money_Euro100", "value": 100 },
{ "className": "TraderX_Money_Euro50", "value": 50 },
{ "className": "TraderX_Money_Euro10", "value": 10 },
{ "className": "TraderX_Money_Euro5", "value": 5 },
{ "className": "TraderX_Money_Euro1", "value": 1 }
]
}
]
}
- Each
currencyTypeis an independent wallet. A trader withcurrenciesAccepted: ["EUR"]counts only EUR items. valueis per unit; stackable money items are multiplied by their quantity. Always include avalue: 1denomination or change can't be given.- Any item class can be money (nails, bullets, custom
TraderX_Money_*items from the Workshop build). TheTraderX_Money_*classes are not defined in this repo'sconfig.cpp; they ship with the Workshop package. - Note: the default generator has a bug and puts the USD denominations into the EUR type. Check the generated file before use.
Money is counted by walking the player's whole inventory (including nested containers).
8. Vehicles
A product is treated as a vehicle if its class inherits Transport (cars, boats). Vehicles:
- can only be bought/sold one at a time;
- on purchase spawn at a free parking spot for that trader; no free spot ⇒ "No parking available";
- on sale the player must be near the vehicle, and it must not be occupied or engine-running.
TraderXVehicleParking/parkings_<npcId>.json
{
"traderId": "3",
"positions": [
{ "position": [11620.0, 58.9, 14690.0], "rotation": [50.0, 0.0, 0.0], "size": [3.0, 2.0, 6.0] },
{ "position": [11625.0, 58.9, 14694.0], "rotation": [50.0, 0.0, 0.0], "size": [3.0, 2.0, 6.0] }
]
}
size = width, height, length of the box that must be free of non-whitelisted objects. traderId = the trader's npcId as a string; the file name must match.
TraderXVehicleParking/parking_config.json
{ "whitelistedObjects": ["Land_Grass", "Land_Stone", "Land_Dirt", "Land_Sand", "Land_Gravel", "ClutterCutter", "Particle", "EffectSound", "Land_Underground", "Terrain"] }
Object class-name substrings that do not count as "blocking" a parking spot. Add your barrier/prop classes here if spots are always reported as occupied.
9. CSV format (Source/)
Files are tab-delimited (despite the .csv extension), first row = header, header names case-insensitive. Multiple packs are supported: 10_vanilla.csv, 50_community_weapons.csv merge in filename order (numeric prefix = priority). Quotes "…" are stripped and allow tabs inside a field.
products.csv columns
| Column | Notes |
|---|---|
productKey |
Becomes the product ID. Use the prod_<name>_NNN form to stay compatible with legacy export. |
className |
|
buyPrice, sellPrice |
-1 = not tradeable that direction |
maxStock |
-1 = unlimited |
buyQtyMode |
EMPTY, FULL, COEFFICIENT, STATIC |
buyQtyValue |
for COEFFICIENT (0–1) / STATIC |
sellQtyMode |
NO_MATTER, EMPTY, FULL, COEFFICIENT, STATIC |
sellQtyValue |
|
destockCoefficient |
0.0–1.0 |
stockBehaviorAtRestart |
0 / 1 / 2 |
attachments |
;-separated productKeys |
variants |
;-separated productKeys |
notes |
ignored by the game |
coefficient(price coefficient) is not a CSV column in this version – the compiler uses the class default (1.0). If you rely on dynamic pricing, use legacy JSON or editCompiled/Products_compiled.jsondirectly (COMPILED mode).
categories.csv columns
categoryKey, categoryName, icon, isVisible (1/0/true/false), licensesRequired (;-separated license IDs), productKeys (;-separated), notes.
presets.csv columns
presetKey, productKey, presetName, attachments (;-separated productKeys), notes – server-defined attachment presets.
Validation
Every restart writes TraderXLogs/Config/ConfigReport.log. Errors (duplicate keys, empty className, unknown product references, out-of-range values) abort compilation and restore the last good Backup/. Warnings do not.
10. Other settings files
TraderXDynamicTranslationSettings.json
Every player-facing notification string (successfullyBought, insufficientFunds, traderStockFull, …). %1, %2 are placeholders. Edit for localisation or flavour.
TraderXLogs/Config/LoggingSettings.json
{ "logLevel": 0, "refreshRateInSeconds": 60 }
logLevel: 0 Debug, 1 Info, 2 Warn, 3 Error. Debug is very verbose (every price calculation); use 1 in production.
TraderXDebugSettings.json
enableDebugMode, runTestsOnStartup + per-suite flags, authorizedAdmins (Steam64 list). Leave runTestsOnStartup: false on a live server – the tests create and destroy items.
TraderXTXSafeZoneSettings.json (TraderXSafeZone module)
{
"version": "1.0.0",
"notificationTimer": 10,
"notificationTitle": "SafeZone",
"msgOnEnteringZone": "You entered the safezone!",
"msgOnLeavingZone": "You have left the safezone!",
"leaveZoneCountdown": 30,
"msgOnLeavingZoneCountdown": "You are leaving the safezone in %1 seconds",
"adminSteamUIDs": ["76561198000000000"],
"enableAfkDisconnect": false,
"kickAfterDelay": 300,
"cleanupTimer": 1200,
"objectsToDelete": ["Paper"],
"locations": [
{ "id": 1, "isActive": true, "name": "Trader Camp", "position": [11600, 58, 14650], "radius": 150,
"isEntitiesCleanActive": true, "allowedAnimals": [] }
]
}
Inside an active zone: no damage, melee, fire, traps, zombies; objectsToDelete are removed every cleanupTimer seconds; AFK kick after kickAfterDelay seconds if enabled. Admins in adminSteamUIDs are exempt.
11. Bulk-editing tips (Linux)
Set every prod_*.json in the folder to unlimited stock and coefficient 0.9:
cd ~/serverprofile/TraderX/TraderXConfig/Products
sed -i -E 's/("maxStock":\s*)[-0-9.]+/\1-1/; s/("coefficient":\s*)[-0-9.]+/\10.90/' prod_*.json
(Reminder: with maxStock: -1 the coefficient is ignored – see §4.2.)
Find product files that will be rejected by the filename validator:
ls | grep -Ev '^prod_[A-Za-z0-9_]+_[0-9]{3}\.json$'
Rename them by appending _001:
for f in prod_*.json; do
[[ $f =~ _[0-9]{3}\.json$ ]] || mv -- "$f" "${f%.json}_001.json"
done
Then update every category's productIds to the new names (they must match exactly), e.g.:
sed -i -E 's/("prod_[a-z0-9_]+)"/\1_001"/g' ../Categories/*.json # review before running
Reset all stock (server stopped):
rm -f ~/serverprofile/TraderX/TraderXDatabase/Stock/*.json
12. Troubleshooting checklist
| Symptom | Check |
|---|---|
| Product missing from trader | Filename matches prod_<name>_NNN.json; product ID listed in a category's productIds; category listed in the trader's categoriesId; no stale Compiled/ folder. |
| Category greyed out with lock | licensesRequired contains license IDs the player doesn't have in PlayerLicenses/. |
| Prices never change | maxStock is -1 or coefficient is 1.0. |
| Price drops to 1 | coefficient too low for your maxStock. |
| "Trader stock is full" on sell | stock ≥ maxStock; raise maxStock or set -1. |
| Vehicle "No parking available" | No parkings_<npcId>.json for that trader, or all spots blocked – add blocking classes to parking_config.json. |
| NPC not spawning | className must inherit PlayerBase or BuildingBase; look for "obj was not created" in the log. |
| Edits ignored | Compiled/ folder present while editing legacy files. |
| Can't pay despite having money | Trader's currenciesAccepted doesn't include the currency type holding those items. |
Log file: TraderXLogs/Logs/TraderX_<date>.log. Startup logs the format used (Configuration format selected: …) and product/category counts (LoadAllProducts - Completed. Total products loaded: N).