TraderX_Tools/docs/traderx-configuration-guide.md
2026-09-06 22:47:27 +03:00

24 KiB
Raw Permalink Blame History

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:

  1. Converts old TraderPlus V1 configs if found (TraderPlusPriceConfig.json, TraderPlusGeneralConfig.json).
  2. Loads SourceConfig.json → decides LEGACY / CSV / COMPILED.
  3. Loads products, then categories, then vehicle parkings.
  4. Applies "stock behavior at restart" to every limited-stock product.
  5. Loads TraderXGeneralSettings.json and 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 no Compiled/ folder exists.
  • Large item lists / spreadsheet editing: set "CSV" + "autoMigrate": true once, then edit Source/products.csv and Source/categories.csv in Excel/LibreOffice. Every restart recompiles. Check TraderXLogs/Config/ConfigReport.log after 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:

  • coefficient does nothing if maxStock is -1. Unlimited stock ⇒ flat price. If you want dynamic prices you need a finite maxStock.
  • With stock s = 1 the price equals basePrice. Every additional unit in stock multiplies by coefficient. 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.98 is 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 in TraderXGeneralSettings.json → acceptedStates (§6). Note: TraderXStates.coefficientWorn/Damaged/BadlyDamaged exist in the settings file but the pricing code uses the hard-coded TraderXItemState multipliers 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. For FULL/EMPTY/NO_MATTER modes this doesn't matter (values are 0). For COEFFICIENT/STATIC products created via CSV, the resulting quantities may be wrong. If you need STATIC/COEFFICIENT quantities, verify in-game, or set tradeQuantity directly 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 / variants is 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 currencyType is an independent wallet. A trader with currenciesAccepted: ["EUR"] counts only EUR items.
  • value is per unit; stackable money items are multiplied by their quantity. Always include a value: 1 denomination or change can't be given.
  • Any item class can be money (nails, bullets, custom TraderX_Money_* items from the Workshop build). The TraderX_Money_* classes are not defined in this repo's config.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 edit Compiled/Products_compiled.json directly (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).