Skip to content

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_emit is delivered synchronously, on the caller thread, to every subscriber but the emitter, and the backend takes modEnter / modLeave around each one (deliverEvent in backend/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 onto aowl.cap.reply/<consumer-guid>, which the consumer subscribed to once. Because delivery is synchronous, that reply has already landed by the time emit returns, 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
    coUnavailable

aowl/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*: string

What 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): CapReply

aowl/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* = 32

In-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* = 32

aowl/src/aowlspt/capability.nim:153

Routines

capOk

nim
func capOk*(body: string): CapReply

aowl/src/aowlspt/capability.nim:184

capFail

nim
func capFail*(message: string): CapReply

aowl/src/aowlspt/capability.nim:187

isOk

nim
func isOk*(r: CapResult): bool

aowl/src/aowlspt/capability.nim:190

status

nim
func status*(r: CapResult): Status

For 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): string

aowl/src/aowlspt/capability.nim:204

strictValue

nim
proc strictValue*(text: string): bool

True 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): bool

aowl/src/aowlspt/capability.nim:346

provide

nim
proc provide*(cap: string; version: int; handler: CapHandler): Status

Publish 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): bool

Stop 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): CapResult

Call 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): string

The 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): string

One 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

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