Level format and build path
A tile’s level is the file that says what stands where. Editing one is how a mod adds a structure to the world, and it is the part of the pipeline where a mistake is hardest to see: a malformed level parses, round-trips, passes every cache and reference check, and produces a tile that is placed, built, and completely empty. Nothing logs and nothing crashes.
This page is the map of that file and of the code that consumes it.
A level is four nested layers
Section titled “A level is four nested layers”<Biome>\Tiles\<Name>.level.ot cooks to a .GameStream.gen, and an edit has to
be legal at every layer. Traced from LevelStream_LoadStep downward:
- The outer cooked container. Type B, declaring class
oCGameStream(id0x14c31bf, version 1.1). Sections areMARK_BEGIN/MARK_ENDdelimited.rsmm.engine.cookedparses and re-emits it. BufferLen, and it is load-bearing.oCGameStream::Deserializereads au32the engine itself namesBufferLen, resizes a buffer to it, and then reads exactly that many raw bytes. Everything past it is never seen. That field is theu32pair at section-payload+0x08/+0x0c, equal tolen(payload) - 16, whichcooked_schemas.asset_refs._restamp_self_sizemaintains. It is not a corpus convention — it is the number the engine reads.- A second cooked container fills that buffer. Type A,
"Cooked"magic, starting at payload+0x10. Its class table declares onlyoCGameLevelIdentifier(id0x6494652, version 1.3) andoISerializable, so every sub-object in a level is a level identifier plus a transform blob. Sections here carry no length fields — the reader scans for the balanced end marker — so an insert needs balanced markers and nothing else. - The identifier itself.
oCGameLevelIdentifier::Deserializereads four fields, gated on class version 1, 2 and 3. Shipped files are 1.3, so all four are read, and a clone must carry the class table through untouched.
What a placement record contains
Section titled “What a placement record contains”Records of one size within a level are byte-identical except for the transform. Measured across a donor’s 29 same-size records, the only varying bytes are position, rotation and scale. There is no per-object identity field, so a record copied as a template cannot collide with the one it came from, and the level’s own identity GUID is not back-referenced by its objects — it appears exactly once in the file.
The build path, and the gate
Section titled “The build path, and the gate”One function builds a level, and it is four calls:
ResourceRef_Resolve resolve the level resourceLevelStream_LoadStep(...) false -> destroy the level, bailgate(db, level, 0) false -> destroy the level, null the slot true -> keep it, and instantiate its objectsdb is the runtime oCGameLevelDatabase (class id 0x064a9acd). No shipped
asset declares it — it is runtime-only, so there is no registry file a mod can
add itself to.
Inside the gate, after appending the level to the database’s vector, a loop walks the level’s objects and calls a load-or-create for each. Three things can leave a level empty, and all three are silent:
| Path | What happens |
|---|---|
| The accept check refuses | the loop never runs at all |
| The object count is zero | there is nothing to walk |
| Each object fails a mask test | they are skipped one by one |
The symbols are in the map as LevelDatabase_BuildAndRegister and
LevelObject_LoadOrCreate; see Engine symbols. Both were
promoted only after the module’s exception table confirmed each is a genuine
function start, which is the rule for any hook target.
What an “incomplete” level load does not mean
Section titled “What an “incomplete” level load does not mean”LevelStream_LoadStep returns false in exactly two cases: the progress tick
says stop, or a state field is not 1. The tick is a frame budget, so a step
that has not finished is ordinarily a deferral and the loader is called again
next frame.
A trace of these fired once per placed mod tile across six playtests and was recorded as a contradiction every time. It was the frame budget. Read the resource names, never the count.
Reading the evidence
Section titled “Reading the evidence”Two habits cost more playtests in this area than every genuine bug combined.
Pick probes by corpus exclusivity
Section titled “Pick probes by corpus exclusivity”A resolve count only means something if the name can only have come from the thing you are testing. In one Dark Hills map:
| Probe | Shipped levels in the chapter that place it |
|---|---|
| A grass patch | 24 |
| A pike | 4 |
| A broken log | 3 |
| The bonfire | 0 — its only tile is in another chapter |
Four runs were spent on the first three, measuring vanilla traffic and concluding nothing. Counting how many shipped levels place a name is one loop over the uncooked corpus, and it is the difference between a measurement and a number. Do it before reading any count.
Decide whether a field is an input or an output
Section titled “Decide whether a field is an input or an output”The gate fills level+0xc8 and level+0xc0 while it runs. A probe that read
them before the call reported zero objects for every level in the game, and
that was twice mistaken for a wrong offset. The offsets were right; the fields
are empty on entry.
Before reading a struct field around a call, decide whether that call consumes the field or produces it, and read on the matching side. Read an output after, and only when the call succeeded — on a failure the engine here destroys the level, so anything read then is freed memory.
A number that is wrong everywhere is a broken instrument
Section titled “A number that is wrong everywhere is a broken instrument”If a probe reports zero for levels that visibly build, the probe is wrong, not the game — either the offset, or the moment, as above. Sanity-check every new measurement against a case you know works before reading anything into the case you are investigating. Where you can, prefer measurements that depend on no struct offset at all, such as counting calls or resource fetches across a window; they survive both mistakes.
Two more instrument traps, both of which produced confident wrong answers here:
- A log budget can hide the one line that matters. A cap of 40 met a map that builds exactly 40 levels, so the mod’s own level was never printed and the run read as “our level never reaches the gate”.
- A trace that reads the wrong string field says nothing, loudly. The
resource trace printed the resource root for its whole life, so every line
read
shadersor3Dand its name filter had never once been seen working. “Resolved zero times” and “the read is broken” were indistinguishable.
Both are fixed, and they are why the findings here cite what was measured rather than what was inferred.
Additive levels
Section titled “Additive levels”A mod can own a level and place its own entity in it. poi supports this with
own_level = true plus a places entry naming @prop, which stands the entity
the def emits at a transform the def chooses. Nothing shipped is modified: no
entity is overwritten, no vanilla object is destroyed to borrow its slot, and
the marker’s icon is cooked under a name the mod owns rather than repainted over
a shipped texture. A clean apply of such a mod creates no backups, which is
the check that it is genuinely additive.
The contrast is swaps, which re-dresses a slot the level author chose and
inherits that slot’s transform whole — position, rotation and scale. That
single fact produced, in order: props tipped over, props buried below ground,
and a structure wedged inside a boulder. Prefer places.
