A deterministic conversion pipeline that turns retail 1997 game data into a standalone UZDoom package: the original maps, objects, mission structure and gameplay values are converted programmatically and never redrawn, and the original executable is run under observation as a measuring instrument rather than quoted from memory.
Mission profile
- Extract and hash-lock the retail tree, keeping a pristine copy beside the patched one and recording the delta between them.
- Decode the original map grid, object table and mission configuration, preserving unknown bytes rather than normalizing them away.
- Convert the intermediate representation into UDMF geometry deterministically, with provenance comments on the generated entities.
- Run the original executable under observation to measure the quantities that would otherwise be guessed.
- Keep the target engine stock and pinned by hash, so a fork is a measured last resort and not a convenience.
Architecture
Verified state
What exists and has been exercised at the time of writing.
- The retail media is inventoried and hash-locked: a pristine tree and a patched tree are kept separately, the delta between them is recorded, and 21 single-player plus 21 multiplayer missions are manifested.
- The map grid format is decoded to the byte: a 64 × 64 grid of 22-byte cell records, with the raw bytes of every cell preserved and the still-unexplained fields named rather than dropped.
- Mission 1 converts end to end: 4,096 cells decoded, 340 walkable cells becoming 340 sectors, 461 vertices, 808 linedefs, 1,360 sidedefs, 28 of 28 texture references resolved and 63 things emitted.
- Corner ordering was decided by measurement, not by preference: a vertical edge-continuity test across the whole map corpus prefers one labelling by more than an order of magnitude.
- Slopes are converted as explicit plane equations for planar quads, with non-planar cells split into triangles; the diagonal choice is still labelled a hypothesis.
- The original executable runs under an isolated compatibility prefix on a private display, and its process memory is read to measure the tick rate, grid scale and movement speed.
- Calibration turned assumptions into verdicts: the height scale was confirmed, the cell scale and the sprite scale were shown to be calibrated-equivalent, the projectile speed mapping was confirmed against the executable, and two beliefs — the music track mapping and the starting weapon — were rejected and corrected.
- The build is deterministic and gated: format and conversion suites, a double build compared for byte identity, and a headless runtime smoke that asserts on screenshots and logs.
Stated limits
What this project is not, written here so nobody has to discover it later.
- One mission is converted. Per-level mission data generation covers that mission only, and campaign-wide conversion of the remaining twenty has not been done.
- This is not a release. There is no distribution page, no package to obtain and no owner acceptance of the reconstruction as a game.
- Several format questions remain open and are listed as open: two texture-slot usage rules, the exact runtime behaviour of four flag bits, two object types, the teleporter special constants and the barrier animation.
- Projectile flight timing was measured indirectly. Direct timing is blocked because firing crashes the original under the current observation host, reproduced three times.
- Retail assets are used through the pipeline on the machine that holds them. Nothing proprietary is redistributed, and no public repository link is published for this project.
- The multiplayer variants are inventoried, not implemented. The manifest counts them; the runtime does not carry them.
Retail media, inventoried and frozen
The source is a mixed-mode disc: an installer-packed data track beside CD audio. The extraction produces two vendor trees kept side by side — one pristine, one patched — with a recorded delta between them, so a later finding can be attributed to the game or to the patch instead of to a merged pile. Both are read-only and hash-locked, and the environment check refuses to build against a tree whose hashes have moved.
The formats, decoded with their unknowns kept
Three formats carry the game: a fixed-size map grid, a 600-record object table with used-slot semantics, and per-mission configuration files. The map parser is where the discipline shows. Its module docstring is the format specification, each field carries the evidence that established it, and the fields nobody has explained are named and preserved rather than skipped.
"""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
"""Note what the specification admits: an assignment marked HYPOTHESIS, a byte that is zero everywhere except two stray cells in one map, four bytes that are constant across every cell of every map, and a flag word whose bits were read out of the original executable at a named address. The unexplained material is documented, not rounded off.
Corner order was measured, not chosen
Each cell stores four corner height pairs, and nothing in the file says which pair is which corner. Guessing produces a map that looks nearly right and is wrong at every seam. Instead the labelling was decided by a property the data must satisfy: adjacent walkable cells share an edge, so the correct labelling makes those shared edges continuous far more often than any alternative.
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)The test scores two candidate labellings across the whole map corpus and asserts that the chosen one wins by more than an order of magnitude. It is a permanent regression test as much as a finding: a future parser change that quietly breaks the corner order fails here rather than in a screenshot.
Slopes as plane equations
Once the corners are known, a cell whose four corner heights lie on a plane becomes one sector with an explicit floor and ceiling plane equation. A cell whose corners do not lie on a plane cannot be represented that way at all, so it is split into two triangular sectors along a diagonal — and that diagonal is recorded as a hypothesis, because the original data does not state it.
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)
The normalization and the sign convention are the whole function: a floor plane and a ceiling plane through the same three points differ only in orientation, and getting that wrong inverts a surface instead of failing. The conversion reads only the intermediate representation, never the original file.
The original runtime as a measuring instrument
Reading a value out of a data file tells you the number. It does not tell you the unit. The original executable is therefore launched under an isolated compatibility prefix on a private display, driven programmatically, and read through its own process memory at known static addresses — which is possible only because the observed process is a descendant of the harness. What comes back is a tick rate, a grid scale and a walking speed in the original's own units.
"""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)
"""The docstring is the protocol: the display depth the original renderer requires, the audio driver that must be disabled to avoid a freeze, the case-merged tree a Windows-era program expects, and the two static addresses the measurements are read from. An oracle is a documented environment plus documented addresses, not a vague reference run.
What the measurements did to the assumptions
The project keeps a burn-down of every assumption it has ever made, and the oracle changed most of their verdicts. The useful part is that the verdicts are not all “confirmed”: a calibrated-equivalent conversion factor, a confirmed scale, a rejected belief that had to be replaced, and several items still marked unknown all sit in the same table.
- Cell scale
- Calibrated equivalent — the original grid measures 256 units per cell; the conversion's 128 map units survive as an exact factor, not as a preference.
- Height scale
- Confirmed — standing height at stepped cells resolves the height unit against the measured cell size.
- Tick rate
- Measured — 36 Hz in the original against 35 in the target engine; every speed carries that ratio.
- Projectile speed
- Confirmed against the executable — the same units per tick as walking, so the mapping is dimensional rather than tuned.
- Music track mapping
- Rejected and fixed — configuration data proves direct track numbering; the previous offset belief was wrong.
- Starting weapon
- Rejected and fixed — the oracle shows a different weapon in hand at spawn than the reconstruction had assumed.
- Teleport cooldown
- Rejected and replaced — the executable proves cell-transition and consumed-charge semantics; a permanent regression test now holds them.
- Two texture-slot rules
- Unknown — unchanged, and listed as unknown rather than filled with a plausible answer.
Stock engine, pinned by hash
The target is a stock engine build, fetched by a bootstrap tool and pinned by SHA-256, never committed and never patched. Everything the reconstruction needs lives in the package: a handwritten runtime layer of scripts and definitions beside machine-generated mission tables that are never hand-edited. Forking the engine remains available and unused: it is what a measured blocker would justify, not what a difficult afternoon would justify.