Lisp Game Jam - "Fantasary" - prototyping an object world (& TCP netlayer)

-- Wed 07 June 2023

Fantasary screenshot in cool-retro-term

Spritely participated in the Spring Lisp Game Jam 2023 last week. We submitted not one, but two, entries. This post is about one of them: Fantasary.

Fantasary is a prototype textual virtual world with interactive objects. Users can move between multiple virtual rooms and interact with the objects (other humans and bots) in those rooms. We used an ncurses-based user interface and cool-retro-term to make it look pretty. The result resembles an IRC chat application but under the hood it is built using object capabilities. Building this prototype under the time constraints of a game jam served multiple purposes:

  • Testing out a new, prototype TCP+TLS netlayer for OCapN.
  • Experimenting with petnames.
  • Experimenting with the unum pattern to model objects in a distributed virtual world.
  • Getting more experience building and releasing applications with Goblins AKA dogfooding.

A new, prototype netlayer

Test driving a new OCapN netlayer was the primary motivation for working on Fantasary. As of today, Goblins ships with a Tor Onion services netlayer. This netlayer has its uses and was relatively simple to implement, but there are significant downsides:

  • It requires running a Tor daemon configured in a particular way.
  • Connecting to a Tor onion service for the first time tends to be slow.
  • There's a lot of lag due to the overhead inherent in onion routing.

It's great to have Tor as an available option, but we'd also like to provide something simpler and faster. To that end, we've written a prototype netlayer, internally called the "Simple TCP" netlayer, that operates over standard TCP sockets and uses TLS to encrypt the data that passes through. The "simple" part refers to using widespread networking protocols to achieve peer-to-peer connections to the extent possible, without being part of a true peer-to-peer network such as one built with libp2p. This prototype seems to be working pretty well so far. The 3-day game jam rating period is still ongoing at the time of writing and people who have never used Goblins before are able to connect to our Fantasary server and play around, and our server hasn't crashed. That said, there are several caveats to using this netlayer at the moment, and we had to build Fantasary carefully to avoid those shortcomings.

For starters, most people who try Fantasary will be behind a NAT gateway and a firewall on their home network, which makes establishing peer-to-peer connections very difficult. We did not solve this problem before writing Fantasary, so we made sure that all network communication was between a client and our public server, never peer-to-peer. In OCapN terms, this meant no third-party handoffs of object references. Looking ahead, we are exploring using public STUN servers to do the necessary NAT hole punching so that clients can be connected directly, much like WebRTC-based chat systems (among others.)

Another major problem is bootstrapping clients onto OCapN. In order to connect to an application using OCapN from the outside world, you need a sturdyref URI. At the moment, we are doing the simplest possible thing and using an encoded form of the X.509 certificate as the host identifier, and rely upon hints to point a client towards the machine that owns that certificate. Here's an abridged example:

ocapn://<encoded-cert>.simple-tcp/s/<swiss-num>?host=example.com&port=8888

It would be unreasonable to print a real world example is because the encoded cert is a huge number of characters. The sturdyref shipped in Fantasary is 4124 characters long! This makes the URIs difficult to share and thus poses a usability problem. We are looking to fix this by using a hash of the certificate as the host identifier and introducing a step prior to the TLS handshake where the server sends over the certificate and the client confirms that it hashes to the expected value.

As an aside, the URI above may seem inverted from typical URIs you are used to because the host name appears in the query string. In OCapN, the essential identifier of a machine is its public key, not its name or location. We reserve the query string as a place for "hints" that can help clients establish a connection to the machine with the given identity. Here we are using a hint that relies upon DNS, but there could be other ways to point a client towards the machine's location.

The last major problem is the X.509 certificate system being a poor fit for the distributed world we are building. We are using TLS because of its ubiquity, not because we love it. X.509 certificates include all sorts of data that is for the centralized certificate authority system. All we care about in Goblins is that we have a public/private key pair that we can use to establish an encrypted channel. In fact, the netlayer does not rely on CA certificate chains (the stuff you'd typically find in /etc/ssl) at all. Because the OCapN URI has the certificate for the remote machine encoded in it, the associated TLS session inserts that certificate into its trust store and nothing else. Also, we didn't want to make users figure out how to generate their own certificates as that would be a major barrier to entry. Instead, we shelled out to OpenSSL to automatically generate one, filling in all text fields with hardcoded values to satisfy the generator. Moving forward, we'd like to preserve automatic certificate generation but want to generate them through library calls within the program rather than spawning a subprocess.

Petnames

A distributed world doesn't rely upon centralized naming authorities like domain name registrars, so we pushed ourselves to keep the server code free of name registration behavior. Instead, users say what their self-proclaimed name is, and other users are free to replace that name with a "petname", a name local to their client that only they will see. It works much like how a contact list on a cell phone associates local names to phone numbers. To distinguish self-proclaimed names from petnames, we introduced a little bit of new syntax into the client interface. A self-proclaimed name appears prefixed with ? like ?Alice. A local petname appears prefixed with @ like @AuntAlice. Because there's no central registry of names, self-proclaimed name collisions are both possible and not a big deal. When clients see many objects with the same self-proclaimed name, they add an integer prefix to distinguish them. If there were three users named Goblin in the room, they would show up as ?Goblin, ?Goblin+1, and ?Goblin+2.

Fun fact: Fantasary started out with centralized names, and when we decided to expand scope to incorporate petnames it actually simplified the code for the objects on the server because they no longer needed to care about names at all, only object identity.

If you'd like to read more about petnames, check out our blog post from last year about two papers we wrote on the subject.

The unum pattern

The unum pattern, diagramed in the E literature and explained in greater detail by Chip Morningstar, is a way of representing an object in a virtual world that has a presence across many machines. The owner of an object views/interacts with that object in one way, a server may have yet another view, the users in the same room may have yet another view, etc., yet they all refer to the same conceptual "thing." For Fantasary, we tried to understand and implement a simple version of this pattern.

Diagram of unum pattern in Fantasary

Our game objects, the things that can exist inside a room, are called "entities." The owner of an entity has the capability to do whatever it would like. Fantasary is very simple, and the only things to be done are setting a name and an event handler. When an entity enters a room, the room is given a "puppet" of that entity. A puppet is a proxy for the entity with limited capabilities. The room has read-only access to the entity's identity and can invoke the event handler to notify the entity of activity within the room. When an entity leaves a room, it cuts the metaphorical puppet strings (represented as dashed lines in the diagram above) to revoke the capability for the room to send messages to it. Other entities in the room see puppets of their roommates, as well, but puppets of a different sort. These puppets are constructed by the room specifically for use by a single roommate. If either entity leaves the room, the puppet strings are cut and they can no longer talk via that puppet. This proxying strategy provides both security (an entity never gets a direct reference to another entity, nor does a room) and a means to layer on context-specific behavior. This initial experiment with the unum pattern suggests to us that it's a pattern worth exploring more in future projects.

Packaging

In order to make Fantasary something that people could actually run without too much time investment, we utilized GNU Guix. We use Guix internally for development and server administration, but this was our first time using it to ship software. We used the guix pack program to produce a redistributable binary bundle that contains all of the dependencies required to run Fantasary, all the way down to glibc. There a couple of caveats:

  • The bundle is big. Nearly 1G uncompressed, ~250M gzipped.
  • The bundle works on Linux only, but that's OK for now.

The trade-off for making minimal assumptions about the Linux host running the game is that the bundle is quite large. It could be made smaller, as there are things included that are not needed at runtime, but that requires optimizing Guix package builds upstream. From what we've seen, the bundle has worked for most people that have tried Fantasary, and some of the failures are due to assumptions made in our own code (I didn't expect (getprotobyname "tcp") to fail on some systems) and are not the fault of Guix.

Guix also made it to easy to write a wrapper script that automatically launches Fantasary in cool-retro-term for that extra coolness factor. This fancy version doesn't work for all users, however, so we also included a fallback script that simply runs in the user's regular terminal.

Conclusion

Participating in the Lisp Game Jam gave us a great opportunity to dedicate a fixed amount of time to develop a prototype that will be important to us moving forward. Fantasary is not without its share of bugs and missing features (like cleaning up users that have disconnected without sending a quit message), but that's part of what a game jam is all about: Ruthlessly reducing scope to just the things that matter the most and trying to sprint to the finish line with something that mostly works. We look forward to shipping the Simple TCP netlayer in a future release of Goblins.