### Eris and Snapshot/Rollback

## What Eris Is

Eris is a third-party Lua serialization library. It can serialize almost anything in a Lua VM — tables, closures, yielded coroutines — into a binary string, and deserialize it later, even in a different VM. It is essentially a rewrite of Pluto (a Lua 5.1 serialization library) targeting Lua 5.2. Eris ships as a custom distribution of Lua: the Lua runtime in our `luprex/ext/eris-master/` directory *is* Eris, which includes Lua plus the serialization code.

The two core operations are:

- `eris_dump` — serialize a Lua value (and everything it references) into a binary stream.
- `eris_undump` — deserialize a binary stream back into a Lua value.

## The Permanent Object Table

C functions (like those registered via `LuaDefine`) cannot be serialized — they are function pointers that may differ between runs. Eris handles this through a "permanent object table": a lookup table that maps C functions to string keys. During serialization, when Eris encounters a C function, it writes the string key instead. During deserialization, it looks up the string key and substitutes the C function.

The `LuaSnap` class maintains both a `persist` table (value-to-key, for serialization) and an `unpersist` table (key-to-value, for deserialization) in the Lua registry. Whenever the source module registers a C function into the Lua environment, it also registers it in these tables.

## How Snapshot/Rollback Works

The class `LuaSnap` (in luasnap.hpp/cpp) wraps Eris to provide snapshot and rollback for the Lua interpreter. It owns a `lua_State` and exposes two operations:

- `serialize(StreamBuffer*)` — uses `eris_dump` to serialize the contents of the Lua registry into a binary buffer. Certain registry entries that don't belong in the snapshot (the main thread, the persist/unpersist tables, the world pointer, diff transmission maps) are excluded.
- `deserialize(StreamBuffer*)` — uses `eris_undump` to restore the Lua registry from a previously serialized buffer.

## Role in Predictive Reexecution

Snapshot/rollback is used to implement the asynchronous model on the client (see "Predictive Reexecution"). There is only one client model, but it alternates between two forms:

- **Synchronous form** — the confirmed state, in lockstep with the server-synchronous model.
- **Asynchronous form** — the synchronous state plus unacknowledged user commands applied on top. This is what the player sees.

To get from synchronous to asynchronous form, apply any unacknowledged user commands. To get from asynchronous back to synchronous form, roll back to the snapshotted state. The model switches between these forms on an as-needed basis. The typical cycle is:

1. The model is in synchronous form. Serialize it (take a snapshot).
2. Apply unacknowledged user commands to produce the asynchronous form.
3. Render from the asynchronous state.
4. When new acknowledgements or difference transmissions arrive, deserialize (roll back) to restore the synchronous form.
5. Apply the updates to the synchronous state.
6. Repeat.

Because this uses Eris serialization rather than raw memory copying, snapshot/rollback only requires value-level determinism, not bit-exact determinism. The deserialized state has the same values but not necessarily the same memory addresses.

## An Alternative Design

A comment in luasnap.cpp describes an alternative approach that was prototyped and found to work: instead of using Eris serialization, use a custom memory allocator that tracks all memory blocks used by Lua, then snapshot by copying those blocks and rollback by restoring them. This would likely be faster than Eris serialization/deserialization, but would require maintaining additional code. Since Eris serialize/deserialize is also needed for save-game functionality, the current design avoids duplicating effort.
