More code necromancy
Written by Emmanuel BENOÎT - Created on 2025-01-04
Having managed to run LW Beta 5 on a modern(ish) stack, I figured I'd try the same thing with Beta 6.
LegacyWorlds Beta 6 was a full rewrite of the game in Java, with a backend server managing everything in the game and a pair of front server (one for the admin pages, and another for the game). It ran from 2010 to 2011 as a tech test - the game was far from feature-complete, compared to LWB5. It did include a single, once-per-minute tick, which was the most important addition I had planned on adding. I grew bored with LegacyWorlds around that time so development stopped.
What I remember about the tech
I'm writing this part before looking at the source code as a way to jog my memory; because of that, what I'm saying here might be inexact.
If I remember correctly, most of the game logic was implemented in the database itself (I was in a "put logic as close to the data as possible" phase at the time), using PL/PgSQL. The backend server would, in most cases, just call a few stored procedures and get whatever results it needed.
Speaking of the backend server, it was based on Spring and could be communicated with using Java RMI and a whole bunch of classes which represented messages to and from the server. It also included a scheduler, similar to Beta 5's tick manager.
Two other Spring-based applications implemented the admin- and user- facing interfaces. They were mostly empty shells that managed a session, called the backend as appropriate, and generated templates to be sent to the client. I believe the client interface was much lighter, including almost no Javascript.
Finally there was a command line tool which could manage the game. I don't remember how that thing worked at all.
The plan
I'll start by trying to get the Java code to build with the appropriate Java version (Java 7? 8 possibly? I don't remember), generating container images using multi-stage builds (I've never done that so it's a good occasion to try it out).
I'll also have to try and load the PostgreSQL code, which is way more hairy than Beta 5's. My idea is to try in on the latest version of the RDBMS, and move back to older and older versions until I get it to load.
At that point I'll be ready to try and upgrade the technical dependencies, which will no doubt be quite painful. One does not simply ~~walk into Mordor~~ upgrade both Java and Spring and expect the code to still build.
In order to limit the amount of work, I'll be using the B6M1 version as a starting point rather than the trunk, which was work in progress towards B6M2. I'll think about integrating the WIP code later on.
Running the original code
Building the Java code
So it turns out that the Maven build file indicates that the code was built using Java 1.6, and there's no official image for that. The oldest Maven + JDK images I could find were of Maven with Java 7, so I decided to build the application using them.
Getting the code itself to build inside a container wasn't all that hard. I took
the time to make sure it isn't built by the root user, though. My initial
attempt went something like this, except I had missed the ,uid=...
part of the
mount option, so the build failed due to Maven being unable to write to its own
directory.
FROM maven:3-jdk-7
ENV BUILD_UID=1000
ENV BUILD=/src
ENV MAVEN_HOME=/var/maven
RUN mkdir -p $MAVEN_HOME && chown $BUILD_UID $MAVEN_HOME
ADD --chown=$BUILD_UID:$BUILD_UID .. $BUILD
USER $BUILD_UID
WORKDIR $BUILD
RUN --mount=type=cache,target=$MAVEN_HOME/.m2,uid=$BUILD_UID \
mvn -e -Duser.home=$MAVEN_HOME package
ENTRYPOINT ["/bin/bash"]
It is easy to turn this into a multi-stage build for the backend and command line tool (I didn't remember that, but it turns out that they are in fact contained within the same Java program), adding a script that would act as an entry point for both and generate the configuration file. I added that to a Compose file in order to test building it from there.
At this point, I wanted to move on to the next task, which was building images for the two sites. I started poking around because I wanted to reuse the build stage from the backend. Turns out I had misunderstood how multistage build worked and how they interact with Compose. I had missed the fact that you could specify the target to build from the same file. I also had to find an image for Apache Tomcat 6 running on Java 7.
I started looking for a way to configure the address of the backend server the
frontends connect to... and found out it was hardcoded to localhost:9137
. I
think I used to run the frontend services with a stunnel
instance connecting
them to the backend container, which was itself running stunnel
. I wasn't
going to do that here, so I had to find another solution. Turns out Spring
supports environment vars if one adds <context:property-placeholder />
element
to the beans file, so I modified both interfaces' definitions like this:
<context:property-placeholder />
<!-- ... -->
<!-- Session service connector -->
<bean id="sessionSrv" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"
scope="prototype">
<property name="serviceInterface" value="com.deepclone.lw.session.SessionAccessor" />
<property name="serviceUrl" value="rmi://${LW_BACKEND_HOST}:9137/sessionSrv" />
</bean>
Loading the database
Taking a quick look at the main database script
revealed that it already supported parameters, although it did not read them
from the environment. It was easy enough to replace these \set
instructions
with \getenv
, and to reuse most of the init script and Dockerfile I had
written for LWB5.
I was quite surprised when the initialization code actually loaded the whole thing without a hitch. I suspect the PL/PgSQL code contains incompatibilities but that they won't become visible until the functions are called.
The backend still did not load, I ended up with an exception when trying to connect:
Caused by: org.postgresql.util.PSQLException: The authentication type 10 is
not supported. Check that you have configured the pg_hba.conf file to include
the client's IP address or subnet, and that it is using an authentication scheme
supported by the driver.
This means I have to either upgrade the PostgreSQL JDBC driver or downgrade
PostgreSQL. I tried the first option, upgrading to 42.2.28.jre7
, the latest
version on Maven Central, and that worked.
But the backend wouldn't start, because of an exception in one of the
components (com.deepclone.lw.interfaces.prefs.PreferenceDefinitionException
so, well, my fault).
Doing some digging
While the README
file found in the LWB5 repo was clearly missing a lot of
stuff, at least it had one. I was quite sure that the exception I faced was due
to some missing initialization, but had no clue what it was and what I was
supposed to do.
I'm the kind of person who usually archives everything. I even have some code
from my teenage years around! So I figured I'd try and see if I had something in
there that could help. And that's when I found a bunch of old tar archives...
The most important one is a copy of the Git repos that were used while working
on B6M2; I thought I had lost these! I'll need to import the commits they
contain on top of the lwb6
archive repo later on.
In addition, I found a bunch of shell scripts that used to run on a separate VM
and that basically implemented a CI/CD pipeline. They would be triggered by
pushes to the staging
repo through CGI, and would merge everything into
another repo (main
), incrementing the build ID in the version number. They
would then try to build the various JARs and WARs then run all tests. If the
build failed they'd email the log to me, but it it succeeded they'd instead add
the binaries to the main
repo as well, leading to something like this:
* 5f5833c (HEAD -> master, origin/master, origin/HEAD) (13 years ago) buildsystem@lw-build.internal.nocternity.net Build failure: 5.99.0-54
* b39f8ff (13 years ago) buildsystem@lw-build.internal.nocternity.net Merge remote branch 'staging/master'
|\
| * 0c6ea3e (13 years ago) tseeker@legacyworlds.com Event database access
| * ee876c8 (13 years ago) tseeker@legacyworlds.com Event processing task
| * 6d2d4d3 (13 years ago) tseeker@legacyworlds.com Package for event handling components
* | 1c1907e (13 years ago) buildsystem@lw-build.internal.nocternity.net Successful build: 5.99.0-53
* | e0a3d21 (13 years ago) buildsystem@lw-build.internal.nocternity.net Merge remote branch 'staging/master'
|\|
| * 36a0bcd (13 years ago) tseeker@legacyworlds.com Events storage procedure
| * b42bc47 (13 years ago) tseeker@legacyworlds.com Event-related functions split
* | 076a2d9 (13 years ago) buildsystem@lw-build.internal.nocternity.net Successful build: 5.99.0-52
* | 2eeccb0 (13 years ago) buildsystem@lw-build.internal.nocternity.net Merge remote branch 'staging/master'
|\|
| * 55b0c44 (13 years ago) tseeker@legacyworlds.com Events database structure
| * 91ea271 (13 years ago) tseeker@legacyworlds.com Priority settings update procedures
| * 1eeaa50 (13 years ago) tseeker@legacyworlds.com Comments in event definition data classes
* | 286617d (13 years ago) buildsystem@lw-build.internal.nocternity.net Successful build: 5.99.0-51
In one of the files used by the build system, I was able to find a piece of script that was used to fully initialize a (B6M2 WIP) server and boot it up, checking whether it would actually start:
java legacyworlds-server-main-*.jar --run-tool ImportText data/i18n-text.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportEvents data/event-definitions.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportResources data/resources.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportTechs data/techs.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportTechGraph data/tech-graph.xml || exit 1
java legacyworlds-server-main-*.jar --run-tool ImportBuildables data/buildables.xml || exit 1
java legacyworlds-server-main-*.jar &
sleep 10
if ! ps ux | grep -q 'java -jar legacyworlds-server-main'; then
exit 1;
fi
java legacyworlds-server-main-*.jar --run-tool Stop || {
killall java
exit 1;
}
java -jar legacyworlds-server-main-1.0.0-0.jar --run-tool CreateUser 'test@example.org 12blah34bleh en' || exit 1
java -jar legacyworlds-server-main-1.0.0-0.jar --run-tool CreateSuperuser 'test@example.org Turlututu' || exit 1
While this bit of code contains some stuff that is definitely B6M2-specific, it is a big clue for which parts to look at / for. I quickly found the corresponding parts in the B6M1 repo:
- the data can be found here;
- the CLI tool only has three import classes.
So, the next step would be to try and run the import commands in order to initialize the game.
Starting the game
I updated the Dockerfile to include the data. When I checked, it turned out the
BUILD.sh
script I had looked at when writing the initial version of the Dockerfile
actually copied the data... It could have been a clue if I had noticed it.
I then added a volume to the backend container. This volume would contain simple marker files corresponding to the initialization stages.
Finally, I modified the entrypoint script to run the three import commands that seemed to be needed for B6M1:
ImportText data/i18n-text.xml
ImportTechs data/techs.xml
ImportBuildables data/buildables.xml
Definitely in that order: technology definitions require translations, and "buildables" (ships and buildings) depend on technologies.
I also fixed a bunch of issues I noticed along the way: a bug in the tool
part of the entrypoint script, incorrect privileges on /app
, the main
interface's WAR being deployed to the admin interface container, and issues with
the builder stage which made rebuilding images take much longer than it should.
With that I was able to start the game. I figured I'd create an user and an administrator account:
docker compose run backend tool CreateUser tseeker@nocternity.net userpass en
docker compose run backend tool CreateSuperuser tseeker@nocternity.net TSeeker
I located the addresses of both web interfaces using docker network inspect
and was able to log on to the admin interface. On the game interface, my account
was disabled (CreateUser
creates inactive game accounts, and should only be
used to add accounts for the administrators). When I tried to activate it,
something familiar popped up.
23:04:26,995 INFO SystemLogger:118 - Mailer - could not send mail to tseeker@nocternity.net
org.springframework.mail.MailSendException: Mail server connection failed; nested exception is javax.mail.MessagingException: Could not connect to SMTP host: 127.0.0.1, port: 25;
I spent some time trying to get the bean configuration
to work using environment variables, but I wasn't able to because having a user
or password set to an empty string would cause it to try and authenticate. So
instead I moved the mail sender bean initialization to data-source.xml
and had
the entrypoint script generate it. This is not very satisfactory but it works. I
couldn't get STARTTLS to work either; setting mail.smtp.starttls.enable
to
true
through the javaMailProperties
property seemed to have no effect.
Conclusion
I decided to stop at this point, at least for now. I got B6M1 working, albeit with a few limitations (for example, the SMTP/STARTTLS part irks me). My worries about the PL/PgSQL code didn't materialize so far. However, it took longer than I thought it would for me to get it to work. On the bright side, I'm quite happy having found that old archive of mine!
I don't know what the next step would be: should I try and incorporate the B6M2 development branch? Should I just upgrade the technical components? Since these versions were mostly tech tests, I'm not really sure I want to spend more time on them, to be honest. At least for now.
The updated B6M1 code, including the Docker configuration parts, can be found here.