Skip to content

aowlspt/server

Source: aowl/src/aowlspt/server.nim — 779 lines.

The high-level backend API — the server half of the pipeline.

Deliberately the same shape as aowlspt/game. A mod author moving between the two should be changing what they reach for, not how they write:

client

var Player = gameType("EFT.Player") discard Player.invoke("Heal", 50)

server

proc status(url, body, session: string): string = var o = obj() o.put("ok", true) envelope(o)

discard serve("/aowlspt/status", status)

What this wraps is route_register, db_get/db_patch and config_get. Underneath it is the same ABI the client uses, which is why one mod binary can serve both sides with a side() guard.

Types

Json

nim
  Json* = object
    text*: string

aowl/src/aowlspt/server.nim:36

JsonObject

nim
  JsonObject* = object
    parts: seq[string]

aowl/src/aowlspt/server.nim:72

Handler

nim
  Handler* = proc (url, body, session: string): string

aowl/src/aowlspt/server.nim:106

DbValue

nim
  DbValue* = object
    ok*: bool
    raw*: string
    error*: string

aowl/src/aowlspt/server.nim:129

ConfigValue

nim
  ConfigValue* = object
    ok*: bool
    raw*: string
    faulted*: bool
      ## The config file exists and did not parse, so **no** setting read
      ## through this mod is real -- not just this one.
      ##
      ## Appended rather than folded into `ok`, and that is the compatibility
      ## story: `ok` is false in this case exactly as it was before, so every
      ## `asText(default)` and every `if v.ok` in every mod already written
      ## behaves identically. What is new is that a mod which cares can now
      ## ask, and the ones that should care are the ones whose defaults are a
      ## *decision* rather than a fallback -- a load order, an enabled set, a
      ## list of maps. `lastError()` carries the file and the fault.

aowl/src/aowlspt/server.nim:512

JsonArray

nim
  JsonArray* = object
    parts: seq[string]

aowl/src/aowlspt/server.nim:598

Stored

nim
  Stored* = object
    ok*: bool
    raw*: string
    error*: string
    missing*: bool
      ## True when there is no such key, false when there *is* one and it could
      ## not be read.
      ##
      ## The difference is not cosmetic and it is not a logging detail. A mod
      ## that keeps a player's progress reacts to "nothing saved yet" by making
      ## a new one, which is right on a first run and is the worst possible
      ## answer to "your save is on disk and the host could not read it": it
      ## writes a new character over a file that a copy out of the store's
      ## history would very likely have recovered. Anything that creates on a
      ## failed `load` must check this first and refuse loudly when it is
      ## false.

aowl/src/aowlspt/server.nim:687

Routines

raw

nim
proc raw*(s: string): Json

Text that is already JSON. The escape hatch, and the one place a mod can produce something malformed.

aowl/src/aowlspt/server.nim:39

escapeText

nim
proc escapeText*(s: string): string

aowl/src/aowlspt/server.nim:44

jstr

nim
proc jstr*(s: string): Json

aowl/src/aowlspt/server.nim:65

jint

nim
proc jint*(i: int): Json

aowl/src/aowlspt/server.nim:66

jfloat

nim
proc jfloat*(f: float): Json

aowl/src/aowlspt/server.nim:67

jbool

nim
proc jbool*(b: bool): Json

aowl/src/aowlspt/server.nim:68

jnull

nim
proc jnull*(): Json

aowl/src/aowlspt/server.nim:69

obj

nim
proc obj*(): JsonObject

aowl/src/aowlspt/server.nim:75

put

nim
proc put*(o: var JsonObject; key: string; value: Json)

aowl/src/aowlspt/server.nim:77

put

nim
proc put*(o: var JsonObject; key, value: string)

aowl/src/aowlspt/server.nim:80

put

nim
proc put*(o: var JsonObject; key: string; value: int)

aowl/src/aowlspt/server.nim:81

put

nim
proc put*(o: var JsonObject; key: string; value: float)

aowl/src/aowlspt/server.nim:82

put

nim
proc put*(o: var JsonObject; key: string; value: bool)

aowl/src/aowlspt/server.nim:83

len

nim
proc len*(o: JsonObject): int

aowl/src/aowlspt/server.nim:85

done

nim
proc done*(o: JsonObject): Json

aowl/src/aowlspt/server.nim:87

text

nim
proc text*(j: Json): string

aowl/src/aowlspt/server.nim:95

okJson

nim
proc okJson*(): string

aowl/src/aowlspt/server.nim:97

errJson

nim
proc errJson*(message: string): string

aowl/src/aowlspt/server.nim:98

serve

nim
proc serve*(url: string; handler: Handler): Status

An exact-match route.

aowl/src/aowlspt/server.nim:108

servePrefix

nim
proc servePrefix*(prefix: string; handler: Handler): Status

A prefix route: everything under prefix reaches this handler, which reads the rest out of the url it is given. The client's endpoints carry ids in the path, so this is how most real routes are written.

aowl/src/aowlspt/server.nim:112

pathAfter

nim
proc pathAfter*(url, prefix: string): string

The part of url past prefix, for a prefix route.

aowl/src/aowlspt/server.nim:118

dbRead

nim
proc dbRead*(path: string): DbValue

Reads a dotted path out of the loaded database.

aowl/src/aowlspt/server.nim:134

dbKeysReady

nim
proc dbKeysReady*(): bool

Whether this host can enumerate keys.

Not a size test, because this is not an ABI entry. The probe is the sigil with no path after it: a host that implements key enumeration refuses that with ErrBadArg -- there is nothing to enumerate -- and a host that does not walks a path called "?keys ", finds no root member by that name, and answers ErrNotFound. One call, no ABI surface, and no guessing.

aowlspt-backend answers true. The simulator and the in-game client host answer false, and will until somebody adds the sigil to them; a mod should treat false as "read the subtree instead", which is what it did before this existed.

aowl/src/aowlspt/server.nim:320

dbKeys

nim
proc dbKeys*(path: string; into: var seq[string]): Status

The immediate member names of the object at path, in document order.

The answer is the same seq[string] that keys(whole(text)) gives for the same object -- it is that call, over the array the host sends back -- so a mod can swap one for the other without changing the code that consumes it.

Four answers, and the difference between the last two is the point:

  • Ok -- into holds the names. An object with no members is Ok and an empty into.
  • ErrBadArg -- the path is an array or a scalar. A list has no keys, and it is not answered with ["0","1",...]: for a large table that is a huge reply to a question whose real form is "how long is it", and a caller that wanted indices already has the length from dbRead.
  • ErrNotFound -- there is no such path. That is not the same as an object with no members, and a caller that treats them alike will create something over the top of a table it failed to read.
  • ErrNotFound again, from a host that does not implement this at all: it looked for a root member literally named ?keys <path> and did not find one. The two are indistinguishable from this call alone, which is the price of not spending an ABI revision. dbKeysReady() separates them for one call, and dbKeysOrRead does it for you.

What it costs

Not nothing. The reply is sum(len(name)) + 3n bytes for n members -- about 2.7 MB for a table of 100k Mongo ids. That is roughly 200x smaller than reading the subtree, and it is still megabytes crossing the ABI and then being parsed into a seq[string] on this side. Ask for it once and keep the answer for as long as you would have kept the read.

It is a snapshot, not an index. Another mod may patch that object between this call and the dbRead of a child, and the order is the document's -- which a merge can change. Nobody promised the order.

aowl/src/aowlspt/server.nim:337

dbKeysOrRead

nim
proc dbKeysOrRead*(path: string; into: var seq[string]; scanned: var bool): Status

dbKeys, falling back to reading the whole subtree on a host that has no key enumeration -- and saying which it did, in scanned.

The fallback is the read a mod was doing before this existed, and it is the expensive one: locations on a stock import is 13.5 MB and on one with loose loot it is over half a gigabyte. scanned is true when that is what happened, so a mod can log what the answer cost instead of leaving a user wondering where the second went.

ErrNotFound from this means the path is genuinely not there: the host was asked twice, the second time for the subtree itself.

aowl/src/aowlspt/server.nim:380

asText

nim
proc asText*(v: DbValue): string

aowl/src/aowlspt/server.nim:410

asFloat

nim
proc asFloat*(v: DbValue; default: float = 0.0): float

aowl/src/aowlspt/server.nim:416

asInt

nim
proc asInt*(v: DbValue; default: int = 0): int

aowl/src/aowlspt/server.nim:449

dbWrite

nim
proc dbWrite*(path: string; patch: Json): Status

Merges patch into the object at path, creating the path if the database has never held it.

A merge, not a replace: two mods editing sibling fields of the same item must not clobber one another. That is the difference between a mod system and a pile of mods that happen to coexist.

aowl/src/aowlspt/server.nim:486

dbWrite

nim
proc dbWrite*(path, patchJson: string): Status

aowl/src/aowlspt/server.nim:495

dbWrite

nim
proc dbWrite*(path: string; patch: JsonObject): Status

The overload that was missing, so dbWrite(path, obj) had to be written dbWrite(path, done(obj)) -- and done(obj) at every call site is ceremony that says nothing.

aowl/src/aowlspt/server.nim:498

dbWrite

nim
proc dbWrite*(path: string; patch: JsonArray): Status

aowl/src/aowlspt/server.nim:504

setting

nim
proc setting*(key: string): ConfigValue

aowl/src/aowlspt/server.nim:527

configFaulted

nim
proc configFaulted*(): bool

Whether this mod's config.json is present and unreadable.

The question asked once, at load, by a mod that would rather refuse than run on defaults it did not choose. It reads the whole document, which is the cheapest way to ask -- an empty key is one round trip on every host.

aowl/src/aowlspt/server.nim:542

asText

nim
proc asText*(c: ConfigValue; default: string = ""): string

The quotes come off here, whichever host is underneath.

They did not, and the three hosts disagreed: the backend and the client host strip them on the way out, while the simulator -- then a C# program, since rewritten in nimony -- handed back the raw JSON literal. So setting("edition").asText() was standard on a server and "standard" in the simulator -- an empty string setting had length two, and a value used to build a database path addressed nothing on one side while working on the other. Two mods had already grown their own private unquoting helper by the time it was noticed, which is the shape of a missing library function.

Stripping here rather than in each host is what makes it true everywhere, including on a host written later.

aowl/src/aowlspt/server.nim:551

asFloat

nim
proc asFloat*(c: ConfigValue; default: float = 0.0): float

aowl/src/aowlspt/server.nim:572

asInt

nim
proc asInt*(c: ConfigValue; default: int = 0): int

aowl/src/aowlspt/server.nim:578

asBool

nim
proc asBool*(c: ConfigValue; default: bool = false): bool

aowl/src/aowlspt/server.nim:581

arr

nim
proc arr*(): JsonArray

aowl/src/aowlspt/server.nim:601

add

nim
proc add*(a: var JsonArray; value: Json)

aowl/src/aowlspt/server.nim:603

add

nim
proc add*(a: var JsonArray; value: string)

aowl/src/aowlspt/server.nim:604

add

nim
proc add*(a: var JsonArray; value: int)

aowl/src/aowlspt/server.nim:605

add

nim
proc add*(a: var JsonArray; value: float)

aowl/src/aowlspt/server.nim:606

add

nim
proc add*(a: var JsonArray; value: bool)

aowl/src/aowlspt/server.nim:607

len

nim
proc len*(a: JsonArray): int

aowl/src/aowlspt/server.nim:609

done

nim
proc done*(a: JsonArray): Json

aowl/src/aowlspt/server.nim:611

put

nim
proc put*(o: var JsonObject; key: string; value: JsonArray)

aowl/src/aowlspt/server.nim:619

put

nim
proc put*(o: var JsonObject; key: string; value: JsonObject)

aowl/src/aowlspt/server.nim:622

add

nim
proc add*(a: var JsonArray; value: JsonObject)

aowl/src/aowlspt/server.nim:625

add

nim
proc add*(a: var JsonArray; value: JsonArray)

aowl/src/aowlspt/server.nim:626

objOf

nim
proc objOf*(key: string; value: Json): JsonObject

A one-member object. The client's bodies are full of single-key wrappers ({"Health":{...}}, {"Counters":[]}), and writing each as three lines of builder buries the shape of the response in ceremony.

aowl/src/aowlspt/server.nim:628

objOf

nim
proc objOf*(key: string; value: JsonObject): JsonObject

aowl/src/aowlspt/server.nim:635

objOf

nim
proc objOf*(key: string; value: JsonArray): JsonObject

aowl/src/aowlspt/server.nim:636

objOf

nim
proc objOf*(key, value: string): JsonObject

aowl/src/aowlspt/server.nim:637

objOf

nim
proc objOf*(key: string; value: int): JsonObject

aowl/src/aowlspt/server.nim:638

objOf

nim
proc objOf*(key: string; value: bool): JsonObject

aowl/src/aowlspt/server.nim:639

emptyArray

nim
proc emptyArray*(): Json

aowl/src/aowlspt/server.nim:641

emptyObject

nim
proc emptyObject*(): Json

aowl/src/aowlspt/server.nim:642

envelope

nim
proc envelope*(data: Json): string

{"err":0,"errmsg":null,"data":...} -- what the client expects from every /client/* route.

aowl/src/aowlspt/server.nim:653

envelope

nim
proc envelope*(dataText: string): string

aowl/src/aowlspt/server.nim:658

envelope

nim
proc envelope*(o: JsonObject): string

aowl/src/aowlspt/server.nim:661

envelope

nim
proc envelope*(a: JsonArray): string

aowl/src/aowlspt/server.nim:662

envelopeNull

nim
proc envelopeNull*(): string

A successful response carrying nothing. Distinct from an empty object: some endpoints are checked for data === null by the client.

aowl/src/aowlspt/server.nim:664

failure

nim
proc failure*(code: int; message: string): string

The error shape. code is the client's own error number; zero would say success, so it is refused rather than sent.

aowl/src/aowlspt/server.nim:669

save

nim
proc save*(key: string; value: Json): Status

Write this mod's value at key, durably. Keys are flat and [A-Za-z0-9._-]; anything else is refused rather than mangled into a filename that might collide with another key.

aowl/src/aowlspt/server.nim:704

save

nim
proc save*(key, value: string): Status

aowl/src/aowlspt/server.nim:710

save

nim
proc save*(key: string; value: JsonObject): Status

aowl/src/aowlspt/server.nim:713

load

nim
proc load*(key: string): Stored

Read this mod's value at key. ok is false and error says why when it was never written -- which is the normal first-run case, not a fault -- and missing separates that case from a value that is there and unreadable.

aowl/src/aowlspt/server.nim:715

saved

nim
proc saved*(key: string): bool

aowl/src/aowlspt/server.nim:727

savedKeys

nim
proc savedKeys*(prefix: string = ""): seq[string]

This mod's keys beginning with prefix, so "every profile" is expressible without the mod knowing where the host put them.

aowl/src/aowlspt/server.nim:731

broadcast

nim
proc broadcast*(name: string; payload: Json): Status

Tell every other mod something happened. The emitter does not receive its own event: a mod that owns an event usually also subscribes to it, and delivering it back would be a loop nobody wrote.

aowl/src/aowlspt/server.nim:755

broadcast

nim
proc broadcast*(name, payload: string): Status

aowl/src/aowlspt/server.nim:761

broadcast

nim
proc broadcast*(name: string; payload: JsonObject): Status

aowl/src/aowlspt/server.nim:762

broadcast

nim
proc broadcast*(name: string; payload: JsonArray): Status

aowl/src/aowlspt/server.nim:764

onEvent

nim
proc onEvent*(name: string; handler: EventHandler): Status

aowl/src/aowlspt/server.nim:767

afterMs

nim
proc afterMs*(delayMs: int; handler: TickHandler): Status

Run handler once, delayMs from now, from the server's own loop rather than from a request thread.

aowl/src/aowlspt/server.nim:770

everyMs

nim
proc everyMs*(intervalMs: int; handler: TickHandler): Status

Run handler on a repeating timer, off the request threads. What a periodic sweep -- expiring offers, posting insurance returns -- is for.

aowl/src/aowlspt/server.nim:775

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