Appearance
aowlspt/capability
Source: aowl/src/aowlspt/capability.nim — 1026 lines.
Named, versioned capabilities: how one server-side mod calls another.
THE GAP THIS CLOSES
call() in aowlspt.nim reaches the aowlspt.host:: namespace and nothing else, and there is no HTTP client anywhere in the SDK. So a mod that has built a feature and needs one operation another mod already implements has had exactly two options: reimplement it, or refuse. mods/admin refuses -- presetApplyStatus() there says so at length -- while the spawner it needs sits finished in mods/tarkov/emu/spawn.nim.
A capability is a NAME (items.spawn), a VERSION (an integer), and a request/response schema the provider documents. A provider publishes one with provide; a consumer invokes one with invoke. Neither knows the other guid, its routes, or its port.
WHAT IT IS BUILT ON, AND WHY THAT AND NOT SOMETHING ELSE
Events, in both directions. That is not a shortcut around a better mechanism; it is the only in-process channel that already has the lifetime discipline this needs, and abi/** and host/** are not this module to change.
event_emitis delivered synchronously, on the caller thread, to every subscriber but the emitter, and the backend takesmodEnter/modLeavearound each one (deliverEventinbackend/aowlbackend.nim). A provider being unloaded is skipped, not called into. So a capability invocation inherits a use-after-free guard that a raw cross-DLL function pointer -- the obvious alternative -- would not have had.- There is no reply buffer on
event_emit, so the answer comes back the same way: the provider emits ontoaowl.cap.reply/<consumer-guid>, which the consumer subscribed to once. Because delivery is synchronous, that reply has already landed by the timeemitreturns, on the same stack. The consumer therefore blocks for exactly as long as the provider takes and no longer, with no polling and no timeout to tune.
None of this goes over HTTP. It never chunks a request, never negotiates an encoding, and never parses a /aowlspt/settings document -- so the three measured wire traps (a chunked POST answered 200 with the schema unchanged; the always-deflate default; value emitted unquoted) cannot reach it. The price is that this is server-side only, in one process. It is not a network protocol and must not become one.
RESOLUTION IS LAZY, AND THAT IS A DECISION
Nothing is resolved at load time and nothing is cached between calls. Every invoke asks, live, whether a provider is answering right now.
That is deliberate, and it is the answer to loadAfter. loadAfter is ORDERING ONLY -- SAIN declares loadAfter aowl.morebots and that is a hint about sequence, not a dependency the host enforces -- so a load-time bind would turn "the provider happened to load second" into a permanent, silent failure for the rest of the session. Here it is not even a transient one: the first invoke after the provider registers succeeds. A mod may call a capability from onLoad and get a refusal, call it again on the first tick and get an answer, and both are correct.
The cost is one event fanout per call: a table walk and a synchronous C call in the same process. A capability invoked in a per-frame loop is being used wrongly regardless of what it costs.
THE REFUSAL TAXONOMY, WHICH IS THE POINT OF THE MODULE
A missing provider must never look like a working one. invoke never returns a body it did not receive, and the outcome enum has no member meaning "probably fine".
coOk a provider answered and its payload parsed strictly. coProviderError a provider answered and said no. message is its words, not ours. coVersionMismatch a provider for this NAME is loaded and answering, at other versions. versions lists them. coProviderNotLoaded the registry says a mod provides this and the selection does not load it. This is the distinction that was missing: a stale override in store/aowl.manager/selection disabled two mods for days while the roster still called them "enabled", and "not installed" would have been a lie about it. coProviderSilent the registry says a mod provides this AND the selection loads it, and nothing answered anyway -- so it is loaded and never called provide, or it failed during onLoad. INCONCLUSIVE, not absent. coNoProvider nothing answered and no installed mod declares this capability in the registry. Genuinely not installed. coRosterUnknown nothing answered and the roster could not be read, so WHY is unknown. Never collapsed into coNoProvider: "I could not look" is not "it is not there". coBadReply something answered with a payload that is not strictly valid JSON, or whose envelope does not match what was asked. Treated as a failure, loudly. coUnavailable no host, or every in-flight slot is taken.
message is populated on EVERY non-ok outcome and is written to be shown to a player verbatim.
STRICTNESS
aowlspt/json is deliberately a finder, not a validator: skipValue counts brackets, so {"a":} walks past it and members on a malformed object returns the prefix it managed to read and no error. That is the right trade for pulling one field out of a 40 MB response and the wrong one for accepting a reply. strictValue below is a real recursive-descent validator and every envelope crossing this module goes through it first. The backend selftest once asserted with substring contains and let a payload that was not JSON at all pass for months; this module does not get to repeat that.
Types
CapOutcome
nim
CapOutcome* = enum
coOk
coProviderError
coVersionMismatch
coProviderNotLoaded
coProviderSilent
coNoProvider
coRosterUnknown
coBadReply
coUnavailableaowl/src/aowlspt/capability.nim:156
CapResult
nim
CapResult* = object
outcome*: CapOutcome
body*: string ## the provider response JSON. Empty unless `coOk`.
message*: string ## why, in words, on every outcome but `coOk`.
provider*: string ## the guid that answered, or that the roster names.
versions*: seq[int] ## on `coVersionMismatch`, what IS offered.aowl/src/aowlspt/capability.nim:167
CapReply
nim
CapReply* = object
ok*: bool
body*: string
message*: stringWhat a provider handler decided. Build one with capOk or capFail; the zero value is a failure, so a handler that falls off the end cannot read as success.
aowl/src/aowlspt/capability.nim:174
CapHandler
nim
CapHandler* = proc (request: string): CapReplyaowl/src/aowlspt/capability.nim:182
Constants
CapSchema
nim
CapSchema* = "aowl.cap/1"aowl/src/aowlspt/capability.nim:141
ReplySchema
nim
ReplySchema* = "aowl.cap.reply/1"aowl/src/aowlspt/capability.nim:142
ProbeSchema
nim
ProbeSchema* = "aowl.cap.probe/1"aowl/src/aowlspt/capability.nim:143
ProbeReplySchema
nim
ProbeReplySchema* = "aowl.cap.probe.reply/1"aowl/src/aowlspt/capability.nim:144
SlotCapacity
nim
SlotCapacity* = 32In-flight invocations across all threads. The backend serves on sixteen workers and an invocation occupies a slot only for the duration of one synchronous fanout, so this is twice the arrival bound. Exhaustion is coUnavailable with a message saying so -- never a wait, because a wait here would be a wait on a thread that is holding nothing.
aowl/src/aowlspt/capability.nim:146
ProviderCapacity
nim
ProviderCapacity* = 32aowl/src/aowlspt/capability.nim:153
Routines
capOk
nim
func capOk*(body: string): CapReplyaowl/src/aowlspt/capability.nim:184
capFail
nim
func capFail*(message: string): CapReplyaowl/src/aowlspt/capability.nim:187
isOk
nim
func isOk*(r: CapResult): boolaowl/src/aowlspt/capability.nim:190
status
nim
func status*(r: CapResult): StatusFor a caller that wants an ABI status rather than the enum. Lossy on purpose: use outcome when the distinction matters, which is most of the time, and this only at a boundary that speaks Status.
aowl/src/aowlspt/capability.nim:192
outcomeName
nim
func outcomeName*(o: CapOutcome): stringaowl/src/aowlspt/capability.nim:204
strictValue
nim
proc strictValue*(text: string): boolTrue only if text is ONE complete, well-formed JSON value with nothing after it but whitespace. This is the check aowlspt/json deliberately does not do; see the header.
aowl/src/aowlspt/capability.nim:337
strictObject
nim
proc strictObject*(text: string): boolaowl/src/aowlspt/capability.nim:346
provide
nim
proc provide*(cap: string; version: int; handler: CapHandler): StatusPublish cap/version. Callable from onLoad or later, from any thread.
The registry entry for this mod SHOULD also list the name in its provides array -- registry/mods.json already carries that field. It is not what makes the call work (this is), but it is what lets a consumer say "aowl.tarkov declares it and the selection does not load it" instead of "not installed", and that sentence is the whole reason this module has a roster reader.
ErrUnsupported on the client: there is no server-side mod set there. ErrBadArg for an empty name, a name containing /, a negative version, or a second provide of a pair already published. ErrGeneric when all ProviderCapacity slots are taken.
aowl/src/aowlspt/capability.nim:604
revoke
nim
proc revoke*(cap: string; version: int): boolStop answering. The subscriptions stay (the ABI has no unsubscribe), but onCapRequest now answers a clean refusal naming this mod instead of calling a handler its owner has retired.
aowl/src/aowlspt/capability.nim:667
providedHere
nim
proc providedHere*(): seq[string]What this mod publishes, for a diagnostic.
aowl/src/aowlspt/capability.nim:683
invoke
nim
proc invoke*(cap: string; version: int; requestJson: string): CapResultCall cap/version on whichever loaded mod provides it, right now.
Resolution is per-call and lazy: see the header. There is no timeout to set, because delivery is synchronous -- when this returns, either a provider ran or none exists.
result.body is populated ONLY on coOk, and only after the provider payload validated strictly. Every other outcome carries a message written to be shown verbatim.
aowl/src/aowlspt/capability.nim:935
invokeOrRefuse
nim
proc invokeOrRefuse*(cap: string; version: int; requestJson: string; outBody: var string): stringThe two-line form for a caller that only wants "did it work, and if not what do I show the player". Returns "" on success, the refusal text otherwise, and NEVER writes outBody unless the call succeeded.
aowl/src/aowlspt/capability.nim:1006
describe
nim
proc describe*(r: CapResult): stringOne line for a diagnostic block. Three outcomes, never two: ok, a hard refusal, or an INCONCLUSIVE one, and the word is in the text.
aowl/src/aowlspt/capability.nim:1018

