Next: Copying, Previous: Designing new levels, Up: Top
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.
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.
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.
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.
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.
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.
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.
Here are a few guidelines which I think are common sense advice, but they are still worth mentionning:
strcpy
or sprintf
anywhere in the code,
use their equivalent strncpy
and snprintf
systematically,
as they are part of the glibc and are an order of magnitude safer,
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.
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.
System functions. Provides access to various utilities which can be used by any other module.
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.
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.
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.
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
.
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.
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.
Todo...
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.
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.
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.
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.
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.
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.
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.
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.