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