ebenoit.info

Faire tourner Legacy Worlds Beta 5 en 2025

Il y a une vingtaine d'années, mon ami Christopher Wicks a écrit les premières versions de Legacy, un jeu de stratégie multijoueur en ligne se déroulant dans l'espace. Plus tard, j'ai réécrit la majeure partie du jeu pour sa cinquième version (il a également changé de nom en raison de la disponibilité du nom de domaine), qui a commencé à fonctionner entre août et octobre 2006 et s'est achevée en 2009.

J'en parlais récemment avec un collègue et cela m'a intrigué. Serait-il possible de le faire fonctionner sur un serveur moderne ? À quel point la tâche serait-elle ardue ? J'ai pris le temps de le découvrir ces trois derniers jours.

Le plan

Je voulais que le jeu fonctionne dans un ensemble de conteneurs contrôlés par Docker Compose, car il me semblait plus simple d'itérer dans ce contexte qu'avec un script Ansible qui mettrait en place une VM. De plus, je me suis dit que j'allais l'exécuter en utilisant la version la plus récente de PHP (8.3 au moment où j'écris ces lignes - je n'avais pas prêté attention au fait que la 8.4 avait été publiée).

Je ne me souvenais plus très bien de la façon dont le jeu fonctionnait et de ses composants. En regardant le fichier README de la version originale du code source, j'ai remarqué que le jeu incluait un bot IRC et un outil qui vérifiait la présence de proxys ouverts sur les adresses IP à partir desquelles les joueurs se connectaient. Ces composants n'avaient aucun sens dans un contexte moderne et j'ai donc décidé de m'en débarrasser. Les instructions semblaient assez claires, j'étais donc assez optimiste pour faire fonctionner le reste du jeu.

Exécution manuelle du site web

Pour faire tourner le jeu, j'ai commencé par essayer de le faire tourner manuellement dans une paire de conteneurs.

La base de données

Le premier d'entre eux contiendrait un SGBDR PostgreSQL basé sur l'image Docker officielle. J'ai transféré les fichiers SQL du jeu dans le conteneur et j'ai essayé de les envoyer à psql après avoir changé les mots de passe directement dans le code. Et ça a explosé, bien sûr. Non pas à cause d'incompatibilités entre les versions de PostgreSQL, mais à cause de tables manquantes. Oh, oh.

Il s'avère que les fichiers d'initialisation de la base de données n'avaient jamais été réutilisés sous cette forme après le lancement initial du jeu. Ainsi, alors que les modèles utilisés dans l'interface d'administration pour créer de nouveaux jeux (fichiers nommés beta5-* dans le répertoire sql/beta5 du code source original) avaient été mis à jour pour inclure tous les fichiers SQL nécessaires, le fichier principal ne l'avait pas été. Pire, des définitions avaient été ajoutées aux parties communes du jeu (comptes, etc.), et n'étaient pas non plus chargées.

L'ajout des lignes \i appropriées a corrigé le problème et j'ai pu créer une base de données pour le jeu (il y a eu quelques couinements concernant la redéfinition de PL/PgSQL, ainsi qu'une clause GRANT incorrecte, mais ce n'était pas vraiment un problème).

Faire fonctionner le site

Le second conteneur que j'ai créé contenait un système Debian 12 minimal sur lequel j'ai installé Apache et PHP. J'ai écrit un fichier Docker initial pour cela, en utilisant les dépôts Sury pour PHP, et en configurant Apache pour qu'il écoute sur les ports 80 et 81 pour le jeu et le site d'administration, respectivement. Le code de LW est exporté vers Docker via un bind-mount, et j'ai ajouté un petit script qui lance simplement Apache au premier plan. J'ai mis à jour les différents fichiers de configuration, comme indiqué dans le README original, et j'ai essayé de charger la page principale du jeu.

Ce qui, sans surprise, n'a rien donné.

C'est aussi à ce moment-là que je me suis souvenu que toute la journalisation dans le code du jeu se faisait via syslog.

Comme je ne voulais pas installer un démon syslog dans le conteneur, j'ai utilisé socat pour envoyer les entrées de journal vers le stdout du conteneur. Ce n'est pas très pratique et cela ne conviendrait pas en production, mais pour ce que je voulais en faire, c'était parfait. Après quelques ajustements, j'en suis arrivé à ce qui suit :

socat -u \
    UNIX-LISTEN:/dev/log,reuseaddr,mode=666,fork \
    SYSTEM:"(sed -z 's/$/\\\\n/'; /bin/echo)" \
    &

La commande sed est utilisée pour remplacer \0 par \n comme séparateur de ligne, et echo ajoute un autre séparateur de ligne à la fin de la sortie.

Une fois cela fait, j'ai configuré PHP pour qu'il enregistre également ses erreurs dans syslog. J'ai relancé le conteneur, essayé de charger la page et pleuré un peu.

Corrections PHP

PHP a beaucoup changé depuis que LW a été écrit. Du bon côté des choses, il est devenu beaucoup plus verbeux en ce qui concerne divers problèmes (d'ailleurs, cela m'aurait beaucoup aidé lorsque je travaillais sur ce code). Du mauvais côté des choses, rien ne fonctionnait.

L'un des premiers problèmes sur lequel je suis tombé est l'utilisation de balises d'ouverture courtes (<? au lieu de <?php). Je ne savais pas qu'il existe une option pour les activer ou les désactiver, alors je les ai simplement remplacées par la version longue, qui est probablement plus propre de toute façon.

Un autre problème majeur était causé par les constructeurs. La plupart des constructeurs utilisaient l'ancien style de déclaration, dans lequel le nom du de la fonction qui en tient lieu devait être le même que celui de la classe. Cela ne fonctionne plus depuis PHP 8.0, et j'ai donc dû tous les renommer en __construct.

Un problème un peu moins répandu mais qui empêchait le jeu de se charger était le fait qu'à l'époque, PHP ne se souciait pas trop de savoir si une méthode était static ou non, ce qui permettait de l'appeler statiquement ou non indépendamment de la présence de ce mot-clé. Il n'était donc pas utiliser pour un grand nombre de méthodes statiques, et les appels à ces méthodes provoquaient donc des erreurs. Heureusement, je n'avais pas abusé de cette « fonctionnalité » à l'époque, donc il n'y avait pas de méthodes qui pouvaient être appelées dans les deux modes.

Enfin, l'autre élément de syntaxe obsolète qui m'a piqué était l'utilisation de {} pour les indices de chaînes (j'utilisais [] pour les tableaux, heureusement). Il y en a probablement encore quelques-uns dans le code, mais j'ai corrigé ceux sur lesquels je suis tombé.

Et puis il y a eu tant d'autres problèmes mineurs : la dépréciation de split en faveur de explode, count qui ne renvoie plus 0 quand il est appelé sur quelque chose qui n'est pas un tableau, la désactivation manuelle des « magic quotes » dans les scripts d'administration, une sensibilité moindre aux types dans certaines opérations...

Cela a pris du temps, mais j'ai finalement réussi à afficher les pages du site de jeu et du site d'administration. Le log est toujours inondé d'avertissements à propos de l'utilisation de clés de tableau non définies et autres, mais bon, ça fonctionne à peu près.

« Vrais » conteneurs et autres corrections

À ce stade, j'ai pu créer des conteneurs appropriés pour que le jeu s'exécute, ainsi qu'un fichier compose.yml. J'ai continué à corriger les problèmes PHP au fur et à mesure qu'ils se présentaient. Il était temps de faire tourner le reste du code.

Le script de contrôle

Le code du jeu inclut un script Perl, control.pl, qui est responsable du démarrage et du contrôle du processus qui exécute les ticks du jeu. Il est également capable de mettre à jour la configuration principale, qui pour une raison quelconque est stockée dans un fichier XML (avec quelques morceaux dans un tableau PHP sur le côté - je ne me souviens pas de la logique derrière cela mais cela me semble un peu bizarre a posteriori).

Quoi qu'il en soit, le processus de contrôle s'exécute en tant que root, et le reste du jeu peut lui donner des ordres par le biais d'un FIFO.

Il s'avère que cela fonctionne toujours dans l'ensemble. La seule chose que j'ai dû corriger est la façon dont le gestionnaire de ticks était démarré - il s'appuyait sur une astuce selon laquelle le gestionnaire était démarré en tant que shell de l'utilisateur système lwticks. L'utilisateur système est toujours créé dans le conteneur, mais le gestionnaire de ticks est maintenant démarré d'une manière un peu plus saine d'esprit (en utilisant runuser).

Le générateur de planètes

Le générateur de planètes est un autre script Perl qui recherche des « requêtes » (des fichiers vides dont le nom suit le motif req-<instance de jeu>-<premier ID de planète>-<nombre>) toutes les minutes, et génère des images de planètes semi-aléatoires en utilisant POV-Ray en se basant sur ces requêtes.

J'avais déjà rencontré quelques problèmes de cet ordre, mais en essayant de faire fonctionner ce composant, il m'est devenu évident que la version du code qui a été publiée à l'origine était soit incomplète, soit basée sur une version en cours d'élaboration. Les images du générateur de planètes n'étaient pas écrites dans les bons répertoires pour que le JS côté client puisse les trouver.

J'ai fini par devoir corriger cela. J'ai pu faire fonctionner le générateur dans un conteneur séparé. Le répertoire des requêtes et le répertoire de sortie sont tous deux partagés en tant que volumes Docker entre le générateur et le conteneur principal du jeu.

Configuration via des variables d'environnement

Le code original s'appuyait sur plusieurs fichiers de configuration (les fichiers de configuration PHP et XML du jeu, ainsi que le tableau de configuration PHP de l'interface d'administration). En outre, quelques éléments étaient codées en dur. Il était nécessaire de modifier cela si je voulais que le jeu puisse être configuré via un seul fichier .env lu par Docker Compose.

Pour la base de données, c'était assez simple : l'outil psql de PostgreSQL peut lire directement les variables d'environnement depuis la version 15 grâce à la commande \getenv. Ces variables peuvent ensuite être injectées dans des requêtes SQL à l'aide de la syntaxe :'ma_var'. Un court script shell a été ajouté pour identifier les noms de toutes les variables nécessaires, lire les valeurs depuis des fichiers si une variable <var>_FILE existe à la place de la variable elle-même, puis utiliser psql pour exécuter l'initialisation SQL.

Le fichier de configuration de l'interface d'administration n'a pas nécessité beaucoup de changements – c'était du PHP pur, il a donc suffi d'ajouter une fonction pour lire les secrets depuis des fichiers et lire le reste depuis les variables d'environnement appropriées.

Cela a été un peu plus compliqué pour le jeu, car la configuration de la base de données se trouve dans la partie XML de la configuration. J'ai dû ajouter un attribut from-env permettant de lire une valeur depuis l'environnement. C'est un hack un peu sâle, et comme la configuration XML est mise en cache sous forme de données sérialisées PHP, il pourrait poser problème si le cache persistait ; heureusement, il est situé sur un tmpfs et disparaît lors des redémarrages de conteneurs, donc cela fonctionne.

Envoi d'e-mails

Le dernier problème restant était le fait que le code s'appuie sur la fonction mail() de PHP pour envoyer des e-mails. Cette fonction nécessite un serveur local d'envoi de mails qui relaye ensuite les e-mails ailleurs. Il faudrait pour cela un autre démon, et je ne voulais pas faire tourner cela dans le conteneur.

Il n'a pas fallu beaucoup de recherches pour découvrir msmtp, un programme offrant une interface compatible avec sendmail et transmettant les e-mails directement à un serveur SMTP arbitraire.

J'ai également supprimé l'adresse From écrite en dur dans le code de LW et l'ai remplacée par une variable de configuration.

Conclusion

Ça a été plutôt amusant de faire revivre ce vieux code. Il est très probable qu'il soit encore plein de problèmes de compatibilité PHP, et qu'il comporte sans doute de nombreux bugs qui pourraient être corrigés en lisant les journaux, mais ce n'était pas le but de l'opération. Par ailleurs, il serait absolument essentiel de remplacer le code d'authentification par quelque chose qui ne stocke pas les mots de passe des utilisateurs en clair dans la base de données si l'on voulait vraiment le faire tourner, même pour un jeu privé.

Cela m'a surpris de voir que la partie cliente du jeu fonctionne encore sans problème, tant d'années après avoir été écrite. Je veux dire, la plupart du code du jeu a été écrite avant la première version de JQuery ! D'ailleurs, la version 1.2.3 de cette bibliothèque est incluse dans les ressources du jeu, mais elle a dû être ajoutée plus tard puisqu'elle date de 2008.

Le code SQL fonctionne toujours aussi, mais il est assez trivial, donc ce n'est pas une surprise.

J'ai bien ri en voyant l'écran d'accueil du jeu :

Ce jeu fonctionne mieux avec des navigateurs modernes comme Firefox 2 ou Internet Explorer 7.

« Modernes », hein.

La version Dockerisée et « fonctionnelle » est disponible sur mon instance Forgejo.