Appearance
aowlspt/game
Source: aowl/src/aowlspt/game.nim — 914 lines.
The high-level client API — what writing a mod should actually feel like.
Underneath, everything reaches the game the same way: resolve a type by name, call a method by name, patch a method to be told when it runs. That is a complete interface and a miserable one to write against:
var h: Handle = 0 discard resolve("EFT.Player", h) var res = "" discard call("EFT.Player::Heal", "[50]", res)
This module is the same three things with the boilerplate gone:
var Player = gameType("EFT.Player") discard Player.invoke("Heal", 50)
proc onDead(target: string) = info "someone died"
discard hook("EFT.Player::OnDead", onDead)
Two things it does beyond tidying.
It caches. A GameType resolves once, on first use, and holds the handle. Resolving walks every loaded assembly, so doing it per call in a tick loop is the difference between a mod you can ship and one that hitches.
It waits. The game's own assemblies are not loaded when a mod starts, so a type resolved at load time is a false negative. A GameType is declared eagerly and resolved lazily, which means a mod can name its types at the top of the file the way it wants to and still be correct.
Types
ValueKind
nim
ValueKind* = enum
vkInt, vkFloat, vkBool, vkString, vkObjectaowl/src/aowlspt/game.nim:37
Value
nim
Value* = object
kind*: ValueKind
i*: int64
f*: float
s*: string
h*: HandleOne argument. A variant rather than a generic so that a call site can mix types: invoke("Move", 1, 2.5, true).
aowl/src/aowlspt/game.nim:40
CallResult
nim
CallResult* = object
ok*: bool
raw*: string
error*: stringDeliberately not a bare string. A call can fail, and a mod that reads the result without noticing gets "" -- which is a plausible value for plenty of methods, so the failure would look like data.
aowl/src/aowlspt/game.nim:91
GameType
nim
GameType* = object
name*: string
handle*: Handle
tried*: boolA type in the running game, resolved on first use.
Declared with gameType at the top of a mod and used whenever; the resolution happens the first time it is needed, which is the only point at which the game is guaranteed to have loaded its assemblies.
aowl/src/aowlspt/game.nim:196
GameObj
nim
GameObj* = object
handle*: Handle
typeName*: string
ok*: boolA live object in the game. ok is false for "the call did not return one", which is an ordinary answer -- get_Instance before the world exists returns null, and that is not a failure.
aowl/src/aowlspt/game.nim:316
HookHandler
nim
HookHandler* = proc (target: string)aowl/src/aowlspt/game.nim:434
ArgHookHandler
nim
ArgHookHandler* = proc (target, args: string): HookResultA hook that is told what the method was called with, and may answer.
args is a JSON array in the same shape call takes, so it is read with aowlspt/json exactly like a request body on the server side. A reference argument arrives as {"handle":n,...} -- the live object the game was about to act on, which is usually the reason to want the arguments at all.
aowl/src/aowlspt/game.nim:436
HookResult
nim
HookResult* = object
stop*: bool
replaceWith*: stringWhat a hook decided. stop suppresses the original; replaceWith is the value the caller gets instead, as JSON, and must match the method's declared return type or the suppression is refused.
aowl/src/aowlspt/game.nim:445
WhenReady
nim
WhenReady* = object
probe*: GameType
fired*: boolA one-shot that fires the first tick after a type becomes resolvable.
This exists because "the game is up" is not an event the client host can observe -- assemblies arrive when they arrive. Polling for a type that only exists once the world does is the honest way to wait for it, and doing it once here beats every mod inventing its own counter.
aowl/src/aowlspt/game.nim:890
Routines
v
nim
proc v*(x: int): ValueNo separate int64 overload: nimony's int is 64-bit, so the two would be the same signature and every call site would be ambiguous.
aowl/src/aowlspt/game.nim:49
v
nim
proc v*(x: float): Valueaowl/src/aowlspt/game.nim:53
v
nim
proc v*(x: bool): Valueaowl/src/aowlspt/game.nim:55
v
nim
proc v*(x: string): Valueaowl/src/aowlspt/game.nim:57
toJson
nim
proc toJson*(v: Value): stringaowl/src/aowlspt/game.nim:71
argsJson
nim
proc argsJson*(args: openArray[Value]): stringaowl/src/aowlspt/game.nim:79
failed
nim
proc failed*(r: CallResult): boolaowl/src/aowlspt/game.nim:99
asText
nim
proc asText*(r: CallResult): stringThe result as text, with the JSON quoting removed if it was a string.
aowl/src/aowlspt/game.nim:119
asInt
nim
proc asInt*(r: CallResult; default: int = 0): intaowl/src/aowlspt/game.nim:123
asFloat
nim
proc asFloat*(r: CallResult; default: float = 0.0): floataowl/src/aowlspt/game.nim:148
asBool
nim
proc asBool*(r: CallResult; default: bool = false): boolaowl/src/aowlspt/game.nim:181
isNull
nim
proc isNull*(r: CallResult): boolaowl/src/aowlspt/game.nim:188
gameType
nim
template gameType*(typeName: string): GameTypeNames a type. Does not resolve it yet — see the note above.
A template rather than a proc, and that is not a style choice. In a nimony --app:lib build, a global initialised by a call is never initialised: literal initialisers are folded at compile time, anything needing runtime evaluation is skipped, and the global is left zeroed. A mod written the obvious way —
var Player = gameType("EFT.Player")
would therefore hold an empty name and fail every lookup, silently. As a template this expands to an object literal at the declaration, which does get folded, so the obvious way is also the correct one.
aowl/src/aowlspt/game.nim:206
resolveNow
nim
proc resolveNow*(t: var GameType): boolResolves if it has not been resolved. Returns whether the type is available; a failure is retried on the next call, because "not yet" and "never" are the same answer early in a run and only one of them is final.
aowl/src/aowlspt/game.nim:222
available
nim
proc available*(t: var GameType): boolaowl/src/aowlspt/game.nim:242
invoke
nim
proc invoke*(t: var GameType; member: string; args: openArray[Value] = []): CallResultCalls a static member.
aowl/src/aowlspt/game.nim:244
invoke
nim
proc invoke*(t: var GameType; member: string; a: Value): CallResultaowl/src/aowlspt/game.nim:261
invoke
nim
proc invoke*(t: var GameType; member: string; a, b: Value): CallResultaowl/src/aowlspt/game.nim:263
invoke
nim
proc invoke*(t: var GameType; member: string; a, b, c: Value): CallResultaowl/src/aowlspt/game.nim:265
invoke
nim
proc invoke*(t: var GameType; member: string; a: int): CallResultaowl/src/aowlspt/game.nim:268
invoke
nim
proc invoke*(t: var GameType; member: string; a: float): CallResultaowl/src/aowlspt/game.nim:270
invoke
nim
proc invoke*(t: var GameType; member: string; a: string): CallResultaowl/src/aowlspt/game.nim:272
invoke
nim
proc invoke*(t: var GameType; member: string; a: bool): CallResultaowl/src/aowlspt/game.nim:274
invoke
nim
proc invoke*(t: var GameType; member: string; a, b: int): CallResultaowl/src/aowlspt/game.nim:276
field
nim
proc field*(t: var GameType; name: string): CallResultA static field.
aowl/src/aowlspt/game.nim:279
setField
nim
proc setField*(t: var GameType; name: string; value: Value): CallResultaowl/src/aowlspt/game.nim:283
get
nim
proc get*(t: var GameType; property: string): CallResultA property getter. In IL2CPP a C# property compiles to get_Name, so this is the same call with the convention applied — which is worth wrapping, because forgetting the prefix produces "no such method" and no hint as to why.
aowl/src/aowlspt/game.nim:286
set
nim
proc set*(t: var GameType; property: string; value: Value): CallResultaowl/src/aowlspt/game.nim:293
noObject
nim
proc noObject*(): GameObjaowl/src/aowlspt/game.nim:324
asObject
nim
proc asObject*(r: CallResult): GameObjThe object a call returned, if it returned one.
aowl/src/aowlspt/game.nim:326
isObject
nim
proc isObject*(r: CallResult): boolaowl/src/aowlspt/game.nim:352
invoke
nim
proc invoke*(o: GameObj; member: string; args: openArray[Value] = []): CallResultCalls a member on this object.
aowl/src/aowlspt/game.nim:356
invoke
nim
proc invoke*(o: GameObj; member: string; a: Value): CallResultaowl/src/aowlspt/game.nim:371
invoke
nim
proc invoke*(o: GameObj; member: string; a, b: Value): CallResultaowl/src/aowlspt/game.nim:373
invoke
nim
proc invoke*(o: GameObj; member: string; a, b, c: Value): CallResultaowl/src/aowlspt/game.nim:375
invoke
nim
proc invoke*(o: GameObj; member: string; a: int): CallResultaowl/src/aowlspt/game.nim:377
invoke
nim
proc invoke*(o: GameObj; member: string; a: float): CallResultaowl/src/aowlspt/game.nim:379
invoke
nim
proc invoke*(o: GameObj; member: string; a: string): CallResultaowl/src/aowlspt/game.nim:381
invoke
nim
proc invoke*(o: GameObj; member: string; a: bool): CallResultaowl/src/aowlspt/game.nim:383
get
nim
proc get*(o: GameObj; property: string): CallResultaowl/src/aowlspt/game.nim:386
set
nim
proc set*(o: GameObj; property: string; value: Value): CallResultaowl/src/aowlspt/game.nim:389
field
nim
proc field*(o: GameObj; name: string): CallResultA field, not a property.
get/set reach a C# property, which compiles to get_X/set_X. A field has no method behind it and is unreachable that way -- and a great deal of what a mod wants to read on this game is a field, often a private one. Reaching it is not a trick: reflection is what the runtime provides for it.
aowl/src/aowlspt/game.nim:392
setField
nim
proc setField*(o: GameObj; name: string; value: Value): CallResultaowl/src/aowlspt/game.nim:401
child
nim
proc child*(o: GameObj; property: string): GameObjThe object a property returns -- player.child("Physical"). Walking a chain of these is how a mod gets from a world to a weapon.
aowl/src/aowlspt/game.nim:404
alive
nim
proc alive*(o: GameObj): boolWhether the object is still there. A handle to something the collector has taken answers false rather than crashing when it is next used, and this is how a mod can ask before it tries.
aowl/src/aowlspt/game.nim:409
release
nim
proc release*(o: GameObj)Lets go. A handle held for the session is a pinned object.
aowl/src/aowlspt/game.nim:418
instanceOf
nim
proc instanceOf*(t: var GameType; property: string = "Instance"): GameObjThe singleton behind a type -- GameWorld, CameraManager and most of the game's managers expose exactly this. Separated out because reaching for the singleton is the first thing almost every client mod does.
aowl/src/aowlspt/game.nim:423
carryOn
nim
func carryOn*(): HookResultaowl/src/aowlspt/game.nim:452
stopWith
nim
func stopWith*(json: string): HookResultSuppress the original and return this value instead.
aowl/src/aowlspt/game.nim:454
stopVoid
nim
func stopVoid*(): HookResultSuppress a method that returns nothing.
aowl/src/aowlspt/game.nim:458
replaceResult
nim
func replaceResult*(json: string): HookResultFor a hookReturn handler: hand the caller this instead of what the original returned.
The same HookResult a prefix uses, and the same field, because it is the same single decision -- what the caller ends up with. Named separately because "stop" is the wrong word once the original has already run, and a postfix handler reading stopWith would reasonably wonder what it was stopping.
aowl/src/aowlspt/game.nim:462
keepResult
nim
func keepResult*(): HookResultFor a hookReturn handler: leave the original's answer alone.
Hooks are kept as two parallel sequences and dispatched by matching the target name the host hands back.
The obvious design — capture the index in a closure per hook — is not available: nimony will not let a nested proc touch its enclosing scope without being made a closure, and the ABI wants a plain function pointer. Matching on the name keeps every callback a top-level proc.
The scan is linear in the number of hooks, on the path inside a patched game method. That is fine for the handful of hooks a mod installs and would not be for hundreds; if it ever is, the fix is a sorted table here, not a change to the ABI.
aowl/src/aowlspt/game.nim:473
hookReturn
nim
proc hookReturn*(target: string; handler: ArgHookHandler; withArgs = true): StatusBe told what a game method returned, and be able to change it.
This is Harmony's postfix, and it is the shape a mod needs whenever the answer depends on the original's own answer -- scaling a sensitivity, clamping a speed, filtering a list the game just built. The prefix alternative is stopWith, which means reimplementing the method, and a reimplementation of a method you cannot read is a guess.
What the handler is given. The hookArgs payload with one more member:
{"this":{"handle":3,...},"args":[4.0],"result":6.0} {"result":6.0} -- withArgs = false
hookResult(payload) reads it as a CallResult, so asFloat(), asInt(), asBool() and asObject() all work on it. Return replaceResult(...) to change it, keepResult() to leave it alone.
result is always present, whether or not arguments were asked for: it is one register read and it is the reason the hook exists. The arguments stay opt-in, and they are the values the method was entered with -- the registers saved on the way in, which is the only place they still are once the original has run.
Two methods cannot carry one, and the host says which. A method returning a value type wider than eight bytes returns it through a buffer the host cannot read the layout of, so a postfix there could neither report the result nor replace it. And a method whose compiled call uses more than four register slots -- its declared arguments, this, and IL2CPP's trailing MethodInfo* -- has stack arguments, which a postfix cannot pass on, because it must call the original rather than jump to it. Both are refused at registration with a sentence naming which; check the status and read lastError().
An exception thrown out of the original does not reach the handler. A postfix means "after it returned", not "after it finished".
aowl/src/aowlspt/game.nim:545
hookArgs
nim
proc hookArgs*(target: string; handler: ArgHookHandler): StatusBe told whenever a game method runs, with its arguments, and be able to stop it.
This is the full Harmony prefix: read what the method was called with, and return stopWith(...) to keep it from running at all. The cost over hook is one JSON payload built per call, which is why it is a separate function rather than the default -- a hook on a method the game calls every frame for every bot should be a hook.
What the handler is given. An object, not an array:
{"this":{"handle":3,"type":"EFT.Player"},"argc":2,"stackArgs":0, "args":[1,2.5]} {"this":null,"argc":0,"stackArgs":0,"args":[]} -- a static method
this is Harmony's __instance, and it is separate from args on purpose: it is not a declared parameter, and folding it into the array would make every argument index depend on whether the method happens to be static -- a difference that shows up as reading the wrong argument, far from the hook. A static method's this is null rather than missing, so "there is no instance" cannot be read as "the host did not say".
this and any reference argument arrive as handles, and they are valid only until the handler returns. The host frees them at that point: a handler on a per-frame method would otherwise pin one object per call, and a contract that depends on a mod remembering to release on that path is a leak with extra steps. Read what you need inside the handler; do not store the handle.
An enum argument arrives as its integer, and a value type too large for a register arrives as {"valueType":"..."} -- named and refused, because it is passed by hidden pointer and there is nothing in the register to report. A type the runtime cannot classify is {"type":"..."}.
An argument past the register window is named, not dropped. Win64 passes four arguments in registers and the thunk saves those four; on an instance method this is one of them, so a method with four declared parameters has its fourth on the stack and a static one has its fifth. There is nothing in the frame to report for it and nothing here can invent it -- but the payload says so rather than falling silent:
sits in that slot, so argument n is always declared parameter n, and "the host did not report this" can never be read as "this was empty". argc is the declared parameter count and stackArgs is how many of them are in that state, so a handler that wants nothing to do with a truncated firing can check one number before it reads anything.
This was the last silent wrong answer on this path. The array used to stop early, so a handler reading argument 3 of a four-argument instance method got the same empty answer an empty argument would give it, and went on to decide something on a value the game never supplied.
aowl/src/aowlspt/game.nim:589
hookTyped
nim
proc hookTyped*(target: string; handler: TypedPatchHandler): StatusA prefix hook that is given the registers rather than a description of them, and may suppress the original.
The handler receives a PatchFrame:
nim
proc onTilt(f: PatchFrame): TypedResult =
let obj = cast[Il2CppPtr](f.selfPointer()) # `this`, no handle, no call
var ok = false
let a = f.argFloat(0, ok) # declared parameter 0
if not ok: return frameContinue()
...
if f.setResultVoid(): frameReplace() else: frameContinue()selfPointer is the address thisPointer(payload) costs a JSON build, a GC handle and a pointerOf round trip to produce; here it is the register the method was entered with. The same lifetime rule applies to it and to every argPointer: they are addresses, the collector moves objects, use them inside the handler. The frame itself is checked -- reading one after the handler has returned is refused and frameWhyText() says so.
Arguments past the fourth register position report akStack and cannot be read; they are named rather than omitted, so argument n is always declared parameter n.
ErrUnsupported on a host older than revision 4 -- test typedPatchesReady() and fall back to hookArgs, which is the point of keeping both.
aowl/src/aowlspt/game.nim:675
hookReturnTyped
nim
proc hookReturnTyped*(target: string; handler: TypedPatchHandler): StatusThe postfix half: the original has run, and resultFloat, resultInt and resultPointer read what it produced -- out of the register the declared return type says it is in. setResultFloat and friends change it, and frameReplace() makes the change stick.
Reading a result on a prefix frame is refused rather than answered, because the original has not run and scaling a number that does not exist yet is worse than not scaling one.
The same two method shapes the host refuses for a JSON postfix are refused here, for the same reasons and with the same sentences: a value-type return wider than a register, and a compiled call needing more than four register slots. See hookReturn.
aowl/src/aowlspt/game.nim:706
memberRaw
nim
proc memberRaw*(payload: string; key: string): stringThe raw JSON of one top-level member of a hook payload, or "" if absent.
Only top-level members: the scan starts at depth 0 and a "result" that appeared inside a nested object would be skipped rather than mistaken for the one being asked for. Worth the extra state, because a game string in an argument can contain anything at all.
aowl/src/aowlspt/game.nim:732
handleIn
nim
proc handleIn*(json: string): HandleThe handle in a {"handle":n,"type":"..."}, or 0.
aowl/src/aowlspt/game.nim:806
thisHandle
nim
proc thisHandle*(payload: string): HandleThe handle for __instance, or 0 for a static method.
aowl/src/aowlspt/game.nim:822
thisPointer
nim
proc thisPointer*(payload: string): uint64The address of the object a hook fired for, for the fast path.
This is the one line that turns a hook from a notification into a place to do work. hookArgs gives this as a handle, and everything reached through a handle goes through call -- roughly a microsecond, which on a method the game runs per entity per frame is a frame tax rather than a feature. The address is what aowlspt/fast's bindOnObject, callFloat and readFloat take, and those are tens of nanoseconds.
Valid only while the handler runs. The host reclaims the handle when the handler returns, and asking afterwards is refused rather than answered with a stale address -- but nothing can refuse an address the mod wrote down and used next frame, because by then it is just a number. Read what you need inside the handler. pinHandle is the way to keep an object, and says what pinning costs.
Zero means there is no address: a static method, a host without a managed heap, or a handler that has already returned. lastError() says which.
aowl/src/aowlspt/game.nim:826
hookResult
nim
proc hookResult*(payload: string): CallResultWhat the original returned, for a postfix handler.
A CallResult rather than a bare string for the same reason invoke returns one: asFloat() on a missing member would answer 0.0, and 0.0 is a plausible sensitivity. ok is false when the payload carries no result, which is what a prefix handler's payload looks like -- so a handler registered the wrong way round finds out here rather than scaling a zero.
aowl/src/aowlspt/game.nim:853
hook
nim
proc hook*(target: string; handler: HookHandler): StatusBe told whenever a game method runs.
The cheap one: it costs a name comparison and nothing else, and the handler is told only that the method fired. Use hookArgs when you need to know what it was called with or want to stop it.
aowl/src/aowlspt/game.nim:869
whenReady
nim
template whenReady*(typeName: string): WhenReadyA template for the same reason gameType is one.
aowl/src/aowlspt/game.nim:900
ready
nim
proc ready*(w: var WhenReady): boolCall from onUpdate. True exactly once, on the first tick where the type resolves.
aowl/src/aowlspt/game.nim:905

