Skip to content

aowlspt/trader

Source: aowl/src/aowlspt/trader.nim — 802 lines.

aowlspt/trader — traders, stock and quests, without the ten things you could not have known.

mods/tarkov IS the game server, and it has no trader type and no quest type. It reads traders.<id> and templates.quests.<id> straight out of the loaded database and serves whatever is there. So the whole mechanism a mod needs is dbWrite, and the whole difficulty is the shape.

This module is that shape, and nothing else. It does not wrap dbWrite, it does not own a registry, and it does not run anything: every proc here either builds JSON or performs one named dbWrite and tells you the path it wrote. You can read install below and see the three writes.

A whole trader:

import aowlspt/trader

var t = newTrader(traderId("ad0000000000000000000001"), "Admin Trader") t.description = "Everything, free." discard t.stockWholeHandbook(4500) # templates.handbook.Items if install(t) != Ok: warn whyNot(t)

and a quest:

var q = newQuest(questId("ad0000000000000000000101"), t, "Admin Induction") q.requireLevel(1) q.requireHandover("544fb37f4bdc2dee738b4567", 1) q.rewardExperience(500) q.rewardStanding(0.1) discard install(q)

What is made unrepresentable here, and why

Each of these was a real way to produce a trader that looks written and is silently broken:

  • An offer with no price. An assort is three collections keyed to each other (items, barter_scheme, loyal_level_items); an offer in the first and missing from the second is one the client draws and cannot price. Here an offer is one Offer value carrying its own cost, and all three collections are emitted from that one list in one loop. There is no API through which they can disagree.
  • "Free" written as a price of zero. There is no price field in this database — a price IS a barter requirement. freeOffer spells that as a requirement for count: 0 roubles. You never see a price field because there is not one.
  • A name written as a dotted path. Real text lives in locales.global.en under "<id> Nickname" — with a SPACE in the key — so dbWrite("locales.global.en.<id> Nickname", …) writes a key nobody reads. localeText takes a key and a value, never a path, and every name this module writes goes through it.
  • Stock from the wrong table. templates.items is 4,673 raw templates including hideout nodes and stashes and is tens of megabytes; templates.handbook.Items is the ~4,300 tradeable things and ~400 KB. stockWholeHandbook names the correct one and takes no table argument.
  • A malformed id. Every id the client handles is 24 hex characters. traderId / questId refuse anything else loudly at construction, and an invalid id makes install return ErrBadArg rather than write a trader nothing can reach.

Two things this module deliberately does NOT hide, because they are not shapes: getting the mod to load at all (the DLL + registry/mods.json + the manager-owned selection — see docs/MOD-ENABLE-PATH.md), and regcheck comparing sides and author to your source verbatim.

Types

Id

nim
  Id* = object
    text*: string
    ok*: bool
    why*: string

A 24-hex-character database id that has been checked. There is no way to make one without the check: traderId and questId are the only constructors, and both refuse.

aowl/src/aowlspt/trader.nim:83

Locale

nim
  Locale* = object
    parts*: JsonObject
    n*: int

A batch of locale entries, flushed with one dbWrite. The keys contain spaces ("<id> Nickname"), which is exactly why this is a patch object and not a dotted path per key.

aowl/src/aowlspt/trader.nim:140

Offer

nim
  Offer* = object
    tpl*: string          ## the item template this offer sells
    count*: int           ## how many of `currency` it costs. 0 is free.
    currency*: string     ## the template id of what it costs
    stack*: int           ## how many are on the shelf
    unlimited*: bool
    loyaltyLevel*: int    ## the LL that unlocks it

One thing on the shelf, and its price. The two cannot be separated: there is no constructor that makes an offer without a cost, so an unpriceable offer cannot be built.

aowl/src/aowlspt/trader.nim:165

Trader

nim
  Trader* = object
    id*: Id
    nickname*: string
    surname*: string
    description*: string
    location*: string
    avatar*: string
    currency*: string       ## "RUB" / "USD" / "EUR"
    balanceRub*, balanceDol*, balanceEur*: int
    gridHeight*: int
    unlockedByDefault*: bool
    availableInRaid*: bool
    availableInPve*: bool
    medic*: bool
    buyerUp*: bool
    customizationSeller*: bool
    discount*: int
    insuranceAvailable*: bool
    repairAvailable*: bool
    minLevel*: int          ## loyalty level 1's requirement
    offers*: seq[Offer]
    why*: string            ## why the last operation refused, if it did

Every field the base object needs, pre-filled with a default that works. Set the two or three that make your trader different and leave the rest alone; traderBase writes all 33 either way, because a missing field is a client hang rather than an error.

aowl/src/aowlspt/trader.nim:204

Quest

nim
  Quest* = object
    id*: Id
    traderId*: Id
    name*: string
    description*: string
    note*: string
    location*: string
    side*: string           ## "Pmc" / "Savage"
    image*: string
    kind*: string           ## the `type` field: "Standing", "Completion", …
    restartable*: bool
    secret*: bool
    instantComplete*: bool
    startConditions*: JsonArray
    finishConditions*: JsonArray
    successRewards*: JsonArray
    nStart*, nFinish*, nReward*: int
    startedText*, successText*, failText*: string
    why*: string

The 30-field quest template with a default for every one of them, plus the conditions and rewards you add. Every human-readable field is a LOCALE KEY here; questWords supplies the text those keys resolve to.

aowl/src/aowlspt/trader.nim:469

Constants

Roubles

nim
  Roubles* = "5449016a4bdc2d6f028b456f"

The rouble template. A barter requirement names a currency by template id; this is the one you almost always want.

aowl/src/aowlspt/trader.nim:76

Dollars

nim
  Dollars* = "5696686a4bdc2da3298b456a"

aowl/src/aowlspt/trader.nim:79

Euros

nim
  Euros* = "569668774bdc2da2298b4568"

aowl/src/aowlspt/trader.nim:80

Routines

traderId

nim
proc traderId*(s: string): Id

A trader's id. 24 hex characters, and not one of the real traders' ids. Refuses loudly rather than writing a trader that cannot be reached.

aowl/src/aowlspt/trader.nim:109

questId

nim
proc questId*(s: string): Id

A quest's id. Same rule as traderId.

aowl/src/aowlspt/trader.nim:114

derivedId

nim
proc derivedId*(base: Id; tag: string; n: int): string

A stable 24-hex id derived from another one, for the sub-objects that need their own (each assort offer, each quest condition, each reward). Deriving them from a counter is what makes the three assort collections agree by construction instead of by care.

aowl/src/aowlspt/trader.nim:118

newLocale

nim
proc newLocale*(): Locale

aowl/src/aowlspt/trader.nim:147

localeText

nim
proc localeText*(l: var Locale; key, value: string)

Record one locale entry. key is a KEY, never a path — this is the only way this module writes text, so the dotted-path mistake has no spelling.

aowl/src/aowlspt/trader.nim:149

install

nim
proc install*(l: Locale): Status

One dbWrite("locales.global.en", …) with everything recorded so far.

aowl/src/aowlspt/trader.nim:155

freeOffer

nim
proc freeOffer*(tpl: string; loyaltyLevel = 1): Offer

Sell tpl for nothing. There is no price field to set to zero — a price IS a barter requirement, so "free" is a requirement for count: 0 roubles, which is a requirement nothing can fail.

aowl/src/aowlspt/trader.nim:176

pricedOffer

nim
proc pricedOffer*(tpl: string; price: int; currency = Roubles; stack = 9999999; unlimited = true; loyaltyLevel = 1): Offer

Sell tpl for price of currency. Same object as freeOffer; free is not a special case, it is price = 0.

aowl/src/aowlspt/trader.nim:183

barterOffer

nim
proc barterOffer*(tpl: string; wantTpl: string; wantCount: int; stack = 9999999; unlimited = true; loyaltyLevel = 1): Offer

Sell tpl for wantCount of some other item. This is the same mechanism as a price — the currency template is just not a currency.

aowl/src/aowlspt/trader.nim:191

newTrader

nim
proc newTrader*(id: Id; nickname: string): Trader

A complete, working, empty trader. Nothing further is required to write him — he will simply have nothing to sell.

aowl/src/aowlspt/trader.nim:231

whyNot

nim
proc whyNot*(t: Trader): string

The reason the last install refused, or "".

aowl/src/aowlspt/trader.nim:244

sell

nim
proc sell*(t: var Trader; o: Offer)

Put one offer on the shelf.

aowl/src/aowlspt/trader.nim:248

sellFree

nim
proc sellFree*(t: var Trader; tpl: string)

Shorthand for sell(t, freeOffer(tpl)).

aowl/src/aowlspt/trader.nim:252

stockWholeHandbook

nim
proc stockWholeHandbook*(t: var Trader; maxOffers = 100000; price = 0): int

Stock everything the handbook lists, at price each (0 = free), and return how many offers that came to.

The catalogue is templates.handbook.Items — the list of tradeable things, about 4,300 entries and ~400 KB. It is deliberately not an argument: templates.items is the other table people reach for, and it is 4,673 raw templates including hideout nodes and stashes, tens of megabytes to read, with the wrong contents for a shop.

aowl/src/aowlspt/trader.nim:256

traderBase

nim
proc traderBase*(t: Trader): Json

The base object: who he is, what currency he takes, what loyalty levels he has. All 33 fields, including the ones the client reads but never shows — and including insurance_price_coef, which is a string in the real data and is matched here rather than corrected.

aowl/src/aowlspt/trader.nim:280

traderAssort

nim
proc traderAssort*(t: Trader): Json

The three collections, emitted from t.offers in ONE loop so they cannot drift: items (what it is), barter_scheme (what it costs) and loyal_level_items (what unlocks it), all keyed by the same derived offer id.

aowl/src/aowlspt/trader.nim:374

traderNames

nim
proc traderNames*(t: Trader; into: var Locale)

The five locale entries a trader needs to render as words rather than as a raw 24-hex id. Keys carry a SPACE, which is why they go through localeText.

aowl/src/aowlspt/trader.nim:419

install

nim
proc install*(t: var Trader): Status

Two writes, both named here so you can see them:

dbWrite("traders.<id>", { base, assort, questassort }) dbWrite("locales.global.en", { "<id> Nickname": … })

and then a read-back, because dbWrite returning Ok means the call succeeded and NOT that the value landed where it was aimed.

aowl/src/aowlspt/trader.nim:429

newQuest

nim
proc newQuest*(id: Id; giver: Trader; name: string): Quest

A quest nobody can start yet — add at least one start condition and one finish condition. Everything else already has a working default.

aowl/src/aowlspt/trader.nim:492

whyNot

nim
proc whyNot*(q: Quest): string

aowl/src/aowlspt/trader.nim:506

requireLevel

nim
proc requireLevel*(q: var Quest; level: int)

Start condition: the player must be at least level. 1 means "from a fresh profile".

aowl/src/aowlspt/trader.nim:508

requireHandover

nim
proc requireHandover*(q: var Quest; tpl: string; count = 1; foundInRaid = false)

Finish condition: hand count of item template tpl back to the trader.

aowl/src/aowlspt/trader.nim:524

rewardExperience

nim
proc rewardExperience*(q: var Quest; xp: int)

Pay xp experience on completion.

aowl/src/aowlspt/trader.nim:564

rewardStanding

nim
proc rewardStanding*(q: var Quest; standing: float)

Pay standing with the trader who gave the quest.

aowl/src/aowlspt/trader.nim:568

questTemplate

nim
proc questTemplate*(q: Quest): Json

The stored quest. Every text field below is a locale KEY, not the text — the client looks each one up in locales.global.en, and a literal sentence here renders only when the lookup misses, which is not something to rely on.

aowl/src/aowlspt/trader.nim:572

questWords

nim
proc questWords*(q: Quest; into: var Locale)

The text every one of the quest's locale keys resolves to.

aowl/src/aowlspt/trader.nim:621

install

nim
proc install*(q: var Quest): Status

Two writes:

dbWrite("templates.quests.<id>", …) dbWrite("locales.global.en", …)

then a read-back of the stored quest's traderId.

aowl/src/aowlspt/trader.nim:635

auditTrader

nim
proc auditTrader*(t: Trader; into: var JsonObject)

Fill into with counters describing what is NOW IN THE DATABASE — not what this mod did. Serve it from a status route and assert on it.

The two offer counters are deliberately NEGATIVE — offersNotFree, offersUnpriced — because "we wrote 4288 free offers" is a claim about our own write and cannot fail, while "no offer in the database costs anything" can.

Three outcomes, never two. Enumerating the traders table needs a host that can list keys; aowlspt-backend can and aowlspt-sim cannot, so listedInTradersTable may read "unknown: …". Reporting false there would be a check that says the trader is missing whenever we were unable to look, which is worse than no check.

aowl/src/aowlspt/trader.nim:679

auditQuest

nim
proc auditQuest*(q: Quest; into: var JsonObject)

The same idea for a quest: read the stored template back and count the four things that make it usable rather than merely present.

aowl/src/aowlspt/trader.nim:773

aoughwl — self-hosted platform for things n stuff. Contact / Support on Discord for access to the private backends.