Running Legacy Worlds Beta 5 in 2025
Written by Emmanuel BENOÎT - Created on 2025-01-02
Around 20 years ago, my friend Christopher Wicks wrote the first versions of Legacy, a web-based multiplayer strategy game set in space. Later on, I rewrote most of the game for its fifth version (it also changed name due to domain name availability), which started running some time between August and October 2006 and ended in 2009.
I was talking about that with a coworker recently and it got me curious. Would I be able to run it on a modern server? How bad would it be? Well, I took some time in the past three days to find that out.
The plan
I wanted to get the game to run as a bunch of containers controlled by Docker Compose, as that seemed easier to iterate over than writing an Ansible script to kick a VM into shape. In addition, I figured I'd run it using the next-to-latest PHP version (8.3 at the time of this writing - I hadn't paid attention to the fact that 8.4 had been released).
I didn't remember much of how the game ran and what its components were. Looking at the original source code release's README file, I noticed that the game included an IRC bot and a tool that checked for open proxies running on the IP addresses players connected from. These components did not make any sense in a modern context so I figured I'd just get rid of them. The instructions seemed pretty clear, so I was pretty optimistic I'd get the rest of it to run.
Running the website manually
In order to get the game to run, I started by trying to get it to run manually in a pair of containers.
The database
The first of these would contain a PostgreSQL RDBMS based on the
official Docker image. I transferred the
game's SQL files
to the container and tried to feed it to psql
after changing the passwords
directly in the code. And it exploded, of course. Not because of
incompatibilities between versions of PostgreSQL, but because of missing tables.
Uh-oh.
As it turns out, the DB initialization files were never really re-used in this
form after the game's initial launch. So, while the templates that are used in
the admin interface to create new games (files named beta5-*
in the
sql/beta5
directory of the original source code) had been updated to include
all the required SQL files, the main file hadn't. Worse, definitions for the
game's common parts (accounts, etc..) had undergone some additions, which were
not being loaded either.
Adding the appropriate \i
lines fixed it and I was able to create a database
for the game (there were some complains about PL/PgSQL being redefined, as well
as a bad GRANT
clause, but they weren't much of a problem).
Getting the site to run
The second container I created contained a minimal Debian 12 system on which I installed Apache and PHP. I wrote an initial Dockerfile that'd do that, using PHP from the Sury repos, and configuring Apache to listen on ports 80 and 81 for the game and admin pages, respectively. The LW code was exported to Docker through a bind mount, and I added a short script that would simply run Apache in the foreground. I updated the various config files, as indicated by the original README, and tried to load the game's main page.
Which, quite unsurprisingly, was blank.
And that's also when I remembered all logging in the game code was done through syslog.
Since I didn't want to install an actual syslog daemon in the container, I used
socat
to pipe the log entries to the container's stdout. This is hacky and
would not be any good in production, but for what I wanted it was just fine.
After some tweaking I came up with:
socat -u \
UNIX-LISTEN:/dev/log,reuseaddr,mode=666,fork \
SYSTEM:"(sed -z 's/$/\\\\n/'; /bin/echo)" \
&
The sed
part is used to replace \0
with \n
as a line separator, and echo
adds another line separator at the end of output.
Once that was done, I configured PHP to also log its errors to syslog. I relaunched the container, tried loading the page and cried a little.
PHP fixes
PHP has changed so much since LW was originally written. On the bright side, it got tremendously more verbose regarding various issues (incidentally, that would have helped a lot when I was working on that code).
One of the first issues I stumbled upon was the use of short opening tags (<?
instead of <?php
). I didn't know an option
existed to toggle them on or off, so I just replaced them with the longer
version, which is likely better anyway.
Another massive pain was caused by constructors. Most of the constructors used
the old style of declaration, in which the constructor's name is the same as the
class'. This no longer works since PHP 8.0, so I got to rename all of them to
__construct
.
An issue that was less widespread but prevented the game from loading anyway was
the fact that back then, PHP didn't care too much if a method was static
or
not, allowing it to be called statically independently of the keyboard. Quite a
few methods were missing the keyword, so static calls to them caused errors.
Fortunately, I hadn't abused the "feature" at the time so there were no methods
that could to be called in both modes.
Finally, the other bit of deprecated syntax that bit me was the use of {}
for
string indices (I used []
for arrays). There are probably still a few of these
around in the code, although I fixed the ones I stumbled upon.
And then there were so many other minor issues: deprecation of split
in favor
of explode
, count
no longer returning 0 when called on something that isn't
an array, manually disabling "magic quotes" in the admin scripts, less
sensibility to types in some operations...
It took a while, but I finally got both the game site and the admin site pages to display. The log is still flooding with warnings about using undefined array keys and the like, but hey, it's working.
Actual containers and other fixes
At that point I was able to create proper containers for the game to run in,
along with a compose.yml
file. I kept on fixing PHP issues as they popped up.
It was time to get the rest of the code to run though.
The control script
The game's code includes a Perl script, control.pl
, that is responsible for
starting and controlling the process that runs game ticks. It is also capable of
updating the main configuration, which for some reason is stored as an XML file
(with some bits in a PHP array on the side - I don't remember the rationale
behind that but it seems a little weird).
At any rate, the control process runs as root, and the rest of the game can order it around through a FIFO.
Turns out, this mostly still worked out of the box. The only thing I had to fix
was the way the ticks manager got started - it relied on some trick were the
manager was started as a system user's shell. The system user is still created
in the container, but the ticks manager is now started in a slightly more sane
way (using runuser
).
The planet generator
The planet generator is another Perl script which checks a directory for
"requests" (empty files whose names follow the
req-<game instance>-<first planet ID>-<count>
pattern) every minute, and
generates semi-random planet pictures using
POV-Ray based on the requests.
I had already run into a few issues that pointed to that, but trying to get this thing to work made it pretty obvious - the version of the code that was originally released is either incomplete or based on a future, work-in-progress version. The planet generator's pictures were not being written to the right directories for the client-side JS to find them.
I ended up having to fix that, in addition to getting the generator to work in a separate container. The "requests" directory and the output directory are both shared as Docker volumes between the generator and game containers.
Configuration through environment variables
The original code relied on multiple configuration files (the game's PHP and XML
config file, as well as the admin interface's PHP config array). In addition,
quite a few things were hardcoded. This had to be changed if I wanted the game
to be configured through a single .env
file to be read by Docker Compose.
For the database, it was easy enough: PostgreSQL's psql
tool can read
environment variables directly since version 15 using the \getenv
command.
These variables can then be injected into SQL queries using the :'my_var'
syntax. A short shell script
was added in order to find the names of all required variables, read values from
files if a <var>_FILE
variable exists instead, and then use psql
to run the
SQL initialization.
The administration interface's configuration file didn't require too many changes - it was plain PHP, so it was a matter of adding a function to read secrets from files, and reading the rest from the appropriate env vars.
It was a little trickier for the game, as the DB configuration lives in the XML
part of the config. I had to hack in a from-env
attribute which can be used
to read an attribute from the environment. Since the XML config is cached as PHP
serialized data, that hack would cause problems if the cache lingered on, but
fortunately it's on a tmpfs
so it goes away on container restarts.
Sending mail
The last remaining problem was the fact that the code relies on PHP's mail()
function to send mail. That function requires a local mailer, which will then
relay the mail somewhere else - and that requires yet another daemon which I
didn't want to run in the container.
It didn't take much research to read about msmtp, a program that provides a sendmail-compatible interface and forwards the mail directly to some arbitrary SMTP server.
I also removed the hardcoded From
address in the LW code and replaced it with
a configuration variable.
Conclusion
It was fun to get this old code to run again. It's very likely still full of PHP compatibility issues, and no doubt comes along with a bunch of bugs that could be fixed by reading the logs, but that wasn't the point. In addition, it would be absolutely critical to replace the authentication code with something that doesn't store user's passwords in clear text in the DB if one wanted to run it even for a private game.
It surprised me to see that the client-side part of the game still works without issues so many years after having been written. I mean, most of the code in the game was written before the first version of JQuery! Speaking of which, version 1.2.3 of it is included in the game's assets, but it must have been a later addition.
The SQL code still works as well, but it is mostly trivial so that's no surprise.
I got a nice chuckle out of the game's home screen:
This game works best with modern browsers such as Firefox 2 or Internet Explorer 7.
"Modern", heh.
The Dockerized, "working" version can be found on my Forgejo instance.