Appearance
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 oneOffervalue 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.
freeOfferspells that as a requirement forcount: 0roubles. You never see a price field because there is not one. - A name written as a dotted path. Real text lives in
locales.global.enunder"<id> Nickname"— with a SPACE in the key — sodbWrite("locales.global.en.<id> Nickname", …)writes a key nobody reads.localeTexttakes a key and a value, never a path, and every name this module writes goes through it. - Stock from the wrong table.
templates.itemsis 4,673 raw templates including hideout nodes and stashes and is tens of megabytes;templates.handbook.Itemsis the ~4,300 tradeable things and ~400 KB.stockWholeHandbooknames the correct one and takes no table argument. - A malformed id. Every id the client handles is 24 hex characters.
traderId/questIdrefuse anything else loudly at construction, and an invalid id makesinstallreturnErrBadArgrather 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*: stringA 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*: intA 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 itOne 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 didEvery 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*: stringThe 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): IdA 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): IdA quest's id. Same rule as traderId.
aowl/src/aowlspt/trader.nim:114
derivedId
nim
proc derivedId*(base: Id; tag: string; n: int): stringA 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*(): Localeaowl/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): StatusOne dbWrite("locales.global.en", …) with everything recorded so far.
aowl/src/aowlspt/trader.nim:155
freeOffer
nim
proc freeOffer*(tpl: string; loyaltyLevel = 1): OfferSell 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): OfferSell 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): OfferSell 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): TraderA 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): stringThe 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): intStock 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): JsonThe 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): JsonThe 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): StatusTwo 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): QuestA 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): stringaowl/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): JsonThe 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): StatusTwo 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

