Next: , Previous: Designing new levels, Up: Top


9 Hacking the game

9.1 Architecture

9.1.1 Cross-platform

Liquid War 6 is developped on GNU/Linux and other platforms are not supported yet. However there is no reason that would prevent it from running on other platforms, such as Microsoft Windows, Mac OS/X, FreeBSD and in a general manner any *NIX system.

Liquid War 6 uses libraries which are cross-platform (OpenGL, Guile...) so porting it is merely taking the time to fix makefiles and build tools for the given platform. This does not mean it is immediate, but it's a feasible task.

9.1.2 OpenGL

Liquid War 6 uses OpenGL to handle all the video rendering. It is a deliberate option to use this high-level API. The advantages are that it is cross-platform and uses the acceleration features of modern video hardware.

The major drawback is that without accelerated hardware, the game will still run using a software renderer such as Mesa, but it will definitely be too slow to be playable. The option for players who do not own such hardware, or who do not have a correct driver - many vendors keep their specifications secret, which forbids Free Software developpers to create free (as in speech) drivers for them - is to use Liquid War 5 instead, which does not require fancy hardware and accelerated drivers.

Liquid War 6 uses SDL to set up the OpenGL environment. Indeed, OpenGL itself does not handle keyboard or mouse input, and requires some platform dependant glue code to be initialized. SDL provides all this.

9.1.3 Guile

One of the lessons learned from Liquid War 5 is that standard C is very convenient for some tasks, but is plainly inadapted in many cases. Handling menus with a high-level API is one example of something really painfull to do with C, whereas it is child's play with a scripting language.

For this reason, Liquid War 6 is a C program which embeds a script interpreter. The C routines provide low-level routines which requires speed and/or are interfaced with C libraries, while the script interpreter handles all the game logic and high-level aspects.

The script interpreter used is Guile, which is recommended by the GNU project. Note that many other scripting languages could have been used, including Python, Perl or Lua. Still, the needs of Liquid War 6 are sufficiently simple that any scripting language can do the job. This is not to say that Scheme is inferior to any language - it might on the contrary be more powerfull - but for Liquid War 6, it was powerfull enough, and no other language had that very special feature which could have justified their used instead of Guile/scheme.

9.1.4 Internal libraries

9.1.4.1 Program splitted in libraries

The liquidwar6 executable itself is fairly small. In fact most of the C code - not even speaking of Scheme code - is located in a set of libraries/modules which are installed with the main program. This is different from the external libraries (libGL, libguile...). Here we are speaking of libraries which are very Liquid War 6 specific.

However, they might prove usefull in other games or applications, and after all, they are ready to use. Moreover, splitting the program into libraries makes modularity a reality. Experience shows than when a program is a monolithic C program, the temptation of putting everything in the same place is very strong, and this leads to spaghetti code.

9.1.4.2 Private and public interfaces

Each library exports a public interface and hides its internal. Since Liquid War 6 uses standard C and no C++, there's no real standard way to handle public/private features. The convention used in Liquid War 6 is to show internal structures as opaque pointers (void *) whenever some function needs to operate on a structure which has possibly private fields. This way the caller function has no way to access the internals, and we are sure that no reference to any internal implementation specific feature will appear.

Here's a code excerpt from src/gfx/setup.c:

     void _lw6gfx_quit(_LW6GFX_CONTEXT *context) {
       /*
        * Implementation here.
        */
     [...]
     }
     
     void lw6gfx_quit(void *context) {
       _lw6gfx_quit((_LW6GFX_CONTEXT *) context);
     }

The function _lw6gfx_quit (note the “_”) is internal, declared in internal.h whereas the function lw6gfx_quit is public, and is therefore exported in gfx.h.

This way, functions in the program using lw6gfx_quit do not know what is in the _LW6GFX_CONTEXT structure, and they need not know it.

This does not mean it is not possible to have public structures, only these structures must reflect some truely public, accessible and safe to access structures.

9.1.4.3 Linking

For now, linking on any of the internal library will pull down all the Liquid War 6 dependencies, including Guile, OpenGL, and the rest, even when it is not needed. This is due to automake/autoconf which automatically set these dependencies when a corresponding AC_CHECK_LIB call is present in configure.ac.

This might be fixed in the future, spending time on this would require that there is actually a real project planning to use one of Liquid War 6 internal libraries.

9.2 Coding guidelines

9.2.1 Project goals

One of the purposes of Liquid War 6 is to make a cleaner implementation of Liquid War than the previous one, namely Liquid War 5. While the latter has achieved the practical goal of providing a playable implementation of the game, it failed at providing an evolutive platform. Network capabilities where finally added to Liquid War 5, but anyone who played on Internet with someone a few hundreds of milliseconds away would agree that it's far from being perfect. The main reason for this is that it is really had to hack on Liquid War 5, especially when you are not the core developper. The core developper himself, even knowing all the various hacks in the game, is very quickly lost when trying to implement major changes.

To put it short, Liquid War 5 is a global variable hell, a pile of hacks on top of a quick and dirty implementation. Still, it works.

With Liquid War 6, the idea is to take the time to make something stable, something nice which will enable developpers to implement the cool features, and have fun along the way.

9.2.2 Common sense

Here are a few guidelines which I think are common sense advice, but they are still worth mentionning:

9.2.3 Application specific issues

9.2.3.1 Unitary tests

Each of the internal libraries in Liquid War has a “test” program associated with it. For instance liquidwar6sys-test is associated to libliquidwar6sys, and its purpose is to test the features of this library.

While it is fairly easy to test out unitary functions which require no peculiar context, testing high-level functions which requires files, graphical and possibly network contexts to exist is obviously harder to achieve. There's no easy way to draw the line, but the idea is to put in these test executables as much features as possible, to be sure that what is tested in them is rock solid, bullet proof, and that one can safety rely on it and trust that code when running it in a more complex environnement.

These test executables are also very good places to see a library API in action, find code fragments, and make experiments.

9.2.3.2 Memory allocation

The libliquidwar6sys provides macros to allocate and free memory. One should use them systematically, except when trying to free something allocated by another library.

See the documentation for module libliquidwar6sys for more information on how to use the macros.

9.3 Modules

9.3.1 libliquidwar6sys

System functions. Provides access to various utilities which can be used by any other module.

9.3.1.1 Log API

A basic log API is provided. The idea is not to try and make better than syslog or any existing standard log API, but simply to wrap log calls so that they are handled in a uniform manner in the application, and that it is trivial to change logs behaviors.

Using this API is pretty straightforward:

     lw6sys_log_info("abc",_("this is %s"),"ABC");

Using lw6sys_log_info means the message is purely informative. Other options are lw6sys_log_debug, lw6sys_log_warning and lw6sys_log_error.

Second argument uses a call to function _ which means the text is i18n enabled with gettext.

Third argument is just to show that the functions can handle string formatting the way printf does.

9.3.1.2 Memory allocation

Dynamic memory allocation is a common pitfall in C programming. One advantage of higher level languages such as Scheme, Perl or Python is that they handle memory management for you and therefore avoid many bugs, and consequently many hours of debugging.

The module provides two macros, LW6SYS_MALLOC and LW6SYS_FREE. Both work the way you think they should, that is like malloc and free.

Still, there's some magic happening under the hood.

Indeed, these functions:

To implement this, some global variables (to hold the global memory allocation/freeing counters) need to be declared, the macro LW6SYS_MALLOC_WIZARDRY does this for you. See the unitary test programs (for instance src/sys/test.c) to see how this work in practice (also check the use of LW6SYS_CHECK_MALLOC_FREE_COUNT).

Concerning performance, calling these macros will obviously be slower than calling directly their glibc equivalents. The choice in Liquid War 6 is to renounce to this form of optimization and prefer the comfort of handy debugging tools to the risk of memory leaks.

9.3.1.3 File utilities

The module contains utility functions which ease up file handling, for instance it allows you to read a whole file in one call, or test the existence of a file.

9.3.1.4 String utilities

The module also contains utilities to handle strings, that is 0 terminated char * pointers.

Most of the time these are only simple wrappers which call three and sometime only one standard glibc function, but it's convenient to use them to:

A good example is string copy. There is a builtin glibc function which is strdup. But we prefer using lw6sys_str_copy for it will allocate memory using LW6SYS_MALLOC and therefore keep track of the call, and expect and check for the matching call to LW6SYS_FREE.

9.3.1.5 Chained lists

The module provides tools to handle chained lists.

Again, the idea is to be consistent with the use of LW6SYS_MALLOC.

While one might argue that chained list handling is typically Scheme's domain of excellence, and that it's akward to do this manually in a Guile enabled program, the answer is that:

The implementation is very basic, no fancy list handling, only push, pop, is_empty and that's about it.

The data is stored in a void * pointer which should point to your data. You'll need to cast it manually when you want to read your data.

A free_func attribute can be defined, which is called when list nodes are deleted. This enable the chained list tools to handle objects which have been allocated dynamically and free them properly through a callback system. A side effect of having LW6SYS_MALLOC defined as a macro is that it can't be used directly as a callback. Use the function lw6sys_free_callback for this. But do not use it systematically instead of the LW6SYS_FREE macro, as the macro gives more debugging informations when it fails, including source file and line number for instance.

See the test program ./src/sys/test.c to see the API in action.

9.3.1.6 Associative arrays

This is very similar to the chained list API, it provides a way to handle associative arrays, AKA dictionnaries.

Note that the code here is highly unoptimized, and that handling large associative arrays with it will be a performance killer. There is no hash-table, and when you query an object all the keys are read and compared with strcmp to figure which key is the right one.

Still, having this is convenient for it avoids limitating the program with hardcoded limits. You can fit any number of items in these associative arrays, if there are too much of them it will be slow, but it will still work.

Liquid War 6 does not make an intensive use of this, it is just here to handle things like representing options in memory after they have been loaded from disk or interpreted from the command line.

The void * pointer on the data, the value of the key/value pair which forms the dictionnary, is handled the same way than data in chained lists. That is it can be freed automatically with the free_func attribute, which is a callback, and can be set to NULL if you do not need that feature.

The char * pointer on the key, which is a string, is handle in a different manner. In fact, it is automatically duplicated when you create an entry, and automatically deleted when you delete an entry. Therefore using a freeing callback for this makes no sense.

See the test program ./src/sys/test.c to see the API in action.

9.3.1.7 i18n support

Todo...

9.3.1.8 XML tools

The module provides a simple wrapper over expat functions. It is used to parse primitive XML files with a general key -> value scheme of the following form:

     <element key="foo" value="bar" />

While the use of XML for storing such simple informations is questionnable, it makes no doubt it's safer to rely on the well tested routines of expat rather than code a home-made parser. Additionnally, using XML will make the transition easier the day we need to store more complex and structured information.

9.3.2 libliquidwar6cfg

Configuration routines. Provides a high-level API to read, update, and save configuration options.

Command-line options and file-based options are handled in a uniform manner.

9.3.3 libliquidwar6gfx

Graphics functions. Also provides input functions. This is logical since input is related to the video output system. For instance using OpenGL with SDL implies that SDL handles the input. The keyboard input is not handled the same way when running X11 and when running in console mode.

The current implementation of libiquidwar6gfx relies on SDL and OpenGL, but this is not mandatory. It is theorically possible to implement a new target (plain X11, ncurses, ...), without changing a single line of code in the other modules. However this is obviously not a priority.

Depends on libliquidwar6sys and libliquidwar6ker.

9.3.4 libliquidwar6map

Loads maps into memory. Basically this module is used to transform .png files located on the file system to a workable memory structure.

This has been separated from the rest since it's a little special, for it requires access to functions which are typically found in graphical libraries (read a .png file) so we need to link it to some graphics related .so files. But it does not do any actual video work. We use graphics formats as a well known easy-to-use storage backend.

Depends on libliquidwar6sys.

9.3.5 libliquidwar6ker

The core algorithm. This is where all the interesting and definitely Liquid Warish code is kept. It contains the shortest path algorithm imagined by Thomas Colcombet back in 1995. It tries to have as few dependencies as possible, to ease its reuse in other software.

Not implemented yet.

Ideally, depends on nothing, might depend on libliquidwar6sys.

9.3.6 libliquidwar6snd

Handles everything related to sound and music. What is planned is the use of CSound. It would allow the writing of cool music, and even contextualize the use of music - making it faster, slower, louder, scarier, whatever... - depending on what's happening within the game.

Not implemented yet.

Depends on libliquidwar6sys.

9.3.7 libliquidwar6net

Will handle all the network stuff. Using a simple POSIX socket API won't be enough, Liquid War 6 has an ambitious goal of getting rid of the server/client mode of connecting to games. Ideally, there would be no server, simply join a game and whenever the person who initialized the game quits, then another player's computer takes the responsability to handle the game. This way one could imagine a never-ending Liquid War game. Wether this will be implemented from scratch, or if a peer to peer enabling library such as GNUnet will be used, is not decided yet.

Will depend on libliquidwar6sys, maybe also libliquidwar6ker.

9.3.8 liquidwar6script

This is not, like the other modules, a shared library, but rather a collection of scheme scripts which contain all the logic of the game. These scripts call the other libraries API, and are the core of the game.

This is probably where hackers would like to start. The scripts are in the ./src/script directory of the source tarball, and installed in /usr/local/share/liquidwar6/script/ by default.