AWE Games · THQ · 2002 · asset-layer report

SpongeBob SquarePants: Employee of the Month Settling the asset layer, so the executable can be specified against known inputs instead of guesses

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

The model, straight out of the game

WebGL is unavailable in this browser.
The report below does not depend on it.
Frame 0 / 75
Drag to orbit, scroll to zoom. 1,030 vertices, 1,264 triangles, 76 frames at 30 fps. Every vertex position you are looking at was read out of 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

What is captured, and what is not

This distinction matters more than the picture. Mixing the two is how a reconstruction quietly becomes a claim about the original.

ElementOriginHow
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

What the four formats turned out to be

.omt — it was already documented

The 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.

A parser bug that reported success

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 exactly

123 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.

TagOn diskSizeCountPer sceneWhat it is
CAMRRMAC274 / 272123exactly 1camera / scene header
LITEETIL52615exactly 5light
STPTTPTS164123exactly 1start points
GRIDDIRG24+s²246exactly 2walk grid
CHARRAHC530 / 5282070–5character placement
2DOBBOD22562450–92D sprite overlay
SPOTTOPS1883760–12hotspot
TRIGGIRT3941440–6trigger volume
ANIMMINA278 / 2763530–12animated object
ITEMMETI274380–5inventory 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 all

The 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.

The dependency graph closes

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

One toolchain, two games

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.

The files are the same

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.

The loader is the same

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.

And the proxy itself now runs against this game

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:

debuggerproxy
meshes captured116
source .grn name attachednoall 16
distinct models18
animation frames, best descriptor761,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

Where this leaves the next phase

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.

Which turns out to be a better position than it sounds

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.

Immediate next steps

  1. Proxy 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.
  2. Overlay the hotspot and trigger rectangles on the scene backdrops. Still the highest-value check that does not touch the executable — it would confirm or kill the rectangle reading for two record types in an afternoon.
  3. Read a save file. Run the game, save to a slot, read it back. It remains the only format in the install with nothing written about it, and it is fifteen minutes of work.
  4. Publish the Granny 1 work as its own repository. No public parser reads Granny 1 — every one targets GR2. The pieces for a clean-room parser are in place: a 1,063-file corpus across two titles sharing a byte-identical header, and the runtime itself usable as an oracle to check any decode against. Nothing of RAD's would be redistributed.
  5. Find Jimmy Neutron's own granny.dll and compare version tokens, to settle the eight of its models that crash this title's build.

creditwhose work this stands on

Credit

awefan4524 — without whose work none of this runs

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.

Other work this stands on