A reverse-engineering programme that decodes a 1999 tactical game from player-supplied media, holds its findings to an explicit evidence grade, and now uses an authorized build of the original engine as a behavioural oracle rather than continuing to invent the answers it could measure.
Mission profile
- Take an immutable copy of player-supplied media and hash every byte before anything is decoded.
- Parse the original formats at a checked boundary, preserving the blocks nobody has explained yet.
- Measure the original runtime's behaviour where it can be observed lawfully, and label everything else as inference.
- Keep a deterministic simulation core with canonical state hashing, separate from any presentation layer.
- Separate the gates a machine may sign from the gates only a human may sign, and refuse to blur them.
Architecture
Verified state
What exists and has been exercised at the time of writing.
- Original media is never redistributed. The player supplies legally held media, an isolated importer converts it locally, and the hash inventory records what was read.
- Format work is graded, not asserted: the project states 95% coverage overall, and specifies the map terminal block to 40.8% of its non-padding bytes rather than rounding that up.
- The parsers fail at the boundary. There is no unchecked accessor in the reader by design, and every refusal reports the offset it refused at.
- The normalized pack carries a resource type for material nobody has explained yet, so unknown blocks travel with the data instead of being dropped.
- An authorized September 1999 build of the original engine now runs under observation and serves as the behavioural oracle; six observations are on the ledger.
- The compatibility layer translates the engine's fixed logical surfaces onto a modern display and maps the pointer back. It never resamples the geometry.
- A determinism substrate exists as executable specification: canonical serialization, a fixed-tick integer simulation and golden state-hash vectors.
- A Godot reconstruction reached 13 of 13 umbrella gates and 511 passing Python tests, and was then rejected on product direction. It is archived under a tag and kept as reference, not as the product.
Stated limits
What this project is not, written here so nobody has to discover it later.
- Nothing here is released. The current release-candidate route is a proposed architecture decision, not an accepted one, and three gameplay acceptance gates remain open.
- Canonical campaign parity is blocked. The retail runtime is protected, and observing it is out of scope: no protection is bypassed and no cracked binary is acceptable evidence.
- Artistic approval is owner-only. The review package is complete and integrity-checked; automation cannot record approval, and this page will not imply one.
- Native Windows execution is absent. Windows candidates have booted headlessly through a compatibility layer, which is useful evidence and is not a native Windows validation.
- Human playtesting of an exported build has not happened. Synthesized input events inside a project tree do not cover a person at a keyboard.
- Several original content bindings — exact roster and placement, action-clip correlation, audio event bindings, multi-mission selection — remain partial or unknown, and replacement behaviour stays labelled as design.
- The prototype engine is not the retail engine. One behavioural regression between them is proven and corrected data-side; others cannot be excluded, because the retail code section is encrypted and will not be decrypted.
Immutable evidence, before anything is understood
The first operation is not decoding. It is taking a read-only copy of the media the player supplied and hashing every file into an inventory, so that every later claim can name the bytes it came from. The media itself never leaves the machine it was imported on, and the build distributes none of it: the importer is a bring-your-own-data boundary, and it is the reason this project can publish a technical dossier without publishing anybody's game.
Formats are parsed at a checked boundary
Binary formats from 1999 media are hostile input, and a parser that guesses through corruption produces confident nonsense. The reader is bounds-checked with no unchecked accessor at all, and a refusal carries the offset, the size requested and the size available — which is what makes a malformed file a diagnosable fact rather than a crash.
/// 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;
}Every read returns a result that can fail, and the failure names where it happened. The comment above the class states the policy rather than leaving it to be inferred from the absence of an unchecked overload.
A normalized pack that keeps what it cannot explain
Decoded material lands in a single content-addressed pack with a typed index: maps, environment banks, tiles, meshes and their poses, animations, audio, string tables, unit and item definitions, mission templates, campaign state, palettes and rule tables. The type that matters most is the last one.
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 is a first-class resource type. Material nobody has explained yet is carried into the pack under its own type instead of being dropped, so a later parser can revisit it without going back to the original media.
The original engine became the instrument
An authorized September 1999 build of the engine boots, renders and plays. That changed the governing question from “how do we express this data in a modern engine?” to “how did the original runtime interpret this data?”, and it turned a long list of design inventions back into measurable quantities. The engine is now the behavioural oracle: rendering, input, movement, animation cadence, simulation tick, audio event mapping and navigation may no longer be invented where the oracle can be observed.
"""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,60The pointer transform is derived from the engine's own visible response rather than from an assumption about window placement: probe a control, bisect the host coordinate at which its highlight switches on, and the translation and scale fall out of the measurement. No hardcoded desktop offset survives that method.
A compatibility layer translates; it never resamples
The engine renders into fixed logical surfaces and reads absolute pointer coordinates in them at scale 1:1. That makes the compatibility surface small and honest: decide where those pixels land on a modern display, map a host pointer back into them, and change nothing about what is drawn. Every departure this forces is recorded as a compatibility concession rather than described as fidelity.
/// 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;
}
};A window smaller than the logical surface still gets a viewport: integer scaling falls back to 1 and the surface is clipped rather than resampled, which keeps the pointer mapping exact instead of guessing at a fraction. The border is explicitly not the game's own screen edge.
Determinism is a substrate, not a hope
A reference simulation exists as executable specification rather than documentation: a seeded integer generator, a canonical serialization, a state hash over it and a fixed-tick simulation whose golden vectors any other implementation must reproduce bit for bit. Its placeholder rules carry no claim about the original game, and the file says so — which is exactly what keeps the substrate separable from the semantics still being recovered.
# --------------------------------------------------------------- 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))The canonicalization is declared before the hash: sorted keys, no whitespace, ASCII. Without that declaration a state hash only converts nondeterminism into hexadecimal, which is why the serialization and the digest live in the same seventeen lines.
Gates a machine may sign, and gates it may not
Automated gates cover parser fixtures, clean builds, determinism, save and reload in a fresh process, campaign continuation, audio, display handling and clean exit. They are necessary and they are not sufficient. Artistic approval, complete human playtesting of an exported build and native Windows validation are separate gates that no automated run may record, and they are open.
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());
}The parser tests assert the refusals, not only the successes: reads past the end fail with a named reason, seeks are bounds-checked, and a fixed-width string trims at the first NUL. A format test that only proves the happy path proves very little about hostile input.
The route that was certified, and rejected
An earlier reconstruction on a modern engine passed thirteen umbrella gates and 511 Python tests, produced deterministic replays and Linux and Windows candidates, and was rejected at product direction. The rejection was not a defect list. The audit trail explains it: an isometric projection that was never measured, environment tiles re-rasterized under invented brightness factors, units baked into sprite atlases from a designed camera, statistics that were never recovered, and a campaign structure the save evidence contradicts. Each of those is defensible as a port decision. Together they produce a game inspired by the original rather than one that is the original.
That work is archived under a tag and kept as a reference implementation, a format test harness and renderer research. It is not the product, and it is not deleted: a rejected route with a complete audit trail is more useful than a rejected route nobody can inspect.

