Every restoration records a game’s internals in the format below, down to file names, field lists and number formatting, so that the documentation of one game reads like the documentation of any other and tools can check it. What has to be documented, and why, is covered in Methodology. How the work that produces it is planned and tracked is covered in the work protocol.
The format borrows from projects that have done parts of this well. The layout tables follow IESDP and the ModdingWiki, the canonical format definitions are Kaitai Struct, the formula entries follow the OpenMW research wiki, the bug records follow UESP’s bug template, and the way a claim is tied to an address in a hashed binary comes from reccmp and the zeldaret projects. None of them gives every claim a stable ID, a status from a fixed list and a link to its evidence, so this standard adds those.
Every restoration we have follows version 1: the specs of Chaos Overlords, Dark Sun: Wake of the Ravager and Conqueror A.D. 1086 are written to this page. Chaos Overlords and Dark Sun were documented before the standard existed and were converted in September 2026. Each of the three runs the checks listed under Checks on every change.
Reading the rules
A rule sits under a heading of its own, such as IDENTIFIERS-1: the name of its section in capitals and a number. The rule is everything from that heading to the next one, including the reasons and examples that go with it. Text between a section’s heading and its first rule introduces the section and adds no requirement of its own. A rule states what a spec or a tool does in the present tense, so “every entry has an ID” means an entry without one fails. “May” marks something a spec is allowed to do and does not have to. A check that enforces numbered rules names them in the Checks list.
Rule headings are the only sixth-level headings on the page, so grep '^###### ' over the Markdown source lists every rule, and a link to a rule’s anchor, such as #status-14, lands on that rule alone. A copy of this file in another repository keeps the same anchors.
A rule keeps its number for good. A dropped rule leaves its number unused, and a new rule takes the next free number in its section wherever it sits on the page. Numbering rules changes none of them, so it does not make a new version of the standard.
The page is being numbered one section at a time. So far that covers Identifiers, Status and the part of Entry types up to the end of Builds. Where a section or subsection has no rules under it yet, everything it says a spec does is required, as it has been since version 1.
Where it lives
Each game repository has a spec/ directory. It describes the original game and nothing else. It never names a class, file or setting from our implementation, so that someone writing a different engine can use it as it is. The implementation is the rebuild: its code, its tests, the parity matrix and the deviation log, as Implementation side describes. It points the other way: code comments, tests and the parity matrix cite spec IDs. The tools that read the original, such as a disassembler, an unpacker or a research script in the repository’s tools/, are not part of it, and a finding’s How to reproduce section may name them, as Findings describes.
spec/
README.md scope, standard version, area list
LICENSE licence texts and the files each one covers
glossary/ one file per term: the names the spec uses for game concepts
index/ by-kind, by-area, by-status and references, each a .md file or a directory, generated by the check script
builds/ BLD-*.md the exact original files the spec applies to, each with a BLD-*.files.yaml manifest, for a long list BLD-*.other-files.yaml, and optionally a BLD-*.listing.yaml record
sources/ SRC-*.md manuals, FAQs, earlier tools and other outside sources
formats/ FMT-*.md file formats and memory structures, binary ones with a matching .ksy definition
rules/ RULE-*.md game rules, formulas and procedures
findings/ FND-*.md individual pieces of evidence
experiments/ EXP-*.md controlled runs of the original, with EXP-*.json fixtures
saves/ save patches, and the rare save that can be shared
recordings/ demos and replays the original recorded, when they can be shared
bugs/ BUG-*.md defects in the original
screens/ SCR-*.md screens, panels and dialogs, with their input and sound
Every entry is one Markdown file named after its ID, with YAML front matter holding its fields and a body holding its sections. One entry per file keeps links stable, gives each entry its own history in git, and lets scripts read the whole spec without parsing prose.
spec/README.md holds, in this order: the scope, the version of this standard the spec follows, and the area list as a table with the columns Area | Covers.
The check script writes the indexes into spec/index/ as Markdown tables: by-kind.md lists entries by kind, by-area.md by area, by-status.md by status, and references.md gives, for each entry, the entries that cite or relate to it. Nobody edits them by hand. They are committed so that they can be read on GitHub, and a change that leaves them out of date fails the check. An index that would pass the size limit is split into a directory the way that section describes: spec/index/references/COMBAT/RULE.md.
A repository can instead update the indexes and PARITY.md on its main branch only. Nearly every change rewrites them, so two branches open at the same time conflict in them on almost every merge. Someone, or a scheduled job, runs the check script on the main branch and commits what it writes, and that commit is the only change that touches them. For every other change the check script neither writes them nor compares them with the spec, and it fails the change if it edits, adds or removes one of them after the point where it forked from the main branch, in place of failing one that leaves them out of date. The main branch’s copies are then as old as the last such run, and a branch’s copies are as old as the main branch it last took in. A repository chooses this with an option of the check script, set in the workflow that runs the check, so the spec itself does not record it and a spec that passes one way can be checked the other way without changes.
The glossary is the directory spec/glossary/, with one Markdown file per term, named after the term as the pseudocode spells it: spec/glossary/turn_order.md. The file opens with the term as a # heading, followed by what the term means and, where the game shows the concept to the player, the name the game uses for it. Two terms never differ only in case, because a file system that ignores case would give them the same file, and no term is a name Windows reserves for a device (con, prn, aux, nul, com1 to com9, lpt1 to lpt9, in any case), because Windows cannot create a file with that name. A glossary file is not an entry: it has no ID and no front matter, and it is renamed along with its term. A file per term gives each term its own history, and two people adding terms at the same time do not edit the same file. The rest of the entry depends on what the term names:
| Term | The entry also gives |
|---|---|
| A structure the game keeps | The ID of its format entry, and where the game keeps it. |
| A list the game keeps | The format ID of its elements, the order the game keeps them in, and where the game keeps the list. |
| Any other value the game keeps | Its type, and where the game keeps it. |
| A value from outside the game | Its type, what the original reads it from, and when it can change (see Files, resources and outside values). |
| A function, table or clock | The ID of the rule that defines it. |
| A random number generator | The ID of the rule that specifies it, and the term for its state. |
| An event or message | The arguments it carries, in order, the rules whose When it runs section names it, in the order they run, and whether they run at once or the queue the event waits on. |
| A queue | The rule that drains it, and the order it drains in if that is not the order the events were emitted. |
| A neutral name | The build its address comes from, and the neutral names the same thing has in other builds. |
Where the game keeps something is either a field of a structure, by format ID and field name, or a global, by its address in each build that has it. Where a format entry has been split by build, the term gives every entry of the split with the builds each one covers.
Some of these are claims about the original: the address of a global, the order a list is kept in, the order an event’s handlers run in, the order a queue drains in, and what the original reads an outside value from. Each is followed by the IDs of the findings or experiments that show it, in brackets, or by (unknown) until there are some. A rule whose procedure relies on one of these claims, by using the global, visiting the list, emitting the event, draining the queue or reading the value, cites that evidence in its own evidence field as well, so that its status covers the claim. While a claim it relies on is (unknown), the rule lists it in its Open questions section, and its status is no higher than supported, since a reading of the files and a run of the original cannot agree on something nobody has shown yet. A term that is renamed is renamed in the same change in its file’s name and heading and in every rule and fixture that uses it.
Three terms from the glossary the examples on this page use, each the whole of its file. spec/glossary/turn_order.md:
# turn_order
The players in the order they take their turns. A list the game keeps, of
FMT-DATA-004, in seat order starting with the human player [FND-COMBAT-011],
kept in the field `players` of FMT-SAVE-001.
spec/glossary/GangDetected.md:
# GangDetected
An event: the police have found a hidden gang. It carries `gang: FMT-DATA-005`.
Its handlers are RULE-COMBAT-008 and then RULE-AI-014, run at once
[FND-COMBAT-011].
spec/glossary/roll.md:
# roll
A function, defined by RULE-RNG-001.
The front matter schemas, the fixture schema, the manifest schema, the listing record schema, the check script and the tool that applies save patches belong to the standard, and their home is refurbished-dinosaurs-toolkit, the toolkit every restoration shares. The check script is there, as the package in packages/standard-checker with a GitHub Action that runs it, and it carries the front matter, fixture and manifest rules itself. The tool that applies save patches has not been written. These files are to be published from the toolkit as a package of their own, the spec package, versioned apart from the toolkit’s other schemas, so a change to those never forces a new version of the standard. A game repository starts from the project template and pins a version of the spec package. Its major version is the version of this standard it checks, so pinning 1.x means following version 1, and spec/README.md states the same number. Until the package is published, a repository pins a commit of the toolkit instead. This page describes version 1. Versions says when the number changes.
Each game’s spec stands on its own, even when games share an engine and file formats. An entry that another game’s spec already covers is written again in this one, and cites the other game’s entry through a source entry like any other outside document. Until this game’s own files or runs confirm it, the entry stays sourced.
The spec describes layouts and behaviour, and it holds what a strategy guide or a wiki holds: names, numbers, formulas and tables. What it never keeps is a substantial copy of the game’s content, which this page uses to mean three things. The game’s writing is the prose it shows or ships, such as dialogue, descriptions, messages, help text and the manual: an entry may quote a short passage as evidence, and refers to anything longer by resource. The game’s art is its images, sounds, music, video and maps, which the spec references by resource and never embeds. The game’s code is the executable’s instructions and the scripts its designers wrote: the spec says what they do in its own words and pseudocode, and never copies a meaningful slice of them, whether as disassembly, decompiler output or a transcription of a script. Names are not content. The names the game gives its concepts and mechanics, such as Influence or Crackdown, and the names of the things a designer made, such as a unit, an item, a site or a character, may be used anywhere in the spec: as terms, in enumeration names, and in the text of entries. Numbers are not content either. Constants the code does arithmetic with are part of the rules and are written down in full. That includes the multipliers and thresholds in a formula, tables in the executable’s data that a formula indexes by a number, such as a damage falloff curve indexed by distance or a sine table indexed by angle, and tables keyed by a game entity, such as the per-unit or per-item statistics a designer filled in. A table of more than 64 values goes in a value file. Scripts are code, and get the treatment described above. Where a game runs logic the designers wrote as bytecode in its own interpreter, the interpreter is specified like any other procedure, with rules for what each instruction does, and the script format is a format entry. The scripts themselves are not transcribed into pseudocode. A finding may name a script and say what it does as evidence about the interpreter.
File size
Every Markdown file this standard defines is at most 1,000 lines long. That covers the entries, the glossary files, spec/README.md, the deviation files and the parity files, and the files the check script writes as well. A long file is hard to review in a diff, and when many records share one file, two branches that each add a record to it conflict. Machine-readable files (Kaitai definitions, fixtures, save patches, build manifests, lists of a build’s other files and value files) are not counted, since their length follows from the original’s files or the runs they record.
Data that has a length of its own, which the entry only describes, is kept in machine-readable files beside the entry, so that it never decides the entry’s length. A build lists its files in a manifest, and a long list of the installation’s other files in a file beside it. Two kinds of table go in a value file: a list of more than 64 values in a procedure (see Rules, functions, tables and clocks), and an enumeration table that would take its format entry past the limit. A value file is a CSV file as RFC 4180 defines it, in UTF-8, in the entry’s directory, named after the entry ID and the table, with a header row of column names. An enumeration table’s value file has the table’s columns and one row per value, formats/FMT-DATA-009.unit_class.csv, and the table’s ### heading stays in the entry, followed by a sentence naming the file. A value file holds only what the entry could have held, so the rules on content apply to it unchanged.
Every table the script writes has at most one row per entry, and a row is one line: where a row lists several IDs, they go in one cell, separated by commas. A generated file that would pass the limit becomes a directory of the same name, without .md, with one file per area that has rows, named after the area, and one file, BLD-SRC.md, for builds and sources, which have no area: spec/index/references/COMBAT.md. An area’s file that would still pass becomes a directory with one file per kind, spec/index/references/COMBAT/RULE.md, and BLD-SRC.md becomes BLD-SRC/BLD.md and BLD-SRC/SRC.md. A kind’s file that would still pass becomes a directory of files that each cover a block of 100 numbers, named after the block’s first number padded to three digits: COMBAT/RULE/000.md for RULE-COMBAT-000 to RULE-COMBAT-099, and COMBAT/RULE/1000.md for RULE-COMBAT-1000 to RULE-COMBAT-1099. A builds or sources file that would still pass is split by the first character of the alias instead: BLD-SRC/SRC/M.md. A block never has more than 100 entries, so its file stays far under the limit however many entries its area gets. Only blocks with rows are written. Each file in spec/index/ and parity/ opens with its path as a # heading, relative to spec/index/ for an index and to parity/ for a parity file, without .md: # references/COMBAT/RULE/000, # COMBAT. The script splits a file only as far as the limit requires, so a spec always produces the same files, and once a kind is split into blocks, adding entries only adds blocks and never moves an entry from one file to another.
An entry that would pass the limit is split by what it describes, never cut at the line where it reached the limit into a first and second part. The entry keeps its ID, the parts moved out of it become entries with IDs of their own, and each part cites the evidence for what it says:
- A rule moves a self-contained step of its procedure into a rule of its own and
calls it there, as it would any other rule. - A format moves each structure the original keeps inside it into a format entry of its own, and its layout table types the field with that entry’s ID.
- A structure with no nested structures to move, but more fields than one file holds, moves runs of consecutive fields into format entries of their own, one for each run that the rules read or write together. Its layout table gets one row per run, typed by the new entry’s ID, at the run’s offset and with the run’s size, and the row’s Meaning says that the original keeps these fields directly in the structure. The bytes are the same, and a procedure reaches a moved field through the row’s Name:
world.occupancy.cell_count. The same change gives every procedure, fixture and save patch that names a moved field its new path. - A screen moves each panel or dialog that has states of its own into a screen entry of its own, and the effects that open it name that entry.
- An experiment whose Observations would pass the limit records what each run gave in its fixture, and the section says what was recorded and where to find each run.
Identifiers
IDENTIFIERS-1
Every entry has an ID that starts with its KIND, one of the directory prefixes above. Builds and sources follow IDENTIFIERS-4, and every other entry’s ID has the form KIND-AREA-NNN, for example RULE-COMBAT-007.
IDENTIFIERS-2
AREA is a short subsystem name from the area list in spec/README.md, such as RNG, COMBAT, AI or SAVE, made of upper-case letters and digits and starting with a letter. It never contains a hyphen, so the ID splits into its three parts at the hyphens.
IDENTIFIERS-3
NNN is a number, zero-padded to three digits and written with more once an area passes 999, unique within its kind and area.
IDENTIFIERS-4
Builds and sources use a short alias in place of area and number: BLD-GOG-EN-1.1, SRC-MANUAL-1996. An alias starts with an upper-case letter and contains only upper-case letters, digits, dots and hyphens, so in those two kinds everything after the first hyphen is the alias. No two builds or sources share an alias.
IDENTIFIERS-5
Areas are added to the list and never removed or renamed, because renaming an area would rename every ID in it. An area whose name turns out to be a poor fit keeps it, and its row in the area list says what it covers.
IDENTIFIERS-6
An ID is never reused or renumbered once it is on the main branch. If two branches create the same ID, the one merged second renumbers its entry before it is merged.
IDENTIFIERS-7
An entry that turns out to be wrong stays in place with status superseded, unless it is squashed as the end of this rule describes, and its superseded_by field names what took its place: the corrected entry, the parts it was split into, or the entry it was merged into. When nothing takes its place, because a mechanic turns out not to exist, superseded_by names the findings or experiments that show it. A bug that turns out to be intended is the one case where it may name sources as well (see Bugs).
With IDENTIFIERS-6 and IDENTIFIERS-7 together, anything that cited an old ID can still find out what happened to it.
Before anyone outside the repository relies on its IDs, a repository may squash a superseded entry instead of keeping it. The entry is deleted, its replacements keep the corrected content, and every mention of the old ID, in entries, the glossary, the parity matrix, deviations and the rebuild’s code, is changed to name a replacement or removed, along with any account in a replacement of what the old entry got wrong. The change that squashes entries lists each squashed ID with its replacements in the options the repository passes to the check script (the toolkit’s checker takes --squashed FND-AI-008=FND-AI-064, with + between several replacements and , between items), and the repository drops the option once the change is on the main branch. The check accepts the deletion only where the entry was superseded by exactly those replacements where the change started and nothing but the indexes and PARITY.md, which may be updated on the main branch only, still mentions it. A squashed ID is not reused, like any other ID (IDENTIFIERS-6).
IDENTIFIERS-8
A finding or experiment is edited in place only where the edit changes nothing it records, or corrects a range end as the end of this rule describes: spelling, grammar, formatting, a broken link, or a sentence reworded to state the same facts. Every entry that cites it then rests on the same evidence as before. A correction to anything it records, such as a constant, an address, a width, a branch condition, an order of calls, a location, the ranges a search covered, a run’s setup or results, a step in a finding’s How to reproduce section, or what its Interpretation or Conclusion says, supersedes the whole entry under IDENTIFIERS-7, even when only one of its observations was wrong. A status, a superseded_by link and a citation each apply to a whole entry, and nothing records which of its observations a claim relied on, so no part of an entry can be superseded on its own.
The observations that still hold go into a replacement entry with the corrected one, under a new ID. Where they are independent of each other and entries cite them for different claims, they may go into one replacement each, so that a later correction to one of them leaves the others alone. The old entry keeps its text as it was, and each replacement’s Alternatives section (an experiment’s Conclusion) names it, says what it recorded that was wrong and how that was found. A replacement starts as recorded with its own recorded_by, even where the old entry was reproduced, because a reproduction followed the old entry’s instructions and matched the old entry’s result (STATUS-21).
Every entry that cited the old one then fails the check until it cites, in its place, the replacements that hold what it drew on (STATUS-17), and so does every glossary claim that cited it. Whoever changes the citation checks what that entry or claim drew from the old one. A claim that rested only on what still holds keeps its status. A claim that rested on the wrong observation takes the status its remaining evidence supports (STATUS-20), and a complete_reading that needed that observation stays complete only where the replacements read that part of the code as fully as the old entry did (STATUS-4). Where the corrected observation contradicts a claim, the entry is disputed, with the replacement in conflicting.
For example, FND-COMBAT-011 records the order in which the detection loop visits gangs, and also the order in which the handlers of GangDetected run, which a later reading shows is the other way round: RULE-AI-014 runs first. FND-COMBAT-012 records the same visiting order and the corrected order of handlers, and FND-COMBAT-011 becomes superseded with superseded_by: [FND-COMBAT-012]. RULE-COMBAT-007, which emits GangDetected but depends on nothing its handlers do, now cites FND-COMBAT-012 and keeps its status. The glossary file for GangDetected lists the handlers in the corrected order and cites FND-COMBAT-012. If RULE-AI-014 relied on RULE-COMBAT-008 having already run, FND-COMBAT-012 contradicts it, and it is disputed until it is corrected.
A citation in tools/, such as the reason a research tool’s input gives for treating a routine as a leaf (one that makes no calls), is checked before it moves the way an entry’s citation is. Where the value it supports rested on the wrong observation, the value is corrected, and each finding or experiment that recorded a run made with the old value is superseded by one that records the run made again. Those entries are found by the tool and commit a finding’s How to reproduce section names, or the harness commit in an emulated-call experiment’s environment. A finding that names the tool but no commit counts as one made with the old value if it was recorded while that value stood. Runs that do not fit in the batch become queue items, one for each entry still to be superseded.
A range whose end names the last thing it covers, where Notation writes the byte after it, is the one correction to a location made in place: an end written as the last byte, or as the first byte of the last instruction, element or function the range covers. It is made only where the bytes the range was meant to cover are not in doubt: the range spans whole functions whose extents a finding records and its end is the last byte of one of them, or the entry’s own text names the item as the last one the range covers, or gives the size or the last byte. An end on the first byte of a function, instruction or element is also where a correct range ends that stops before it, so extents alone never show that such a range was meant to cover the item. The end moves to the byte after that last item, in the location and wherever the entry’s text or tables give the same range, and the entry then describes the bytes it always described. A range whose intended end is in doubt, or that the entry lists in ends_on_last_byte (ENTRY-TYPES-22), is corrected by superseding the entry. A row of a build’s Code ranges section written the same way is corrected in place under STATUS-23, since the build still identifies the same files, and in the same change as any location whose raised end would otherwise pass the row’s end.
Status
The spec records how well the original is understood. How well our rebuild matches it is recorded in the parity matrix, which uses the same statuses and adds implemented, deviated and validated. OpenMW keeps the same split between analysis status and implementation status.
STATUS-1
Rules, formats, screens and bugs describe the original, and each carries one of these statuses:
| Status | Meaning | Must cite |
|---|---|---|
unknown | Nothing is claimed yet. The entry is listed so that it is not forgotten, and may hold open readings, with the evidence on each side, in its Open questions section. | Nothing. |
sourced | Described from the manual or another outside source only. | At least one source. |
supported | Backed by direct evidence from the original of one kind: static findings, or dynamic findings and experiments. | At least one finding or experiment. |
established | A reading of the files and a run of the original agree, or a complete reading of the code shows everything the entry says. | At least one static finding, and either at least one dynamic finding or experiment, or static findings listed in complete_reading. |
disputed | Findings or experiments contradict the entry or each other. The body says which and how. | At least one finding or experiment in conflicting. |
superseded | Replaced, or shown to be wrong, by the entries in superseded_by. | Nothing further. |
The first four form a scale, from unknown up to established.
Direct and circumstantial evidence
STATUS-2
A finding is direct evidence for a claim when its observation locates what produces the behaviour: the code that reads or writes the field, takes the branch, makes the calls in that order or computes the value, or, for a dynamic finding, the value or the frame the original produced. Anything else that fits the claim is circumstantial: sizes that divide exactly, a pattern in the values of a shipped file, a name, what the manual says, or what similar games do.
STATUS-3
Circumstantial evidence is recorded as a finding like any other, and an entry names it in its Open questions section, for or against a reading, but never lists it in evidence, and it never counts towards a status. An entry whose only evidence from the original is circumstantial is unknown, or sourced if a source describes it.
Complete readings
The code decides most of what a game does, so a reading of it can establish an entry without a run.
STATUS-4
Static findings establish an entry on their own when together they read all of it in its first build, and the entry lists them in complete_reading, a field that rules, formats, screens and bugs may have and that is otherwise absent or empty. A reading is complete when:
- it covers every branch of every procedure the entry describes, and for a format every place the code reads or writes the file or structure, and its findings name the instructions they checked wherever the decompiler’s types, signedness or casts decide a result, which takes in each part that STATUS-5 to STATUS-13 names;
- it locates every caller of those procedures and every write to the state they read, and resolves every indirect call and jump they take;
- what the entry says depends on nothing the code does not decide: no point where an interrupt handler or a second thread can change the result (a
# may run:comment in the procedure), no read of memory that nothing wrote first, no timing, such as the length of a tick or the rate at which frames are drawn, and no result from the operating system or a library outside the build, other than a value from outside the game that the procedure reads by its glossary name.
STATUS-5
Loops and recursion. The reading says why each loop ends and why a recursive call cannot go round forever, or names what that rests on. Reaching every branch of a procedure says nothing about a callee or a callback that never returns, and a cycle in the call graph needs its own argument, while a function called from two places is not recursion.
A register a callee returns can be what moves a loop on. For example, a decompiler shows a scan whose coordinate x never changes inside the loop, so the loop looks endless. In the instructions, a helper called in the loop body returns x + 1 in EAX, wrapped to 16 bits, and the jump at the bottom of the loop goes back into the first block part way through, at an instruction that stores EAX into x before the test. The decompiler gave the helper no return value and started the loop at the first instruction of the block, so the store does not appear in its output. Before it says whether a loop progresses, the reading follows each value a callee returns in a register to every store of it on the way back to the test, including stores at an entry inside the loop. It then names the measure that shrinks on each pass and the inputs for which it does. Here the instructions compare the 16-bit x with a 16-bit limit as unsigned values, so the measure is the unsigned distance from x up to the limit, and it shrinks for every x below the limit. Against a 32-bit limit above 0xFFFF the same loop would never end, since x wraps to 0 before it gets there. A test of the signed distance between the two would end the loop for a different range of inputs, and the finding says which test the instructions make and gives the widths of both operands. A loop that looks endless in a decompiler’s output is a lead, and a rewrite of the loop that ends when run on test inputs shows nothing about the original’s loop.
STATUS-6
The paths that fail. The reading says which writes happen before each failure and stay, and which values are set on each way out, since a failure return undoes nothing unless the code does. A caller that tests only part of a return value, such as the low byte of a word, is read as testing that part. Findings says what a finding about a failure path records.
STATUS-7
Widths and ranges. The reading gives the width and signedness of each value the result depends on, where it wraps or truncates, and the range of values the callers it has read actually pass. A bound comes from the code that sets it, never from the data that happens to follow a table or a size that divides evenly, and a count of what a procedure produces is kept apart from the bytes allocated for it, headers and terminators included.
STATUS-8
Identity. A field is named by the segment or object that holds it, its offset and width, and the paths that set it. An argument is read by the width and order the caller pushes and the callee reads, and by who removes it. Two addresses are the same storage only where the reading shows it, by giving the segment or object each address is formed in and the one each access goes through. In 16-bit code that is the segment the instruction actually uses, after any override, and the caller or callee that sets or restores that segment register. An offset taken from BP and passed to a helper that reads through DS shows that the helper reads the caller’s stack only where the reading shows that DS and SS hold the same segment at that point. Until it does, the finding keeps the accesses apart and says what evidence is missing. The Setup section of an emulated call says which segments its harness makes equal, and a harness never makes them equal only so that a reading works.
The value a callee reads from its arguments is whatever last wrote those bytes before the call, which need not be what the caller pushed there. For example, a caller pushes EBX to make room for a 4-byte slot, passes the slot’s address to a thiscall wrapper as the object it writes into, and then calls the target, which reads the slot. The target reads the value the wrapper wrote, and a decompiler that shows EBX as the target’s argument shows a value nothing reads. The finding names the last write before the call to every byte the callee reads from the caller’s frame, writes through a pointer into the outgoing arguments included, and it matches the width pushed, the width read and the bytes removed on return before it calls a value an argument. A decompiler’s signature for the call, or a count of the pushes before it, is a lead for how many arguments the callee takes and which values they carry, and a rewrite that passes EBX and works on test inputs shows nothing about which value the original target reads.
In 32-bit code the reading gives ESP’s offset from its value at the procedure’s entry at each instruction that forms an address from ESP, and follows it through each cleanup. Under a convention where the caller removes the arguments, a caller can form the address of a local with LEA from ESP before the ADD ESP that removes the arguments of the call it just made. It can also leave those arguments on the stack, push the arguments of its next call on top of them, and remove both calls’ arguments with one ADD ESP after the second call. Until that instruction, ESP is lower by the bytes still on the stack, so [ESP+n] addresses a slot that many bytes below the one it addresses afterwards. A decompiler that names every slot as though ESP were balanced gives such an address the wrong local. Where the trace puts every slot in a stretch of code the same number of bytes away from where the decompiler puts it, the trace is checked again before the finding says anything about the original, and callers that appear to work do not show that the offsets are right.
A callee can also remove an argument it never reads. For example, a caller pushes the direction an actor faces and calls a helper that returns with RET 4, but the helper never reads that slot: it looks the actor up and reads the direction stored in the actor’s record. The finding records the 4 bytes pushed and removed as part of the call’s contract and the value the helper uses as a read of the actor’s record, gives the calling convention it read from the callee’s return and the caller’s cleanup, and keeps the two values apart. They are sure to agree only when the caller’s value came from the same record and nothing wrote the record between the push and the read. The finding names where the caller’s value came from and each write to the record between the push and the read, with the point where that write becomes visible to the helper, or the entry’s Open questions section says that this is not shown. Tests in which the caller passes the actor’s own direction cannot tell the two readings apart. An argument the decompiler leaves out of the signature still takes up the bytes the callee removes, and an argument the caller passes shows nothing about whether the callee reads it.
STATUS-9
Stored calls. A value the code stores and later calls through can hold more than the address of the code: an adjustment added to the object pointer before the call, as a C++ pointer to a member function carries, or a context value passed along with it. The reading follows each part from the code that produces it, through the structure that stores it, to the call that uses it, before the format entry names its fields. Two adjacent words, or a value returned in two registers such as DX:AX, can be one far pointer, so they show two functions only where the reading finds two calls. The finding gives the calling convention of the call, and the width and signedness of any adjustment. A part whose role the reading has not shown keeps its neutral name.
STATUS-10
Boundaries. The start of a function and the size an analyzer gives its body are leads. A body can share its tail with another function or be entered part way through, and covering its bytes shows neither that every instruction was read nor that every caller was found.
STATUS-11
Overlapping writes. Each read and write is recorded with its full width and the bytes it covers. Where a byte is stored into a word that is later read whole, the reading also names what writes the other byte, since clearing the low byte of a word leaves it nonzero when the high byte is nonzero. Where the callers the reading has shown can reach that case, the entry describes it and an emulated call of the function includes a run that reaches it. Calling the low byte a flag does not make the whole word one.
STATUS-12
Allocations. A reading of an allocation keeps apart the number of bytes requested, the width that number is computed in and where it wraps, the unit the allocator counts in, how the returned pointer is normalized, the size the block’s header gives, and the range that is later cleared or written. It follows each conversion between bytes, paragraphs and elements back to the code that produces it. A loop that fills the block in bounded chunks says nothing about how large the whole block is. Where the allocator’s state or the meaning of a header is not known, the entry’s Open questions section says so. An allocator can change its state before it rejects a request, over alignment for example, and return null with those changes still in place, so the reading follows the pointer it saved and the pointer it returned separately.
STATUS-13
Output counts. Where a procedure produces a list from its inputs, the reading bounds the number of outputs on its own, apart from the bound on each input, and follows every append, split or pairing of inputs to the check that guards the write into the destination. Two input counts that each pass their checks do not show that their product fits. A loop body with four appends shows at most four writes per pass, and does not show that any input the original produces takes all four. The entry keeps apart that bound, the size of the destination and what the inputs would have to be to reach it, and a bug entry that describes an overflow says which buffer the outputs are built in and what else points at that buffer. An emulated call with arguments chosen to reach the bound shows what the code does with them, and does not show that the original’s callers ever pass them.
STATUS-14
A reading that leaves any of STATUS-4 to STATUS-13 open is not complete, and the entry’s Open questions section says which part.
STATUS-15
An entry whose result depends on anything the third condition of STATUS-4 rules out stays supported until a run of the original agrees with the reading. A complete reading that later meets a finding or experiment it does not explain makes the entry disputed, the same as any other.
STATUS-16
Only evidence from the original can dispute an entry. Manuals are often wrong, and a source that disagrees with the findings is cited in evidence like any other source, with the disagreement described in the entry’s body (a rule’s What the sources say section, and the Open questions section of other kinds) and in the source’s Known errors section. Two sources that disagree with each other, and no finding or experiment to settle it, leave the entry sourced with the disagreement in its Open questions section.
STATUS-17
An entry that is not superseded must not cite a superseded entry anywhere: in its builds, a location, its evidence, conflicting or related fields, a draw in an experiment’s fixture, or a row of a build’s Code ranges section. A superseded entry keeps the links it had when it was replaced, so its history stays readable.
STATUS-18
Status is not tracked per build. An entry’s status is what its evidence shows for the first build in its builds list, which is the build it was studied in most closely, and another build it lists may rest on less evidence. The entry’s Differences between builds section says what was checked in each build.
STATUS-19
There is no separate confidence scale. An entry is as certain as its status says, and when part of an entry is less certain than the rest, that part goes in its own entry or in the entry’s Open questions section.
STATUS-20
The citations in the STATUS-1 table are the least a status needs. An entry takes the highest status its evidence supports for everything it says. Evidence can meet the citation requirement and still cover only part of the entry, such as an experiment that only ever took one branch of a procedure. The entry then stays at the lower status, and its Open questions section says which part the evidence does not reach.
STATUS-21
Findings and experiments are evidence rather than claims, so they have a shorter list of their own:
| Status | Meaning |
|---|---|
recorded | Written down by the person who found it. |
reproduced | Repeated by someone other than the person in recorded_by, from the entry’s own instructions, with the same result, and that person is named in reproduced_by. For a random outcome, the same result means a distribution that passes the experiment’s own comparison with the recorded one. |
superseded | Shown to be wrong. superseded_by names the entries that show it. |
STATUS-22
A claim’s status does not depend on whether its evidence has been reproduced, because a project with one person working on it could then never establish anything. The status index lists established entries that rest only on recorded evidence, and separately those established by a complete reading alone, which shows where a second person’s time does the most good.
STATUS-23
Builds and sources carry no status, but they can still be wrong: a build entry can hold a hash taken from a damaged copy, or a source entry can describe a different printing from the one that was read. A correction that leaves what the entry identifies unchanged, such as a typo in a title or a new URL for the same document, is made in place. Any other correction is a new entry under a new alias, and the old entry’s superseded_by names it. Every entry that cited the old one then fails the check until it cites the new one, which is the point at which someone confirms that its evidence still holds.
Bounded analysis reports
STATUS-24
A shared reporter answers a declared query over a named build and file. Its output gives the source fingerprint, tool version, address mapping, entries and ranges searched, instruction, repeat and result limits, positive controls and exclusions. It distinguishes instruction paths reached from established entries from raw operand or call-byte candidates. A declared code range bounds the search; it does not make every byte an instruction. Undecoded ranges, unresolved targets and exhausted limits stay in the report, including when the result is zero. A missed positive control rejects a negative result.
STATUS-25
The report derives the parts it claims to compute from the instructions or source metadata. Supplied contracts, mappings, initial values and external-call models are identified as inputs with their provenance. Unknown values stay unknown. A researcher can supply an observation point, such as the register holding a returned extent, but a label alone does not establish its units or meaning. An instruction the reporter cannot interpret ends that path or leaves an explicit gap. It cannot silently become a call with no effects or a successful operation. A path that reaches the same instruction more times than the query’s repeat limit allows stops there and names that limit. Stopping there shows nothing about whether the loop ends. A division that could fault on values the reporter does not know, through a zero divisor or a quotient too wide for its register, continues only under a stated assumption that it does not fault.
STATUS-26
A reporter that reads a format’s own tables, such as relocations, overlay descriptors, fixups and trampolines, checks them before it answers any query and reports the counts they yield. A query can give the counts a build is known to have, and any difference rejects the query, so a table read with the wrong flag or cut short at a bound cannot narrow that query’s search without anyone noticing. A query that gives no counts has only the reported ones to go on, and the report says that nothing checked them. Tests cut a table short and give the build’s counts, and the query is rejected.
STATUS-27
A reporter need not support every query, and each one it does support meets the contract that one of the rules after this one gives for it. The tests use synthetic instructions and state, so they need none of the original’s files.
STATUS-28
A variable-use inventory follows established entries, retains effective operand widths and separates unverified overlapping candidates from counted reads and writes. It checks known instruction hits and reports ranges it could not decode. A test puts data between two entries that both use the field, and another makes the positive control fail. An operand reached only past a stop, such as a call whose callee has not been read or an instruction the reporter could not follow, is listed apart from the reads and writes on traced paths, and each such access names the stops it depends on. Its value, segment state and reachability assumptions remain separate from propagated path effects, and it cannot support a negative result. A test uses a field whose only access follows such a call, and the inventory reports no traced uses and one access that names that call, without assuming the callee preserves state.
STATUS-29
A memory report retains the segment in which an offset was formed and the effective segment of every later access, including implicit defaults and overrides. It follows segment assignments across calls and leaves possible aliases unresolved. Tests carry a BP-derived offset through BX with unequal DS and SS, then make the two registers equal with instructions and show that the result changes.
STATUS-30
An argument report maps pushed bytes to the widths the callee consumes, accounting for near and far return frames, widening, saved registers and cleanup. A relocated segment identifies a segment operand; it cannot decide which neighbouring word belongs to a pointer. Tests place an independent word beside a far pointer and check both frame shapes. A push of CS followed by a near call may supply a far-return frame. The report checks the pushed word, return width, stack balance and returned segment, and a test checks them once with the callee’s own code and once with an external-call model in its place. Unresolved grouping remains unresolved.
STATUS-31
An effect report preserves ordered writes and calls for each exit, including early bypasses, partial acquisition and failure. It identifies the producer of each branch’s flags, keeps caller and callee predicates, and says which continuations assume a call returns. Tests retain a write before failure and omit a later bypassed write. Branches on two producers whose flags the reporter cannot compute stay uncorrelated, since two unknown conditions are not the same condition. Tests keep two such producers apart while still pairing complementary branches on one producer that nothing changes in between. A rollback claim needs the path that performs it; a failure return supplies none.
STATUS-32
A return report preserves each producer’s result width and evidenced failure encodings, each caller’s truncation or extension, stored width and actual predicate. A raw field with the same bits has its own meaning. Tests return a failure word with a nonzero low byte and a successful word with a zero low byte, then follow a caller that tests only that byte. Discarded results and normalized wrapper returns stay visible.
STATUS-33
An overlapping-access report keeps the complete byte interval of every read and write and the producers of neighbouring bytes. Tests clear a low byte before a word test with a nonzero high byte, and leave that high byte unknown on another path. A named byte flag cannot replace the wider consumer’s predicate in the report or its fixtures.
STATUS-34
An incoming-call report preserves encoding and mapping provenance, including raw relocation words, decoded overlay descriptors, trampolines and canonical targets where the format has them. It separates resident relocated calls, overlay fixups and relative candidates, and checks segment aliases against the same file target. A relative search covers the complete declared segment, overlay or section, including callers placed at higher addresses than the target’s own code; a smaller search is labelled partial. The report names the segment, overlay or section each searched range belongs to and lists the parts of it left unsearched. Each candidate off the established paths says whether it lies inside an instruction the paths reach or in bytes no path reaches, and the report lists the computed transfers on the paths it followed, wherever they lie, since those may reach such bytes. Tests include such a late caller, an alias, a raw candidate that is not on an established instruction path, a candidate inside another instruction’s operand and a search of part of a declared segment. Computed and unrelocated routes remain explicit exclusions.
STATUS-35
A call-target report takes one direct call or jump. It gives the raw operand words, whether a relocation or overlay fixup covers the segment word, the load segment and the loaded address. A near call or jump has no segment word, and the report says that its segment is the caller’s. For an overlay fixup it adds the stored word, the descriptor that word decodes to, that descriptor’s segment and the trampoline the loaded address names. It ends with the code the transfer reaches and the address this standard cites it by, in the form Notation gives. A far word that nothing relocates gets no target. An address an analyzer shows for the same call is an input: the report says which of these identities the analyzer’s address equals, and never puts it in their place. Tests use an unrelocated far call, a fixup whose stored word differs from its descriptor index, a call through a trampoline, and analyzer addresses equal to the raw operand, to the target and to neither.
STATUS-36
A boundary report starts at one established entry and follows every branch and fall-through to each return, halt or tail transfer, without entering callees. It gives the byte runs it reached, the holes between them, every exit, and every call, interrupt or port access whose return it assumed. A computed jump whose targets it cannot resolve is listed as an exit with unknown targets, and the runs it gives do not then show the whole body. A size an analyzer gives is compared as a count of body bytes and is never added to the start to make an end. Tests put a data byte between two returns, end a body with a jump to another entry, and give a size that would cut off the second return.
STATUS-37
An ownership report says which established entries reach one site as an instruction start, which reach an instruction that contains it, and whether an analyzer’s function is among them. A return or prologue that lies between an entry and the site by address is reported as a warning, and the report does not treat it as the end of the entry’s body. Tests put a call just past an adjacent function’s return and give two entries a shared tail.
STATUS-38
A guard report checks the value that the protected access or call actually uses, which an earlier test of the same variable does not show by itself. It retains ordering, branch polarity, intervening writers and callees, and distinguishes a checked snapshot from a later reload. Tests dereference before a null check and reload an indirect target after a possible writer. Unknown callee effects cannot establish preservation of the checked value.
STATUS-39
An allocation report keeps requested bytes, arithmetic width and overflow, admission units, pointer normalization, header extent units and actual write ranges apart. It follows saved and returned pointers separately and retains changes before a failed request. Tests wrap a request, convert paragraphs to bytes, compare a bounded write with a separately observed extent and reject after an earlier state change. An unknown allocator or header contract leaves capacity unresolved.
STATUS-40
A dispatch report retains the decoded input-to-index transformation and range gates beside the bounded table layout and the source of its count. Raw positions stay separate from input cases until the transformation and source mapping have been read. Tests use inputs made equivalent by normalization, an input rejected before dispatch, and an index outside the declared layout. A configured stride must agree with the encoded access.
STATUS-42
An import report says which import fills each slot of a PE file’s import address table. It reads that from the import directory: for each descriptor, the DLL name, the import lookup table and the import address table, walked in step one entry at a time at the file’s thunk width (4 bytes in a 32-bit file, 8 in PE32+) up to the null entry. An entry with the top bit set gives an ordinal and no name, and any other entry gives the hint and name it points at. The report lists each slot by its address with the DLL and either the name and hint or the ordinal, and says which table it read the names from. A descriptor whose time stamp is not zero was bound, so its import address table as stored in the file holds addresses in the DLLs, and such an address can have its top bit set, as every address in KERNEL32.DLL did under Windows 95. A descriptor with no lookup table, which some linkers of the period wrote, has its names read from the import address table as stored in the file when it was not bound, and the report says so. A slot of a bound descriptor with no lookup table gets no import. A listing of imports, such as the one dumpbin /imports prints, gives the descriptors in the order the directory holds them, which need not be the order their import address tables sit in, and it leaves out the null entry that ends each descriptor’s slots, so counting lines of a listing never identifies a slot. A query names at least one slot as a positive control, with the import that other evidence, such as a run of the original under a debugger, shows the code calls through it, and a slot that maps to anything else rejects the report. The arguments a call site passes can confirm the mapped import or show that something is wrong, either in the mapping or in the reading of the call, but they never identify an import by themselves. The import directory holds only what the loader resolves when it loads the file, so delay-loaded imports and functions found through GetProcAddress are exclusions the report names, and a listing of the directory never shows that the code calls nothing else. Tests use a synthetic PE whose descriptors come in a different order from their import address tables, with named and ordinal imports from two DLLs, in a 32-bit and a PE32+ form, with one descriptor that has no lookup table, with a bound descriptor that has no lookup table and a stored address with its top bit set, which gets no import, and with a positive control that names two imports swapped, which the report rejects.
STATUS-41
A report that says its search is complete says so only for its stated domain and model. It establishes neither native reachability nor a complete reading by itself. Its reports and configurations stay in GAME_DIR and are not committed; findings record the checked observation in the researcher’s words. A request for reporter behaviour is closed only after its stated acceptance cases pass against the reporter as it runs. Review guidance or a synthetic example of the desired conclusion does not implement that behaviour.
STATUS-43
A table-contents report reads the entries of one table of pointers, such as a table of string offsets, from the build’s bytes. An analyzer’s listing of the same table is an input: the report says for each entry whether the listing matches the bytes it read, and never puts the listing in their place. The query gives the table’s address, the stride between entries, the offset and width of the pointer in each (a near offset with the segment it is formed in, a far pointer, or a 32-bit address), and the count. The stride, the pointer’s offset and width and the segment come from the code that reads an entry, and the count from the code that bounds the index, as STATUS-7 requires, or from the test that ends a walk at a sentinel entry. Any of these that the query supplies without naming that code is reported as an unchecked input. The query also gives the value that counts as a null pointer and the test in the code that treats it so. Without that test, an entry holding that value is read like any other, since offset 0 of a data segment is an address like the rest. For each entry the report gives the raw pointer, whether a relocation or fixup covers it, the address it maps to and the file range that holds that address, or that it maps to no range in the build. A far pointer whose segment word nothing relocates gets no address, as in STATUS-35. An address in memory the file does not initialize, such as the uninitialized tail of a segment or the part of a section past its raw data, has no file range, and the report says so and reads nothing there, because the file holds no bytes for it. For an entry that points at a string, it reads from that address up to the terminator the query names, at most a stated number of bytes, and gives the length. A null pointer, a pointer to a terminator, a read that meets no terminator before the limit or the end of the file range, a target in memory the file does not initialize, and a target outside every mapped range are five different results. The last three are errors, and the report never gives any of them as an empty string or as a string cut at the limit. The report covers every entry up to the count, or lists the indices it read, and claims nothing about the others. The query names at least one entry other than the first, whose target another reading has shown to hold a string that is not empty, so that a wrong stride, base or segment cannot pass it, and a report that gets that entry wrong is rejected. Tests use a synthetic table with an ordinary string, an empty string, a null pointer, an entry with no terminator before the limit, an entry that points into memory the file does not initialize, an entry that points outside the mapped ranges, and an analyzer listing that shows the ordinary string cut short, the empty string as empty, and the null pointer and the last three alike as empty. The report gives six different results for them and says that the listing matches the bytes for the empty string and does not match them for the other five entries. Further tests give a count without naming the code that bounds it, which the report gives as an unchecked input, and a wrong stride, under which the positive control fails and the report is rejected.
STATUS-44
An NE import report says which import the loader puts at each place in an NE file’s segments that an import’s relocation record covers. An NE file has no import slots: a far call to an imported function is stored as 9A with a placeholder in its operand, and a record in the relocation table that follows the segment’s data names the target. The report reads that table for every segment whose flags say it has one. A segment whose flags mark its data as iterated is expanded first, because the offsets in its records and links refer to the expanded data. A record gives the source type (3 for a far pointer, 2 for a segment word, 5 for an offset), the flags, the offset of a site in the segment and the target. A record of any other source type is an error the report names with the record, and its sites are unknown. When the flags give an imported ordinal, the target is an index into the module reference table, counted from 1, and an ordinal. When they give an imported name, it is that index and an offset into the imported-names table, where the name is stored with its length in front. The module reference table holds the offset of each module’s name in the imported-names table. A record whose additive flag is clear covers a chain: the word at its site holds the offset of the next site in the same segment, and so on until a word holding FFFF. A record for an internal reference chains the same way. The report walks every such chain and lists each site it reached, with the segment, the offset, the source type and the record that covers it, and for an import the module and either the ordinal or the name. It lists the sites of internal references apart from the imports. A record whose additive flag is set covers its own site alone, and the word stored there is added to the target, so the report never follows it as a link. A record whose flags give an operating-system fixup, which Windows used for its floating-point emulator, also covers its own site alone and names no import. Reading only each record’s own offset misses every other site in its chain. The report gives, for each segment, the number of records and the number of sites, counting records and sites of every kind, and these are the counts STATUS-26 checks. It also gives the cap on the length of one chain, counted in sites. A chain whose site at the cap holds a word other than FFFF stops there, and the report says the chain was cut short and that its remaining sites are unknown. Every site, a record’s own offset as much as a link, needs all the bytes its source type writes (four for a far pointer, two for a segment word or an offset) inside the segment’s data. It must not overlap a site the report has already reached, and a link must not reach the offset another record gives. A site that breaks any of these is an error the report names with the record and the link that reached it, and that site and the ones past it are unknown. A site is where the loader writes, so whether it is the operand of a call is a question about the instruction around it, which the report answers only as a candidate, as STATUS-24 describes. An ordinal is a number until something names it. The report takes the name from the exporting module’s own resident or non-resident names table in a copy of that module from the period, which the query names by file, version and hash, or from an outside table that the query names with its version, such as the .spec files of a Windows reimplementation. The report gives each name with the table it came from, and an ordinal that neither names stays an ordinal. The arguments a call passes, read under the calling convention of the module that exports the function, can confirm a name or show that something is wrong, but they never name an ordinal by themselves. A query names at least one site as a positive control, with the import that other evidence, such as a run of the original under a debugger, shows the loader puts there, and a site that maps to anything else rejects the report. Functions the program finds through GetProcAddress, and modules it loads with LoadLibrary, are not in the relocation tables, so they are exclusions the report names. Tests use a synthetic NE file with one code segment of 16 bytes holding two far calls, 9A 06 00 00 00 at offset 0 and 9A FF FF 00 00 at offset 5, and one record of source type 3 for an imported ordinal at offset 1, naming module 1, ordinal 128, with a module reference table whose one entry names KERNEL. The report gives two sites, at offsets 1 and 6, both KERNEL ordinal 128, and one record. Further tests use an additive record whose site holds a word other than FFFF, which gives one site, an import by name, a module reference table of two entries with a record naming the second, a chain longer than the cap, which is reported as cut short, a chain whose last link points back to its first site, one whose link points past the segment’s data, one whose link points at the last two bytes of the segment, where a far pointer does not fit, and one whose link reaches another record’s offset, which are all errors, an internal-reference chain in the same segment, whose records and sites count toward the segment’s totals, an exporting module’s names table that names the ordinal, an outside table that names it, which the report gives with its version, an ordinal that neither names, which stays an ordinal, and a positive control that names the wrong ordinal, which the report rejects.
Notation
Integer types use the ModdingWiki names: UINT8, INT8, UINT16LE, INT16LE, UINT32LE, INT32LE, UINT64LE, INT64LE, and the BE forms where a file is big-endian. Signed types are two’s complement. FLOAT32LE and FLOAT64LE are IEEE 754. FLOAT80LE is the x87’s 80-bit extended format, stored in 10 bytes, and REAL48LE is Turbo Pascal’s 6-byte Real. char[n] is a fixed-length string whose encoding (CP437, Windows-1252, ASCII) and padding are stated in the field’s meaning. char[] is a string that ends at its first NUL byte, which belongs to the field, with its encoding stated the same way. bits[n] is an unsigned field of n bits packed inside a larger integer, with bit 0 as the least significant. BYTE[n] is an opaque block.
A pointer names the type it points at: PTR16<FMT-DATA-005> is a 16-bit near pointer, an offset into the segment the field’s meaning names, FARPTR<FMT-DATA-005> is a 16-bit offset followed by a 16-bit segment, the order far pointers are stored in, and PTR32<FMT-DATA-005> is a 32-bit flat address. A pointer to a list element points at the element’s type, a pointer to something not yet identified points at BYTE, and 0 is a null pointer. A pointer stored in a file, such as a save written by dumping memory, is described the same way, and its meaning says what the value is once it has been loaded, since the address it held when the game wrote it no longer points anywhere.
Every hash in the spec is the 128-bit form of xxHash3 (XXH3_128bits, which xxhsum -H2 prints), written as 32 lower-case hexadecimal digits in the byte order of its canonical form. The hashes tell a wrong or damaged copy from the right one and name files nobody can commit. Nothing in the spec depends on them resisting a file made on purpose to match, so a fast hash is enough. The fields that hold one are called xxh3.
File offsets and addresses are hexadecimal with a 0x prefix and upper-case digits. File offsets are padded to at least two digits (0x1C). How an address in code is written depends on the executable’s format, which the build entry gives for each file:
- PE (Windows): the full virtual address with the file loaded at the image base its header gives, eight digits for a 32-bit file (
0x00478CD0) and sixteen for a 64-bit PE32+ file. A DLL that Windows moves elsewhere at run time is still addressed from its header’s image base. - ELF (Linux ports): the virtual address the program headers give, eight digits for a 32-bit file and sixteen for a 64-bit one. A shared library is addressed the same way, which usually puts its first segment at 0.
- LE and LX (32-bit DOS extenders such as DOS/4GW): the address the code has when each object is placed at the relocation base address its object table gives, in the same eight-digit form.
- MZ (16-bit DOS):
segment:offsetwith four digits on each side and no prefix. The segment is the one the code has when the load image is placed at segment0x1000, which is where Ghidra puts it by default, so an address can be pasted straight into a Ghidra project (3D72:0515). - COM (16-bit DOS, a program with no header):
segment:offsetin the same form, with the file’s first byte at1000:0100, where DOS puts it after the program segment prefix. Ghidra imports a COM file as a raw binary, so that address is set as the base when importing it. - NE (16-bit Windows):
segment:offsetin the same form, with the segment number from the segment table (0003:0034). A disassembler may show the segment under a selector of its own. Where it does, the finding’s How to reproduce section gives the tool’s address as well.
An executable in any other format, such as a classic Mac OS application, cannot be documented until a convention for it is added to this list. Adding one raises the spec package’s minor version.
An executable compressed by a packer, such as PKLITE, LZEXE or EXEPACK for DOS or UPX for a DOS extender, has no usable addresses until it is unpacked, and two unpackers can give different files. Its item in the build’s files list adds packer, the packer and its version where known, and unpacked, which gives the size, xxh3 and format of the unpacked file and, in tool, the unpacker and version that produced it. Addresses in it follow the convention for the unpacked file’s format. A location still names the file by the path it ships under, and the How to reproduce section of a finding there starts by unpacking it with that tool. A member of an archive is addressed in its expanded form (ENTRY-TYPES-21).
Overlay code has no fixed load address, so it is located by the path the build entry gives for the file that holds it, which may be the executable itself, and its offset in that file padded to eight digits: GAME.OVL+0x0003A2F0.
Sizes and counts are decimal. Ranges are half-open and written with two dots, so 0x20..0x3C covers 0x20 up to but not including 0x3C. Every range is written this way: an address or offset in a location, a range in an entry’s text or tables, and a row of a build’s Code ranges section. A range of values is written the same way, as the pseudocode’s for i in a..b counts, so a die roll from 1 to 6 is 1..7. Analyzers often give the last byte instead. Ghidra’s maximum address for a function or a block is its last byte, so a function whose last byte is 0x0045E7CD is written 0x0045E04D..0x0045E7CE. The end is not the start of the last instruction or element either: a range that ends with a five-byte call at 0x00401010 ends at 0x00401015, and four INT32LE values from 0x00482000 end at 0x00482010. A table that lists last bytes says so in its column heading and does not write them as ranges.
Where the game repository has function inventories (coverage/, described in the work protocol’s Measuring progress section), the check fails a range in an entry for that build and file whose end is the last byte of an inventoried function. It passes the range when an inventoried function also starts at that byte, since a correct range that stops before a one-byte function ends there, so it cannot catch the slip in a range whose last item is a one-byte function. It also passes a range the entry lists in ends_on_last_byte (ENTRY-TYPES-22).
An offset into overlay code locates code only if that part of the file holds code, and the file’s length does not say which parts do. The Code ranges section of the build entry (see Builds) gives the half-open ranges of the file that hold code, each with the finding that shows it. A range cited as code lies wholly inside one of them. One that crosses a hole or the end of a code payload is split into ranges that each lie inside one, or it is not cited. Lying inside a code range does not show that an instruction starts at the offset. Code in a packed file is still addressed in the unpacked file, as above, because an offset into the compressed data does not point at any one instruction.
The check script compares each such range with the Code ranges section of its build and fails any range that lies outside all of them, so a range that only lies inside the file does not pass. The script’s own tests include ranges that end exactly at either end of a code range, and ranges that cross a hole, the end of a code payload or into another bank. The code ranges are written in the body of the build entry, so they add no field to the manifest or to a location.
Screen coordinates are pixels on the game’s native canvas, with the origin at the top left. Every screen entry gives the canvas it is drawn on in its resolution field, which matters in a game with more than one. A rectangle is written (x, y, w, h).
A resource is referenced by the file that holds it, with its path as the build entry writes it, and its place in that file: PANELS.RES#12 for the entry at index 12, counted from 0 in the order the file’s format entry defines, or PANELS.RES#COMBAT for a file whose entries have names. A resource in the resource section of a PE or NE executable is referenced by its type, without the RT_ prefix, and its numeric ID or name: Chaos Overlords.exe#BITMAP/132, Chaos Overlords.exe#STRING/1024. Any other resource held in an executable is referenced by its address, or by the file and offset for overlay code. An audio track on a CD is not a file, so it is referenced by its disc and its track number from the disc’s table of contents, counted from 1 and written with two digits: CD:track02, or CD2:track05 for a game on more than one disc. The build entry lists each track the spec uses among its files, under that reference, with format: cdda, and gives the size and hash of its raw audio: the track’s sectors as a ripper reads them, 2352 bytes each, with no header. CD audio has one fixed layout, so a track needs no format entry. Music the game plays through its own driver, such as MIDI, XMI or MOD data, is a file or a resource like any other.
Durations are in milliseconds or in ticks of a named clock. A clock is anything the game counts time with: the step of its own logic, the display refresh (70 Hz in VGA mode 13h), the BIOS timer’s 18.2 Hz, or a rate it programs into the timer chip. Each clock the game uses is defined once, in a rule, with clock (see Rules, functions, tables and clocks). The glossary gives the name with the ID of that rule, and a duration in ticks names its clock: 12 ticks of logic_tick.
Things that have not been identified get neutral names derived from where they are: fn_00478CD0 for a function, unk_2A for a field at offset 0x2A, g_004C1F20 for a global. In 16-bit code the colon becomes an underscore: fn_3D72_0515. In overlay code the file’s path comes first, with every character in it other than a letter or digit, such as a dot, a space or a slash, turned into an underscore: fn_GAME_OVL_0003A2F0. Logic the game runs as bytecode in its own interpreter, such as SCUMM scripts or a compiled Lua chunk, sits in a data file, so an unidentified script is named from the file and its offset the same way: scr_SCRIPTS_DAT_00001A40. They are renamed only when a finding or experiment shows what they do. A name the original itself carries, such as a debug symbol or a string beside the code that names a function, is recorded in a finding like any other observation, and it is circumstantial evidence for what the thing does, so it does not rename anything on its own. The zeldaret projects put it well: it is better to leave something unnamed than to name it wrongly.
A field’s neutral name is its Name in the format entry’s layout table, like any other field name, and its purpose is an open question of that format. The rest of this section is about functions, globals and scripts.
A neutral name belongs to one build, since the same function or global usually sits at a different address in another. When a rule has to use something that has not been identified, it uses the neutral name from the first build in its builds list. The glossary gets an entry under that name, giving the build the address comes from and the neutral names it has in other builds, and the rule lists it in its Open questions section. When a finding or experiment shows what it does, the glossary entry and every rule that uses it are renamed in the same change.
Formulas and procedures
Rules are written in a small pseudocode whose behaviour is fixed by the sections below. A procedure in it reads like this (the rule is illustrative):
# One detection pass, run at the start of each police phase
for each player in turn_order:
for each gang in player.roster:
if gang.hidden and roll(100) < gang.detection_chance:
gang.hidden = false
emit GangDetected(gang)
Only hidden gangs cost a draw here, which is the kind of detail the order of draws depends on.
The pseudocode describes what the game does in the spec’s own terms. It is never a cleaned-up copy of decompiler output, and it never contains decompiler variable names, the structure of the original function, or addresses other than those inside the neutral names from the glossary.
Every function the sections below define is built in, and needs no glossary entry.
Statements and operators
- Blocks are marked by indentation.
=assigns, andletin front of an assignment declares a local. There is no compound assignment: a rule writesx = x + 1.and,orandnotare the logical operators, andandandorstop at the first operand that decides the result, so a random draw on the right is not made when the left has already decided it. Branches areif,else ifandelse. Loops arefor each x in list,for i in a..b, which counts fromaup to but not includingband runs no passes whenbis not greater thana, andwhile. Aforloop overa..breadsaandbonce, before its first pass.for eachvisits a list the game keeps in the order the glossary gives for it, and any other list, such as a local or an array field, from index 0 up. A loop that counts down is written withwhile. Awhileloop tests its condition before each pass and reads every name in it as it stands then, so a limit the original loads again on each pass is written in the condition, and one it loads once is copied into a local before the loop. A loop whose body runs before its test, as C’sdo ... whiledoes, is written aswhile truewith the test and abreakat the end of the body, so the body runs once before anything is tested, and a count of 0 on entry gets one pass and wraps on its decrement. Acontinuein C’sdo ... whilegoes to the test, so in this form the rule repeats the test and itsbreakbefore thecontinue.breakleaves the innermost loop, andcontinuestarts its next pass. A line starting with#is a comment. - Integer literals are decimal, or hexadecimal with a
0xprefix. As in C, a decimal literal has the first of the default type,INT32andINT64that holds it, and a hexadecimal literal the first of the default type, its unsigned form,INT32,UINT32,INT64andUINT64. A decimal value too large forINT64is written in hexadecimal. A literal with a decimal point is aFLOAT64. Where the original uses a single-precision constant, as C’s1.5fis, the rule writesFLOAT32(1.5), since a bare1.5would widen aFLOAT32operand and compute in double precision. - The other operators, from the tightest binding to the loosest: unary
-and~;*,/and%;+and-;<<and>>;&;^;|; the comparisons==,!=,<,<=,>and>=;not;and;or.&,|,^and~are bitwise. Operators on the same level group from left to right. The bitwise operators bind tighter than the comparisons, as in Python, soflags & 4 == 4tests the bit, where C would compare first. Comparisons do not chain the way Python’s do, and a rule never writesa < b < c. A rule adds parentheses wherever a reader coming from C could misread the grouping. a.bis the fieldbof the structurea, andlist[i]is an element of a list, counted from 0. Whenbis a pointer,a.b.cfollows it to the fieldcof the structure it points at, anda.b == 0tests for a null pointer.[3, 5, 8]is a list written out in full, and[]an empty one.trueandfalseare the results of comparisons and logical operators, and storing one in an integer gives 1 or 0. A value stored as an integer counts as true when it is not 0, unless the rule says otherwise.
Types and arithmetic
- A type is given after a name, using the notation’s types without the byte order:
count: UINT16. A structure’s type is its format ID:gang: FMT-DATA-005. On aletit gives the local its type:let total: UINT8 = a + b. A type name used as a function converts a value in place,UINT8(a + b), the same way storing it would. A list type is the element type followed by a count in brackets,UINT8[4], for a list whose length never changes, or by empty brackets,FMT-DATA-005[], for one that grows and shrinks. Only the second kind can be given toappend,insertandremove_at. A layout array whose count is a number is a list of the first kind. One whose count is an earlier field is of the second kind, and a procedure that changes its length sets that field as well, at the point the original does. A list written out in full has elements of the default type unless itsletgives a list type:let falloff: UINT8[4] = [100, 75, 50, 25]. - An integer with no type given is signed and has the default width of the build being described, which the build entry gives in
int_width: 16 for a game built for a 16-bit machine, 32 otherwise. A rule whose builds have different widths, and whose result depends on the width, is split by build. - Integer operands are widened before an operation the way C widens them. An operand narrower than the default width becomes a signed value of the default width. If the operands still differ in width, the narrower one takes the type of the wider one, so a
UINT16added to anINT32becomes anINT32, as C gives when a 16-bitunsigned intmeets along. Widening uses sign extension if the value’s original type is signed and zero extension if it is unsigned. When a signed and an unsigned value of the same width then meet, the signed one is converted to unsigned first, and a rule where that changes the result says so. - The result has the width of the widened operands and wraps on overflow at that width, as the original machine did. A rule that depends on overflow says so. Storing a value in a name or field of a narrower type keeps its low bits, so 300 stored in a
UINT8becomes 44. - A procedure keeps each step of arithmetic its result depends on at the width the original computes it in, and never rewrites it as a step that agrees only where nothing wraps.
z < limit + 1andz <= limitdiffer whenlimitis the largestINT32: the sum wraps to the smallestINT32, so the first comparison is false for everyz.min,maxandabsare not written in place of a wrapped operation and the correction the original makes after it. Where the original stores a value and then corrects it, such as a counter it decrements, stores, and sets to 0 when it is negative, the procedure makes both stores, since another rule or an interruption can see the value between them, and the smallestINT32decremented wraps to the largest, which the correction keeps. A conversion between types is written where the original makes it: withaandbof typeUINT32,INT32(a - b) / 2subtracts with wrapping, reads the difference as signed and divides toward zero, so a difference of -1 gives 0, where dividing the unsigned difference gives 2147483647. A test in the procedure tests the value the original tests at the point it tests it. A check that a roll is positive, made before the roll is scaled, says nothing about the scaled value, and the procedure adds no check the original does not make. Where the callers the reading has shown never pass a value that reaches the wrapped case, the procedure still does the original’s arithmetic, and the Edge cases section gives that range with the evidence for it. - A rule where the original keeps the full product of a multiplication, as
imulleaves it in EDX:EAX (ormulin DX:AX in 16-bit code), widens an operand first so the product has room:INT32((INT64(a) * b) >> 16)is a 16.16 fixed-point multiply. /on integers truncates toward zero, and%takes the sign of the left operand.>>is an arithmetic shift on signed values and a logical shift on unsigned ones, sox >> 1andx / 2differ for negativex, as they did in the original. Where a shift count can reach the width of the value, the rule says what the original gives, because the 8086 uses the whole count and later processors keep only its low five bits (six for a 64-bit value). A test of the count that the original makes later reads the count as it was stored, so on any processor after the 8086 a count of 32 shifts a value of 32 bits or fewer by nothing and still fails a test that it is 0. Where a divisor can be 0, or a quotient can fall outside the type the division produces, as the smallestINT32divided by -1 does, the rule says what the original does then (a crash, a processor exception the game catches, a fixed result), or lists it as an open question until that is known. The same goes for an index that can fall outside its list: the rule says what the original reads or overwrites there, which is often the next field or the next structure in memory, or lists it as an open question.- Floating-point values have the width the original stores them in. When an integer and a floating-point value meet in an operation, the integer is converted to the floating-point type first, and when two floating-point widths meet, the narrower one is widened.
%takes integers only, and a rule where the original takes a floating-point remainder, as C’sfmoddoes, writes it out as a function. Converting a floating-point value to an integer truncates toward zero, and a rule where the original rounds another way usesfloor,ceilorround_evenat that point. The x87 keeps intermediate values in its registers at the precision set in its control word and rounds a value to the width of its type only when it stores it to memory, so a rule whose result depends on which intermediate values the original stores says which, and usesFLOAT80for a value the original stores at full extended precision. The procedure makes each of those stores where the original does, with each constant in the format the original holds it in, and never drops one of those stores by computing the steps around it inFLOAT64and narrowing only the final result.FLOAT32(1) / FLOAT32(3)stored asFLOAT32, then multiplied byFLOAT32(5)and stored, has the bits0x3FD55556, and the same chain computed inFLOAT64and narrowed once has0x3FD55555. A constant changes the bits the same way:FLOAT32(5) * FLOAT32(0.01)has0x3D4CCCCC, andFLOAT32(5.0 * 0.01), with the constant inFLOAT64, has0x3D4CCCCD. Turbo Pascal didREAL48arithmetic in software, with rounding of its own, so a rule that computes withREAL48values and whose result depends on that rounding writes out how the original rounds. - The x87 control word sets the precision and rounding of every x87 operation, and which precision a game runs with depends on its compiler’s runtime. The processor starts with a 64-bit significand, the 80-bit extended format, and Borland and Watcom runtimes leave it there. Microsoft’s C runtime sets a 53-bit significand, the precision of
FLOAT64, at startup. A program can change it later, as Direct3D does when it creates its device and sets a 24-bit significand. A rule whose procedure computes with floating-point values gives, in its Inputs section, the precision and rounding in effect when it runs and cites the finding that shows them, and each operation’s result is rounded to that precision. - Where the original converts a floating-point value to an integer through a helper, the rule gives the width the helper produces and the part of it the caller uses. Microsoft’s
_ftolsets the rounding to truncation, stores a 64-bit integer, returns it in EDX:EAX and restores the control word it saved, so a caller that keeps EAX keeps the low 32 bits:INT32(INT64(x)), which differs from limiting the value to theINT32range. Later Microsoft compilers call_ftol2or_ftol2_sseinstead._ftol2leaves the control word alone and corrects the rounded result toward zero itself, and_ftol2_sseon a processor with SSE2 converts withcvttsd2sistraight to 32 bits, so the rule names the helper the build calls. A value outside the range of the integer the helper stores, or a NaN, such as the square root of a negative sum, gives the integer indefinite (0x80000000for a 32-bit result,0x8000000000000000for a 64-bit one) when the invalid-operation exception is masked, and raises the exception when it is not, so the rule gives the masks in effect in its Inputs section or lists the case as an open question. Restoring the control word leaves the status word as the conversion left it, with any exception flags it set, and a rule whose result depends on those flags says so. - Code compiled for SSE, which every 64-bit Windows and Linux build uses for floating point, ignores the x87 control word. Each operation rounds to the width of its type, and the MXCSR register sets the rounding and whether denormal values are flushed to zero. A rule whose code uses SSE gives those settings in its Inputs section the same way, where they are not the defaults of rounding to nearest even with no flushing.
floor,ceilandround_eventake a floating-point value and give an integer of the default type, rounded down, up, or to the nearest integer with ties going to the even one, which is the rounding the x87 and SSE start with.
Names, structures and pointers
- A local is a parameter, a loop variable, or a name declared with
leton its first assignment:let total = a + b. It lasts until the procedure returns, and it hides any glossary term with the same name, so a rule that needs the term gives its local another name. Since a local is always declared, adding a term to the glossary never turns an existing rule’s local into game state. A name assigned withoutletthat is not a local comes from the glossary, including the neutral names of things not yet identified, and assigning to it changes the game’s state. Event and message names come from the glossary too. - A field name after a dot comes from the Name column of the format entry for that structure, whose type the glossary, the rule’s Parameters section, a
let, or the Outputs section of the rule that returned it gives. A name that holds a structure or a list refers to the one the game keeps, sogang.hidden = falsechanges the gang itself, and so does the same assignment in a rule that receivedgangas a parameter. Integers and floating-point values are copied, so assigning to an integer parameter changes only the called rule’s copy. - A name given a pointer field’s value refers to the structure the pointer points at, or is null. Assigning a structure to a pointer field stores its address, so
a.target = gangmakesa.targetandgangrefer to the same structure, anda.target = 0makes the pointer null. Assigning to a field that holds a structure or a list inline copies every field or element into it, so aftera.stats = b.statsthe two are still separate. A name given an inline structure or list refers to it in place, so afterlet m = gang.members,m[0] = xchangesgang.members[0].copymakes a separate one.==and!=between two names that refer to structures compare which structure they refer to, as a comparison of two pointers does, soattacker.target == defenderis true only when the pointer points at that same gang. A rule that compares the contents of two structures compares their fields one by one. - A pointer to a list element moves the way C moves one:
p + npointsnelements further on andp - npointsnelements back,p[n]is the elementp + npoints at, andp - qbetween two pointers into the same list is the number of elements between them. A pointer that moves outside its list reads or overwrites whatever lies there, and the rule says what that is, the same as for an index outside its list. free(x)gives back the memory of a structure made withnew, at the point where the original frees it. A pointer that still points at the structure afterwards is dangling, and a rule that reads or writes through one says what the original finds there, which depends on what has reused the memory since, or lists it as an open question until that is known.new FMT-DATA-005makes a structure of that format with every field 0, every list of fixed length at its full length with each element made the same way, every other list empty and every pointer null, and the rule then sets the fields that start with other values:let gang = new FMT-DATA-005,gang.hidden = true,append(player.roster, gang). Where the original leaves a field holding whatever was in that memory before, the rule says so, and lists what that can be as an open question until it is known. A rule that has to return more than one value returns a structure made this way.- A value from a format’s enumeration table may be written by its name from the Name column, such as
if site.kind == SITE_BAR, and means the number in its Value column. Enumeration names are upper-case letters, digits and underscores, start with a letter, and are unique across the spec, apart from the entries of a split format, which define the same names. A name describes the value in plain words: a category, asSITE_BARdoes, or the one thing a designer made that the value stands for, such as one unit type or one named character, by the name the game gives it:UNIT_ARCHER. The Value column holds what the original compares, so the name labels the number and never replaces it, and two values the game names alike get names that tell them apart. Where the game gives a value no name, the name is the field’s name and the value:UNIT_TYPE_3. An enumeration name always leads to one row of one format entry, and a rule that uses one lists that format in itsrelatedfield.
Rules, functions, tables and clocks
call RULE-COMBAT-012(attacker, defender)runs another rule, passing one argument for each item of its Parameters section, in that order.return xends a procedure and givesxas its result, solet damage = call RULE-COMBAT-012(attacker, defender)uses it.returnon its own ends a procedure that gives no result, and its Outputs section lists no return value.- A rule can define a function that other rules use, such as the generator’s
roll(n), withdefine roll(n: UINT16) -> UINT16:followed by an indented body that ends inreturn. The types of the parameters and the result may be left out when they are the default integer type. Other rules call it asroll(100), withoutcall, with one argument for each parameter itsdefinegives. A call to a rule or function on a line of its own runs it and discards the result. A function may call itself, and a rule maycallitself, wherever the original recurses. - A table that more than one rule uses is defined once, in a rule, with
table falloff: UINT8[4] = [100, 75, 50, 25]before its procedure, and a finding in the rule’s evidence gives the address it was found at, as for any other constant. Other rules use it by name. A table of more than 64 values takes them from a value file inrules/instead of a list:table sine: INT16[1024] from "RULE-MATH-003.sine.csv". The file has the single columnvalueand as many rows as the type’s count, one per element in index order, and each value is converted to the element type as a list’s would be. - Constants from the executable appear by value, and a finding in the rule’s evidence gives the address each was found at. A table the code indexes, used by one rule only, is written as a list assigned to a local at the start of the procedure:
let falloff: UINT8[4] = [100, 75, 50, 25]. A list of more than 64 values is atabledefinition with a value file instead, even when one rule uses it. - A clock is defined the same way, with
clock bios_tick: 1193182 / 65536 Hzbefore the procedure, where the rate is a number or an expression of constants. A clock whose rate the game reprograms while it runs gives the rate it starts at, and the rules that change it say so in their Outputs sections. - Function, table and clock names are unique across the spec, and the glossary gives each one with the ID of the rule that defines it. Every rule a procedure calls, and every rule whose functions or tables it uses, is listed in its
relatedfield. - A rule split into one entry per group of builds defines the same functions and tables in each of its entries, and the glossary entry for each name lists all of them. A
callto a split rule names any one of its entries and runs the one that lists the build being described. Every build the calling rule lists must be listed by one of those entries, and itsrelatedfield lists all of them. A format split by build works the same way wherever it is named: as a type, afternew, in a layout row, or in arelatedfield. Naming any one of its entries means the one that lists the build being described, every build the naming entry lists must be listed by one of them, and arelatedfield lists all of them.
Lists and strings
- The built-in functions are
min,max,abs, andcount(list)for the number of elements in a list.append(list, x)addsxat the end,insert(list, i, x)puts it at indexiand moves the later elements up,remove_at(list, i)takes out the element atiand moves the later ones down, andcopy(x)gives a new list or structure with the same elements or fields. A procedure does not change a list while afor eachloop is visiting it. Where the original adds to or removes from a list while walking it, which can skip the next element or visit one it just added, the rule writes that loop withwhileand an index, so the skip or the extra visit shows in the procedure. stable_sort(list, key)puts a list in ascending order ofkey(element), wherekeyis the name of a function that returns an integer or a string, and elements with equal keys keep the order they had. Many originals sort with an unstable algorithm such as the C library’sqsort, and then the order of equal elements depends on the algorithm. A rule that sorts either shows that no two keys can be equal, or writes out the original’s algorithm as a function of its own, or lists the order of ties in its Open questions section until it knows.- A
char[n]orchar[]field is a string. The comparison operators compare two strings byte by byte as unsigned values, up to the first NUL or the end of the field, the way C’sstrcmpdoes. A rule where the original compares in another way, ignoring case or through a collation table, writes that comparison out as a function. A string literal is written in double quotes. A procedure uses one where the original compares or builds a string itself, such as a file name, a key or a name the game matches, and in the arguments given toresource,read_file,write_fileandsprintf. Text the game shows is referenced withresourcewhere the game takes it from one, and a procedure never holds more of the game’s writing than a short label. sprintf(pattern, args)gives achar[]built the way C’ssprintfbuilds it, from a pattern in double quotes and the values after it:read_file(sprintf("SAVE%d.GAM", slot), FMT-SAVE-001). It supports%d,%u,%x,%X,%c,%sand%%, with C’s width and zero-padding flags. It is for the file names, keys and other strings the original builds for its own use, and for turning a number the game computes into the text a screen shows. A pattern for text the game shows holds only conversion specifications and the digits, spaces and punctuation around them, such as"$%d"or"%02d:%02d". Any word in that text comes from a resource, and a formatting step the patterns cannot express, such as a thousands separator, is written out as a function.
Events, randomness and interrupts
emit Name(args)produces an event or message. Its glossary entry lists the arguments it carries and the rules that handle it, and each handler’s Parameters section lists those arguments in the same order. The handlers either run at once, one after another in the glossary’s order, before the line after theemit, or the event waits on the queue its glossary entry names.drain qruns the events waiting on the queueqin the order they were emitted, each with its handlers in the glossary’s order, and an event emitted while the queue drains joins its end. A queue the original drains in another order says so in its glossary entry. An event with no handlers, such as one that plays a sound, only records that it happened, and a test compares it with the original.show SCR-MAP-001makes that screen the one the game draws and takes input on, at the point where the original switches to it. The screen’s States table says which state it opens in, and the rule lists the screen in itsrelatedfield.draw(gen)is one raw value from the random number generatorgen, exactly as the original’s generator returns it. Each generator is specified once, in its own rule, and has a glossary entry under its name. A game with a single generator may leave the name out ofdraw. The ways the game reduces a raw value to a range, such asroll(n)for an integer from 0 to n - 1, are defined in the same rule as functions that do arithmetic ondraw, because two reductions of the same draw give different numbers. Every draw appears explicitly and in the order the original makes it, because the order of draws decides whether a replay stays in sync. A draw whose result the original ignores is written on a line of its own,draw(gen)orroll(6), since it still moves the generator on.- A procedure runs one line at a time. Code that the original runs in between, such as a timer interrupt handler in a DOS game or a second thread in a Windows game, is a rule of its own, whose When it runs section names the interrupt or the thread. A procedure that such a rule can interrupt marks each point where the interruption can change what it reads next, or draw from a generator it uses, with a comment,
# may run: RULE-TIMER-002, and lists that rule in itsrelatedfield. Where the result depends on where the interruption falls and that is not yet known, the rule lists it in its Open questions section.
Files, resources and outside values
resource(...)refers to a resource, taking the parts of a resource reference as arguments in the order the notation writes them, with a file name, a resource type or a resource name in double quotes:resource("SOUNDS.RES", 12 + n),resource("Chaos Overlords.exe", "BITMAP", 132),resource("PANELS.RES", "COMBAT"). A CD audio track takes its disc and its track number:resource("CD", 2). It is how a rule passes a sound or an image to an event.read_file(path, FMT-SAVE-001)reads a file and gives a structure of that format, decoded as its layout table describes, andwrite_file(path, x)writes the structurexto a file in the layout of its format.pathis achar[]value, a file name in double quotes or a name built withsprintf, written the way the build entry writes paths. A third argument, an offset, reads or writes one structure at that place in the file, for an original that reads a record at a time. Where the file can be missing, shorter than the format or impossible to write, the rule says what the original does then, or lists it as an open question until that is known.- Everything else the game takes from outside itself is a value from outside the game: the time from the BIOS or from
GetTickCount, the state of a key or the mouse position, a byte read from a port, what a detection routine reports about the sound card. Each has a glossary entry giving its type, what the original reads it from (the interrupt, port or API function), and when it can change. A procedure reads it by its glossary name and never assigns to it. An experiment whose result depends on such a value records the values it took in its fixture’s inputs, and a test supplies them from there. - Behaviour that depends on the machine or on how the game was set up, rather than on the build, is not split by build. A choice stored in a configuration file, such as the sound card or the detail level picked in a setup program, is game state like any other, read from that file’s format entry. A property of the machine, such as how many passes of a busy loop fit into one timer tick, is a value from outside the game. The rule then says how the result changes with it, and each experiment’s
environmentgives enough of the machine or emulator settings (CPU cycles in DOSBox, for example) to reproduce the value.
Entry types
ENTRY-TYPES-1
Body sections appear in the order given for each type, each under a ## heading. The heading is the text before the colon in the list item, or the whole item without its full stop if it has no colon.
ENTRY-TYPES-2
A section with nothing to say is kept and says None known., so a reader can tell an empty section from a forgotten one. Where it is certain that there is nothing, such as the Parameters of a rule that takes none, it says None. instead.
ENTRY-TYPES-3
The examples of entries on this page use Chaos Overlords, and a spec’s entries follow them in their field names, their section headings and the way they write numbers. The IDs, statuses, addresses and other values in them are illustrative, apart from the file size and hash in the build example, which are the real ones for the GOG release.
ENTRY-TYPES-4
Links between entries go one way: a claim cites its evidence, and evidence does not list the claims that use it. The check script generates the reverse lists into the indexes. Every entry except builds and sources has these fields, and superseded_by is always present, as an empty list unless the status is superseded. Builds and sources have id, title and superseded_by, and no status or builds:
id: FND-COMBAT-011
title: Police detection loop visits gangs in player order, then roster order
status: recorded
builds: [BLD-GOG-EN-1.1]
superseded_by: []
Each kind adds fields of its own, and the section for each kind below shows a complete example.
ENTRY-TYPES-5
Rules, formats, screens and bugs also have evidence, the findings, experiments and sources they rest on, and conflicting, the evidence that contradicts them. Both are always present. conflicting holds only findings and experiments, and may only be non-empty when the status is disputed. They also have split_with, which is always present and lists the other entries of a split by build, described below, or is empty, and they may have complete_reading (see Complete readings).
ENTRY-TYPES-6
Rules, formats, bugs and screens also have related, links to other claims, which go one way as well. A rule lists the rules it invokes, the formats it reads or writes, and the screens it shows. A format lists the rules its tables name, such as the rule that decompresses a compressed block. A bug lists the rules, formats and screens it occurs in. A screen lists the rules and screens its effects lead to, and the rules that format the values it shows. A format’s links to other formats are the types in its layout table, and the check script reads them from there. The indexes carry every link in the other direction.
ENTRY-TYPES-7
An entry with status supported or higher may list a build only if at least one finding or experiment it cites lists that build too. An unknown or sourced entry lists the builds it is believed to apply to. A finding lists a second build only when it was checked in that build as well, and then gives a location in each, or, for a finding that has no locations, says in its observation how it was checked in each. An experiment lists exactly one build. Running it in another build is a second experiment with its own fixture, and its Question section names the first.
ENTRY-TYPES-8
When builds differ in a way that changes what an entry says (a different procedure, a different layout, a different set of states), the entry is split into one entry per group of builds that behave alike, and no build is listed in two of them. Each one lists all the others in split_with, and its Differences between builds section says how they differ. That section otherwise records the differences that leave the entry true for every build it lists, such as code or data at another address.
ENTRY-TYPES-22
Any entry may have ends_on_last_byte, the ranges it gives whose end is the last byte of an inventoried function on purpose, each written as the entry writes it. A correct range ends there when it stops just before a function’s one-byte final instruction, such as a function body cited without its one-byte return, because that instruction’s first byte is the function’s last. Inventories record no instruction boundaries, so without the listing the check cannot tell such a range from one that gives the last byte where Notation writes the byte after. A listed range covers every place the entry gives it: a location, the entry’s text and its tables. An entry with no such range leaves the field out. A function at 0x00401000 whose last byte is a one-byte return at 0x0040103F, cited without that return:
ends_on_last_byte: [0x00401000..0x0040103F]
The check passes a listed range. It fails a listed range that the entry does not give, and one that the check would pass everywhere the entry gives it without the listing, so the list holds only the ranges that need it. A listed range that turns out to be wrong is corrected by superseding the entry, never in place (IDENTIFIERS-8). The field records nothing about the original, so a finding or experiment may gain it, or have a range added to it, in place, and may have a range removed from it in place once the check would pass that range without the listing.
Builds
A build entry identifies one exact set of original files. Every other entry lists the builds it applies to, the way IESDP pages open with “Applies to” and MAME identifies every ROM chip by hash.
ENTRY-TYPES-9
A build entry has these fields, with listing present only where the build keeps a listing record (ENTRY-TYPES-16):
id: BLD-GOG-EN-1.1
title: Chaos Overlords 1.1, English, GOG release
superseded_by: []
developer: Stick Man Games # the studio that made the game
publisher: New World Computing # the company that released the game
publisher_version: "1.1"
distribution: GOG
languages: [en] # ISO 639-1 codes of the languages the build can be played in
int_width: 32 # 16 or 32: the width of int in the compiler that built the game's logic
manifest: BLD-GOG-EN-1.1.files.yaml
listing: BLD-GOG-EN-1.1.listing.yaml
developer is the studio that made the game and publisher the company that released the build, which are often different companies. A re-release keeps the original developer, and its publisher names the company that published the original, with the re-release’s seller in distribution. Where several studios made the game, developer names each of them, in the order the game’s credits give them.
int_width is the width of the executable that runs the game’s rules, where a build also ships a setup program or launcher of another width. A build has one such executable. An installation that ships two, such as a CD with a DOS and a Windows version of the game over the same data files, is two builds, one for each executable, and both list the data files they share.
ENTRY-TYPES-10
The manifest is a YAML file next to the entry, named after its ID, that lists the build’s files. A CD game can ship hundreds of them, so they are kept out of the entry, the way No-Intro and Redump keep the hashes of a dump in a DAT file apart from any description of the game. The build’s files list is the manifest’s only key, and it lists every file the game uses, with its format, size and hash, including files whose format or use has not been studied yet. A packed executable adds packer and unpacked (see Notation), and an archive whose members an entry cites adds members (ENTRY-TYPES-20). builds/BLD-GOG-EN-1.1.files.yaml:
files:
- path: Chaos Overlords.exe
format: PE # MZ, COM, NE, PE, LE, LX, ELF, cdda for a CD audio track, or data for any other file
size: 664576
xxh3: a82da6843188901e1d4fce1c76a925ef
ENTRY-TYPES-11
A path uses forward slashes and is relative to the directory the game is installed to. A file the game reads from its CD and never installs is written CD: followed by its path on the disc, or CD1:, CD2: and so on for a game on more than one disc. A name on an ISO 9660 disc is taken from the disc’s primary volume, the plain ISO 9660 directory tree that every such disc has, so a disc gets the same paths whether or not it also carries Joliet or Rock Ridge names. The Joliet name and the primary name of a file point to the same sector, which is how a listing that gives Joliet names is matched to primary ones. The name is written without its version suffix and without the dot that ends a name with no extension, so SETUP.EXE;1 on the disc is CD:SETUP.EXE and README.;1 is CD:README. Where dropping them would give two names in one directory the same path, such as README.;1 and README.;2, both keep their full names, CD:README.;1 and CD:README.;2. The toolkit’s disc reader gives names in this form, and a listing made with another tool is converted to it before it is compared.
ENTRY-TYPES-12
Body sections:
- Obtaining: how to get the build.
- Compared with other builds: how it differs from the other known builds.
- Other files: how the installation and the media the game reads were listed, and every path in that listing the manifest leaves out (installers, wrappers, compatibility shims), each with the reason.
- Code ranges: the parts of each file that hold code located by offset.
ENTRY-TYPES-13
The Survey and the progress measures take the game’s files from the manifest, so the manifest holds every file the game uses, even where a few hashes would be enough to tell one release from another. A file whose use is unknown stays in the manifest until evidence shows the game does not use it.
ENTRY-TYPES-14
Every path in the listing of the installation and the media the game reads, other than a member of an archive the listing went inside, is either in the manifest or in the Other files section, which gives each path the manifest leaves out with its reason. Those paths are the build’s list of other files. Neither the manifest nor the list of other files gives a path twice, and no path is in both. The section describes how the listing was made closely enough that anyone with the build can make it again and compare. Where the list of other files would take the entry past the line limit, it goes in a YAML file next to the manifest, named after the build’s ID, BLD-GOG-EN-1.1.other-files.yaml, whose only key other_files gives each path with its reason, and the section names that file. Where the listing went inside an archive or a compressed file, the Other files section says which ones and how far it went. An archive in the manifest does not mean its members’ formats have been surveyed. Each kind of member gets a format entry of its own that lists the archive in files, as Formats describes. Listings, listing records and manifests hold paths, sizes and hashes, never the files themselves.
ENTRY-TYPES-15
A path in the list of other files that ends in / is a directory exclusion and stands for every file under that directory. It is for a directory that ships empty or that something fills after installation, such as DOSBox’s capture/, a directory of saves, or a project’s local analysis output kept beside the game files, where naming each file would change the list every time the game runs. Its reason says what fills the directory. A directory exclusion never covers a path the manifest names, and it does not account for a link or a stopped item under it, which the list of other files gives by its own path (ENTRY-TYPES-18).
ENTRY-TYPES-16
A build may keep its listing in a YAML file next to the manifest, named after the build’s ID, BLD-GOG-EN-1.1.listing.yaml, and give that file’s name in listing. This listing record holds how the listing was made in its fields, and what the listing found in items. The Other files section of a build that has one names the tool that wrote it and does not need to repeat what its fields say. builds/BLD-GOG-EN-1.1.listing.yaml:
tool: build-listing.mjs a979396 # the program that made the listing, with its version or commit
date: 2026-10-06
links: listed # listed, followed or refused: what it did with symbolic links, junctions and other reparse points
cycles: null # how it stopped a followed link that leads back to a directory it was in, or null when links are not followed
media:
- prefix: "" # the installation directory
source: null
layout: null
- prefix: "CD:"
source: game.iso # the image as the manifest or the list of other files writes its path, or null for a physical disc
layout: "2048" # the sectors read: 2048, MODE1/2352 or MODE2/2352
archives: # every archive or compressed file the listing went inside, or []
- path: CD:DATA.ARJ
depth: 1 # 1 lists its members, 2 also lists the members of archives among them, and so on
items:
- path: CD:DATA.ARJ
size: 18341562
- path: CD:DATA.ARJ|INTRO.FLI
size: 401220
- path: CD:README.;1
size: 4210
- path: CD:README.;2
size: 4388
- path: CD:TRAILER.AVI
stopped: read error at sector 201344
- path: Chaos Overlords.exe
size: 664576
- path: Saves
link: C:/Users/Public/Documents/Chaos Overlords/Saves
media names each thing the listing covered: the installation directory with an empty prefix, and each disc by the prefix its paths take. The listing goes into every directory under each medium. A record whose media leave out a disc the game reads says in the Other files section which disc and why.
ENTRY-TYPES-17
Each item has a path, written as ENTRY-TYPES-11 gives it, and one of three fields: size for a file, link for a symbolic link, junction or other reparse point, giving its target as stored and not followed, or stopped for a path the listing could not give as a file, with the reason, such as a cycle, a reparse point it refused, the depth limit of an archive, a directory it could not enter, or a file it could not read. A member of an archive the listing went inside is written as the archive’s path, | and the member’s path inside it, CD:DATA.ARJ|INTRO.FLI. Windows and ISO 9660 names cannot hold |, so a member never shares a path with a file on disk. A listing that reads a disc’s audio tracks gives each one as a file item under its track reference, CD:track02, with the size of its raw audio. The record lists every file under a directory exclusion like any other, so how many there are stays visible. No path appears in the record twice, and items are sorted by path compared byte by byte, so two listings of the same files give the same items in the same order.
ENTRY-TYPES-18
Where a build has a listing record, the record agrees with the manifest and the list of other files:
- every file item, other than an archive member, is in the manifest with the manifest’s size, is in the list of other files, or lies under a directory exclusion;
- every archive member is under an archive that is a file item of the record;
- every path in the manifest is a file item, and every path in the list of other files that does not end in
/is an item, apart from paths on a disc the record’smedialeave out and CD audio tracks on a disc whose tracks the listing did not read; - every link and stopped item is in the list of other files by its own path, with a reason;
- no path in the manifest lies under a directory exclusion.
Agreement shows that the record, the manifest and the list of other files name the same paths and nothing more. It does not show that the game uses a file in the manifest, that the members of an archive have been surveyed, or that the Survey is complete. A listing that missed a directory agrees with a manifest that missed it too, which is why the record says how it was made.
ENTRY-TYPES-19
The Code ranges section is a table with the columns File | Range | Overlay | Finding: the file’s path as the manifest writes it, or a member’s path as ENTRY-TYPES-21 writes it, one half-open offset range, the overlay or bank number needed to read that range or - where there is none, and the ID of the finding that shows the range holds code. That finding has a location in that file which is not kind: file-data. A file with several separate code ranges has a row for each. A build whose code is all located by address has None. in place of the table.
ENTRY-TYPES-20
Some files that a finding reads code or data from ship only inside an archive, such as the second stage of an installer, which the first stage expands from a compressed archive on the disc. No path on disk holds such a file, so the manifest cannot list it by a path of its own. The archive’s item in files lists it in members instead, giving the path the archive stores it under, with forward slashes, the size, xxh3 and format of the member once expanded, and in tool the program and version that expanded it. builds/BLD-CD-EN-1.0.files.yaml:
files:
- path: CD:INSTALL.LIB
format: data
size: 1204331
xxh3: 5f0e2c71b9a4d3e8061c7a92e4b5d1f3
members:
- path: SETUP2.EXE
format: NE # the format of the expanded member, from the same list as a file's
size: 454016
xxh3: 0b7d93e15a2c48f6e1d07a3c95b2f864
tool: lib-expand 1.2.0
members lists every member that a location, a row of a Code ranges section, a resource reference or a function inventory names, and may list others. No member’s path appears in it twice, starts with /, has a .. segment or contains |, so the inventory path ENTRY-TYPES-21 builds from it stays under the archive’s directory. A member’s item has no packer, unpacked or members of its own, so a member that is itself packed, or is itself an archive, cannot be cited until a convention for it is added, which raises the spec package’s minor version. A self-extracting archive, an executable with an archive appended to it, keeps its own format for the stub’s code and lists the appended archive’s members in members. Where its stub is packed, the item also gives packer and unpacked for the stub. Where the build’s listing record went inside the archive, each member members lists is an item of the record, written as ENTRY-TYPES-17 gives it, with the member’s size, or a stopped item where the listing could not expand it.
ENTRY-TYPES-21
A member is written as its archive’s path as the manifest writes it, | and the member’s path, the form a listing record gives it in: CD:INSTALL.LIB|SETUP2.EXE. A location in a member gives that as its file. Its address is written in the notation for the member’s format, and an offset is into the expanded member and is checked against the member’s size. A location in the archive’s own bytes as they ship, such as its directory or the compressed stream that holds a member, names the archive by its own path, and its offset is checked against the archive’s size. It does not locate anything in the expanded member. In a self-extracting archive those bytes sit in an executable, so the location gives kind: file-data.
A row of a Code ranges section, a resource reference (CD:INSTALL.LIB|SETUP2.EXE#STRING/12) and an overlay offset (CD:INSTALL.LIB|SETUP2.EXE+0x0001A2F0) write a member the same way. In a Markdown table, such as a Code ranges section or a screen’s Resource column, the | is written \|, inside backticks too, because GitHub ends a cell at any | that is not escaped: CD:INSTALL.LIB\|SETUP2.EXE.
A function inventory of a member is kept under the archive’s path with @ added, which becomes a directory, and then the member’s path, since Windows allows no | in a file name: coverage/BLD-CD-EN-1.0/@CD/INSTALL.LIB@/SETUP2.EXE.tsv. The How to reproduce section of a finding with a location in a member starts by expanding the member with the tool the manifest gives, as it does for a packed file.
Sources
A source entry records a document or tool from outside the project: the manual, a FAQ, an earlier reverse-engineering effort, a format page on a wiki.
id: SRC-MANUAL-1996
title: Chaos Overlords manual
superseded_by: []
author: Stick Man Games # who wrote it
date: "1996"
location: https://archive.org/details/example # URL or archive location
xxh3: null # set when the source is a file
licence: null # set when it limits what may be quoted
Body sections:
- Use: what the source is good for.
- Known errors: where it is known to be wrong.
Sources are leads. A claim that rests only on sources has status sourced.
Text from a source is not copied into the spec beyond short quotations. Facts taken from it are restated in our own words and the source is cited. This matters most for wikis under share-alike licences, whose text cannot be relicensed under the spec’s licence.
Findings
A finding is one piece of evidence from the original: something read in its files, or something seen while it ran. Rules, formats, screens and bugs cite findings as their evidence.
id: FND-COMBAT-011
title: Police detection loop visits gangs in player order, then roster order
status: recorded
builds: [BLD-GOG-EN-1.1]
superseded_by: []
recorded_by: kibertoad
reproduced_by: []
method: static # static or dynamic
locations:
- build: BLD-GOG-EN-1.1
file: Chaos Overlords.exe
address: 0x00472775..0x00472A10 # or offset: for data files, overlay code and kind: file-data
tool: Ghidra 12.1.3
environment: null # what a dynamic finding ran in, in the form experiments use; null for a static one
recorded_by and reproduced_by hold GitHub usernames. The check script compares them, so it can tell that a reproduced finding was repeated by someone else.
A static finding comes from reading the original’s files without running them: disassembly, decompilation, or a data file in a hex editor. A dynamic finding comes from the original running: a breakpoint, a memory watch, a trace. A dynamic finding gives its environment in the same form as an experiment, and a static finding sets it to null. A run set up to answer a question, with a fixed starting state and repetitions, is an experiment and gets its own entry.
Body sections:
- Observation: what is present, stated as facts. Constants, branch conditions, the order of calls, which fields are read or written, and the address of each.
- Interpretation: what the observation means for the game.
- Alternatives: other readings that were considered and why they were ruled out, or that they have not been.
- How to reproduce: enough for someone else with the same build to find the same thing, such as the function’s entry address and the constant or string reference that leads to it.
How to reproduce may name the tool and its version, a script in the repository’s tools/ by its path and the commit it was run from, and the command or query that was run, because that is how someone checks the evidence. The Observation and Interpretation still say what was found without them, so the finding survives a change of tools. The parameters of that query, such as the entry address, the ranges searched, the limits and the positive control, are facts about the build, and the finding gives them itself: in the Observation when they describe how a search looked, and in How to reproduce otherwise. A configuration file or report made from the original stays in GAME_DIR and is not committed (STATUS-41 says the same of a complete search’s reports and configurations), so a finding that names one also gives every value in it that the result depends on, and someone with the same build and the tools it names needs nothing else. None of this lets a finding hold bytes, disassembly or strings from the original beyond the constants and short quotations an Observation already gives. A rule, format, screen or bug names no research tool and gives no steps for checking its evidence. It cites the finding or experiment that has them. A bug’s Trigger conditions still say what has to happen in the game for the bug to occur.
A rule whose Procedure says the detection loop is the one in the rebuild’s PoliceSweep class breaks the requirement in Where it lives, however accurate it is. A finding whose How to reproduce section says to run tools/research/list-callers.mjs against Chaos Overlords.exe from the entry 0x00472775, with the commit it was run from, the ranges searched, the result limit and the call that served as the positive control, follows that requirement, and so does one that also names the configuration in GAME_DIR that holds those values.
A finding that something is absent, such as no other caller of a function or no other write to a field, says in its observation how it looked: the build and file, the ranges searched, what counted as a match, any cap or exclusion, and a positive control, a case the search was shown to find. A search of raw bytes is not a search of instructions, the strings an analyzer lists are not every string in the file, and a scan for relocated far calls misses near calls, computed calls and calls nothing relocates. A search whose control failed, that hit its cap or that could not map part of the file shows nothing about what it could not see. The output of a tool, such as a list of candidate callers or a coverage report, is a lead until a finding records what was checked, and it never raises a status by itself.
A search for references to a piece of code or data first turns the location the finding cites into the value the code would hold, such as a virtual address or a segment and offset, through the build’s address mapping. If that mapping is wrong (a wrong image base, a section or segment placed at the wrong address, an overlay loaded at the wrong place), the search looks for a value nothing holds, finds no references, and reports no error. So the positive control for a search that comes back empty is a reference whose target the reading located without that mapping, such as an absolute address held in the instruction or in a stored pointer, a relocation entry or a run under a debugger, and it points into the same section, segment or overlay as the searched target. The control passes only when the search finds it where the mapping puts its target. A control whose target went through the same conversion as the searched one passes whether the mapping is right or wrong. That includes a relative call, jump or branch: its target is a distance from the instruction’s own address, which the mapping also gave, so a section placed at the wrong address moves both by the same amount. The observation says how the control’s target was located. Where the section, segment or overlay holds no such reference, a run under a debugger can still supply one, and until something does, the search has no control and its empty result shows nothing.
The observation also lists the kinds of reference the search looked for and the kinds it did not. For code they are direct near and far calls, unconditional jumps, conditional branches and loops, fall-through from the instruction before, addresses stored as data (in a table, pushed as an immediate, loaded into a register for a computed call or jump), calls through a thunk, an import slot or an overlay manager’s trampoline or descriptor, which hold the address of the stub or the number of the descriptor in place of the target’s address, and entries the operating system or the hardware uses, such as an interrupt vector, a callback, an export or the program’s entry point. For data they include every addressing form the code can use, a base register with an index or displacement among them. A kind counts as searched only when the search found a known reference of that kind, since the control above shows that the mapping is right and nothing about how the search matches the other kinds, such as the width of a stored pointer or whether it was looked for at unaligned offsets. A kind the build gives no known reference of is listed as not searched. A search that comes back empty shows that nothing in the ranges it searched refers to the target through the kinds it searched, under the mapping its control checked, and the interpretation claims no more than that. Code with no call, jump or stored pointer to it can still run, entered by a conditional branch or by falling through, so a finding that calls code unreachable or never run lists every kind above as searched. It also says what it assumed about targets the code computes while it runs, by arithmetic on an address, by changing a return address or by copying or writing code, since no search for a stored value finds those. An entry that cites an empty search and relies on the code never running, or on the references found being the only ones, lists in its Open questions each kind the search did not cover, and does not say the code never runs. Checking every candidate a search found, or every path from the established entries, shows nothing about a kind of reference the search left out.
As a test of these rules, take a synthetic image with one section whose code starts at 0x00401000, a call at one place through a stored pointer that holds 0x00401100, and a function at 0x00401200 that nothing calls, jumps to or stores, entered only by a conditional branch at the end of the code before it. A search that maps the section to 0x00402000 looks for references to 0x00402200 and finds none. Its control, the pointer whose value 0x00401100 was read from the file, is not found at 0x00402100 where that mapping puts its target, so the search is rejected. With the mapping corrected, a search for calls, unconditional jumps and stored pointers finds a known reference of each of those kinds, the control among them, and nothing at 0x00401200. The finding records that result with conditional branches, fall-through and the other kinds listed as not searched, and says nothing about whether the function runs. A search that includes conditional branches finds the branch that enters it.
A finding about whether the code ever does something, such as start another program, can rest on a census of the mechanism every use of that capability has to pass through: a request to the operating system with the value that selects the service, an import slot, a range of ports, an interrupt vector or a dispatcher in the runtime library. The census covers the whole code the question is about and lists every site of the mechanism with the value that selects the service there. A site that builds its request while the program runs, such as a runtime routine that writes an interrupt instruction to the stack and calls it, is listed with each of its callers and the value each one passes, and a site whose value is loaded from memory or computed is listed as undecided. The observation names each kind of transfer that leaves the code the census covers, such as a chained interrupt vector, a call into a driver or a buffer loaded at run time, or a table of far pointers filled while the program runs, and an entry that cites the census lists each of them in its Open questions. The positive control for an empty census is a site of the same mechanism that other evidence locates, such as a request seen under a debugger, which the census finds with the value that evidence shows. Each kind of site the census looks for, such as interrupt instructions and the callers of a routine that builds its request on the stack, counts as searched only when the census found a control of that kind. Its interpretation claims that no site it reached selects the service, and nothing about the undecided sites or the transfers it names.
A DOS program that has DOS start another program does it through interrupt 0x21 with AX = 0x4B00, or 0x4B01 followed by a jump into the code it loaded, or through interrupt 0x2E. AX = 0x4B03 loads an overlay and starts nothing, so a site with AH = 0x4B answers the question only together with the value of AL there. A far call to the handler through a saved copy of the interrupt 0x21 vector, after a pushf, is a site of the same mechanism. A program that reads another program into memory and jumps to it asks DOS for no such service, and the census lists that jump as a transfer it does not cover.
Each site a census reports, whether it selects the service or not, is an instruction start that the code reaches from an established entry: the program’s entry point, an export, an entry of a function table, an interrupt vector or callback the code installs, a handler whose address the code passes as a pointer, or an entry of a jump table read only up to the bound the code checks. The observation names the entries the census started from and the kinds of entry it did not have, such as vectors other programs install, transfers built on the stack and code written while the program runs, since a site reached only through those is outside the census. Decoding from every byte offset finds candidates and nothing more. An x86 decode that starts inside an instruction usually falls into step with the real instructions within a few bytes, so a candidate that most decodes from the offsets before it agree on can still lie inside an instruction. A census that picked its sites by such agreement says so and reports them as candidates.
As a test of these rules, take a synthetic 16-bit program with three 0x21 interrupt instructions that set AH to 0x3D, 0x3F and 0x4C just before them, a runtime routine that builds an interrupt instruction on the stack and has two callers that pass interrupt 0x21 with AH set to 0x30 and 0x44, and a far call into a buffer the program fills from a file. The census decodes from the entry point and the handlers the code installs, finds four sites, the runtime routine among them with a value for each caller, none of them 0x4B or interrupt 0x2E, and lists the call into the buffer as a transfer it does not cover. The open of a file, seen under a debugger at the 0x3D site, is its control for interrupt instructions, and the 0x30 request, seen under the debugger at the runtime routine, is its control for the sites built on the stack. In the same program, take the bytes C7 06 34 12 00 FF 1E 07 C3 at offset 0x0000: mov word [0x1234], 0xFF00, push ds, pop es, ret. A decode that starts at 0x0005 reads FF 1E 07 C3 as a far call through [0xC307], and the decode from 0x0000 never visits 0x0005, so a census that took that call as a site would report a transfer the program never makes.
An analyzer lists only the references it finds in code it has placed inside a function, so a finding that a function has no other callers, where it rests on that list, checks it with a second search that does not depend on where the analyzer thinks functions start and end. Its observation says which kinds of call each search can find. For direct calls the second search can be a scan of every range of the build’s files that can hold code, overlay files included, with each hit checked to be an instruction start that an established entry reaches, as for the sites of a census above. Decoding the bytes around a hit from nearby offsets does not show where an instruction starts, and a hit in bytes no established entry reaches stays a candidate, as STATUS-34 says of incoming calls. Bytes that look like a call opcode are not a call until that check, and a scan for opcodes finds no indirect calls, which the reading still resolves on its own. The finding lists separately the calls the scan confirmed outside any function the analyzer knows of, and accounts for every difference between the two searches. A query over a range that returns no instructions says whether the range holds none, is not mapped, or starts where the analyzer has not placed a function, since a tool that exits without an error has observed nothing by that alone. A caller that neither search could see leaves the reading incomplete.
A finding that names the operating system or library function a call reaches through a PE file’s import address table gives the slot’s address and the import the file’s import tables put there, read the way STATUS-42 describes, and never the import’s position in a listing. Its observation names the call it used as a positive control, a call through a slot whose import other evidence shows. When the arguments at a call site do not fit the function the mapping names, the mapping and the reading of the call are both checked again before the finding records anything about the call. An analyzer’s mark that an imported function does not return, or a name that sounds like an error routine, is a lead. The finding keeps three things apart: the import the file’s tables put in the slot, what that function’s published documentation says it does, and what the implementation loaded at run time does, which lies outside the build (STATUS-4), so an entry whose result depends on it stays supported until a run of the original settles it (STATUS-15). Until evidence about the loaded implementation settles it, the reading follows the code after the call as a possible continuation, and padding between the call and the next function shows nothing either way. Documentation that says the function returns does not show that the original’s call returned, which registers it kept, or that the caller’s frame was still usable. The rebuild’s tests cover a model of the function that returns and one that does not, with the stack and argument obligations of each written out. The number of parameters the documentation gives is kept apart from the bytes the caller pushes for the call and the bytes removed on return, as STATUS-8 says.
A finding that names the function a far call in an NE file reaches gives the call’s site as segment:offset, the relocation record that covers it, and the module and ordinal or name that record gives, read the way STATUS-44 describes. Where the record gives an ordinal, the finding also names the table that put a name to it: the exporting module’s own names table, with the copy it was read from, or an outside table, with its version. Its observation names the site it used as a positive control, as for a PE import.
A finding about a dispatch table, where the code turns an input into an index and jumps or calls through the entry at that index, reads that step as well as the table. Its observation gives where the input comes from, the width of each field used, what the code does to it before indexing (masking, shifting, subtracting a base), the range check that rejects an index and what happens then, and the target each accepted index selects. The number of entries comes from that range check or from another read of the code, which the observation names, since the table’s bytes show neither where it ends nor which inputs reach it. Until the step before indexing has been read, the finding lists the entries by their position in the table, apart from any input values, and its interpretation says that which input selects each one is not yet known. Where the code clears a flag before indexing, inputs that differ only in that flag select the same entry. A test on synthetic input covers that case, and an index the range check rejects, wherever the code has them.
Where two dispatch tables sit next to each other and the code that indexes them looks alike, such as a primary and a secondary table of sound handlers, the finding works out each table’s extent on its own: from the range check before its own indexing step, the values its selector can take, where its targets end and what structure follows it. Similar handlers do not show that the tables have the same length. Take a primary table of seven targets and a secondary table of six, followed by a run of selector bytes. Reading the secondary table as seven entries long turns the first selector bytes into a seventh target that no index reaches, and a count of the targets computed calls can reach then includes code that the table never names. The observation gives each table’s count with the code that bounds it, and lists which selector values occur and the target each one maps to, including any value that maps to an exit. The rebuild’s tests give each table its own length, and test an index one past the end of the shorter table: where that table has a range check, the index takes its rejection path, and where only the selector values bound it, the test expects what the entry says for that index.
A finding about what a table of pointers holds, such as the strings a table of string offsets points at, reads the entries from the build’s bytes. An analyzer’s listing of the table shows the analyzer’s decoding of them, in which a pointer it could not follow, a target outside the mapped ranges or in memory the file does not initialize, or a string with no terminator can look the same as an empty or a short string. The observation gives the table’s address, the stride, the offset and width of the pointer in each entry, the count and the code that bounds it, how each pointer maps to an address in the build, and, for each entry it describes, the length it read and the terminator that ended it. The file holds the table and its strings as they are at load, so where the code writes to either before reading them, the observation names those writes or says that nobody has looked for them. An entry that points outside the mapped ranges or into memory the file does not initialize, or has no terminator before the end of its range or the read limit, is recorded as unread, and the entries that cite the finding name that entry in their Open questions sections. A finding that read only some of the entries names them and claims nothing about the others. How to reproduce names the check: the report STATUS-43 describes with its query, or the reads made by hand and the tool they were made in. The finding quotes no more of the strings than an Observation’s short quotations allow.
A finding about whether a call succeeded follows the callee’s return value into each caller: the width it is returned in, the values that mean failure, how the caller truncates or extends it, the width it is stored in and the test the caller branches on. If a function returns the word 0xFFFF on failure and its caller keeps the low byte and tests it for nonzero, the caller cannot tell failure from any other result with a nonzero low byte, and it takes a success such as 0x0100 for zero. A handle and a failure value that happen to have the same bits are still different results, and the finding names the code that produces each. Its Observation says where a caller throws a result away or maps it to a fixed value, such as any nonzero result to 1. A wrapper that returns zero, or a pointer set to null, does not show that the operation behind it succeeded.
Cleanup code that several paths reach is read once for each path into it, including a request that fails before anything was acquired and one that fails after acquiring part of what it needs. For every variable or field the cleanup reads, the finding names the write that set it on that path, or says that what it held before is unknown. Comparing a variable with the failure value neither sets it nor shows that it is a valid index. A test built for the purpose can show what leftover contents would do, but a bug entry that says the original does it also needs evidence of the state its callers leave and of how the game reaches that failure path.
A finding about code that allocates a record, fills it and links it into a container, such as a node added to a balanced tree, gives each step in the order the code takes it: the request, the test of the returned pointer if there is one, the writes to the header and the payload, the update to the count, the linking and the balancing. A test of a pointer derived from the base, such as the base plus the payload’s offset, does not test the base. When the allocator returns null, the derived pointer is the payload’s offset, which is nonzero, so the test passes. An access through the base that comes before that test reaches address zero. In a 32-bit Windows program (PE) it faults. In 16-bit code it reads or overwrites whatever is there: under DOS a far pointer reaches the interrupt table, and a near pointer in DOS or 16-bit Windows reaches the start of its data segment. Under a DOS extender it does whatever the extender’s mapping of address zero gives. The finding names the first access through the base on that path, says that no test guards it and gives what that access does on the build it describes. The requested size, the payload’s extent, the header’s extent and the block’s capacity stay apart, and the capacity stays unknown while the allocator’s contract is unread, as STATUS-12 says. A retry, a callback or an exception handler the allocator can reach is listed with what it may change, and the finding assumes neither that it returns nor that it undoes anything. A removal is read the same way. Where the code advances an iterator, unlinks the node, calls a release routine and then decrements the count, the routine runs while the node is unlinked and the count still holds its old value. A decrement that loads the count after the routine returns sees whatever the routine left there, and one that loaded it before the call stores a value the routine never saw, so the finding says which load the decrement uses, records the count at each of those points and gives the node each arm of the unlinking (no child, one child, or replacement by the successor) passes to the routine.
Cleanup that releases what a structure holds and clears the fields that held it is read in the order the code takes, since a release routine can read the fields that are not cleared yet. Code that releases and clears one field at a time, code that saves a field’s pointer and clears the field before releasing it, and code that releases every reference before clearing any of them leave different states for the routines to see. The finding records each release request with the pointer passed and the field it was read from, reads each later target from the structure as the earlier calls left it, and records a write a routine makes into a field the cleanup has already cleared as staying until the code clears that field again. Two fields that hold the same pointer give two requests, and the finding neither merges them nor says what the original’s allocator does with the second. A cleanup that walks a list makes one request per element, which the finding keeps apart from a single call to a routine it has not read. What the cleanup leaves need not be all zeros, and it is the state the next read of the structure starts from.
An error passed back through recursive calls is followed to the base case, or the call to the operating system or a library outside the build, that can produce it. When every base case the recursion reaches from a valid state returns zero, nothing inside the recursion is shown to produce the failure value, even though its callers test for it. The reading states what it assumes about valid states and about the recursion ending, keeps a failure value that reachable code returns apart from a branch that only handles one, and leaves results from outside the build open. A finding or experiment about these paths keeps the ones where zero means that nothing was done, and a claim that a value survives a failure lists every path and test it depends on.
A cycle in the call graph can be bounded by state that the calls change, and STATUS-5 then needs two arguments that the finding keeps apart. Take a stepper that calls a restart routine and itself, where the restart routine can call the stepper again, and where each path into the cycle stores 0 into a mode field before it calls, so that the next pass through the stepper takes its ordinary path and calls nothing. That store bounds how deep the calls go, provided nothing between the store and the next test of the field writes it again, and the finding names what it checked for that: the code on the path, callbacks and interrupt handlers that can run there, and other names for the same storage. The bound on depth says nothing about the stepper’s loop over its control records, which ends only when the records it walks reach an end. The reading argues that separately, from the code that produces the records and the step that moves the loop on.
A caller that calls several callees in turn, or one callee until a condition holds, combines their results in a way the finding names: it stops at the first callee that reports an event, keeps the last result, combines the results (a bitwise OR of status bits, for example) or drops them. The reading follows each path that continues after a callee reports completion to the caller’s return. A replay loop that keeps calling after its active flag has dropped to 0 can replace an end result returned earlier with zero results returned later, so the caller returns 0 although a callee reported the end. A caller can also map a child’s result to something else. One that always returns 0, and calls a fallback only when its child returns exactly 1, neither passes the child’s status on nor falls back on every nonzero status, and the finding gives the comparison it branches on and the width of the value compared. The finding names the instruction that keeps, combines or drops each result. A caller that chooses a warning to report and then returns success has not failed, and the finding keeps the warning apart from the value returned.
A location is a build, a file in that build or a member of an archive in it (ENTRY-TYPES-21), and a half-open range within it (Notation): an address range in the notation for the executable’s format, or an offset range for a data file, for overlay code or for data in an executable located with kind: file-data. A static finding has at least one location for each build it lists, and one for each separate range it describes, such as each function in an order of calls. Memory the game allocates while it runs has no fixed address, so a dynamic finding about it gives the address of the code that reads or writes it, and its observation names the structure and the field, by format entry ID once one exists. Line numbers from a decompiler listing are never used as locations, because they change with the tool version and the analysis settings.
Bytes in an executable that are read as data, such as its header, a container table or a packer’s header, can be located with kind: file-data and an offset into the file as it ships, which also locates bytes the loader does not map. The offset is checked against the file’s size in the manifest, which for a packed executable is the size of the packed file. Bytes that exist only once the file is unpacked, such as a table inside the compressed load image, are addressed in the unpacked file the way code is, because an offset into the compressed data does not point at them. Bytes the unpacker writes outside the load image, such as the relocation table of an unpacked MZ, have no address, so they are located with kind: file-data, unpacked: true and an offset into the unpacked file, checked against the unpacked size in the manifest. A file-data location does not locate an instruction or show that bytes hold code, and it cannot establish an overlay code range. Locating code as file data to keep it out of the Code ranges check is a review failure. Every other location in an executable keeps the address and overlay rules above, whether it locates code or data the loader maps. It omits kind, or gives kind: code where it locates code. A location in a data file gives no kind, and no other kind value is used.
A dynamic finding about what the player sees or hears, such as a frame count measured from a video capture, may have an empty locations list. Its observation then says how it was captured in each build it lists and gives the hash of each capture, and describes what was measured closely enough that someone else can record it again. The capture is not committed if it shows the game’s images or plays its sound (see Licence). It remains a single observation: once the question needs a fixed starting state or repeated runs, it becomes an experiment.
A capture of the screen, for a finding or an experiment, is taken at the size of the canvas in the screen entry’s resolution, with no scaling, filtering or aspect correction, and in the colours the game set in its palette, so that a test can compare it with the rebuild pixel for pixel. DOSBox’s own screenshot saves the emulated video memory that way, and a wrapper such as DxWnd needs its scaling and filtering turned off. The entry that records a capture says which tool took it and with what settings.
Experiments
An experiment is a controlled run of the original, following the protocol in Methodology.
id: EXP-COMBAT-004
title: Which of two players' hidden gangs is detected first under Crackdown
status: recorded
builds: [BLD-GOG-EN-1.1]
superseded_by: []
recorded_by: kibertoad
reproduced_by: []
environment: Windows 11 24H2, DxWnd 2.06.10 preset "chaos-16bit"
starting_state: saves/EXP-COMBAT-004.patch.json # a save patch, a save, new-game, emulated-call, or null
recording: null # the path of a demo or replay the original recorded (see below), or null
repetitions: 200
fixture: EXP-COMBAT-004.json
Body sections:
- Question: what the experiment is meant to settle.
- Setup: how the starting state was produced, step by step, including how to make the base save a patch applies to, and how the random state varies between runs.
- Procedure: the input that is varied, and everything held constant. For a distribution, the varied input is the generator’s state.
- Observations: what was recorded in each run, with the hashes of captured screenshots and saves.
- Results: counts and distributions for random outcomes, with the comparison a test applies to them, and exact values otherwise.
- Conclusion: what the results show, and what they do not.
Running the same save again only gives a different result if the generator’s state differs between runs. Many games store that state in the save, and reloading them gives the same outcome every time. The Setup section says how the state varies: the game reseeds itself from the clock when it loads, or the experimenter writes a chosen value into it before each run.
Many saves hold content copied from the game’s data, such as maps, text or scripts, so a save can usually not be committed (see Licence). The usual starting state is a save patch instead: a JSON file in saves/, following the standard’s fixture schema, that lists writes to make to a base save.
Each write gives the format ID of a structure in the save, a record index where the structure repeats (null where it does not), a field path, and the value. A field path is a field name from that format’s layout table, followed by . and a field name for each nested structure it goes into, with an index in brackets for an array element: stats.strength, members[3].rank. A group of bits is named like any other field. The patch for the example experiment, saves/EXP-COMBAT-004.patch.json, turns Crackdown on and hides one gang of each player:
{
"writes": [
{ "format": "FMT-SAVE-001", "record": null, "field": "crackdown", "value": 1 },
{ "format": "FMT-DATA-005", "record": 4, "field": "hidden", "value": 1 },
{ "format": "FMT-DATA-005", "record": 9, "field": "hidden", "value": 1 },
{ "format": "FMT-DATA-005", "record": 9, "field": "detection_chance", "value": 40 }
]
}
The spec package’s patch tool applies a patch through the save format’s Kaitai definition, so a patch needs that definition, and every field it writes must be supported or established in its layout table, since a guessed layout would put the value in the wrong bytes. A save in a text format cannot be patched, and its experiment’s Setup section gives the edits to make by hand. A patch writes numbers and names, never the game’s writing, art or code, so it can always be committed.
The Setup section says how to make the base save, usually by starting a new game with stated choices and saving at once, and the patch sets every field the experiment depends on. The base save holds content, so it is not committed. The maintainer keeps it with the captures, named by its hash (see Parity matrix), and keeps the patched save there too, which is where a test finds them. A save is committed to saves/ only when it holds none of the game’s content. starting_state is null when there is neither a patch nor a save that can be committed, for example while the save format is not yet understood well enough to patch. The Setup section is then the only way for a person to recreate the starting state, and a test finds the save the runs started from in the captures.
The fixture is a JSON file that follows the standard’s fixture schema. It holds the starting state with the hash of the save the runs started from (for a patch, the base save’s hash as well as the patched one’s), the hash of the recording for an experiment that has one, the inputs, the expected events by their glossary names, and the expected end state. Each value in the end state is addressed the way a patch addresses a write, by format ID, record index and field path, or by glossary name for a value the game keeps in a global. An experiment that starts from a new game has starting_state: new-game and no save hash, and its Setup section gives every choice made on the way into the game. The implementation’s test suite reads the fixture directly.
Where the generator’s state can be read, the fixture records the state at the start of each run, so a test can replay single runs as well as compare the distribution. For random outcomes it also holds the tolerance: the statistical test a comparison uses and its significance level. A test that compares a distribution runs the rebuild from the states the fixture records, or, where the original’s state could not be read, from the list of states in the fixture’s seeds. It then gets the same result on every run, and a correct rebuild never fails it by chance, which a test drawing fresh random states would do once in every twenty runs at a significance level of 0.05.
Where the original’s calls to its generator can be watched, a run can also list them in order under draws, as a recorded run does. Each draw gives the ID of the rule it was made under, the bound it asked for and the result it got, and a test compares the rebuild’s draws with them one by one. A draw is never identified by the address of its call in the original’s code. The rebuild cites the same rules, so the rule ID is something both sides can name.
Like a patch, a fixture never records the game’s writing, art or code. Where an event or the end state involves a named thing, the fixture identifies it by its index or ID in the game’s files, which is what a test resolves, and may give its name beside that as a label. It leaves out any field of the end state that holds the game’s writing, art or code.
A save recreated from the Setup section and a patch will usually differ from the recorded hash in fields the patch does not set. The hash identifies the save the recorded runs used, and a test compares only what the fixture lists.
The fixture for the example experiment, with one of its 200 runs:
{
"experiment": "EXP-COMBAT-004",
"build": "BLD-GOG-EN-1.1",
"starting_state": {
"patch": "saves/EXP-COMBAT-004.patch.json",
"base_xxh3": "3c1f0e5a9b7d42e68a0c5d1f2b4e6a80",
"xxh3": "e7b24d91c06f3a58b1d2c4e6f8091a3b"
},
"recording_xxh3": null,
"clock": "logic_tick",
"inputs": [
{ "tick": 0, "name": "key_down", "value": "Enter" }
],
"seeds": [],
"runs": [
{
"rng_state": 2851206427,
"events": [
{ "tick": 1, "event": "GangDetected", "args": [{ "format": "FMT-DATA-005", "record": 9 }] }
],
"end_state": [
{ "format": "FMT-DATA-005", "record": 4, "field": "hidden", "value": 1 },
{ "format": "FMT-DATA-005", "record": 9, "field": "hidden", "value": 0 }
]
}
],
"comparison": {
"outcome": "record of the gang in the first GangDetected event, or none",
"counts": { "4": 97, "9": 61, "none": 42 },
"test": "chi-squared",
"significance": 0.01
}
}
The schema in the spec package is the authority on these files once it exists. The examples show the fields this page requires.
Some originals record play themselves, as a demo that plays back on the title screen or a replay file the player saves. A recording like that is an experiment whose inputs come from the file, and the fixture holds the states and events the original reaches as it plays back, the same as for any other experiment. Its starting_state is what playback starts from: new-game for a recording that holds its own starting choices and seed, or the save or patch for one that the original plays back from a save. The recording’s own layout is a format entry. A demo that ships with the game is one of the build’s files, so recording gives its path as the build entry writes it, and a test finds it with the game’s other files. A recording made for the experiment usually holds only a starting seed and the player’s inputs. Then it is committed to recordings/, recording gives that path, and spec/LICENSE lists it as covered by neither licence. One that holds any of the game’s content is not committed. The maintainer keeps it with the captures, named by its hash, and recording gives it as captures/ followed by that hash. The Setup section says how a recording was made or where it comes from.
An experiment can also call one function of the original in a CPU emulator, following the work protocol. Its environment names the emulator and its version and the commit of the harness that loaded the executable, its starting_state is emulated-call, and its Setup section names the function by address, the memory the harness writes before each call, each stub the harness supplies in place of the operating system or the hardware, and any video memory it maps as ordinary RAM. Its fixture has no save hash and no inputs. Each run gives the arguments, by the names in the rule’s Parameters section, and the memory written before the call, by format ID and field path or by glossary name, never by register, stack offset or address, with the generator’s state in rng_state and each port value a stub supplies by its glossary name, and its end state gives what the function returned, under return, and the memory read back after it, addressed by format ID and field path or by glossary name like any other end state. The experiment gives the share of the function’s branches its runs reached, and it covers only those: an entry it supports reaches established only when its runs reach every branch the entry describes and the static reading of the function’s callers and inputs is complete. An emulated call is a run of the original only for what the code decides. It never confirms a reading of something that depends on an interrupt, timing, the operating system or the hardware, because the harness supplies those itself.
Formats
A format entry describes one file format, one structure inside a file, a structure the game keeps only in memory, such as its state block, or a message it sends over a network or serial link. A memory structure or a message has an empty files list, and its evidence locates it by the code that builds or reads it.
id: FMT-DATA-002
title: Site records in SITES.DAT
status: sourced
builds: [BLD-GOG-EN-1.1]
superseded_by: []
files: ["SITES.DAT"] # names or patterns, empty for a memory structure
byte_order: little # little or big, null for a text format; memory structures use the machine's, little on a PC
size: 28 # bytes the layout table covers: one record here, null when it varies
text: false # true for a file the game parses as text (see below)
definition: fmt_data_002.ksy # null only while the status is unknown, or for a text format
evidence: [FND-DATA-002, FND-DATA-004, EXP-DATA-001, SRC-MANUAL-1996]
conflicting: []
split_with: []
related: []
Each item in files is matched against the path of the files in the build entries, where * stands for any run of characters other than /. size is the size in bytes of what the Layout table describes: the whole file for an entry that describes a file, and one record for an entry that describes the records a file repeats, as the example does. A structure inside a larger file, such as a table in the executable’s data or an entry in an archive, lists that file in files, gives its own size in size, and cites a finding that says where in the file it sits in each build.
Body sections:
- Layout: a table with one row per field.
- Enumerations and flags: a table for each, under a
###heading that gives the names of the fields it applies to, with the columnsValue | Name | Meaning | Status | Evidence. Names follow the form described in Names, structures and pointers, and the.ksydefinition uses each one in lower case. A table kept in a value file has the same columns there, and its rows count as the entry’s rows everywhere this page speaks of a format’s tables. - Differences between builds.
- Coverage: which files in which builds the definition has been checked against, and with what result.
- Open questions: the fields whose purpose is still unknown, then anything else.
In the layout table, fields are never grouped into one row, and unknown bytes and padding get rows of their own. The last row of a record table gives the total size:
| Offset | Size | Type | Name | Meaning | Status | Evidence |
|---|---|---|---|---|---|---|
0x00 | 20 | char[20] | name | Site name, ASCII, space-padded, may contain an early NUL | established | FND-DATA-002, EXP-DATA-001 |
0x14 | 2 | INT16LE | id | Site number, 0 to 21 | established | FND-DATA-002, EXP-DATA-001 |
0x16 | 2 | INT16LE | resistance | Starting resistance to Influence | supported | FND-DATA-004 |
0x18 | 2 | INT16LE | support | Support gained when the site is influenced | sourced | SRC-MANUAL-1996 |
0x1A | 2 | INT16LE | unk_1A | Purpose unknown. 0 in every record of the shipped file, and never read by the executable | established | FND-DATA-002, EXP-DATA-001 |
0x1C | Total size 28 |
Each row’s status follows the same table as a whole entry, so an established row cites a static finding and a dynamic finding or experiment (here FND-DATA-002 and EXP-DATA-001), or only static findings that are all in the entry’s complete_reading, and, as for a whole entry, a finding or experiment counts only if it lists the first build in the entry’s builds. A row takes any status but superseded: a row found to be wrong is corrected in place, since rows have no IDs for anything to cite, and git keeps the old reading. A row whose Name changes, such as unk_1A once its purpose is known, is renamed in the same change in the .ksy definition and in every rule, fixture and save patch that uses it. A row’s status covers what the row claims. A row whose purpose is unknown claims its offset, size and type, and whatever has been observed about its values and use, so it can be established while its purpose stays in the Open questions section, as unk_1A is here. Padding claims that the original never reads the bytes. The entry’s own status is the lowest status among the rows of all its tables, layout and enumerations alike, which is why the example is sourced, and it is disputed if any row is. A disputed row’s Evidence cell lists the evidence on both sides. The IDs on the side that contradicts the row go in the entry’s conflicting, and every other ID cited in its tables goes in its evidence.
A field that holds another structure has that structure’s format ID as its type (FMT-DATA-003). An array gives the element type and the count, either a number or the name of an earlier field that holds it (INT16LE[22], FMT-DATA-003[site_count]). A field whose size depends on the data has that size in the Size column as an expression in the pseudocode’s terms (name_length, site_count * 28). Every row after such a field leaves the Offset column empty, because its position follows from the rows before it, and the total row gives the size as an expression. A char[] field leaves the Size column empty, since only the data says where it ends, and a total row after one says variable. A field that exists only under a condition has the condition after its size, 2 if version > 1, and counts as a field whose size depends on the data.
An integer whose bits hold separate values gets a row of its own, followed by one row per group of bits with type bits[n] and the bit range, half-open, in the Offset column: 0x08 bits 0..4, or only bits 0..4 after a variable-size field, where the integer’s own Offset is empty. Those rows leave the Size column empty and do not add to the total. Unused bits get a row as unknown bytes do.
A compressed block is a BYTE[n] row whose meaning names the rule that decompresses it and the format entry for what comes out. The decompression is a procedure like any other, so it is a rule.
The .ksy file next to the entry is the machine-readable definition, in Kaitai Struct. Kaitai only accepts lower-case identifiers with underscores, so the file and its meta/id use the entry ID in that form: FMT-DATA-002 becomes fmt_data_002.ksy, and a definition that uses another one imports it by that name. Its doc fields carry the meanings, its doc-ref fields carry the entry and finding IDs, and meta/license names the licence. A definition can import only one file per format, so a format whose builds span more than one entry of a split format it uses is split as well. Kaitai has no type for FLOAT80LE, REAL48LE or pointers: the first two are read as bytes (size: 10, size: 6) and pointers as the integer they are stored as, with the type from the layout table in doc. An integer split into bit fields is read as those fields, with bN types and meta/bit-endian: le in a little-endian format so that bit 0 is read first, and a compressed block names its rule in a process routine, which each parser implements from that rule. Kaitai can generate parsers in more than a dozen languages and a diagram of the layout from the same file, which is why the standard uses it for every binary format.
Kaitai reads bytes by position and cannot describe a format the game parses as text, such as an INI file, a configuration file of key=value lines, or a table of comma-separated lines. Such a format has text: true, definition: null, size: null and byte_order: null, and the byte order of a UTF-16 encoding goes in its encoding. Its Layout section opens with a paragraph giving the syntax: the encoding, the line ending, how keys, values and sections are separated, what counts as a comment, whether case matters, and what the original does with a missing key, an unknown key or a malformed line. The table after it has the columns Key | Type | Name | Meaning | Status | Evidence, one row per key or column, in the order the original writes them, where Key is the key or the column’s position counted from 0, and Type is the type the original converts the text to. Rows follow the same status rules as a binary layout.
Rules
A rule entry describes one piece of game behaviour: a formula, a procedure, the order in which something is resolved.
id: RULE-COMBAT-007
title: Police detection visits gangs in player order, then roster order
status: established
builds: [BLD-GOG-EN-1.1]
superseded_by: []
evidence: [FND-COMBAT-011, EXP-COMBAT-004]
conflicting: []
split_with: []
related: [RULE-RNG-001, FMT-DATA-004, FMT-DATA-005]
Body sections:
- Summary: one or two sentences a player would understand.
- When it runs: the phase, event or input that triggers it.
- Parameters: the values a
callor anemitpasses to it, in order, with their types. - Inputs: the state it reads, by glossary name, and for a procedure that computes with floating-point values, the precision and rounding in effect (see Types and arithmetic).
- Procedure: the rule in pseudocode, including every random draw in order.
- Outputs: the value it returns and its type, then the state it changes and the events, messages and sounds it produces, in order.
- Edge cases: limits, ties, empty sets, overflow.
- What the sources say: each source’s description with a page or section reference, and where it disagrees with the executable.
- Differences between builds.
- Open questions.
The check script can count the parameters of a Parameters section that is None., for a rule that takes no parameters, or a list with one item per parameter, in the order a call or an emit passes them. Each item opens with a code span that holds the parameter’s name and type, written the way define writes them, then a colon and what the parameter holds:
## Parameters
- `attacker: FMT-DATA-005`: the gang that attacks.
- `defender: FMT-DATA-005`: the gang it attacks.
The type may be left out for the default integer type, as in define. The script counts the items to check every call to the rule and every event the rule handles, so a section in this form holds nothing besides the list. An item that names two parameters, such as - `x`, `y`: the cell, or puts anything between the code span and the colon, makes the list one the script does not count. A section in any other form, None known. included, still passes, but the script cannot count its parameters, and it names every call and emit it could not check because of that.
A procedure keeps each call whose changes a later decision depends on as a step of its own, and says what the rule reads after that call. The Outputs section says which of those changes are left in place when the call is rejected, retried or abandoned part way through (a unit given a new order in the middle of a move, for example), as far as the reading shows, and the Open questions section lists the cases the reading does not settle. Installing a task or callback and running it are separate steps unless the evidence shows they happen together. A callee whose effects the reading has not resolved is listed in the Open questions section, since describing its caller does not show what the callee changes.
Where other actors or parts of the game see the same state at different times, the procedure marks each point at which a change becomes visible to them with a comment that says what becomes visible and to whom, such as # visible: destination square reserved, to other units' route searches. In a game with pathfinding these points include a unit reserving its destination, its position on the map changing, and its drawn position moving between two squares. The procedure says when a shared table is refreshed from the record it copies, and what another actor reads between those points. For a reservation, the Outputs section says whether it survives a refresh of the table that holds it and whether it survives a save and restore, and the Open questions section lists whichever of these the evidence does not show yet. A check made by whatever requests an action and a check made again inside the action itself are separate gates. The When it runs section lists, for each caller, the arguments it passes that change what the gate checks, the conditions under which it skips the gate, and what it does after a rejection or a retry.
The whole of rules/RULE-COMBAT-007.md:
---
id: RULE-COMBAT-007
title: Police detection visits gangs in player order, then roster order
status: established
builds: [BLD-GOG-EN-1.1]
superseded_by: []
evidence: [FND-COMBAT-011, EXP-COMBAT-004]
conflicting: []
split_with: []
related: [RULE-RNG-001, FMT-DATA-004, FMT-DATA-005]
---
## Summary
At the start of each police phase, the police may find each hidden gang. They check the players in turn order, and each player's gangs in roster order.
## When it runs
At the start of each police phase, as the first handler of `PolicePhaseStarted`.
## Parameters
None.
## Inputs
`turn_order`, each player's `roster`, each gang's `hidden` and `detection_chance`, and the state of `rng` through `roll`.
## Procedure
```text
for each player in turn_order:
for each gang in player.roster:
if gang.hidden and roll(100) < gang.detection_chance:
gang.hidden = false
emit GangDetected(gang)
```
## Outputs
No return value. Sets `hidden` to false on each gang found, and emits `GangDetected(gang)` for each, in the order they are found. Makes one draw from `rng` for each hidden gang and none for a gang that is not hidden.
## Edge cases
A `detection_chance` of 0 never finds the gang and one of 100 or more always does, since `roll(100)` gives 0 to 99. The draw is made either way.
## What the sources say
SRC-MANUAL-1996, in the chapter on the police, says hidden gangs can be found each turn and gives neither the chance nor the order.
## Differences between builds
None known.
## Open questions
None known.
Bugs
A bug entry records a defect in the original: behaviour the game’s own design does not appear to intend. The format follows UESP’s bug template, with fields added for what a rebuild needs to decide.
id: BUG-COMBAT-002
title: Freeze after the detailed combat sequence ends
status: supported
builds: [BLD-GOG-EN-1.1]
superseded_by: []
impact: hang # crash, hang, save-corruption, rules, presentation or performance
intent: unintended # unintended or unclear
player_reliance: unknown # relied-on, not-relied-on or unknown
evidence: [EXP-COMBAT-006]
conflicting: []
split_with: []
related: [RULE-COMBAT-012, SCR-COMBAT-003]
The status says how well the behaviour itself is shown, the way it does for a rule. Whether the designers meant it is a separate judgement, recorded in intent. A behaviour that may have been a design decision is still recorded as a bug, with intent: unclear and the reasons on both sides in its Mechanism section. One that turns out to be intended is superseded by the evidence that shows it. That is usually a source: the manual describing the behaviour as a feature, a designer’s statement, or patch notes that keep it on purpose. A source is enough here because it settles what the designers meant, and the behaviour itself is not in dispute.
Body sections:
- Symptom: what the player sees.
- Trigger conditions: what has to happen for it to occur.
- Mechanism: what goes wrong in the original.
- Frequency: how often it happens under those conditions.
- Player reliance: whether players rely on it, with evidence such as strategy guides or speedrun rules.
- Fixes elsewhere: official patches, community patches, wrappers.
- Differences between builds.
- Open questions.
What our rebuild does about the bug belongs in the implementation’s deviation log, because another engine may choose differently.
Screens
A screen entry describes one screen, panel or dialog: the resources it draws with their positions, its input, its sounds, its states and the transitions between them. Its front matter holds the same fields as a rule’s, plus the native canvas it is drawn on. Its tables cite evidence the same way a format’s do: the IDs that contradict it go in conflicting, and every other ID cited in its tables goes in evidence. Its tables have no Status column, so the entry’s status covers the whole screen, and a part that is less certain than the rest goes in its Open questions section.
id: SCR-COMBAT-003
title: Combat results panel
status: sourced
builds: [BLD-GOG-EN-1.1]
superseded_by: []
resolution: 640x480
evidence: [SRC-MANUAL-1996]
conflicting: []
split_with: []
related: [RULE-COMBAT-012, SCR-MAP-001]
Body sections, each table with an Evidence column as its last:
- Drawn elements:
Element | Resource | Shows | Position | Shown when. - Mouse input:
Region | Rectangle | Enabled when | Effect. - Keyboard input:
Key | Enabled when | Effect. - Other input:
Device | Input | Enabled when | Effect, for joysticks and anything else that is not the mouse or keyboard. - Sounds:
Sound | Resource | Played when. - States:
State | Entered when | Left when. - Timing: how long animations and transitions take, in milliseconds or ticks.
- Differences between builds.
- Open questions.
A mouse input row’s Region cell names the region, then a comma and the event that triggers the effect: a button (left, right, middle or any) followed by press, release or double click, or one of pointer enter, pointer leave and pointer move, as in OK button, left release. A modifier key held with the button goes in front of it, joined by + the way keys are (Map, Shift+left press), and an event the list has no word for, such as a drag or the wheel, is written after the comma in plain words. A region’s name has no comma in it, so the event is everything after the comma, and a Region cell with no comma says the event is not known.
A region has a row for each event it responds to, and a row for each state in which one event has a different effect, told apart by their Enabled when cells. An any row says every button has that effect, so the region has no row for a single button with the same action. A double click row’s Effect says whether the second click also has the effect of the region’s press and release rows.
An effect names a rule or another screen. A key is named by what is printed on it on a US keyboard (A, F1, Enter, Left), with modifiers joined by + (Ctrl+S). Keys that work on every screen are described in a rule, and a game that reads scan codes, or behaves differently with another keyboard layout, says so in that rule. Text shown on the screen is referenced as a resource, in the notation for resources. The entry may quote a short label beside the reference, such as a button caption or a screen title, and never copies longer text. An element that draws a value the game computes, such as the player’s cash or the turn number, gives in its Shows cell the value by glossary name and the rule that turns it into text, and its Resource cell gives the font it is drawn in. An element that draws only a resource says None in Shows. Every rule a Shows cell names is in the screen’s related field.
Checks
The check script runs in each game repository on every change, and the build fails if any check fails. The Dark Sun and Conqueror repositories run it through the toolkit’s action, and Chaos Overlords runs it from tools/check-documentation.mjs, which passes the game’s own settings to the toolkit’s checker. Save patches are written and committed as usual, but no tool applies them yet, so the Setup section of an experiment that uses one also gives each write as a byte offset and a value, worked out from the layout table.
The script checks that:
- every entry’s front matter validates against the schema for its kind, its file name is its ID, and it sits in the directory for its kind (ENTRY-TYPES-4 to ENTRY-TYPES-6);
- every body has its sections, under the headings given here, in order (ENTRY-TYPES-1);
- every ID has the form for its kind, with an area from the area list where the kind has one, every ID cited anywhere resolves to an entry, and no ID is used twice (IDENTIFIERS-1 to IDENTIFIERS-4);
- no entry that exists on the main branch has been deleted or renamed, except one the change lists as squashed into the replacements its
superseded_bynames and that nothing but the indexes andPARITY.mdmentions any more, and no area has been removed from the area list or renamed (IDENTIFIERS-5 to IDENTIFIERS-7); - where function inventories exist, no range for that build and file ends on the last byte of an inventoried function where no inventoried function starts, unless its entry lists it in
ends_on_last_byte, and every range that field lists is one the entry gives and one this check would fail without the listing (Notation and ENTRY-TYPES-22); - every status is from the list for its kind, and no row of a format’s tables is
superseded(STATUS-1 and STATUS-21); - every entry cites what its status requires, counting any source, and counting a finding or experiment only when it lists the first build in the entry’s
builds(STATUS-1 and STATUS-18); - every ID in a
complete_readingis a static finding that the entry lists inevidenceand that lists the entry’s first build, and no rule whose procedure has a# may run:comment has a non-emptycomplete_reading(STATUS-4); - every
reproducedentry names someone inreproduced_byother than itsrecorded_by, and every other finding and experiment has an emptyreproduced_by(STATUS-21); - every build a
supportedorestablishedentry lists is covered by the evidence it cites, and every experiment lists exactly one build (ENTRY-TYPES-7); - every static finding has at least one location for each build it lists, every location names a build the finding lists and a file in that build’s
fileslist or a member its archive’smembersgives, and everyaddressis written in the notation for that file’s format (the unpacked format for a packed file, the member’s format for a member), with anoffsetin its place only for a data file, overlay code or an explicitkind: file-datalocation within the shipped file or the expanded member, and everykindiscodeorfile-data, given only in an executable, with afile-datalocation giving anoffsetand never anaddress, andunpacked: truegiven only on afile-datalocation in a packed file, whose offset is then checked against the unpacked size; - every build entry’s Code ranges section is a table in the form ENTRY-TYPES-19 gives or says
None., every finding a row names lists that build and has a location in that file of that build that is notkind: file-data, and everyoffsetrange in overlay code lies wholly inside one of that section’s rows for its file; - every build’s
manifestexists and validates against the manifest schema, and every manifest belongs to a build entry (ENTRY-TYPES-10); - no path in a build’s manifest is listed twice, lies under a directory exclusion, or is also in its list of other files, and every item in the list of other files has a path and a reason and is listed once (ENTRY-TYPES-14 and ENTRY-TYPES-15);
- every build’s
listing, where it has one, exists, validates against the listing record schema, belongs to that build, lists no path twice, gives its items in sorted order, and agrees with the manifest and the list of other files, and every listing record belongs to a build entry (ENTRY-TYPES-16 to ENTRY-TYPES-18); - every value file belongs to the entry its name gives and is named by that entry, every
tablethat takes its values from one has as many rows as its count, every list written out in a procedure or atabledefinition has at most 64 values, and every value file of an enumeration table has that table’s columns; - every packed file in a build entry gives its packer and the size, hash, format and unpacking tool of its unpacked form;
- every member in an archive’s
membersgives its path, size, hash, format and the tool that expanded it, no archive lists a member’s path twice or one that starts with/, has a..segment or contains|, no member haspacker,unpackedormembers, every location, Code ranges row, resource reference and function inventory that names a member names one its archive lists, everyoffsetin a member lies within the member’s size, and, where the build’s listing record went inside the archive, every member it lists is an item of the record with the same size or astoppeditem (ENTRY-TYPES-20 and ENTRY-TYPES-21); - every pattern in a format entry’s
filesmatches a file in each build the entry lists; - no entry that is not superseded cites a superseded entry, whether in
builds, a location,evidence,conflicting,related, a draw in an experiment’s fixture or a row of a build’s Code ranges section, and every entry that is not superseded has an emptysuperseded_by(STATUS-17 and ENTRY-TYPES-4); - every superseded entry names what replaced or disproved it, and no chain of
superseded_bylinks leads back to where it started (IDENTIFIERS-7); - a finding’s or experiment’s
superseded_bynames only findings and experiments, a build’s only builds, a source’s only sources, and a source appears in any other kind’ssuperseded_byonly in a bug’s (IDENTIFIERS-7 and STATUS-23); conflictingholds only findings and experiments and is empty unless the status isdisputed, andrelatedlinks only to the kinds allowed for the entry (ENTRY-TYPES-5 and ENTRY-TYPES-6);- a rule’s
relatedfield holds every rule its procedure calls, takes a function or table from, or names in a# may run:comment (all the entries of a split rule), every format whose structures it reads, writes, makes withnewor passes toread_fileorwrite_file, every format whose enumeration names it uses, and every screen it names withshow; - every
callpasses one argument for each item of the called rule’s Parameters section, in each entry of a split rule that lists one of the calling rule’s builds, every call to a function a rule defines passes one for each parameter of itsdefine, everyemitpasses one for each item of the Parameters section of each rule the event’s glossary entry names as a handler, in each entry of a split rule that lists one of the emitting rule’s builds, and twoemits of one event in rules that share a build pass the same number of arguments. An event with no handlers is checked only against its otheremits, since its glossary entry gives what it carries in prose. The script names each call andemitit could not count because a Parameters section is not in the form Rules gives, and those do not fail the check; - every rule a format’s tables name is in the format’s
relatedfield, every rule and screen a screen’s effects or Shows cells name is in the screen’srelatedfield, and every bug’srelatedfield names at least one rule, format or screen; - every entry in a
split_withlist names the others back and lists none of their builds, and every build an entry lists is listed by one entry of each split rule it calls or relates to and of each split format it names (ENTRY-TYPES-8); - every finding has
environment, set for a dynamic finding andnullfor a static one; - every glossary file is named after the term in its
#heading, no two terms differ only in case, and no term is a device name Windows reserves; - every glossary entry gives what the table under Where it lives asks of its kind of term, and every address, order and outside source it claims is followed by evidence IDs that resolve to entries that are not superseded, or by
(unknown); - every rule cites in its
evidencethe evidence of each glossary claim its procedure relies on, lists in its Open questions section each such claim that is(unknown), and is notestablishedwhile any of them is; - every name in a procedure that has the form of a neutral name for a function, global or script, every name it assigns without
letthat is not a local, every value from outside the game it reads, every function, table, clock and generator it uses other than the built-in ones, every queue it drains, and every event it emits has a glossary entry; - no procedure assigns to a value from outside the game, and every upper-case name in a procedure, other than an ID or a type, is an enumeration name from a format entry;
- every neutral name for a function, global or script that a procedure uses is the one from its first build and is listed in its Open questions section, and no procedure contains an address outside a neutral name;
- no list of fixed length is given to
append,insertorremove_at, and every field name whose structure has a known type is in that format entry’s layout; - every row of a format’s tables cites what its own status requires, and a format entry’s status matches the rows of all its tables;
- the
evidenceandconflictingof a format or screen entry together cover every ID in its tables; - every enumeration table sits under a heading naming fields in its format’s layout, and every enumeration name is unique across the spec apart from the entries of a split format;
- every resource a screen or procedure references, and every file a procedure reads or writes, is in a file that a format entry lists, with a member of an archive counting as the archive (ENTRY-TYPES-14), apart from CD audio tracks, which the build entry lists;
- every binary format entry’s
definitionexists unless the status isunknown, and every text format entry’sdefinition,sizeandbyte_orderare null; - every Kaitai file belongs to the format entry its name and
meta/idgive, compiles, names the licence inmeta/license, and has fixed sizes that match the layout table in its entry; - every experiment’s
fixtureexists and validates against the fixture schema, every event it names has a glossary entry, every format, field path and glossary name in its end state exists, every draw names a rule entry, it gives the hash of the save its runs started from unlessstarting_stateisnew-gameoremulated-call, and, for a patch, the hash of the base save as well; - no rule whose procedure has a
# may run:comment isestablished, or has avalidatedrow, when every experiment it cites hasstarting_state: emulated-call(STATUS-15); - every
starting_statethat names a save or a patch points to one insaves/, and every patch validates against the fixture schema and names formats and field paths that exist in their layout tables with statussupportedorestablished; - every save in
saves/and every file inrecordings/matches the hash in its fixture, is named by some experiment, and is listed inspec/LICENSEas covered by neither licence; - every
recordingthat gives a path outsiderecordings/andcaptures/names a file in the experiment’s build, and every experiment with arecordinghas that recording’s hash in its fixture; - no Markdown file in
spec/outside the generatedspec/index/names a path in the directories that hold the rebuild’s code and tests, or gives the file name of one of the rebuild’s source files in them on its own, as Where it lives requires. The repository gives the script those directories, and they aresrc/andtests/unless it names others. A tool that reads the original is not part of the rebuild, so a finding’s How to reproduce section and an experiment’senvironmentmay still name one; - the standard version in
spec/README.mdis the major version of the spec package the repository pins; - every Markdown file in
spec/,parity/,deviations/andvalidation/, andPARITY.md, is at most 1,000 lines long; - the four indexes in
spec/index/are regenerated and up to date, each split as File size describes, by area, then by kind, then by block, exactly where the limit requires it, or, where they are updated on the main branch only, the change leaves them as they were where it forked (see Where it lives).
The status index doubles as a progress report: how much of the game is established, how much is still a guess, where the open questions are, which established entries rest only on evidence nobody has reproduced yet, and which rest on a complete reading alone.
Implementation side
The implementation keeps three things that refer to the spec without being part of it. Code comments and tests cite the spec IDs they implement or check, so a search for an ID finds everything that depends on it. The deviation log records every place the rebuild departs from the spec on purpose. The parity matrix records how much of the spec the rebuild does, and the project status is worked out from it.
Deviation log
The deviation log is the directory deviations/ at the root of the game repository, with one Markdown file per deviation, named after the deviation’s ID: deviations/DEV-COMBAT-002.md. The file opens with the ID as a # heading. DEV is not a spec kind, but its IDs take the same DEV-AREA-NNN form and use the spec’s area list. Under the heading comes a list with these items, in this order, and then any explanation in plain paragraphs. The Departs from item names at least one rule, format or screen, so that the deviation shows up in a parity row: a fix for a bug names the bug and the entries the fix changes.
# DEV-COMBAT-002
- Departs from: BUG-COMBAT-002, RULE-COMBAT-012
- Reason: The original freezes after the detailed combat sequence ends.
- Setting: None
- Default: mandatory
- Justification: The original stops responding at this point, so there is nothing after it for a player to keep.
- Tests: tests/Combat.Tests/DetailedCombatEndTests.cs
- Dropped: no
Setting is the name of the setting that controls the deviation, or None. Default is off when the setting starts with the original’s behaviour, on when it starts with the rebuild’s and a player who wants the original’s turns it off, and mandatory when Setting is None and the rebuild’s behaviour cannot be turned off.
off is the default for any deviation. A fix of a bug whose intent is unintended and whose player_reliance is not-relied-on is on, or mandatory with the reasoning below. Any other deviation is on or mandatory in two cases. The first is where the rebuild’s behaviour is strictly better than the original’s, so that no player loses anything by it they would miss: it adds to the interface, it changes only what happens once the original would have crashed or hung, or it removes a failure, a delay or a hidden dependency without taking away an outcome the player could reach. The second is a judgement call that makes the game better to play and that a reasonable player would not want undone, such as keeping precision the original threw away, pacing the game by a fixed clock where the original followed the speed of the machine, or laying out text correctly where the original broke on an edge case. A judgement call stays small: it changes nothing players build strategies around and takes away no outcome the player could reach. A change that some players would reasonably prefer the original’s way, because it is a matter of taste or because it changes what the rules produce in a way players notice, is not a judgement call of this kind. It gets a setting, and its Default is off. A quirk that may be deliberate or that players rely on never qualifies for on or mandatory, and its deviation stays off.
A deviation that is mandatory, or on without being such a bug fix, has a Justification item between Default and Dropped that makes that argument: why the rebuild’s behaviour is strictly better, or, for a judgement call, what it improves for the player and why no player would miss the original’s behaviour. For mandatory it also says why the original’s behaviour is not worth a setting, for example because nothing after a crash can be played, because the original’s version cannot be reproduced at all, or because nobody would switch it back. No other deviation has a Justification item.
A mandatory deviation may have a Replaces item directly after Departs from. It names the entries in Departs from that the deviation replaces entirely, so that nothing the entry describes is left in the rebuild for a test to compare with the original. An entry the deviation changes only in part is not named there. DEV-COMBAT-002 above changes what happens once the detailed combat sequence ends, and the rest of RULE-COMBAT-012 can still be compared with the original, so it has no Replaces item. A deviation that replaces no entry entirely has no Replaces item, and neither does a deviation with a setting, since the original’s behaviour is still there to switch back to.
A deviation may have a Tests item between Justification (or Default, where there is none) and Dropped. It lists, by their path from the repository root and separated by commas, the test files that check the rebuild does what the deviation’s Reason says it does in place of the original’s behaviour, and a deviation with no such tests has no Tests item. Each of them mentions the deviation’s ID. These tests compare the rebuild with the deviation’s own description, so they are not parity tests and the validation record does not list them. Nothing records a local run of them, so each one runs in CI and none of them needs GAME_DIR. A Reason that a Tests item checks describes the rebuild’s behaviour in terms a test can confirm: what the player sees or can do, what the rebuild writes or reads, which file or value it uses. A Tests item on a deviation with a Replaces item is what lets the rows it replaces become deviated in the parity matrix.
Deviation IDs are never reused or renumbered, the same as spec IDs. A deviation that is dropped keeps its file, and its Dropped item gives the date, written YYYY-MM-DD, and the reason in place of no, so a comment or row that cited it can still find out what happened. Its Departs from, Replaces and Tests items stay as they were when it was dropped, the way a superseded entry keeps its links, and it no longer appears in any parity row.
Parity matrix
The parity matrix sits at the root of the game repository, next to spec/, and outside it because it describes our rebuild and the spec never does. Its rows are in the directory parity/, with one Markdown file per area, named after the area: parity/COMBAT.md. PARITY.md holds the totals and a link to each area’s file, or to its directory where the area is split. The check script writes it, the same as the indexes: nobody edits it by hand, and a change that leaves it out of date fails the check, or, where it is updated on the main branch only, a change that edits or removes it fails (see Where it lives).
The matrix has one row for every rule, format and screen entry in the spec that is not superseded. Behaviour without a spec entry cannot have a row, so work on anything new starts with an unknown entry in the spec. Bugs have no rows of their own: reproducing a bug is part of the rule, format or screen it occurs in, and not reproducing it is a deviation. PARITY.md has two tables, one counting rows by Status and one counting them by Code, since Status alone shows an established row the same way whether its code is missing or half written, and then the list of area files in the order of the area list in spec/README.md. An area file opens with the area as a # heading, followed by one table of its rows sorted by ID. An area whose file would pass the size limit is split the way that section describes, by kind and then by block: parity/COMBAT/RULE.md, parity/COMBAT/RULE/000.md. A cell with nothing in it says None, a word no column uses as a value:
| Spec ID | Title | Spec status | Code | Tests | Deviations | Status | Notes |
|---|---|---|---|---|---|---|---|
RULE-COMBAT-007 | Police detection visits gangs in player order, then roster order | established | complete | tests/Combat.Tests/DetectionOrderTests.cs | None | validated | None |
RULE-COMBAT-012 | Detailed combat resolution | supported | partial | None | DEV-COMBAT-001, DEV-COMBAT-002 | supported | Hit chance is a placeholder until an experiment measures it. |
SCR-COMBAT-003 | Combat results panel | sourced | missing | None | None | sourced | None |
Spec ID, Title and Spec status are copied from the spec entry. Notes says in plain words what is missing or wrong.
Code is missing, partial or complete, and complete means the rebuild does everything the entry describes. A deviation listed in the row does not count against it: Code measures the rebuild against the entry as its deviations change it, whatever their Default, so a row whose entry a deviation replaces entirely is complete once the rebuild does what the deviation says instead. Code records how much of the work on an entry is done, and a part the rebuild does differently on purpose is done. A row whose spec status is unknown cannot be complete, since the entry does not describe anything yet. A placeholder in the code, such as a guessed formula, is marked with a PLACEHOLDER: <spec ID> comment, and a row whose ID appears in one cannot be complete.
Tests lists test files by their path from the repository root, and only files whose tests compare the rebuild with evidence from the original: experiment fixtures, including those whose inputs come from a recording the original made, and distributions measured in it. A test that replays a fixture runs against the experiment’s build, and a test that compares a distribution runs from the generator states the fixture gives (see Experiments). Listed tests run with every deviation that has a setting switched off. A mandatory deviation cannot be switched off, so a listed test that reaches the behaviour it changes cites the deviation’s ID and leaves that case out or compares with the original’s result as the deviation changes it.
For a format row, the evidence is the original files themselves: a test counts if it decodes every file the entry lists from every build the entry lists, reads every byte, and gets the same value for every field as the entry’s Kaitai definition. A format with no files, a memory structure or a message, is compared with what was recorded from the original running: the state in an experiment fixture, or memory dumps and captured messages, which a test decodes the same way it would decode a file.
For a screen row, the evidence is captures of the original that the screen entry’s findings and experiments record by hash. A test counts if it brings the rebuild to the state a capture shows, from the same starting state and inputs, and compares each frame with the capture pixel for pixel. Sound is compared by which resource starts playing and on which tick, from the events in an experiment fixture, since a recording of the mixed output differs with the sound card and the emulator. A dump or capture that holds content is kept with the maintainer’s copy of the game and found through GAME_DIR, as described below.
Decoder tests on synthetic files, which cover edge cases the shipped files never reach, compare the rebuild with the spec and are left out of the column. So are the tests over synthetic state that an implementation batch writes, such as the one for each branch of an entry (see the work protocol’s Implementation batches), tests that compare the rebuild with an earlier version of itself, and the tests in a deviation’s Tests item. A row whose only tests are of these kinds has Tests None, and its Status shows that nothing has compared it with the original yet.
Each listed test file shows what it compares with, so the check can tell it from those tests. A file that reads the original’s files carries the needs: GAME_DIR comment described below. Any other listed file names, by its ID, an experiment that the row’s entry lists in its evidence, or that a bug whose related field names the entry lists in its own, and its tests replay that experiment’s fixture or compare with the distribution measured in it. The bug’s experiments count because reproducing a bug is part of the row of the entry it occurs in. A listed file that does neither fails the check, because nothing in it shows a comparison with the original.
Deviations lists every deviation log entry that has not been dropped and whose Departs from names the row’s ID, and no others.
Status is worked out from the other columns:
| Status | When |
|---|---|
| the spec status | Code is missing or partial, or the spec status is disputed. |
implemented | Code is complete, Tests is None, the spec status is not disputed, and the row is not deviated. |
deviated | Code is complete, Tests is None, the spec status is not disputed, Deviations lists at least one mandatory deviation whose Replaces item names the row’s ID, and every mandatory deviation it lists has a Tests item. |
validated | Code is complete, Tests lists at least one file, and the spec status is supported or established. |
deviated is a finished row, the same as validated, for an entry the rebuild replaces on purpose and has no way to compare with the original: a network protocol nobody can connect to, a save format the rebuild keeps in a form of its own, a display mode that current systems do not offer. A mandatory deviation cannot be switched off, so no test of the rebuild can reach the original’s behaviour, and what is checked instead is that the rebuild does what the deviation says. That check is the deviation’s Tests item. A row reaches deviated only when one of its mandatory deviations names it in a Replaces item and every mandatory deviation it lists has a Tests item. A deviation that changes only part of an entry leaves the rest to compare with the original, so the row needs tests that compare it, the same as a row with no deviations. A row that a deviation names in Replaces has nothing left to compare with the original, so its Tests is None and the tests of what the rebuild does in its place are listed in the deviation’s Tests item. A deviation with a setting never makes a row deviated, since the suite runs with the setting switched off and the original’s behaviour is there to compare.
A row where Code is complete, Tests lists a file and the spec status is sourced fails the check. The spec is behind: the evidence behind those tests belongs in the spec entry first. A validated row also needs each of its test files that reads the original’s files in a run file of the validation record, with the hash it has now.
An entry can become disputed after its row was validated, when a new finding or experiment contradicts it. The row then shows disputed whatever its Code and Tests say, because nobody can yet tell which side of the dispute the tests should agree with, and its Notes names the evidence in the entry’s conflicting. Code and Tests stay as they are, so the row goes back to validated once the entry is settled in favour of what the tests check. If it is settled the other way, the entry is superseded, and the code and tests move to the entry that replaces it.
Manual play never counts as a test.
The spec check script also checks the parity matrix: every area file is named after an area in the area list and holds only rows of that area (and of its kind and block, in a split file), is split exactly where the size limit requires it, every eligible spec entry has exactly one row, the copied columns match the spec, every status follows the table above, every listed test file exists, mentions the row’s ID, and carries the needs: GAME_DIR comment or names an experiment that the row’s entry, or a bug whose related field names it, lists in its evidence, no complete row has its ID in a PLACEHOLDER: comment, every listed deviation exists in deviations/, has not been dropped and names the row in its Departs from, a row that a listed deviation names in Replaces has Tests None, every test file the Tests item of a deviation that has not been dropped lists exists, mentions the deviation’s ID and does not mention GAME_DIR, every deviation that has not been dropped is listed in each row it names, no listed test file mentions GAME_DIR without the needs: GAME_DIR comment, every test file of a validated row that has the comment is in a run file in validation/ with the hash it has now, every run file is named after its date and commit and still records at least one such file with the hash it has now, validation/ holds nothing else, there is no VALIDATION.md at the root, and PARITY.md is regenerated and up to date, or, where it is updated on the main branch only, the change leaves it as it was where it forked.
It checks the rest of the implementation’s references too. Every spec ID and deviation ID in the code, the tests, parity/ and deviations/ must resolve, and a citation of a superseded spec entry fails the check until it is moved to what replaced it, so code does not keep pointing at a claim that was withdrawn. The Departs from and Replaces items of a dropped deviation are the one exception. Deviation IDs are unique and follow the ID form, every deviation file is named after the ID in its heading, every deviation has its list items in order, every deviation that has not been dropped departs from at least one entry that has a row, its Default follows the rules for deviations above and it has a Justification item exactly when they require one, only a mandatory deviation has a Replaces item, every ID in a Replaces item is a rule, format or screen that its Departs from also names, and no deviation file that exists on the main branch is deleted or renamed.
Every listed test is a test of the rebuild; runs of the original itself are experiments, which produce the fixtures. A listed test runs without the original game’s files wherever its evidence allows, which is when everything it compares with is committed: a fixture of an emulated call, for example, holds the arguments and the end state and nothing else. Other listed tests need content that cannot be committed: a format test reads the shipped files themselves, a screen test compares frames with captures that show the game’s images, and a fixture that starts from a base save needs that save from the player’s copy. Such a test file carries the comment needs: GAME_DIR, in whatever comment syntax its language uses, so the check can tell it apart, and it finds the files under the directory named by the GAME_DIR environment variable. It holds one directory per build, named by the build’s ID, with the files laid out as the build entry’s paths give them and a file from a disc under a directory named after the disc (CD, CD2): GAME_DIR/BLD-GOG-EN-1.1/Chaos Overlords.exe. An audio track is the raw audio the build entry hashes, in that directory under its track name with a .cdda extension: GAME_DIR/BLD-GOG-EN-1.1/CD/track02.cdda. Next to them, GAME_DIR/captures/ holds the dumps, captures, recordings and saves that cannot be committed, each named by its hash. A test checks every file it reads against the hash its build entry or its finding gives. A test that needs a build or a capture that is not there reports itself as skipped. A format test needs every build its entry lists, and a marked test that replays a fixture needs the experiment’s build, so the copy a maintainer runs them with holds every build that a validated row’s marked tests use.
The test suite runs on every change and writes a report of which tests passed, failed or were skipped. A failing test fails the build. The original’s files can never be uploaded anywhere a CI runner could fetch them, so CI runs without them. There every test without the needs: GAME_DIR comment runs and must pass, which is what confirms the unmarked tests of a validated row, and the marked tests report themselves as skipped, which does not fail the build. The marked tests run on a maintainer’s machine, with a copy the maintainer owns, and the validation record is how that run reaches the repository.
Validation record
After a run in which every test in every marked test file of every validated row passed and none was skipped, the maintainer records the run with the check script. It writes a run file to validation/ at the root of the game repository, next to PARITY.md, named after the day of the run and the first 12 hex digits of the commit it tested, such as validation/2026-09-26-3f9c2d4e8a1b.md:
# Validation run
The test files of the validated parity rows that read the original's files, as they were when every test in them passed against those files.
- Commit: 3f9c2d4e8a1b7c6d5e4f3a2b1c0d9e8f7a6b5c4d
- Date: 2026-09-26
- Builds: BLD-GOG-EN-1.1
| Test file | SHA-256 |
|---|---|
| `tests/Formats.Tests/UnitTableTests.cs` | `9b0e4c1d2a3f4e5d6c7b8a9f0e1d2c3b4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d` |
Commit is the commit the run tested, Date the day it ran, and Builds the IDs of the builds in GAME_DIR it used. The table lists every marked test file that a validated row lists, once each, sorted by path, with the SHA-256 of the file’s contents with each CRLF read as LF, so that a Windows checkout and a Linux one give the same hash. The run file is committed with the change it validates. Nobody edits it afterwards, and only a second run of the same commit on the same day, which gets the same name, replaces it.
A marked test file of a validated row counts as validated while any run file in validation/ records it with the hash it has now. A run file none of whose rows matches a marked test file of a validated row as it is now records nothing about the tree any more, and the check script deletes it when it records a new run. Each run has a file of its own, so two branches that record runs add two files and do not conflict, and once they merge each run keeps counting for the test files it still matches. A row of a run file may name a file that has changed since or is no longer a marked test file of a validated row, as long as some other row of that run file still matches. A run file can also come to match nothing without a new run: after a merge of two branches that between them changed every file it still matched, or when the rows whose files it matched stop being validated. Such a file is deleted by hand, which needs no run.
The check fails a validated row whose marked test file no run file records with the hash it has now, a run file none of whose rows matches a marked test file of a validated row as it is now, a run file whose name does not give its Date and the first 12 hex digits of its Commit, any other file in validation/, a VALIDATION.md at the root (drafts of this version kept a single record there, and a repository that has one moves it into a run file), and a listed test file that mentions GAME_DIR without the comment, since that test would skip itself in CI while its row claims to be validated. A change to a validated row’s marked tests therefore needs a new local run before it merges. The hash covers only the test file, so a maintainer who changes code that a validated row’s marked tests exercise runs those tests again and records the run before the change merges, even though the check cannot tell. For the same reason, a test file put back as it was in an earlier run counts as validated by that run while its file is still there. The check cannot see an unmarked test that skips itself for some other reason either, and such a test is a bug in the test.
Project status
A restoration that has a spec and a parity matrix has one of four statuses, which say how far its rebuild has come. The game pages on this site use the same four. A game nobody has started work on has no status.
The conditions are counted in the parity matrix, from the Code column, which a row’s deviations do not count against (see Parity matrix). Rule and screen rows are counted together, and format rows only for complete, because a rebuild usually keeps its state and its saves in formats of its own and leaves many of the original’s record layouts out. Percentages are of all the rows counted, including those whose spec status is unknown.
Playable means that a player can start a new game in the rebuild and reach one of the original’s endings through its own interface, without editing a file or using a debugging tool. For a game that has no ending, it means reaching every mode of play the original offers. The repository’s README.md says how to get the rebuild running.
| Status | When |
|---|---|
early-prototype | Work has started, and the conditions of limited-fidelity-playable do not hold. |
limited-fidelity-playable | The rebuild is playable, and at most 10% of rule and screen rows are missing. |
high-fidelity-playable | The rebuild is playable, no screen row and at most 5% of rule rows are missing, and at least two thirds of rule and screen rows are complete. |
complete | The rebuild is playable, and every row, formats included, is complete. |
A complete rebuild can still differ from the original in the places its deviation log records, and in no others. A restoration has the highest status whose conditions hold. A new spec entry adds a row, which can take a condition away, and the status then drops until the rebuild catches up.
A status counts how much of the spec the code does and says nothing about tests. A complete rebuild whose rows are implemented and not yet validated has not been compared with the original, and the Status table in PARITY.md shows how far that comparison has got. A deviated row has no original behaviour left to compare with, and counts as finished without it.
The check script does not work out the status. It is stated by hand wherever it is used, and anyone can confirm it from the area files in parity/.
Licence
The Markdown files in spec/, entries as well as README.md, the glossary and the indexes, are published under Creative Commons Attribution 4.0 (CC BY 4.0). Anyone can copy and adapt them, commercially or not, as long as they credit the project. That includes adding them to a wiki under CC BY-SA, the licence many community wikis use. The licence covers our own text. Short quotations from manuals and other sources, and values quoted from the original, remain under their owners’ terms.
The machine-readable parts (Kaitai definitions, experiment fixtures, save patches, build manifests, listing records and lists of other files, value files, schemas and scripts) are code, and are published under the MIT licence, the same as the rebuild, so they can go into another engine’s source tree without a second licence to track.
Save files are written by the original game, so neither licence covers them. A save that contains any of the game’s content, as Where it lives defines it, is not committed, which rules out most saves and is why experiments start from save patches. A save that holds none is committed to saves/ and listed in spec/LICENSE as covered by neither licence. Demos and replays the original recorded are treated the same way, in recordings/. Screenshots, video and audio captured from the original are not committed either, since they show the game’s images or play its sound. Entries record their hashes, and the entry’s text says how to capture them again.
spec/LICENSE holds both texts and says which files each covers, and which are covered by neither, so the spec/ directory keeps its licence when it is copied to a community wiki or into another project.
PARITY.md, parity/, validation/ and deviations/ describe the rebuild and sit outside spec/, so they are under the game repository’s own licence, the same as the code.
Versions
A change that would make a spec that follows the current version fail its checks gets a new version number, listed in the table below with what changed and how to convert an existing spec. A change that every spec passing the old checks still passes, such as a new optional field, a new value in a field’s list, or a check that no valid spec can fail (such as a new check of a rule the version already had), raises the spec package’s minor version and keeps the standard’s number. A clarification that no check notices changes neither. A breaking change to the spec package that leaves the standard alone, such as a renamed command-line option, waits for the next version of the standard, so that a major version number always means one version of the standard.
When a new version is published, this page as it stood for the previous one stays online at its own address, such as /documentation-standard/v1/, and the table links to it, so a spec that pins an older version can still be read against its text.
| Version | Changes | Converting a spec |
|---|---|---|
| 1 | The first version. | Nothing to convert from. |