Reconstruction et programme de parité du commerce vers UZDoom

Rebel Moon Rising: Pomegranate Reconstruction

La fidélité archéologique, c’est une géométrie fonction des octets d’origine, des vitesses fonction d’une mesure, et tout ce qui est inventé écrit comme tel.

PythonUDMF / ZScriptUZDoom 5.0.0 (stock, hash-pinned)pytestWine + XvfbProcess-memory readingSHA-256 manifests
Statut
Programme de parité / une mission convertie, calibrée et jouable
Code
RMR-16
Domaine
Reconstruction et programme de parité du commerce vers UZDoom
Porté par
Pomegranate Interactive
Moteur
UZDoom 5.0.0 · standard · épinglé par hash
Méthode
Ingénierie de reconstruction
Données source
Média commercial · non redistribué

Un pipeline de conversion déterministe qui transforme des données de jeu commerciales de 1997 en un paquet UZDoom autonome : cartes, objets, structure de mission et valeurs de gameplay d’origine sont convertis par programme et jamais redessinés, et l’exécutable d’origine tourne sous observation comme instrument de mesure plutôt que d’être cité de mémoire.

42Missions commerciales inventoriées (21 SP + 21 MP)
340Secteurs générés depuis 4 096 cellules décodées
36 HzTick d’origine, mesuré et non supposé
1Mission convertie de bout en bout

Profil de mission

  • Extraire et verrouiller par hash l’arbre commercial, en gardant une copie vierge à côté de la copie patchée et en consignant le delta entre les deux.
  • Décoder la grille de carte, la table d’objets et la configuration de mission d’origine, en préservant les octets inconnus au lieu de les normaliser.
  • Convertir la représentation intermédiaire en géométrie UDMF de façon déterministe, avec des commentaires de provenance sur les entités générées.
  • Exécuter le binaire d’origine sous observation pour mesurer les grandeurs qui seraient sinon devinées.
  • Garder le moteur cible standard et épinglé par hash, afin qu’un fork soit un dernier recours mesuré et non une commodité.

Architecture

État vérifié

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

  • Le média commercial est inventorié et verrouillé par hash : un arbre vierge et un arbre patché sont conservés séparément, le delta entre eux est consigné, et 21 missions solo plus 21 missions multijoueur sont manifestées.
  • Le format de grille de carte est décodé à l’octet : une grille 64 × 64 d’enregistrements de cellule de 22 octets, les octets bruts de chaque cellule préservés et les champs encore inexpliqués nommés plutôt que jetés.
  • La mission 1 se convertit de bout en bout : 4 096 cellules décodées, 340 cellules praticables devenues 340 secteurs, 461 sommets, 808 linedefs, 1 360 sidedefs, 28 références de texture résolues sur 28 et 63 « things » émis.
  • L’ordre des coins a été décidé par la mesure, non par préférence : un test de continuité d’arête verticale sur tout le corpus de cartes préfère un étiquetage de plus d’un ordre de grandeur.
  • Les pentes sont converties en équations de plan explicites pour les quads planaires, les cellules non planaires étant découpées en triangles ; le choix de la diagonale reste étiqueté hypothèse.
  • L’exécutable d’origine tourne sous un préfixe de compatibilité isolé sur un affichage privé, et sa mémoire de processus est lue pour mesurer la fréquence de tick, l’échelle de grille et la vitesse de déplacement.
  • La calibration a converti des hypothèses en verdicts : l’échelle de hauteur confirmée, l’échelle de cellule et l’échelle de sprite montrées équivalentes après calibration, la correspondance de vitesse de projectile confirmée contre l’exécutable, et deux croyances — la correspondance des pistes musicales et l’arme de départ — rejetées puis corrigées.
  • Le build est déterministe et sous gates : suites de formats et de conversion, double build comparé pour identité octet à octet, et fumée d’exécution sans affichage qui assère sur captures et journaux.

Limites assumées

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

  • Une mission est convertie. La génération de données de mission par niveau ne couvre que celle-là, et la conversion des vingt autres n’a pas été faite.
  • Ce n’est pas une sortie. Il n’y a ni page de distribution, ni paquet à obtenir, ni acceptation du propriétaire de la reconstruction en tant que jeu.
  • Plusieurs questions de format restent ouvertes et sont listées comme telles : deux règles d’usage d’emplacements de texture, le comportement runtime exact de quatre bits de drapeau, deux types d’objets, les constantes spéciales de téléporteur et l’animation des barrières.
  • Le temps de vol des projectiles a été mesuré indirectement. La mesure directe est bloquée : le tir fait planter l’original sur l’hôte d’observation actuel, reproduit trois fois.
  • Les assets commerciaux sont utilisés à travers le pipeline sur la machine qui les détient. Rien de propriétaire n’est redistribué, et aucun lien de dépôt public n’est publié pour ce projet.
  • Les variantes multijoueur sont inventoriées, non implémentées. Le manifeste les compte ; le runtime ne les porte pas.

Média commercial, inventorié et gelé

La source est un disque en mode mixte : une piste de données empaquetée par un installateur, à côté de l’audio CD. L’extraction produit deux arbres fournisseurs conservés côte à côte — l’un vierge, l’autre patché — avec un delta consigné entre eux, de sorte qu’une découverte ultérieure puisse être attribuée au jeu ou au correctif plutôt qu’à un tas fusionné. Les deux sont en lecture seule et verrouillés par hash, et le contrôle d’environnement refuse de construire sur un arbre dont les hashs ont bougé.

Les formats, décodés avec leurs inconnues conservées

Trois formats portent le jeu : une grille de carte de taille fixe, une table d’objets de 600 enregistrements à sémantique d’emplacement utilisé, et des fichiers de configuration par mission. C’est dans le parseur de carte que la discipline se voit. Sa docstring de module est la spécification du format, chaque champ porte la preuve qui l’a établi, et les champs que personne n’a expliqués sont nommés et préservés plutôt qu’ignorés.

CODE DU DÉPÔTtools/rmrtool/rmrtool/formats/threede.py:1-37@3a58dbf8a6e5Python
"""Parser for Rebel Moon Rising .3DE map files.

Layout (derived from the 11 supplied .3DE files, all 90,140 bytes):

    offset 0:  u16le width  (always 64)
    offset 2:  u16le height (always 64)
    offset 4:  4 bytes: 74 63 47 00 — constant across all files (magic/version)
    offset 8:  20 bytes: zero in all supplied files (reserved)
    offset 28: width*height cell records, 22 bytes each, row-major (y*64+x)

Cell record (22 bytes) — see docs/RMR_FORMATS.md for the evidence:

    [0]      wall texture (face A)          \
    [1]      wall texture (face B)           | level INI [TEXTURES] ids
    [2]      ceiling texture                 | (HYPOTHESIS: [2]/[21] floor/ceil
    [3]      wall texture (face C)           |  assignment; see ASSUMPTIONS.md)
    [4]      wall texture (face D)          /
    [5..12]  4 x (ceilH:u8, floorH:s8) corner height pairs, corner order
             NW, SE, NE, SW (edge-continuity test over all maps: 554 vs ~12
             vertical adjacency matches for the alternatives).
             walkable iff ceilH > floorH
    [13]     unknown (0 everywhere except 2 stray cells in LVL12)
    [14..17] FF FF FF FF in every cell of every map (runtime fields)
    [18..19] u16 wall/barrier flag word. RUNTIME-DERIVED: Rmr.exe 0x436ae0
             clears it for all cells and rebuilds it from type-4 OLS records
             (cell[u16@18] = rec.u16@16 at cell rec.u16@28). Bits:
               0x000F per-side wall-solid bits
               0x0040 unknown attribute (tested at 0x436cf4)
               0x0100 wall attribute, propagated to wall descriptors
                      (present on switch-openable barriers)
               0x0200 attribute on phase/objective-gated barriers
               0x0400 barrier: force all four sides solid (0x436bdb)
               0x1000 attribute (LVL2 only in shipped data)
    [20]     barrier face texture id (matches type-4 rec.u16@20 in 6/8
             LVL0 barriers; doors differ - runtime texture swap suspected)
    [21]     ceiling texture slot 2
"""

Notez ce que la spécification admet : une affectation marquée HYPOTHESIS, un octet nul partout sauf dans deux cellules isolées d’une carte, quatre octets constants dans chaque cellule de chaque carte, et un mot de drapeaux dont les bits ont été lus dans l’exécutable d’origine à une adresse nommée. La matière inexpliquée est documentée, non arrondie.

L’ordre des coins a été mesuré, non choisi

Chaque cellule stocke quatre paires de hauteurs de coin, et rien dans le fichier ne dit quelle paire correspond à quel coin. Deviner produit une carte presque juste et fausse à chaque jointure. L’étiquetage a donc été décidé par une propriété que les données doivent satisfaire : des cellules praticables adjacentes partagent une arête, si bien que le bon étiquetage rend ces arêtes partagées continues bien plus souvent que toute autre variante.

CODE DU DÉPÔTtests/formats/test_phase2.py:23-45@3a58dbf8a6e5Python
def test_corner_order_edge_continuity():
    """Slots (5/6, 7/8, 9/10, 11/12) map to corners NW, SE, NE, SW: the
    vertical-adjacency continuity test must decisively prefer this labeling."""
    def score(perm):
        n = 0
        for m in _maps():
            for y in range(60):
                for x in range(64):
                    a, b = m.cell(x, y), m.cell(x, y + 1)
                    if not (a.walkable and b.walkable):
                        continue
                    fa = (a.raw[6], a.raw[8], a.raw[10], a.raw[12])
                    fb = (b.raw[6], b.raw[8], b.raw[10], b.raw[12])
                    if len(set(fa)) == 1 and len(set(fb)) == 1:
                        continue
                    A = {c: fa[s] for s, c in enumerate(perm)}
                    B = {c: fb[s] for s, c in enumerate(perm)}
                    if A[2] == B[0] and A[3] == B[1]:   # A south == B north
                        n += 1
        return n
    ours = score((0, 3, 1, 2))       # NW, SE, NE, SW
    other = score((0, 1, 3, 2))
    assert ours > 400 and ours > 10 * max(other, 1)

Le test note deux étiquetages candidats sur tout le corpus de cartes et affirme que celui retenu l’emporte de plus d’un ordre de grandeur. C’est un test de régression permanent autant qu’une découverte : un futur changement de parseur qui casserait discrètement l’ordre des coins échoue ici plutôt que dans une capture d’écran.

Les pentes comme équations de plan

Une fois les coins connus, une cellule dont les quatre hauteurs de coin sont coplanaires devient un secteur unique doté d’équations de plan explicites pour le sol et le plafond. Une cellule dont les coins ne sont pas coplanaires ne peut pas être représentée ainsi : elle est découpée en deux secteurs triangulaires le long d’une diagonale — et cette diagonale est consignée comme hypothèse, car les données d’origine ne l’énoncent pas.

CODE DU DÉPÔTtools/rmrtool/rmrtool/convert/udmf.py:86-102@3a58dbf8a6e5Python
def plane_from(p1, p2, p3, invert: bool):
    """UDMF plane equation a*x+b*y+c*z+d=0 through 3 (x,y,z) points,
    normalized, with c>0 for floors (invert=False) / c<0 for ceilings."""
    ux, uy, uz = (p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2])
    vx, vy, vz = (p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2])
    a = uy * vz - uz * vy
    b = uz * vx - ux * vz
    c = ux * vy - uy * vx
    ln = (a * a + b * b + c * c) ** 0.5
    if ln == 0:
        return None
    a, b, c = a / ln, b / ln, c / ln
    if (c < 0) != invert:
        a, b, c = -a, -b, -c
    d = -(a * p1[0] + b * p1[1] + c * p1[2])
    return (a, b, c, d)

La normalisation et la convention de signe font toute la fonction : un plan de sol et un plan de plafond passant par les trois mêmes points ne diffèrent que par l’orientation, et se tromper là-dessus inverse une surface au lieu d’échouer. La conversion ne lit que la représentation intermédiaire, jamais le fichier d’origine.

Le runtime d’origine comme instrument de mesure

Lire une valeur dans un fichier de données donne le nombre. Cela ne donne pas l’unité. L’exécutable d’origine est donc lancé sous un préfixe de compatibilité isolé sur un affichage privé, piloté par programme, et lu à travers sa propre mémoire de processus à des adresses statiques connues — possible uniquement parce que le processus observé est un descendant du harnais. Ce qui en revient est une fréquence de tick, une échelle de grille et une vitesse de marche dans les unités de l’original.

CODE DU DÉPÔTtools/oracle/oracle.py:2-18@3a58dbf8a6e5Python
"""Original-runtime measurement harness.

Launches the retail Rmr.exe (patched tree copy) under an isolated Wine
prefix on a private 16-bpp Xvfb, drives it with xdotool, and reads the
game's memory via /proc/<pid>/mem (the game is our descendant, satisfying
yama ptrace_scope=1).

Environment requirements (reports/original_runtime/original_runtime_setup.md):
  - Xvfb 16 bpp (DirectDraw needs 640x480x16)
  - Wine audio driver disabled (weapon-fire freeze otherwise)
  - case-merged game tree (one directory per Windows directory)

Known static addresses (Rmr.exe, base 0x400000, no ASLR under Wine for
this non-relocatable 1997 PE):
  0x462c88  pointer to cell data (+0 = 3DE header W,H; cells at +0x1c)
  0x4584e0  OLS live array (600 x 44)
"""

La docstring est le protocole : la profondeur d’affichage qu’exige le moteur de rendu d’origine, le pilote audio qu’il faut désactiver pour éviter un gel, l’arbre à casse fusionnée qu’attend un programme d’époque Windows, et les deux adresses statiques d’où les mesures sont lues. Un oracle est un environnement documenté plus des adresses documentées, non une exécution de référence vague.

Ce que les mesures ont fait aux hypothèses

Le projet tient un burn-down de chaque hypothèse jamais formulée, et l’oracle a changé la plupart des verdicts. L’utile est que ces verdicts ne sont pas tous « confirmé » : un facteur de conversion équivalent après calibration, une échelle confirmée, une croyance rejetée qu’il a fallu remplacer et plusieurs points encore inconnus figurent dans la même table.

Échelle de cellule
Équivalence calibrée — la grille d’origine mesure 256 unités par cellule ; les 128 unités de carte de la conversion survivent comme facteur exact, non comme préférence.
Échelle de hauteur
Confirmée — la hauteur debout sur des cellules à marche résout l’unité de hauteur contre la taille de cellule mesurée.
Fréquence de tick
Mesurée — 36 Hz dans l’original contre 35 dans le moteur cible ; chaque vitesse porte ce rapport.
Vitesse de projectile
Confirmée contre l’exécutable — mêmes unités par tick que la marche : la correspondance est dimensionnelle, non réglée à l’oreille.
Correspondance des pistes musicales
Rejetée et corrigée — les données de configuration prouvent une numérotation directe ; la croyance de décalage antérieure était fausse.
Arme de départ
Rejetée et corrigée — l’oracle montre au spawn une arme différente de celle que la reconstruction supposait.
Recharge de téléporteur
Rejetée et remplacée — l’exécutable prouve une sémantique de transition de cellule et de charge consommée ; un test de régression permanent la tient désormais.
Deux règles d’emplacement de texture
Inconnues — inchangées, et listées comme inconnues plutôt que comblées par une réponse plausible.

Moteur standard, épinglé par hash

La cible est un build de moteur standard, récupéré par un outil d’amorçage et épinglé par SHA-256, jamais commité et jamais patché. Tout ce dont la reconstruction a besoin vit dans le paquet : une couche runtime écrite à la main, scripts et définitions, à côté de tables de mission générées par machine et jamais éditées à la main. Forker le moteur reste disponible et inutilisé : c’est ce qu’un blocage mesuré justifierait, non ce qu’un après-midi difficile justifierait.