Development:Structure
This article describes the structure of the code of Librae. The code itself is available on GitLab.
Part of the game engine is called rustorion. It's the older name for the project, and intended to be the name of the engine, when it is fully separated from game logic.
Language
The game is written in Rust. It tries to be pure Rust, but some unique foreign components are still used, most prominently GTK4 for the user interface.
Main parts
- rustorion — game logic library
- librae-gui — gtk-based UI
- librae-cli — CLI tools
Game state
The game is structured to support trustless (except the server), simultaneous multiplayer. The game state is stored as a single universe structure (rustorion::Universe). Client receives a view which contains only the information the client is supposed to know. Client sends actions to the server that contain the information about what the client wants to do on the current turn. When all the players are ready (submitted their actions or it's a timeout), the server processes all the actions (they are designed to be executed independently of each other, so there are no race conditions), performs various updating on the universe and notifies clients that they can download new views.
In the future, clients might be able to download previous views to browse them at will or use for gathering statistics.
Entities and IDs
Most things, entities within the universe, or anything else that implements EntityStored have an ID. It is an unique identifier of an entity within the storing structure. Using EntityStored::get() and other similar methods you can obtain a reference to the entity by its ID. IDs are used to store links between entities through plain inclusion on a structure, or using types from rustorion::storage::links for more complex relations like many-to-many.
IDs are issued sequentially, but then the number is encrypted with Blowfish to ensure there is no leaking information about number of entities in an universe. Because the encryption key is stored in Universe, only Universe can issue new IDs.
Working with state
To modify the state, the only option is to work with the storage type (e.g. Universe) directly. However, this is not easy, you have to obtain IDs, use them to extract data, etc., manually. This appears to be the only way to keep the structure mutable and avoid using some kind of GC.
However, there are simplified interfaces for Universe and Universeview called rustorion::universe::interface::Universe and rustorion::universeview::interface::Universeview, respectively. These allow for object-oriented, safe examination of data and contain many useful routines. The intention is to use these to prepare the modifications, and later execute them on the storage type after dropping the interface.
RPC
Communication with client is done using CBOR-serialized RPC. It's rather haphazardly done using Tokio's example-like length-delimited codecs, and needs to be improved or replaced with something less monstrous than gRPC. Light macro usage might be in order.
Authentication
Clients authenticate themselves with client TLS certificates to the server. Currently BLAKE3 hashes of the client certs are used, this seems to work but is probably not the best idea.
State Machines
Interaction between parts of the game, like network client, server, GUI or CLI is based on state machines. A state machine in that context is an entity which, after being started, manages itself and interacts with others using messages. For example, if you want to use the 'client' machine:
1. Start the machine (this launches it in another 'thread', in case of the client, it's a Tokio green thread) 2. Subscribe to its events (start listening on message channels connected to it) 3. Send the 'connect' message 4. Wait for the other messages, like 'connected', and change your own state accordingly.
State machines are implemented using the [state_machine https://gitlab.com/librae/state-machine] library. The library supports the Tokio and 'futures' crate primitives, the latter are also used by GTK4.