AWE Games · THQ · 2002 · asset-layer report
Four file formats in this game were unidentified. All four now read end to end, across all 1,441 files in the install, with no parse failures. Below is what they turned out to be, a viewer playing geometry and texture captured out of the running game, and an honest list of what is still open — including the parts where the first answer was wrong.
Granny 1.2b1,030 vertices76 framescaptured live
sb2.exe's memory while the game was running — see
provenance for exactly which parts are captured and
which are computed here.provenancewhat is evidence, what is decoration
This distinction matters more than the picture. Mixing the two is how a reconstruction quietly becomes a claim about the original.
| Element | Origin | How |
|---|---|---|
| Vertex positions, all 76 frames | captured live | Read from the game's own buffers by a debugger that breakpoints
granny.dll's LockNextRenderingState in the
running process. These are the posed, already-deformed vertices Granny
hands the renderer each frame. |
| Index buffer (1,264 triangles) | captured live | Same capture, the uint16 stream. |
| Normals | captured live, 6 vertices patched | 1,024 of 1,030 are finite and every one of those is unit length. Six
vertices are NaN in the capture and are written as
(0,0,0) here, because glTF cannot carry NaN.
The converter refuses to do this silently. |
| Texture coordinates | captured live | The same capture's float stream 3 — 2,258 distinct values across the unit square, against 2 distinct values for each of the colour streams beside it. Their V axis runs top-down, the Direct3D convention, so a WebGL renderer must not flip the image on upload; doing so moves 28% of the rendered pixels. |
| Texture | captured live | The 256×256 bitmap the game uploaded to Granny, taken from
LockNextNewTexture by a forwarding granny.dll
shim. It arrives as 16-bit RGB565; that layout was not guessed but
settled against the same image decoded independently from the model's
.omt canvas, where the right answer sits five times closer
than any other candidate. The two are not byte-identical — they are
separate conversions of the same source art, and the capture runs
slightly warm. |
| Shading, camera, lighting | this page | A plain Lambert term written for this viewer. The game's own lighting
lives in the .scn LITE records and is not used
here, so this is the game's surface under this page's light,
not a screenshot. |
| Frame rate | assumed | 30 fps is a playback choice. The capture records frame order, not timing. |
One number ties all of it back to the game. Frame 0, vertex 0 is
(31.4856, 16.6441, 57.9688) — a value read independently out of the
running game's state buffer. The data in this page was pushed through the whole
offline pipeline and then transformed back, and it returns that triple to within
5×10-5. If any stage of the conversion were wrong,
that check would fail, and the build script refuses to emit a page when it does.
game (running)
|
\_ gwatch.exe 32-bit debugger, breakpoints granny.dll [captured]
| \_ out/sbcap/*.bin posed vertex streams, and stream3 = the UVs
| \_ sbcap_to_grn.py rewrite into the JN engine's records
| \_ grnanim_to_glb.py the JN exporter, run UNMODIFIED
| \_ sb_model.glb glTF 2.0, morph-target animation
| \_ glb_to_page_payload.py + round-trip check
| \_ this page
|
\_ granny.dll shim forwarding proxy, in-process [captured]
\_ out/sbproxy_cap/*.grntex the 16-bit bitmaps as uploaded
\_ grntex_probe.py decides the layout vs the .omt canvas
\_ grntex_to_png.py RGB565 -> PNG
\_ this page
.omt / .scn / .sb21,441 files0 parse failures
.omt — it was already documentedThe container is OMediaFormattedStream from Yves Schmid's
Open Media Toolkit, which is LGPL and whose sources were already on disk.
The chunk table was read straight off the engine's own
save_header_data() rather than inferred from a hex dump — which
matters, because a field table copied from the writer also tells you about cases
the shipped files happen not to exercise. All 315 containers parse.
Two results worth carrying forward. First, every scalar is big-endian
on disk, because the toolkit byte-swaps on little-endian hosts; that single fact
explains why OMT FourCCs read as plain ASCII while the FourCCs in this game's
own .scn format appear reversed. Second, there is not one
3DSh mesh chunk in the entire game — all geometry is Granny, and
OMT carries only textures, materials and audio.
awefan's omt_parse.py reads the header
offset little-endian first and accepts it whenever it lands inside the file. For
every container under about 16 MB the wrong reading falls past the end and
is correctly rejected, so the heuristic is invisible — it is right 314 times out
of 315.
sounds/comments1.omt is 18 MB. Its
true header offset is 18,018,560; byte-swapped that is 15,798,785, which
is inside the file. So the parser seeks into the middle of a WAV, reads
a nonsense count, trips its own sanity guard — and returns True with
zero chunks.
That is the dangerous shape of failure: not an exception, a confident empty answer. A sweep reporting "opened 315, failed 0" is wrong about this file. Reading strictly big-endian, as the engine source says is the only correct reading, recovers 121 chunks — and they are the hint voice-over bank, an entire namespace of the asset graph.
.scn — a scene database that tiles itself exactly123 files, previously "magic RMAC, version 0x17, unidentified".
It is a flat sequence of tagged records with no directory and no count: the first
starts at offset 0, every record's size is computable from its own header,
and the last ends precisely at EOF. All 123 files walk cleanly with no unknown
tag and no leftover bytes.
The magic is not RMAC. It is 'CAMR' — a camera
record — stored as a little-endian u32, so it appears reversed. Ten
record types follow the same convention.
| Tag | On disk | Size | Count | Per scene | What it is |
|---|---|---|---|---|---|
CAMR | RMAC | 274 / 272 | 123 | exactly 1 | camera / scene header |
LITE | ETIL | 52 | 615 | exactly 5 | light |
STPT | TPTS | 164 | 123 | exactly 1 | start points |
GRID | DIRG | 24+s² | 246 | exactly 2 | walk grid |
CHAR | RAHC | 530 / 528 | 207 | 0–5 | character placement |
2DOB | BOD2 | 256 | 245 | 0–9 | 2D sprite overlay |
SPOT | TOPS | 188 | 376 | 0–12 | hotspot |
TRIG | GIRT | 394 | 144 | 0–6 | trigger volume |
ANIM | MINA | 278 / 276 | 353 | 0–12 | animated object |
ITEM | METI | 274 | 38 | 0–5 | inventory item |
The 0x17 that was being read as a file version is the
CAMR record's struct version, and each record type carries
its own. Nine scenes were exported by an older build of the authoring tool: they
carry CAMR 22, and those same nine carry CHAR 31
and ANIM 27, each exactly two bytes shorter. That was predicted
from the size table and then tested, with no exceptions. A reader that switches
on a file-level version mis-sizes nine files and desynchronises.
.sb2 — not bytecode at allThe 168 script files were recorded as "binary bytecode", and the plan for the
next phase rested on modelling the resulting VM. There is no VM. The files
are tab-separated text under a single XOR of 0x30, framed with
explicit line lengths:
u16 line count
repeat:
u16 line length, INCLUDING the trailing newline
u8[length] the line, every byte XOR 0x30
0x39^0x30 is a tab and 0x3a^0x30 is a newline —
which is exactly why the raw bytes looked like opcode-and-operand pairs. The two
commonest bytes in the file were the field and record separators. The whole of
scripts/c1/scarwash.sb2, all eleven bytes of it, is
darts -> 0.
What the format actually holds is a 55-verb command language — 1,126 lines across the 168 files. A complete script reads:
walk 240 -44
rotate 232
attach isp2 Bone01 Bip01 R Hand
show isp6
sound digsand right
anim dig
detach
comment h158b
add isp1 Shovel and Pail
block 11 8
attach's third operand is Bip01 R Hand — a 3D Studio
Max biped bone name. The scripts address the Granny skeleton by name, which is a
direct link from the script layer into the model layer.
The 52 dialogue files use the identical container; only the
payload convention differs, so the "different header shape" was just the XOR of
different first characters. They are 11-column conversation tables. And all 101
files under saved/ are zero bytes — there is no save format
in the shipped install to describe.
Cross-referencing 3,039 name references out of the scene records against the install's own filenames, script names, dialogue keys and audio chunk names: every field that looks like a reference resolves at or near 100%, and the only fields that do not resolve are precisely the ones holding human-readable labels. Two complete chains, verified end to end:
scripts/c1/sdig1.sb2 comment h158b
\_ dialogue/*.sb2 row keyed H158B -> the line of text
\_ sounds/comments1.omt Wave "H158B" -> the voice-over
scenes/c1l2s1.scn ANIM.dialogue = "h121b"
\_ dialogue/c1l2s1.sb2 row 121 A A A -> the line of text
\_ sounds/c1l2s1.omt Wave "121AAA" -> the voice-over
The rule composing that audio key was not read off the data — four candidate rules were tested against all 1,828 audio chunk names, and concatenating columns 0–3 won outright at 1,042 of 1,239 rows, where 182 of the remainder are continuation rows with no key of their own. The first chain is only visible at all with the endianness fix above.
.grn674 + 389 filescross-titleproxy built
The sibling title Jimmy Neutron vs. Jimmy Negatron already solved this capture problem, so the instruction was to not rebuild any of it and find out how much transfers. Nearly all of it does.
All 674 Employee of the Month .grn files
and all 389 Jimmy Neutron ones share a byte-identical 16-byte header
prefix — not just the 8-byte magic already known, but the eight bytes after it,
with no variation in either corpus.
A Jimmy Neutron model loaded through Employee of the
Month's own granny.dll returns the identical six-descriptor layout,
the same enabled pattern, the same element types and the same
0x3B lock flags. Only the counts differ.
Of the 389 Jimmy Neutron files run through Employee of the Month's Granny
runtime, 79 produced geometry and 302 did not — and that split is not a failure
rate. Every file whose name contains base produced geometry, all 48
of them, and every file that did not is named *move,
*stop, *run: they are animation clips, which have no
mesh to hand back. Eight model files, including jimmybase.grn, crash
the DLL outright with an access violation; that one is unresolved and is written
up as such.
The capture shown at the top of this page was rewritten into Jimmy Neutron's record format and baked by that engine's exporter, run unmodified. One exporter serves both titles.
The forwarding granny.dll shim was rebuilt against this title's
own export table — identical to Jimmy Neutron's in all 101 names
and ordinals, measured rather than assumed — and pointed at
sb2.exe. One 75-second run:
| debugger | proxy | |
|---|---|---|
| meshes captured | 1 | 16 |
source .grn name attached | no | all 16 |
| distinct models | 1 | 8 |
| animation frames, best descriptor | 76 | 1,396 |
Only two changes were needed, and the first is worth knowing about. Upstream hardcodes its log to the drive root, and it enables capture only if that log opens — so on a machine where the root is not writable, the game runs perfectly and the capture is silently empty. The trap has a sting: the root permits creating a directory but not a file, so the dump folder duly appears while the log beside it fails. A confident empty answer, the same shape as the parser bug above.
The two instruments then agree where they overlap. The index buffer is byte-for-byte identical between the external debugger and the in-process shim, as are the colour and texture-coordinate streams — while the normals, which are per-frame deformed output, differ exactly as they must. Every static stream matching while the one dynamic stream differs is the right pattern. The debugger is now the verification oracle rather than the extraction path.
It also settled a loose end: the second, unidentified object interleaved
through the original capture is patalt_model.grn. The debugger
dumped frames without names and could not tell you that; the proxy writes one
file per object with its source filename attached.
nexta premise that did not surviveand 1,728 named symbols
The point of settling the asset layer first is to choose what to model next against known inputs. That choice now has to change, and it is better to find that out here than after the spec sheets are written.
The plan named the .sb2 script VM as the
subsystem to model, on the grounds that a bytecode interpreter is deterministic,
input-driven and entirely in-process. That premise is gone. There is no
interpreter and no opcode table — there is a text file of engine verbs. The
determinism that made it attractive lives in the engine executing the verbs, not
in the format.
The replacement candidate was the walk-grid path-finder: the
GRID records give an N×N walkable mask — 41,488 cells across
the game, every one of them 0 or 1 — and the scripts supply the input alphabet
directly as small integers. That candidate was then tested rather than assumed,
and it half-passed.
What passed. Three verbs take operands that are non-negative and below
32 in 100% of 141 uses (walkgrid, block,
unblock), while walk and move range to
−3406 and are plainly world-space. Every script's cell coordinates fit the
grid of every scene that invokes it — and because small coordinates fit big grids
by luck, that was checked against a permutation null: shuffling which scene
invokes which script 2,000 times, only 0.7% of shuffles do as well as the
real pairing. The two really are one coordinate space.
What did not. walkgrid supplies the path-finder's
input. The path itself — the output — appears in no file, because it
only exists while the game runs. And the obvious shortcut is dead: the capture's
per-frame world transform is the identity matrix in all 1,287 samples, so the
character's position in the scene is not in the Granny data at all. An oracle
here needs the engine instrumented, not the renderer.
OMT.dll exports 1,728 C++ symbols with full MSVC name
mangling — 182 classes, every method name, and every parameter type encoded
in the decoration. sb2.exe imports 208 of them across 53 classes.
Among those imports:
OMedia3DNavigator::find_direction_vector
OMedia3DNavigator::set_move_on_axis
OMedia3DNavigator::update
OMedia3DNavigator::update_angles
OMediaAutoMove::automove_update
OMedia3DElement::collide, get_collision_model
OMediaWorld::get_world_position_info
A named, exported find_direction_vector that the game
demonstrably calls is not something you have to hunt for in a disassembler. And
49 of those 182 classes have published LGPL source, because this is Open
Media Toolkit — so much of the surrounding structure is documented rather than
inferred.
The forwarding-proxy technique that just worked on granny.dll
applies directly to it. That is the next step, and it is targeted rather than
speculative.
OMT.dll and hook OMedia3DNavigator.
Four named methods, already proven to be called. This is the walk-grid oracle,
and the instrument for it is built and already validated against a second
title.granny.dll and compare version
tokens, to settle the eight of its models that crash this title's build.creditwhose work this stands on
github.com/awefan4524 · awefan.neocities.org/mechanix
Every OMT container this work touches is decoded by awefan's code. Not "uses a
library that happens to include it" — the OMT parsers are his, and they
sit at the bottom of the path. omt_parse.py's
OMTFormatParser and OMTCanvasDecoder, and
omt_3d.py's 3DSP mesh decoder, are what turn an
.omt blob into images and geometry. The 315-container sweep here was
validated against his parser, agreeing on 314 of 315 files.
That work did not start with this project. It comes out of years of modding AWE Games titles — the studio behind Hot Wheels Mechanix and the Jimmy Neutron games, and the OMT format they share. The format knowledge encoded in those parsers is the accumulated result of that, and it was freely published.
The one disagreement found here is offered in that spirit and nothing more: an endianness heuristic that is correct on 314 of 315 files and silently returns nothing on the 18 MB one. It is a one-line fix in his terms, it was found only because his parser made a 315-file sweep possible in the first place, and his parser is the reference the strict reader is checked against — not the other way round.
What is not his, so that nothing here reflects
on him: the .scn record decode, both .sb2 decodes, the
cross-reference resolution, the Granny capture and conversion, every reading of
the original this work settled on, and every mistake among them. If something
here is wrong, it is wrong on this side of the wall.
.omt did not have to be reverse-engineered
at all..grnmesh/.grnanim exporters, reused here unmodified.granny.dll is proprietary and is not redistributed; no RAD
code appears in this page or in any tool described here. Nothing extracted from
the game is redistributed either — the geometry in the viewer above is the single
exception, published as a demonstration of the capture.