Round-4 research pass on the material decl type: what the format looks like, what the two-sided/translucent/emissive/disintegrate/wound "flags" the engine is believed to have actually are, which of those are safe to use on a static-instance kit-map piece, and whether a brand-new material that references a brand-new, self-authored texture can be appended to a real retail container. All of it is static analysis plus offline round-trip builds against copies of real archives — nothing was tested in a running game. The headline finding: there are no boolean flags on a material at all; "two-sided"/"emissive"/etc. are baked into which of a fixed catalog of 429 compiled shader programs a material names. A from-scratch material + texture pair was built, appended to a copy of a populated retail container, and passed every structural check this project has (round-trip, both header hashes, a dangling-reference check, pixel decode) plus three negative controls — confirmed structurally sound, not confirmed to render in game.
1. Format
A material decl is UTF-8 text: {\n, then one key\tvalue\n line per property (engine-written order is alphabetical), }\n. Keys are renderParm names — renderParm is itself a decl type (1,661 entries) whose payload just declares a type/default/scope, e.g.:
renderParm ambientprogram { Program 0 surface }
renderParm emissive { Vec 0.00 surface }
renderParm lightmapscalebias{ Vec { 1, 1, 0, 0 } } # no scope keyword -> instance-scoped
Across 11,469 retail materials (28 archives, dlc_0/dlc_4 excluded as known non-retail copies), 201 distinct keys occur. The ten most common: the 8 loose* PBR slots (~7,938 each, the ordinary textured-surface case), specularmap/powermap/bumpmap (legacy mirrors), surfacetype (7,731 — a numeric index into something not traced this round, plausibly a physics/decal table), stageprogram (2,559), ambientprogram (1,060), the 4 decal-atlas slots (651/457/383 each), shadowprogram (469), zprepassprogram (374).
2. "Flags" are program names, not booleans
No material in the 11,469-entry corpus sets a separate twosided/translucent/emissive/decal boolean. Instead, ambientprogram/stageprogram/shadowprogram/zprepassprogram/blendprogram each pick one of a closed catalog of 429 distinct renderProgResource names (case-insensitive) — compiled, binary shader bytecode + a reflection table, not authorable as text. The property is encoded in the program name itself:
| key | distinct values | example names carrying the "flag" |
|---|---|---|
ambientprogram |
15 | outsidetwosided(76), outsideemissive(184), outsidedisintegrate(168), outsidewound(111), outsideunique(39) |
stageprogram |
86 | glassrefractiontwosided, basicemissive, basicemissivedisintegrate |
shadowprogram |
8 | shadowdisintegratetranstwosided, shadowvmtrtransunique |
A literal emissive key does exist (86 occurrences, a Vec4 color+intensity) but is a different mechanism — additive glow color on particle-FX materials, not the outsideEmissive program self-lit architecture uses. decalallowed (default 0) is never set by any shipped material; decal reception is resolved elsewhere, most likely through surfacetype — not settled this round.
Corroborated in the disassembly, not just inferred from data: ambientProgram/stageProgram are real string literals in .rdata (RVA 0x2635db0/0x2658ef0), and ambientProgram is read by compiled code at RVA 0x5B1D61–0x5B1D8D — consistent with this being the default-resolution fallback for the 6,878 loose materials that never set the key explicitly (read from raw bytes, not a full decompile).
3. Which programs are safe on static instances (kit-map specific)
Every idStaticInstanceEntity in a kind-2 world has its program's "Unique" (lightmapped) bit forced to 1 unconditionally — an existing finding this round narrows to concrete materials. The kit map's 160 pieces use 57 distinct materials, all resolved (100%):
| group | count | has a compiled "Unique" program? |
|---|---|---|
| implicit default (no override) | 50 | yes — outsideUnique + 8 permutations |
ambientprogram=outsideemissive |
3 | yes — outsideUniqueEmissive |
glass (stageprogram=glass/blendprogram=glasstransmittance) |
2 | no — 0 of 429 names combine "glass" and "unique" |
stageprogram=glassrefraction |
1 | no |
stageprogram=basicadd |
1 | no |
53/57 (93%) are evidenced-safe. The 4 glass/basicadd pieces either render through a path that never consults the forced Unique bit (plausible — they look like forward/transparency-pass materials, not the deferred G-buffer pass the Unique mechanism gates) or would hit a missing compiled-program combination — not settled either way (no shader-bytecode decompiler exists in this toolkit). Also flagged for any future kit piece: outsidedisintegrate, outsidewound, and the specific combination outsidetranstwosided likewise have no compiled Unique counterpart, even though some of their individual components do.
4. Adding a new material + own texture
Proven this round by building and verifying one: appending a from-scratch-authored material and a brand-new image onto a copy of an existing, already-populated retail container (not just an empty freshly-built one, and not a clone-and-swap of an existing material).
Recipe:
- Build a flat-color image with the project's existing
author_flat()helper. Keep the edge ≤32px so it stays fully inline (no.texdbrow needed) — sidesteps an open+0x60policy question entirely. - Append it as a new
imageentry (existing append-entry tooling, unmodified). - Author the material text by hand: the 8
loose*slots +diffusemap+surfacetype, alphabetical order.loosealbedo=lower(diffusemap) + "$mtlkind=albedo$streamed". For every other slot, copy a whole retail value verbatim rather than composing a syntactically-plausible new one — see the corrected assumption below. - Omit
ambientprogram/stageprogram/etc. entirely unless you need an explicit override; the implicit default is the evidenced-safe choice for a static-instance kit piece. - Append the material text as a new
materialentry, chained onto the same container. - Verify with all four checks, not just hashes: the project's data-hash and header-hash tools,
stale_data_hashes(), and a dangling-reference check (a material can pass both hashes while pointing at an image name nothing provides — see negative control below). - To actually be rendered, the new material's name still has to be referenced from a model's surface list or an entity — building the decl pair alone places nothing.
Verified result: image and material round-trip byte-exact, 0 self-contradictions, both header hashes OK, 0 dangling references, the stored pixel data decodes to exactly the authored color. Three negative controls, all caught: a corrupted image header (caught by the bitmap self-check), a material pointing at a nonexistent image name (caught only by the dedicated dangling-reference check — the hash checks alone report OK, since hash validity and reference validity are different things), and a single flipped payload byte (caught by the stale-hash check).
Corrected assumption: a first attempt invented a plausible-looking loosesmoothness value (.../grey_md.tga$mtlkind=smoothness$smoothnessnormal=.../flat_normal$streamed) — it parses and passes both header hashes but resolves to no real image anywhere in the corpus, because that slot's identity is the literal name of a specific baked image, and retail only ever bakes specific combinations, not every syntactically valid one. Fixed by copying an exact retail-precedented string instead.
PROVEN
- Material format, round-trip, the
loosealbedoderivation rule — cross-validated again this round (11,469/11,469 parse, 0 failures). - 201 distinct material keys exist; none is a literal two-sided/translucent/decal boolean — those properties are which of 429 compiled program names a material picks.
- All 57 kit-map materials resolved; 53/57 have a confirmed compiled "Unique" program variant, 4/57 (2 glass, 1 glassrefraction, 1 basicadd) do not.
- A brand-new
image+materialpair appends cleanly onto a copy of an existing populated retail container and passes every structural check this project owns, plus 3/3 negative controls.
INFERRED
- The code at RVA
0x5B1D61–0x5B1D8Dis the default-program-resolution fallback — plausible from the branch shape and the census, not from a full decompile. - Whether the 4 glass/
basicaddkit materials are excluded from, or silently broken by, the forced static-instance Unique bit. surfacetype's numeric code plausibly indexes a global physics/decal/sound table — not traced.
Open questions
- Whether a streamed new image (edge >32px, needing a real
.texdbrow) works the same way — deliberately sidestepped this round by staying inline. - What
surfacetypeand thetabledecl type it plausibly indexes actually encode. - Whether
unknown_0x72on the 5 near-duplicate copies eachrenderProgResourcename ships with is a platform/quality-tier selector.
In-game test (not yet run)
Apply the built material/image pair on top of the already-proven magazine-size-edit technique in the same small container, and redirect one existing, visible material's loosealbedo to the new texture's name. If the targeted surface renders flat green after loading, the new, retail-nonexistent image entry was found and textured — the one thing this round could not settle at a desk. Separately, if either glass kit piece is ever placed as a static instance and renders black, mislit, or crashes, that is the first concrete evidence for the open Unique-bit question in §3.
Related: TNC Modding Surface, TNC Texture Database, File Formats.