Archéologie logicielle et reconstruction d’exécution

Abomination

Préserver la logique, l’atmosphère et la singularité historique d’un logiciel mort sans voler son cadavre ni polir son étrangeté — et sans laisser les abstractions d’un moteur moderne devenir en douce la réponse à ce que faisait l’original.

C++20SDL3PythonGhidraCMake + CTestSHA-256 manifestsWine / Xvfb observation
Statut
Reconstruction active / route de release candidate proposée, non acceptée
Code
AB-04
Domaine
Archéologie logicielle et reconstruction d’exécution
Porté par
Pomegranate Interactive
Méthode
Ingénierie de reconstruction
Média
Fourni par le joueur · jamais redistribué
Bloqué par
Runtime commercial protégé · gates humaines
HUD tactique de la reconstruction archivée affichant des poses d’origine décodées
Le candidat archivé : techniquement certifié, puis rejeté sur la direction produit. Conservé comme référence et preuve de formats, non présenté comme la route actuelle.
Unités et environnements d’origine importés par le chemin bring-your-own-data
Chemin bring-your-own-data : le joueur fournit un média légalement détenu, converti localement. Le média d’origine n’est jamais distribué avec le build.

Un programme de rétro-ingénierie qui décode un jeu tactique de 1999 depuis un média fourni par le joueur, tient ses conclusions à un grade de preuve explicite, et emploie désormais un build autorisé du moteur d’origine comme oracle comportemental plutôt que de continuer à inventer les réponses qu’il pouvait mesurer.

95 %Couverture de formats revendiquée par le projet
40,8 %Du bloc terminal de map spécifié
6Observations d’oracle au registre
3Gates d’acceptation de release encore ouvertes

Profil de mission

  • Prendre une copie immuable du média fourni par le joueur et hasher chaque octet avant tout décodage.
  • Analyser les formats d’origine à une frontière contrôlée, en préservant les blocs que personne n’a encore expliqués.
  • Mesurer le comportement de l’exécution d’origine là où elle peut être observée licitement, et étiqueter tout le reste comme inférence.
  • Tenir un cœur de simulation déterministe à hash d’état canonique, séparé de toute couche de présentation.
  • Séparer les gates qu’une machine peut signer de celles que seul un humain peut signer, et refuser de les confondre.

Architecture

État vérifié

Ce qui existe et a été exercé au moment de la rédaction.

  • Le média d’origine n’est jamais redistribué. Le joueur fournit un média légalement détenu, un importateur isolé le convertit localement, et l’inventaire de hash consigne ce qui a été lu.
  • Le travail de formats est gradué, non affirmé : le projet annonce 95 % de couverture globale, et spécifie le bloc terminal de map à 40,8 % de ses octets non-padding plutôt que d’arrondir vers le haut.
  • Les parseurs échouent à la frontière. Le lecteur ne comporte, par conception, aucun accès non contrôlé, et chaque refus rapporte l’offset auquel il a refusé.
  • Le paquet normalisé porte un type de ressource pour la matière que personne n’a encore expliquée : les blocs inconnus voyagent avec les données au lieu d’être jetés.
  • Un build autorisé de septembre 1999 du moteur d’origine tourne désormais sous observation et sert d’oracle comportemental ; six observations figurent au registre.
  • La couche de compatibilité translate les surfaces logiques fixes du moteur vers un affichage moderne et remappe le pointeur en retour. Elle ne rééchantillonne jamais la géométrie.
  • Un substrat de déterminisme existe comme spécification exécutable : sérialisation canonique, simulation à tick fixe en entiers et vecteurs dorés de hash d’état.
  • Une reconstruction Godot a franchi 13 gates parapluie sur 13 et 511 tests Python, puis a été rejetée sur la direction produit. Elle est archivée sous un tag et conservée comme référence, non comme produit.

Limites assumées

Ce que ce projet n’est pas, écrit ici pour que personne n’ait à le découvrir plus tard.

  • Rien ici n’est publié. La route de release candidate actuelle est une décision d’architecture proposée, non acceptée, et trois gates d’acceptation de gameplay restent ouvertes.
  • La parité de campagne canonique est bloquée. Le runtime commercial est protégé et son observation est hors périmètre : aucune protection n’est contournée et aucun binaire cracké ne constitue une preuve recevable.
  • L’approbation artistique appartient au seul propriétaire. Le paquet de revue est complet et vérifié en intégrité ; l’automatisation ne peut pas enregistrer une approbation, et cette page n’en suggérera pas.
  • L’exécution native sous Windows est absente. Les candidats Windows ont démarré sans affichage via une couche de compatibilité : preuve utile, mais pas une validation Windows native.
  • Le playtest humain d’un build exporté n’a pas eu lieu. Des événements d’entrée synthétisés dans un arbre de projet ne couvrent pas une personne au clavier.
  • Plusieurs liaisons de contenu d’origine — effectif et placement exacts, corrélation des clips d’action, liaisons d’événements audio, sélection multi-mission — restent partielles ou inconnues, et le comportement de remplacement demeure étiqueté comme design.
  • Le moteur prototype n’est pas le moteur commercial. Une régression comportementale entre les deux est prouvée et corrigée côté données ; d’autres ne peuvent être exclues, la section de code commerciale étant chiffrée et ne devant pas être déchiffrée.

Une preuve immuable, avant toute compréhension

La première opération n’est pas le décodage. C’est la prise d’une copie en lecture seule du média fourni par le joueur et le hachage de chaque fichier dans un inventaire, afin que chaque affirmation ultérieure puisse nommer les octets dont elle provient. Le média lui-même ne quitte jamais la machine où il a été importé, et le build n’en distribue rien : l’importateur est une frontière « bring your own data », et c’est la raison pour laquelle ce projet peut publier un dossier technique sans publier le jeu de qui que ce soit.

Les formats sont analysés à une frontière contrôlée

Les formats binaires d’un média de 1999 sont une entrée hostile, et un parseur qui devine à travers la corruption produit un non-sens assuré. Le lecteur est borné, sans le moindre accesseur non contrôlé, et un refus porte l’offset, la taille demandée et la taille disponible — c’est ce qui fait d’un fichier malformé un fait diagnosticable plutôt qu’un plantage.

CODE DU DÉPÔTnative/include/abr/core/reader.hpp:10-46@c9436993ababC++
/// Bounds-checked little-endian cursor over a borrowed byte range. Every read
/// is checked; there is no unchecked accessor by design — binary formats from
/// 1999 media are hostile input.
class Reader {
public:
    explicit Reader(ByteView data, std::size_t offset = 0) noexcept
        : data_(data), offset_(offset) {}

    [[nodiscard]] std::size_t offset() const noexcept { return offset_; }
    [[nodiscard]] std::size_t size() const noexcept { return data_.size(); }
    [[nodiscard]] std::size_t remaining() const noexcept {
        return offset_ <= data_.size() ? data_.size() - offset_ : 0;
    }
    [[nodiscard]] bool has(std::size_t n) const noexcept { return remaining() >= n; }

    Status seek(std::size_t offset) noexcept {
        if (offset > data_.size()) {
            return Error("reader-seek-out-of-bounds")
                .with("offset", offset)
                .with("size", data_.size());
        }
        offset_ = offset;
        return {};
    }
    Status skip(std::size_t n) noexcept { return seek(offset_ + n); }

    Result<ByteView> bytes(std::size_t n) noexcept {
        if (!has(n)) {
            return Error("reader-out-of-bounds")
                .with("need", n)
                .with("remaining", remaining())
                .with("offset", offset_);
        }
        ByteView out = data_.subspan(offset_, n);
        offset_ += n;
        return out;
    }

Chaque lecture retourne un résultat susceptible d’échouer, et l’échec nomme l’endroit où il s’est produit. Le commentaire au-dessus de la classe énonce la politique au lieu de la laisser déduire de l’absence de surcharge non contrôlée.

Un paquet normalisé qui garde ce qu’il ne sait pas expliquer

La matière décodée aboutit dans un paquet unique adressé par contenu, doté d’un index typé : maps, banques d’environnement, tuiles, maillages et leurs poses, animations, audio, tables de chaînes, définitions d’unités et d’objets, gabarits de mission, état de campagne, palettes et tables de règles. Le type qui compte le plus est le dernier.

CODE DU DÉPÔTnative/include/abr/io/abrpack.hpp:21-41@c9436993ababC++
enum class ResourceType : u16 {
    Map = 1,
    EnvironmentBank = 2,
    Tile = 3,
    Bitmap = 4,
    Texture = 5,
    Mesh = 6,
    MeshPose = 7,
    Animation = 8,
    Audio = 9,
    Music = 10,
    Fmv = 11,
    StringTable = 12,
    UnitDefinition = 13,
    ItemDefinition = 14,
    MissionTemplate = 15,
    CampaignState = 16,
    UiResource = 17,
    Palette = 18,
    RuleTable = 19,
    UnknownPreservedBlock = 20,

UnknownPreservedBlock est un type de ressource de plein droit. La matière que personne n’a encore expliquée est portée dans le paquet sous son propre type au lieu d’être jetée : un parseur ultérieur peut la reprendre sans retourner au média d’origine.

Le moteur d’origine est devenu l’instrument

Un build autorisé de septembre 1999 du moteur démarre, rend l’image et se joue. Cela a déplacé la question directrice de « comment exprimer ces données dans un moteur moderne ? » vers « comment le runtime d’origine interprétait-il ces données ? », et a reconverti une longue liste d’inventions de design en grandeurs mesurables. Le moteur est désormais l’oracle comportemental : rendu, entrées, déplacement, cadence d’animation, tick de simulation, correspondance des événements audio et navigation ne peuvent plus être inventés là où l’oracle est observable.

CODE DU DÉPÔTtools/forensics/prototype_pointer.py:2-15@c9436993ababPython
"""Measure the host-pointer -> engine-logical-pointer transform, exactly.

The engine answers the pointer visibly: hovering a control changes pixels in its
framebuffer. That makes the transform measurable without guessing at Wine's
window placement. For a control whose highlight is found at frame-space
bounding box B, bisecting the host coordinate at which the highlight switches on
gives the same edge in host space; the difference of the two edges is the
translation, and comparing two edges a known distance apart proves the scale.

No hardcoded desktop offset: the caller names a control to probe, and the
transform comes out of the engine's own response.

    .venv/bin/python -m tools.forensics.prototype_pointer --display :94 \
        --probe 707,367 --region 625,340,170,60

La transformation du pointeur est dérivée de la réponse visible du moteur lui-même, non d’une hypothèse sur le placement de fenêtre : sonder un contrôle, dichotomiser la coordonnée hôte à laquelle sa surbrillance s’allume, et la translation comme l’échelle tombent de la mesure. Aucun décalage de bureau codé en dur ne survit à cette méthode.

Une couche de compatibilité translate ; elle ne rééchantillonne jamais

Le moteur dessine dans des surfaces logiques fixes et lit des coordonnées de pointeur absolues dans celles-ci à l’échelle 1:1. La surface de compatibilité en devient petite et honnête : décider où ces pixels atterrissent sur un affichage moderne, remapper le pointeur hôte vers eux, et ne rien changer à ce qui est dessiné. Chaque écart que cela impose est consigné comme concession de compatibilité, non décrit comme de la fidélité.

CODE DU DÉPÔTcompat/include/abrcompat/presentation.hpp:8-34@c9436993ababC++
/// How the engine's logical surface is placed inside the host window.
///
/// The original engine renders 800x600 menus and a 640x480 playfield, both
/// 16-bit, and reads absolute pointer coordinates in those surfaces at scale
/// 1:1 (docs/prototype/PROTOTYPE_POINTER_MAPPING.md). Nothing here changes what
/// it renders; this only decides where those pixels land on a modern display
/// and how a host pointer maps back into them.
enum class ScaleMode {
    IntegerScale,   ///< largest whole multiple that fits; never crops
    Fit,            ///< largest proportional fit; letterboxed
    OriginalSize,   ///< 1:1, centred
};

/// The rectangle the logical surface occupies in the host window, in host
/// pixels. Everything outside it is border and must never behave like the
/// game's own screen edge.
struct Viewport {
    int x{};
    int y{};
    int width{};
    int height{};
    double scale{1.0};

    [[nodiscard]] constexpr bool contains(int hx, int hy) const {
        return hx >= x && hy >= y && hx < x + width && hy < y + height;
    }
};

Une fenêtre plus petite que la surface logique obtient tout de même un viewport : la mise à l’échelle entière retombe à 1 et la surface est rognée plutôt que rééchantillonnée, ce qui garde la correspondance de pointeur exacte au lieu de deviner une fraction. La bordure n’est explicitement pas le bord d’écran du jeu.

Le déterminisme est un substrat, pas un espoir

Une simulation de référence existe comme spécification exécutable plutôt que comme documentation : un générateur entier à graine, une sérialisation canonique, un hash d’état sur celle-ci et une simulation à tick fixe dont toute autre implémentation doit reproduire les vecteurs dorés bit pour bit. Ses règles de remplissage ne portent aucune affirmation sur le jeu d’origine, et le fichier le dit — c’est précisément ce qui garde le substrat séparable de la sémantique encore en cours de récupération.

CODE DU DÉPÔTcore/refsim.py:46-62@c9436993ababPython
# --------------------------------------------------------------- hash ------
def fnv1a32(data: bytes) -> int:
    h = 0x811C9DC5
    for b in data:
        h ^= b
        h = (h * 0x01000193) & MASK32
    return h


def canonical(state: dict) -> bytes:
    """Canonical serialization: JSON, sorted keys, no whitespace, ASCII."""
    return json.dumps(state, sort_keys=True, separators=(",", ":"),
                      ensure_ascii=True).encode("ascii")


def state_hash(state: dict) -> int:
    return fnv1a32(canonical(state))

La canonicalisation est déclarée avant le hash : clés triées, aucun espace, ASCII. Sans cette déclaration, un hash d’état ne fait que convertir le non-déterminisme en hexadécimal — d’où la présence de la sérialisation et du condensat dans les mêmes dix-sept lignes.

Les gates qu’une machine peut signer, et celles qu’elle ne peut pas

Les gates automatisées couvrent les fixtures de parseurs, les builds propres, le déterminisme, la sauvegarde et le rechargement dans un processus neuf, la continuation de campagne, l’audio, la gestion de l’affichage et la sortie propre. Elles sont nécessaires et ne suffisent pas. L’approbation artistique, le playtest humain complet d’un build exporté et la validation Windows native sont des gates distinctes qu’aucune exécution automatisée ne peut enregistrer, et elles sont ouvertes.

CODE DU DÉPÔTnative/tests/formats/test_reader.cpp:12-34@c9436993ababC++
TEST_CASE("reader refuses reads past the end") {
    const std::array<u8, 4> data{1, 2, 3, 4};
    Reader r(ByteView(data.data(), data.size()));
    CHECK(r.u32v().ok());
    CHECK_FALSE(r.u8v().ok());
    CHECK(r.u8v().error().reason() == "reader-out-of-bounds");
}

TEST_CASE("reader decodes little-endian scalars") {
    const std::array<u8, 8> data{0x78, 0x56, 0x34, 0x12, 0xFF, 0xFF, 0x00, 0x00};
    Reader r(ByteView(data.data(), data.size()));
    CHECK(r.u32v().value() == 0x12345678u);
    CHECK(r.u16v().value() == 0xFFFFu);
    CHECK(r.u16v().value() == 0u);
    CHECK(r.remaining() == 0);
}

TEST_CASE("reader seek is bounds checked") {
    const std::array<u8, 4> data{1, 2, 3, 4};
    Reader r(ByteView(data.data(), data.size()));
    CHECK(r.seek(4).ok());
    CHECK_FALSE(r.seek(5).ok());
}

Les tests de parseur affirment les refus, pas seulement les succès : les lectures au-delà de la fin échouent avec une raison nommée, les positionnements sont bornés, et une chaîne de largeur fixe est tronquée au premier NUL. Un test de format qui ne prouve que le chemin heureux ne prouve presque rien sur une entrée hostile.

La route certifiée, puis rejetée

Une reconstruction antérieure sur moteur moderne a franchi treize gates parapluie et 511 tests Python, produit des rejeux déterministes et des candidats Linux et Windows, et a été rejetée sur la direction produit. Le rejet n’était pas une liste de défauts. La piste d’audit l’explique : une projection isométrique jamais mesurée, des tuiles d’environnement re-rasterisées sous des facteurs de luminosité inventés, des unités cuites en atlas de sprites depuis une caméra conçue, des statistiques jamais récupérées, et une structure de campagne que la preuve de sauvegarde contredit. Chacun de ces points se défend comme décision de portage. Ensemble, ils produisent un jeu inspiré de l’original plutôt qu’un jeu qui est l’original.

Ce travail est archivé sous un tag et conservé comme implémentation de référence, banc de test de formats et recherche de rendu. Ce n’est pas le produit, et ce n’est pas supprimé : une route rejetée dont la piste d’audit est complète est plus utile qu’une route rejetée que personne ne peut inspecter.