# 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. ``` /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__NNN.json ← LEGACY format (one file per product) │ ├── Categories/ cat__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_.json │ └── DebugTestCases/ *.json ├── TraderXDatabase/ ← runtime state, do NOT hand-edit while server is up │ ├── Stock/ .json ← current stock per product │ └── PlayerLicenses/ playerLicense_.json ├── TraderXPresets/ presets_.json ← player-saved attachment presets └── TraderXLogs/ ├── Config/LoggingSettings.json ├── Config/ConfigReport.log ← CSV validation report └── Logs/TraderX_.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/.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__NNN.json ``` - must start with `prod_` - `` = 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/.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/.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/.json` File name rule is the same as products, but with the `cat_` prefix and **lowercase** letters only in the middle part: ``` cat__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": "", "licenses": [ { "licenseId": "", "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_.json`: ```json { "steamId": "76561198000000000", "playerName": "Someone", "licenses": [ { "licenseId": "", "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_.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_.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__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__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_.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_.log`. Startup logs the format used (`Configuration format selected: …`) and product/category counts (`LoadAllProducts - Completed. Total products loaded: N`).