Invoking Functions on Distributed Game Objects
Tags: webassembly, mud, rust, wascc
Published: 2020-05-29 15:52:30

This is the second post in my series on developing a modern MUD using [waSCC](https://wascc.dev) and WebAssembly; taking old-school monolithic concepts from the MUD worlds of yore and migrating them to the new, cloud-native, wasm-based future. 

I've found that writing posts like this _before_ coding helps me organize my thoughts and saves me some time when I get to the IDE. I like to call this method _blog-ahead compilation_.

# LPC Function Calls

In LPC, there are countless reasons why a block of code might need to invoke a function on another game object. This could be in combat to reduce hitpoints, it could be the code that changes the state of a set of armor from `unworn` to `worn`, a sword from `sheathed` to `wielded`, etc. We need to make function calls to buy things from stores, to send text messages to players, to flip quest bits when a player places an ancient relic in the base of a fountain. The list goes on. 

In the world of the single-process monolith, function invocation is pretty simple, and in LPC it looks something like this:

```c
my_object->function(parameters);
call_other(my_object, "function", parameters);
```

In most variants of LPC that I remember, you were able to make function calls in either an OOP-like (the "arrow") fashion or the regular C style syntax.

What's important to notice about this code is that the variable `my_object` is an instance variable just like any other, and is sitting in memory directly in the monolithic process.

If we need to invoke a function call on a game object in a world where game objects are _spread across a cluster of driver processes_, then we need to re-think things quite a bit.

# Managing Distributed Game Objects

One possible way to deal with distributing game objects is to just simply distribute instances across a grid and then provide some kind of RPC plumbing to allow remote invocation. This means that for every single game object active in the virtual world, there is an instance variable consuming a stack pointer and heap memory somewhere in a process in the cluster. 

This feels inefficient to me, like we're reaching for the safety blanket of old techniques because that's what we're familiar with. The only reason we want to manage game object instances like traditional language primitive objects is because these things have _state_ exposed through simple fields or properties. In the cloud, we don't want stateful, long-running variables.

Further, if each of these long-running stateful variables is actually a WebAssembly interpreter (since our user code is all wasm actors), that's a ton of duplicated overhead per game object. If we get to hundreds of thousands of game objects, this will absolutely not scale well.

Instead, what we want to do is load the _prototype_ of the game object, and then provide per-instance data as parameters to each function call. This means that the prototype remains stateless, and every function call is able to be executed by the prototype (the actor wasm code). 

With this approach, we can have a single actor like `std/sword` handle all function invocations for all of the thousands of instances of swords in the game. If the path through `/std/sword` starts to get too "hot", then we can simply spin up more copies of this prototype on different nodes in the cluster and distribute load across them.

If the function call needs state for processing, it'll query it from a K/V store or cache or get it as part of the call (those are optimization details I can worry about later).

# Making Distributed Game Object Function Calls

Recall that an instance of an object in an LPMud is a combination of its prototype and some form of unique instance identifier. Our distributed mudlib capability provider can keep track of the mapping between what the game calls a prototype (e.g. `std/sword` or `areas/kevin/dragon1`) and what waSCC calls an actor. waSCC uses URL-friendly encodings of [ed25519](https://ed25519.cr.yp.to/) keys to uniquely and globally identify actors. Our mudlib will keep a mapping between the game prototype name and the public key of an actor, so that it can hide the complexity of those public keys from developers and convert a content-developer-friendly function call like this:

```rust
let sword: &dyn Sword = get_object("/std/sword#5672458").unwrap();
sword.wield();
```

into code like the following that is safely hidden from game content creators:

```rust
fn get_object(obid: &str) -> &dyn GameObject {
    let (public_key, instance) = obid.pk(), obid.instance()); // a value like ("MB4OLDIC3TCZ4Q4TGGOVAZC43VXFE2JQVRAXQMQFXUCREOOFEKOKZTY2", "562478")
    &RootGameObject::new(public_key, instance)
// Could do &RootGameObject::from_objectid(obid) as a shortcut
}
. . .

sword.call(this_interactive(), "wield", []); // This is an RPC efun
```

In the above code, the `call` function would then forward an invocation up to the mudlib capability provider (running in the waSCC host process) and in turn execute the appropriate network code to find the prototype, build context, invoke the function, and return the result.

## A Note on Polymorphism

I don't want to support inheritance in this MUD because wasm modules (actors, game objects) are intended to be treated as freestanding, portable units of compute. Trying to build inheritance into them would be an affront to WebAssembly, in my opinion.

But, I do want to be able to treat things similarly and allow code to be shared. How do we deal with the fact that all weapons can be wielded? What if I want to make a scenario where a chair can be wielded or a cardboard box can be worn as armor? **_That's a topic for an upcoming blog post_**, because translating the flexibility of LPC's multiple inheritance hierarchies into freestanding WebAssembly modules without forcing a ton of code duplication is no small task. 

# Managing Per-Call Context

When we invoke the `wield` function on a sword, we need some context in order to properly deal with that call. This context is especially important because the actor in which this function resides is _stateless_ by design. 

We need to know _who_ wielded the sword. We also needed to know _which_ sword this is, and the values of all of the variables currently set on that sword.

An absolutely **crucial** aspect of LPC that I enjoyed so long ago is that wizards were able to be productive and create programmatic content without needing to know the underlying details of how the MUD and game driver worked. In my case, I spent months building an entire area with no knowledge of the intricate interplay between the mudlib and game driver, or what the difference was between library functions and external functions.

If this actor-based wasm MUD is going to be successful, we need to be able to hide the irrelevant details so content creators can easily do their jobs.

To keep the API clean, I don't want wizards to have to code things like `context.get_something("foo")` over and over again, nor do I want them to know the details of how we're representing context internally. Instead, the context will be transparently set in a variable hidden in the mudlib actor API prior to the invocation of a function. If a function call needs context, then it can obtain it from the underlying variables.

Let's take a look at some sample Rust wasm code that might be found in `/std/sword.wasm`:
```rust
public_functions! { wield };

fn wield() -> Result<()> {
    let me: &dyn Sword = this_object();
    let ti = this_interactive();
    write(ti, "You wield the sword. Is it glowing??")?;
    say(ti.third_person() + 
      " wields the sword. It begins to glow!\n")?;
    me.set_wielded(true);
    Ok(())
}
```
This _looks_ like super simple code, but there's a "metric crap-ton" of complicated things happening, and that's **_as the world should be_**. 

Feature developers should write simple logic, and boilerplate and plumbing belongs elsewhere.

Let's take it apart step by step:

* `this_interactive()` returns a `&dyn Interactive`, a game object reference that supports core functionality, plus _interactive_ functionality (it can send and receive text). All of this is done inside the wasm module boundary through the mudlib API by accessing the thread-safe (actors are single-threaded) context that was hydrated prior to invocation.
* `write()` sends a text message to the interactive thing (which could be a player or a game-controlled NPC). This creates a remote invocation request that will bubble out across the wasm boundary, through the mudlib _capability provider_, across the network to the prototype for the current interactive object (e.g. `/std/player`). Context is built remotely prior to invoking the `say` function in the player prototype (which is a waSCC-hosted actor).
* `say` sends a text message to every object listed in the inventory of the room the wielder is standing in 
* `this_object()` is essentially the "self" reference, and is a `&dyn GameObject` which is in turn automatically cast to `&dyn Sword` via supertrait casting.
* `set_wielded` makes a call to the host's key-value capability to set the `wielded` value of actor `Mxxxxx....x` instance `12345` in the distributed cache.

All of these functions could fail, which can in turn cause `wield` to fail, which would eventually send a message to the player that typed `wield sword`, which would ultimately be delivered via socket over the telnet connection (or web socket to the browser client).

# Summary

Building an elastically scalable cloud-native online multiplayer world is a _difficult_ thing. However, my hypothesis is that as developers building business functionality day in and day out in the cloud, we deal with a lot of the same problems. 

If we remain vigilant about hiding complexity from feature developers (in the case of our MUD, _wizards writing actors_), then we can make the act of creating clean, crisp, secure, functional code an actual **joy**, and maybe writing code won't suck as much as it does today.

p.s. all of the code in this post is purely hypothetical, and will likely change radically once I sit down to code some of this up.