Round-10 pass: a developer map for testing mods, built on retail c2v1 geometry and added as a further map to the same dlc_0 chunk that already holds the kit level and the box map. The build keeps every kit-level entry byte-identical and passes all its checks. Not proven in game; the report lists what is unproven, worst first, and gives the install steps and the in-game test.
Date: 2026-10-02. The game was not started. Nothing was written under
<Steam library> or Saved Games; the install was only read. Builds went only
to D:/wolf-decomp/tmp/modtestmap/. New files: tools/build_modtestmap.py and
this report. No other file was changed. tools/build_boxmap.py and
wolfsdk/mapinstall.py were reused without edits.
Result
python tools/build_modtestmap.py (about 8 minutes; --once skips the
determinism builds) writes:
| File | Content |
|---|---|
D:/wolf-decomp/tmp/modtestmap/modtest/dlc_0/base/chunk_22.resources |
166,267,606 B, sha256 7c030b2a0c2c52605a87790c82f5edbcc422be5fee4c0488cc5c0b07b31c06be. Contains kit level H (c2v1, all 7,130 entries byte-identical), the box (29 entries, unchanged), and modtest (120 added entries). |
.../modtest/dlc_0/base/packagemapspec.json |
c2v1_custom, c2v1, game/wolfsdk/box, game/wolfsdk/modtest (what mapinstall.install writes for this chunk) |
.../modtest/dlc_0/dlcinfo.json |
{"dlcid": 4} |
.../modtest/side_files.json |
The 14 dlc_2 side files, plus 2 textures.cache links (box, modtest) → dlc_2's c2v1 textures.cache |
.../modtest/devmenu_entry.txt |
The dev menu item (below) |
.../modtest/manifest.json |
Hashes, dropped classes, every new block with its donor and position, added entries and rows, check results |
.../modtest/../box/ |
The stage: build_boxmap.run() output (H + box, sha 1fc43560…, its own 31 checks) |
D:/wolf-decomp/tmp/modtestmap/build_log.txt |
The full run: 79 OK, 0 FAIL |
How it composes with the box: build_modtestmap.run() calls
build_boxmap.run() (H → H + box, all box checks), then adds modtest's
entries to that chunk with the same build_kitmap.rebuild() and
idclwrite.finalize(). Nothing in the box code is duplicated (maplevel(),
ENGINE_NAMES, DEVMENU and clone logic are imported).
Design choice: retail c2v1 geometry, not a kit/glTF room
Chosen: copy the RETAIL c2v1 map (dlc_2's chunk_22) under the new name. The mission is removed and our test content is added.
Why:
- Enemies need a navmesh. One cannot be generated (
r3_navdata.md§6: no encoder; the engine only cuts holes, it never adds surface). A kit or glTF room has no navmesh under it, so AI would stand still at best. Retail c2v1 shipsc2v1.hknavmesh. The build copies it byte-identical (gated) and checks every enemy against it. - Collision is solved there. The player walks on retail's playerclip
carrier mesh (
memory: tnc-custom-map-route, variant D analysis). New kit geometry needed the| 0x18cm workaround and still had stair problems. - Lighting is solved there. Baked lightmaps (
.proc), 102 light probes, 408 retail lights and the Umbra tome/pvs/shadows all match the geometry they were baked for. The kit/glTF route borrows single lightmap texels (r9 risk 5: "blotchy or dark"). - Scene points can stay. The 176 combat, 54 roam, 41 search and 10
traversal scene points belong to the retail geometry. Enemies keep cover and
search behaviour. A new room has none (
r2_ainav.md§5). - Precedent: a retail-based c2v1 container served from dlc_0 loaded and
played in game on 2026-09-24 (relit lights, normal mission start, memory
tnc-custom-map-route). The kit/glTF route has no in-game AI evidence.
Cost: the map is the Gestapo office, not a clean white room. The test area uses two of its rooms (below). The rest of the building stays walkable and empty.
What the new name needs (and gets)
- 18 map-level entries of retail c2v1, copied under the new name:
.entitiesand.mapresources(both our own payload);_combo/modtest.proc,modtest.prockd;_world.tome+-metadata.json,_world.bmodel,sky.bmodel,world.hkshape,_world.pvs,_world.bshadows;modtest.ambientsh,modtest.hknavmesh,devmapinfo.json;mega.decl,megatrans.decl,megafoliagetrans.decl;generated/staticstreamtree/game/wolfsdk/modtest.bsstree.
These are retail bytes. H's modified tome and hkshape are not used, and H's
removed _world.bshadows is taken from retail.
- 102 light-probe images under maps/game/wolfsdk/modtest/lightprobes/<entity>_row.bimage.
The engine builds that name from the map name ('maps/%s/lightprobes/%s_row.bimage', r9 #7).
The box has no probes, so it did not need this; modtest keeps all 102 LIGHT_PROBE entities.
- All 15 engine-derived names exist under the new name: the box's 14
format strings plus _combo/_world.bshadows.
- Per-entity resources stay under c2v1's names. These are the 90
trigger/volume/collision-only cms; the text names them explicitly
(clipModelName). Every such reference is checked to resolve.
- mega*.decl keeps landpagefile on c2v1's VT pages (r9 #9).
- textures.cache is one more hardlink, dlc_0/base/maps/game/wolfsdk/modtest/textures.cache.
mapinstall.install already creates it for every extra map.
What was taken out of retail c2v1
8,902 of 9,206 entities are kept verbatim. 304 are dropped:
| Removed | n | Why |
|---|---|---|
| idKisculeEntity | 33 | mission scripts |
| idAI2, idTarget_AISpawn, idScenePoint_Spawn | 44, 9, 9 | the mission's hidden enemies and their spawn chain |
| idAlarm, idAlarmSystem | 11, 2 | their reinforcement spawners are gone |
| idTrigger, idTrigger_Hurt, idWorldMarker | 48, 1, 8 | mission triggers, hurt volume, objective markers |
| idSoundClip, idEnvironmentSpeaker | 38, 29 | positional sound from c2v1's banks; no bank loads for a new map name (r9 #10) |
| idPlayerStart | 2 | replaced by modtest_start |
idProp2 prop/pickup/* |
67 | retail pickups and collectibles (collectibles count as DLC progress) |
| challenge-layer-only | 2 | layer/dlc/c2v1_challenge1 |
| bound to a dropped entity | 1 | Hans's cigarette (bindInfo.bindParent → the boss) |
Five kept entities lost references to dropped ones (drop_refs):
- the 4 automap images lose their loot-reveal reference to the map collectible;
- the group manager loses its 2
alarmSystemreferences.
The world keeps everything except devmapInvLoadout. It changes from
devinvloadout/dlc/c2v1 to devinvloadout/code_referenced/default_sp_devloadout
(all 9 weapon families, r4_pickups §5.2).
Kept: geometry, lights, probes, props, doors, glass, decals, scene points, volumes and the group manager.
The kept layered entities, 54 in all, are all in layer/dlc/c2v1 (gated):
- 46 door props;
- 3 idAnimated;
- 4 idInfo;
- 1 idVolume.
What is placed where
Floor z = 0 in both rooms. The positions come from the retail collision grid
(re_probes/viewer/collision.json via design_kitmap.retail_grid).
Armoury: the room the retail challenge start is in (x 23..35, y −66..−78).
- Start:
modtest_startat (29, −73), facing north toward the one door into the hall, which is at (29, −65.5). - 5 white fill lights (
modtest_light_*), each a clone of the hall chandelierc2v1_lights_lights_light_point_1603252612(point_frac40):- 2 over the pickup rows: z 3, radius 6, intensity 5;
- 3 down the hall's centre aisle: z 4.5, radius 10, intensity 4.
23 pickups, all on the armoury floor. Each is a deep copy of a retail placement of the same inherit. Ammo is next to its weapon:
| x, y | Pickup (inherit) | Gives (useableComponentDecl) |
Donor |
|---|---|---|---|
| 24.6, −70.5 | prop/pickup/weapons/handgun/handgun |
weapon/sp/handgun (Pistole) |
c0v2 |
| 25.8, −70.5 | prop/pickup/ammo/handgun/medium |
ammo/sp/handgun/medium |
c2v1 |
| 24.6, −71.5 | prop/pickup/weapons/smg/smg |
weapon/sp/smg (MP) |
c0v2 |
| 25.8, −71.5 | prop/pickup/ammo/smg/medium |
ammo/sp/smg/medium |
c0v2 |
| 24.6, −72.5 | prop/pickup/weapons/shotgun/shotgun |
weapon/sp/shotgun (Schockhammer) |
c1v3 |
| 25.8, −72.5 | prop/pickup/ammo/shotgun/medium |
ammo/sp/shotgun/medium |
c07p2 |
| 24.6, −73.5 | prop/pickup/weapons/battlerifle/battlerifle |
weapon/sp/battlerifle (Sturmgewehr) |
c11p1 |
| 25.8, −73.5 | prop/pickup/ammo/battlerifle/medium |
ammo/sp/battlerifle/medium |
c07p2 |
| 24.6, −74.5 | prop/pickup/weapons/kampf/kampf |
weapon/sp/kampf (Kampfpistole) |
c1v3 |
| 25.8, −74.5 | prop/pickup/ammo/kampfpistol/medium |
ammo/sp/kampf/medium |
c07p2 |
| 33.4, −70.5 | prop/pickup/weapons/lasergewehr |
weapon/sp/lasergewehr (heavy) |
c06p1d1 |
| 32.2, −70.5 | prop/pickup/ammo/lkw/battery_medium |
ammo/sp/lkw/battery_medium (LKW) |
c06p1d1 |
| 33.4, −71.5 | prop/pickup/weapons/dieselgewehr |
weapon/sp/dieselgewehr (heavy) |
c07p2 |
| 32.2, −71.5 | prop/pickup/ammo/dkw/diesel_medium |
ammo/sp/dkw/medium (DKW) |
c06p1d1 |
| 33.4, −72.5 | prop/pickup/weapons/hammergewehr |
weapon/sp/hammergewehr (heavy) |
c09p2 |
| 33.4, −73.5 | prop/pickup/weapons/ubergewehr |
weapon/sp/ubergewehr (heavy) |
c09p2 |
| 33.4, −74.5 | prop/pickup/weapons/axe/axe |
weapon/sp/hatchet |
bonusmap01 |
| 32.2, −74.5 | prop/pickup/weapons/knife |
weapon/sp/knife_throw |
c2v1 |
| 32.2, −72.5 | prop/pickup/armor/helmet/soldier |
armor/helmet/soldier |
c0v2 |
| 27.6, −70.5 | prop/pickup/health/kits/medium |
health/medium |
c07p2 |
| 30.4, −70.5 | prop/pickup/armor/medium |
armor/medium |
c09p1 |
| 27.6, −75.5 | prop/pickup/health/kits/large |
health/large |
c07p2 |
| 30.4, −75.5 | prop/pickup/armor/large |
armor/large |
c1v3_venus |
How the donors were chosen:
- Each donor is the first retail placement without a layer that has
isStatic = false, so physics settles it. If no such placement exists, it is the first placement without a layer. The scan runs overbase/*.resources, thendlc_1..dlc_3, in name order. - Static pickups get z + 0.03 (retail rests them at 0.02–0.04 in c2v1). Dynamic ones get z + 0.10 and drop onto the floor.
Not available as pickups:
- LKW and DKW: there is no pickup entityDef, and no map in the corpus places one (r4_pickups). The world's dev loadout should give them; their ammo is above.
- Grenades: there is no
prop/pickup/weapons/grenade*entityDef.
Range: the two-storey office hall north of the armoury (x 21..37, y −35..−65). It has a centre aisle, two side aisles, cubicle desks and partitions. The retail chandeliers and wall lamps stay.
8 enemies, all of c2v1's own types:
- each is a deep copy of the first retail c2v1 placement of that inherit;
flags.hide = false;- roam target (
targets) removed; - facing north, with their backs to the door.
| Name | Inherit | x, y | From start |
|---|---|---|---|
| modtest_ai_soldier | ai/dlc/silentdeath/soldier/generic/soldier_light |
29, −60 | 13.0 m |
| modtest_ai_nailgun | ai/dlc/silentdeath/soldier/generic/soldier_nailgun |
22.5, −56 | 18.2 m |
| modtest_ai_heavy | ai/dlc/silentdeath/soldier/generic/soldier_heavy |
35.5, −56 | 18.2 m |
| modtest_ai_elite | ai/dlc/silentdeath/elite/generic/elite_heavy |
29, −50 | 23.0 m |
| modtest_ai_supersoldier | ai/supersoldat/generic/supersoldat_lasergewehr |
35.5, −48 | 25.8 m |
| modtest_ai_commander | ai/dlc/silentdeath/commander/generic/commander_handgun |
22.5, −43 | 30.7 m |
| modtest_ai_dog | ai/kampfhund/generic/kampfhund |
35.5, −41 | 32.7 m |
| modtest_ai_ubersoldat | ai/dlc/silentdeath/ubersoldat/ubersoldat_cannon |
29, −37.5 | 35.5 m |
Their groupName values come from the donors and are kept groups of the
retained idEncounterGroupMgr (FirstArea/SecondArea/ThirdArea/Commander), so
an alerted group shares awareness.
Mapresources:
- retail c2v1's 20,832 rows, with the 120 map-named rows renamed;
- plus 34 always-load rows: every row a donor's own map lists for that donor's values (entityDef, propItem, model/cm, modelAsset, md6Def, propHealth) that c2v1 lacks. Example: shotgun, battlerifle-ammo and armour models.
Checks (build_log.txt: 79 OK, 0 FAIL)
Container
- Before finalize,
probe_idclhashrejects the file (negative control). - Finalize moves the data hash of exactly the 120 added entries.
- Entry count is 7,159 + 120.
- Every H + box entry is unchanged: payload, version, flags, dependencies.
- Every one of H's 7,130 entries is byte-identical, compared directly against
re_probes/kitmap/variants/H/chunk_22.resources. - The 120 entries are each present once, as written.
probe_datahashreports+0x50 7278/7278(the 1 mode-4 entry is skipped, as in every build).probe_idclhashreports+0x08 1/1 +0x20 1/1.
New name complete
- 18 map-level and 102 light-probe entries were copied.
naming_problems()returns[]. It checks:- every retail map-named entry has a copy;
- every
LIGHT_PROBEentity has its image under the new name; - no mapresources row still names an old map-named resource;
- every such row exists renamed, and the map list is the new name;
- every quoted
maps/game/dlc/c02/c2v1/...value in modtest's texts names an existing per-entity resource, never a map-named one; - the only exception is
mega*.decl.
- All 15 engine-derived names exist under the new name.
- c2v1 and the box keep their own entries.
- No map-named row is unresolved.
Controls, each of which fails as it should:
- A:
modtest.ambientshleft under the old name only; - B:
_world.bshadowsleft under the old name only; - C: one light-probe image left under the old name only;
- D: a mapresources row left on the old name;
- E: the text names c2v1's own
.proc; - F: the text names a nonexistent c2v1 cm;
- G: the navmesh is missing → the engine-name check fails.
.entities
- The authored text reads back unchanged and round-trips byte-exact.
build_custom_map.checkfinds nothing: no dangling references, and everyinheritis among the 996 mounted entityDefs. Control: a fake inherit fails.- Every new block's donor is a retail block of the same inherit and class.
- Every new block has its donor's exact field set. Only the documented edits
differ: orientation,
lightColor,targets, a foreigngroupName. Control: an extra field fails. - Every decl, model and entityDef value resolves in the mounted archives
(
kit.jsonmount with our chunk). This covers the new blocks and the world's new loadout. A field counts as a decl reference when the retail donor's value there resolves. Control: a fakeuseableComponentDeclfails. - Kept retail blocks are verbatim except the world and the 5 with dropped references.
- No mission class or retail pickup is left.
- Every enemy has
hide = false, no roam target, and a valid group. - Every kept layered entity is in
layer/dlc/c2v1.
Placement
- The modtest navmesh is byte-identical to retail's.
- The start and the 23 pickups stand on retail-clear floor over the start's navmesh component, inside the armoury. The pickups are ≥ 1 m apart.
- Each enemy:
- has a navmesh disc in the start's component (radius 0.6 m, 0.9 m for the supersoldier, 1.2 m for the Übersoldat);
- stands on clear floor;
- has clear sight down its aisle from the hall's south cross aisle (no solid 1.3–1.8 m above the floor).
- Sight straight from the door is 72–100 % (reported).
Placement controls, each of which fails as it should:
- a point in the hall wall fails the enemy test;
- the sight test across the cubicle partitions fails.
Mapresources
- Retail rows are kept in order, the map-named ones renamed, plus the 34 rows.
- Every added row resolves.
- Every new entityDef is preloaded.
Install side
mapinstall.check_containerreturns['game/wolfsdk/box', 'game/wolfsdk/modtest'].tools/verify_mapinstall.pypasses ("Alles in Ordnung").
Determinism: two builds give the same sha256, and the box stage passes its own determinism check.
Not proven in game (worst first)
- The dev menu command branch (
tnc_devmenu_fix),devmapwith a non-retail name, and the box itself have never run (r9 "Not proven" 1–2). modtest depends on the same chain. - Enemies placed with
hide = falseand no spawner have no in-game evidence yet. The precedents are the kit map and a c2v2 debug dummy. They should idle and then fight, using the retail scene points, but this is unseen. - Campaign mode with no chapter part makes the player the generic
player. WhetherdevmapInvLoadout(all weapons) is applied on thedevmappath is still open (r4_pickups open question 1). The pickups work either way. - Collision of 91 shared cms: H's variant-D fix ORed
0x18into 91 model-named cms that retail c2v1 also uses. Those cms are shared by name and cannot change without changing c2v1. Retail static instances with these models (floors, walls, caps, bookcases, chairs) now also block the player and AI.- Expected effect: none on the floors.
- Possible effect: snagging on stairs. The test area has none.
- Pickup heights. A static pickup from another map may float or sink a few centimetres. Its rest height was taken from c2v1's measurements.
- Sound. There are no map sound banks: expect the warning
[SOUND] No banks found for map 'game/wolfsdk/modtest'. Ambience is silent; weapons and VO use global banks. - Light probes. The probes' images are byte copies under the new name. That the engine finds them there is read, not seen.
Install (game closed; Freedom Chronicles installed)
- Revert the mods in the loader (or
python -m wolfsdk revert). While mods are applied, mapinstall may only swap the chunk; the new textures.cache link is a new file. - Install the chunk:
python -m wolfsdk map-install D:\wolf-decomp\tmp\modtestmap\modtest\dlc_0\base\chunk_22.resourcesThis writes dlc_0's packagemapspec with all four maps, hardlinks both new textures.cache names, and records everything for uninstall. - Dev menu: add the item below to
devmenuoption/mainintools/build_devmenu_fix.py(another agent owns that file). It goes after the box item: num 4 → 5. Without the box, useitem[3]and num 3 → 4.
As a dict next to BOX_ITEM:
python
MODTEST_ITEM = {"devMenuDisplayName": "WolfSDK: Mod test map",
"cmds": ['si_spawnspot "modtest_start"', 'si_nextlayeractive "layer/dlc/c2v1"', 'si_filllayeractive ""',
'si_devprogression ""', 'si_checkpointName ""', "si_gamemode 0", "devmap game/wolfsdk/modtest"]}
Then rebuild that mod.
4. Apply the mods again and start the game.
Undo: python -m wolfsdk map-uninstall, or install the H or box chunk again.
In-game test
- Dev menu → "WolfSDK: console test" must show
g_debugLayerChanges= 1. Otherwise the command branch is broken and nothing below means anything. - Dev menu → "WolfSDK: Mod test map".
- Pass: you stand in a lit room with weapons, ammo, health and armour on the floor to the left and right. A door to the north leads into an office hall with 8 enemies standing with their backs to you.
- Pick up each weapon. Open the weapon wheel and check whether all weapons, including LKW/DKW, are there from the start (the loadout question).
- Walk through the door and shoot:
- the soldier at 13 m;
- the elite at 23 m;
- the Übersoldat at the far end at 35 m.
Do they react, take cover behind the desks, and come after you?
God mode from cheats_gamemode helps.
5. condump modtest1.txt and send the file.
| Log line | Meaning |
|---|---|
Map game/wolfsdk/modtest is not present in this package |
the spec is not installed |
The map ... isn't present in the cache file |
the old chunk is still in dlc_0 |
MountMemoryDatabase: Failed to open ...modtest/textures.cache |
the link is missing |
[SOUND] No banks found for map 'game/wolfsdk/modtest' |
expected |
Failed to load player entityDef |
the player fallback broke |
lines naming lightprobes/..._row.bimage |
the probe lookup does not use the map name the way it was read |
Open (skipped for this version, per the coordinator)
- Robot enemy (panzerhund): built and checked once, then left out as
experimental.
- It is retail c2v3 data in the same chunk, with the same mounts.
- It needs the donor from c2v3's text plus 365 preload rows matched by name from c2v3's list. Every one of those rows resolved.
ROBOTin the builder is the ready entry: append it toENEMIESand rebuild.- Its spot (24, −36.5) passed the 1.2 m navmesh-disc check.
- Enemy respawn/reset:
- Not built. No retail evidence says an
idTarget_AISpawncan fire twice (r4_aispawnwaves §3). - A new kiscule decl entry is unproven.
- Reloading the dev menu item is the reset.
- Not built. No retail evidence says an
- Laserhund or other robots, and a second wave behind a trigger: not attempted.
- LKW/DKW as world pickups: none exist in the game. They are only reachable through the dev loadout.
Related: Research Report: R3 Navdata, Research Report: R2 Ainav, Research Report: R4 Pickups, Research Report: R4 Ai Spawn Waves, Research Report: R9 Box Map, Research Report: R9 Dev Starts, TNC Custom Map Status.