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

565 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`
```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`
```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:
```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`:
```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
```
```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:
```json
{
"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`:
```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`
```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`
```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`
```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`
```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)
```json
{
"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:
```bash
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:
```bash
ls | grep -Ev '^prod_[A-Za-z0-9_]+_[0-9]{3}\.json$'
```
Rename them by appending `_001`:
```bash
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.:
```bash
sed -i -E 's/("prod_[a-z0-9_]+)"/\1_001"/g' ../Categories/*.json # review before running
```
Reset all stock (server stopped):
```bash
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`).