The most basic code is contained in this module, which means that you will definitely need to use it when writing code for Adonthell. As seen before, all the other modules depend on base.
One goal of the Adonthell engine is portability, and a number of services to aid this goal are implemented in the base module:
- Primitive data types
All primitive integer types are redefined by the base module. Using them instead of the data types provided by the underlying C library ensures that a short will always be 16 bit and integers 32 bit, regardless of the hardware architecture and operating system. Note that this is not implemented yet, but by sticking to the types defined in base, it can be added easily. For example, the configure script could define types apporpriately at compile time.
The same data files should be usable on both little and big endian hardware. Also, save games should be endian independent and both system types should be able to exchange data over a network. That means that there must be a way to convert data from little to big endianness and back. This is done through macros defined in the base module. Make sure to use them whenever data is read or written. The I/O methods described later are already endian independent â€“ sticking to them where possible will avoid problems too. Btw., data is always stored in little endian format and converted at need.
- Game speed
Since computers tend to get faster and faster, it is important to make the engine independent from CPU speed. The base module allows to specify the duration of a single game cycle. When calling base::Timer.update after each cycle, the engine ensures that each cycle consumes exactly that timespan. That way, games will run at constant speed, given a fast enough CPU. Thereâ€™s also limited support for slower CPUs. If computation of a cycle takes longer than allowed, the engine could react by skipping frames or by disabling costly, but otherwise unneccessary graphic or sound effects.
The engine needs to be able to save the state of the gameworld and restore it at a later date. For that purpose, most objects must be serialized and written to disk. Since objects will change as development progresses, a mechanism is required to keep serialized data at least partially compatible with newer versions of the engine.
This can be achieved by storing each variable with an id and type information. That way, an old engine can skip over new fields in the serialized data or provide defaults for missing fields if possible. Even if an object canâ€™t restore its state from such a data file, it will at least notice that fields are missing and can abort loading gracefully.
Internally, an id, size, type and of course the value of a variable are written to a byte stream that can be later saved or transfered over the network. Multiple variables can be grouped together in a block to avoid confusing variables with the same id. It is therefore recommended to put the contents of each object into a separate block. Blocks are also useful for storing array data with empty ids, thus saving space and time. These array elements can be easily restored by iterating over the contents of their block.
The figure above shows the bytestream format of a single variable. Multiple variables are concatenated without delimiter. Blocks are treated like variables, with their own type code and contents stored in the value area.
A checksum can be calculated for the whole stream, so data corruption can be detected, although no error correction is possible.
As mentioned above, the bytestream is kept in memory. To save it to disk, wrappers around both zlib and libxml are provided by the base module that can be used to write it into a compressed binary file or a structured XML file.
The zlib wrapper reduces the overhead somewhat and keeps file size small, so it is mainly useful for distributing finished game data. It can also be used independently from the serialization, to write primitive types or blocks of memory to compressed files. This should be avoided, however, as no compatibility is guaranteed that way. The XML wrapper is meant to be used by editors that will create the initial game data. That way, version tracking in a Source Repository will be easier than with a binary file.
Since both formats are different representations of the same byte stream, it's easy to convert one to the other without any risk of data loss.
The base module provides a way to store configuration parameters in an XML file. The format of the file is very simple. The first level consists of sections, each of which can contain options with a value each. To retrieve a certain parameter, the name of the section and option it is kept in needs to be known by interested modules. A default value can be passed â€“ if an option does not exist it is created with that default value. That way, the engine can start without configuration as long as it can provide a sensible default value for each parameter.
As discussed before, a default configuration is read during engine startup. It is automatically written back to disk when the engine quits.
With respect to a graphical configuration system, different option types are defined. One of them allows any type of value, others are limited to boolean values or a integer range. A last type allows only selection from a predefined list of values. The parameter type isnâ€™t stored in the configuration file, but must be explicitly set by the module requesting the parameter. That way, the configuration is kept free of metadata and easily editable by hand.
Usually, there will be one directory for static gamedata shared by all users and a data directory in the users $HOME, where saved games and configurations reside. That way, the engine can be installed by a privileged user and later be used by everyone else. When developing a new game, one might not want to install newly created game data, so a user defined data directory exists as well.
To ease the handling of data files, the base module provides a search strategy that operates on those three directories (which will differ for different games, of course). The method path::open tries to locate a file in one of the directories in the following order:
- Saved game directory (if specified)
- User supplied data directory (if specified)
- Game specific data directory
The first matching file is opened. Data files should always be opened this way instead of requiring them to sit in a fixed location. Later on, this mechanism might be expanded by add-on or patch directories that will have to be included in the search path above.