Repository navigation
Kitmap Map Skeleton
Task kitmap/skeleton, 2026-09-24. Files: tools/survey_kitmap_skeleton.py, re_probes/kitmap/skeleton/ (skeleton.json, skeleton.entities.txt, classification.tsv, this report). All numbers come from a single run, which takes about 10 seconds: 38/38 checks green, all negative controls caught. Read from the installation, nothing written there.
Structurally valid, runtime unconfirmed.
A new map built in game/dlc/c02/c2v1 must exactly keep 3 of the 9206 Retail-Entities: world (idWorldspawn) and the two idPlayerStarts sacramento_gameplay_player_start_1 and c2v1_ps_challenge_01. 160 are optional, 9043 fly out. Chronicles equipment does not depend on the map; it comes from the playerDef of the chapter variant. In the container, three entries change besides .entities, a fourth is optional:
| Entry | Action |
|---|---|
_combo/_world.tome |
Payload empty (0 Bytes). This means no Umbra Culling and no PVS anymore. |
_combo/_world.bshadows |
Remove entry. No tool exists for that yet. Interim solution: +r_useStaticShadows 0. |
_combo/world.hkshape |
Replace payload with the one from c06p2: 6 boundary plates, ≥ 295 m away. |
_combo/_world.pvs |
Bytes remain. Without Tome, the file is never loaded. |
c2v1.ambientsh |
Optional empty. This results in only a warning. |
| Entity | Why |
|---|---|
world |
Carries envSettings = game/ac/c2v1/c2v1_default (Sky/Fog/Env), globalAIsettings, and devmapInvLoadout. Each of the 39 Retail maps has exactly one. |
sacramento_gameplay_player_start_1 |
Start of the campaign (Layer layer/dlc/c2v1). |
c2v1_ps_challenge_01 |
Start of the Challenge, without Layer, so always spawned. |
Why both starts? The chapter variant dlc/c2v1 (patch_3) requires spawnSpot = "c2v1_gameplay_player_start_1". This name does not exist in the map at all. The code then only outputs a warning (0x101B796/0x101B79D, Level 5) and takes an entry from the Spawn list as a substitute. Which one, is undecidable offline. Therefore:
- Both starts remain. The layout step sets all starts to the same new spawn position.
-
Recommendation: add a third start named
c2v1_gameplay_player_start_1, cloned fromsacramento_gameplay_player_start_1. Then the name search hits, and the fallback path is no longer needed. Verified: The name is missing in Retail, and the skeleton plus clone validates cleanly.
Equipment: playerDef = snapmaps/chronicles/player/agent_shade (patch_3_chunkbase_6, mounted over basechunk 6). The start equipment is silentfeet_shade, kampfmesser61 with knife ammo, handgun, and the upgrade weapons/handgun/silencer. None of this is in .entities. The equipment remains intact even though all scripts are removed.
Game-Challenge: None of the 39 Retail maps places an idGameChallenge* entity. The engine generates them itself. Therefore nothing needs to be kept.
| Rule | Verdict | Classes | Count |
|---|---|---|---|
| K1 | keep | idWorldspawn | 1 |
| K2 | keep | idPlayerStart | 2 |
| R1 | remove | Layer layer/dlc/c2v1_challenge1
|
46 |
| R2 | remove | idKisculeEntity | 31 (+2 already in R1) |
| O1 | optional | idAutomapMapImage/Volume | 8 |
| O2 | optional | idSoundEnvironment | 30 |
| O3 | optional | idSoundPortal | 53 |
| O4 | optional | idEnvArea | 2 |
| O5 | optional | idSoundClip, idEnvironmentSpeaker | 67 |
| R3 | remove | idStaticInstanceEntity (the old building) | 7306 |
| R4 | remove | AI, Spawners, Alarms, ScenePoints | 362 |
| R5 | remove | Trigger, WorldMarkers, Splines, idInfo, idVolume | 102 |
| R6 | remove | idLight, idLensFlare | 549 |
| R7 | remove | Props, Dynamics, Glass, Animated, Ladders, FX | 647 |
The list containing all 9,206 entities is located in classification.tsv.
Optional groups are self-contained. The hull check follows references from .entities, Kiscule, and then validates whether keep + group + hull is valid. This applies to all five cases. Only O1 (Automap) generates an entry: the collection map map_dlc_c2v1 (idProp2, via mapUnlockSecret). By default, all five remain excluded because they describe floorplans, reverb, and doors of the old rooms.
All RVAs reference re_probes/runtime_87400. 37/37 Anchors (exact Disassembly and target string) and 3/3 Live-Cvars are confirmed. Shifting by 1 byte results in 0/37 and 0/3.
The actual problem is the Tome, not the PVS. The Tome is Umbras occlusion database and is baked from the walls of the Gestapo office. If you retain it, the old walls continue to occlude, even though they no longer exist: new rooms behind an old wall would be culled. The solution is an empty Tome payload:
- An idFileResource carries its length at
+0x50(0xAA380Ecopies it into the length fields of the memory file).SetTomecheckscmp [rax+0x50], 0; jle(0x82DE14) and then reports "file not found" as a Warning (0x82DF88). The caller does not evaluate the result (0x381E91), so the Tome remains NULL. - When loading the Render World, NULL is caught (
0x382873), as is inidUmbraViewContext::Init(Warning,0x82F274). The visibility query reports visible for a NULL Tome (0x8303E4→mov al, 1). Of the 25 code locations at the Tome context (+0x246BD8), the 16 direct read accesses are all NULL-checked. 9 locations only pass along the address; of those, 3 are tracked: at SetTome (0x381E8A), at loading the Render World (0x38255B), and at loading the PVS (0x11B61E2→r8from0x6DC130). The remaining 6 are not tracked. The review was manual work; only a selection is listed as an Anchor in the script. - World regions do not come from the Tome. Live applies
r_useKdPartitioning = 1andr_useUmbraPartitioning = 0, so the regions originate from.prockd: an axis-parallel k-d grid with 105 regions and 104 nodes, which is independent of the layout. The Tome there only provides the cluster count, which is caught for NULL. -
PVS:
0x6DC169loads the PVS only if a Tome exists; otherwise it immediately returnsfalse. Thus the entry is never read and can remain. Queries return −1 (0x6DC4B1).InCurrentPVS(0x11D26C0) returns true when PVS or Handle is missing −1: no entity will be put to sleep due to the PVS. A PVS from the old layout would instead put enemies to sleep in "other old rooms".bonusmap01is delivered in Retail entirely without.pvs(measured here). - The tool exists:
idcl.pack_payload(tome, b"")→(b"", 0, stored), followed byidclwrite.finalize.
Consequence for visibility: There is no Umbra culling anymore. Only Frustum and GPU culling remain. Everything new is visible, and nothing old occludes anymore. Whether the entire render path draws cleanly without a Tome was not tracked beyond the check points. However, the dev path "Umbra tome is stale, ignoring until rebuilt" shows that the engine anticipates this state.
The 11 MB baked shadow geometry contains the old building, which would result in ghost shadows. Confirmed: if it is missing, it is skipped silently (0x382036). If it is loaded but empty, it is also set to NULL (0x38203F/0x382047, +0x158 == 0). For files that are too large, the console hack 0x81FEA0 intentionally empties them.
- Preferred: Remove the entry. This can only be done by a new Writer; it would constitute a new ability.
-
Interim solution for the first test: Start with
+r_useStaticShadows 0. The query0x381F92executes before loading. It acts globally, affecting all maps. -
Not recommended: An empty payload. What the parser (
0x820960, 4,7 KB Code) does with 0 bytes is untracked.
-
Cannot be omitted. If the file is missing,
0x11A8EC0reports "No world collision shape. Do you need to rebuild the world collision?" via0x12386B0with Level 7, terminating map loading as an error. For comparison: warnings run at Level 5 (0x12385E0). -
Contents: The world collision in c2v1 does not include the building walls; those are stored per instance in
cmentries. It consists of 34 polygons, all strictly parsed and byte-exactly rewritten:-
31 small invisible blockers in the old building frame (largest side 1.91 m):
- thin sheets (0.01–0.02 m thick) at heights of 2.2–3.4 m or 11–12.4 m,
- crates from 0.45–1.13 m on floors z = 0 / 4.5 / 9.0,
- 1 blocker just outside (x = 45.17),
- 2 corner boxes of 5.5 × 6.2 × 3.8 m at (−51.6/−232.8/−12) and (52.2/7.8/18.7). These are the world boundaries.
-
31 small invisible blockers in the old building frame (largest side 1.91 m):
-
Replacement: The Retail map c06p2 provides a
world.hkshapeconsisting of only 6 boundary plates (±900…1000 m, floor at z −400…−300). The next distance to the building is 295.5 m. The file parses strictly, can be byte-exactly rewritten, and shares the samecmversion as c2v1. A world collision without game geometry is thus Retail practice; c06p2 is also the only Retail map entirely lackingidStaticInstanceEntity. Payload swapping is possible with existing tools. -
Fallback: Retain Retail bytes and treat the 31 blockers as blocked zones. They are listed under
world_hkshape.retail_blockersinskeleton.jsonwith coordinates. - Open: The limits of the Havok broadphase grow to ±1000 m. Falling out of the map lands on the plate at z = −300. Both remain unconfirmed at runtime.
The 33 Kiscule Decls (code search locations are 33/33, case is ignored) reference 212 different Entities of the map. These references are not in .entities, so build_custom_map.check does not see them: During the negative control "Keep main Kiscules", check finds 0 problems, while the Kiscule rule finds 26. The main script sacramento_gameplay_kiscule_c2v1 contains:
- 8 mission nodes (targets
obj_01_find…obj_04_kill_all), -
ActionNextMap(leveling), -
ActionChronicle completed = true, -
CustomPlayerMode(muted, duplicated in playerDef), - 5 autosaves.
The mission Decls themselves (cobalt/c2v1/mission_c2v1_main*) do not reference an Entity. If all Kiscule entities are removed, no Decl is instantiated anymore, leaving 0 hidden references. Doors, triggers, markers, spawners, and AI fall away with R4, R5, and R7.
| Entry | Reason it can remain |
|---|---|
.proc |
Lists all 7306 instances and 2 additional ones without an Entity (c2v1_geo_vista_func_staticinstance_4476/4477). Entries without an Entity are thus Retail practice. The envAreas block retains 2 boxes with Env c2v1_nomoon in [20…41]×[−110…−78]×[−1…5]. |
.prockd |
The area grid does not depend on the layout. |
hknavmesh |
Only reusable. The skeleton contains no AI. |
| staticStreamTree | Rebuilt at runtime (see mapmanifest.md). |
.mapresources |
Retail manifests also list missing resources. |
| lightprobe images | Become unused once the probe lights are missing. |
| Check | Result |
|---|---|
| Retail-Text Round-Trip, 9206 Entities | byte-exact |
| Each Entity matches exactly one rule | 9206/9206 |
validate(skeleton) = build_custom_map.check (including inherit against the mounted entityDefs) + mandatory rules + Kiscule rules |
empty |
| References of retained Entities to removed ones | 0 |
Skeleton + Clone c2v1_gameplay_player_start_1
|
empty |
Negative: sacramento_gameplay_player_start_1 omitted |
caught |
Negative: c2v1_ps_challenge_01 omitted |
caught |
Negative: world omitted |
caught |
Negative: Spawner c2v1_combat_target_ai_spawn_firstarea retained |
caught (hanging targets) |
Negative: sacramento_gameplay_kiscule_c2v1 retained |
26 findings, check alone finds 0 |
| Negative: Start Duplicate | caught |
| 5 optional groups with wrapper | validate |
| Retail Precedence over 39 Maps | exactly 1 Worldspawn and ≥ 1 Start; 0 idGameChallenge; c06p2 without instances; only bonusmap01 without .pvs
|
| Code Anchors / shifted by 1 byte | 37/37 + 3/3 / 0/37 + 0/3 (--shift 1 ends with exit 1) |
- Not covered by Retail: There are no Retail maps without idKisculeEntity, without idSoundPortal, or without idSoundEnvironment. The skeleton has null values for all three. It sounds incorrect on the first test, but bringing back O2/O3 fixes it; both groups are closed.
-
No Level End: Without Kiscule, there are no objectives, no
NextMap, and no Chronicle completion. The map can be played but not finished. This requires a new Kiscule (Text-Decl in compfile, not yet built). Additionally, without O1, the collection mapmap_dlc_c2v1is missing, and the chapter's secret statistics remain open. - Which Start is taken is only unambiguous with the recommended name.
-
Lighting: The Ambient SH (
c2v1.ambientsh) remains baked from the old build as long as it is not cleared. If it is empty, there is only a warning (0x75A5C7). What it looks like then is unknown. -
For the Geometry Step: All 7306 Retail instances are listed in the
.proc. New instance names without a.procentry do not exist in Retail. Only a single instance is not megatextured:c2v1_geo_lobby_func_staticinstance_1(models/editor/box.lwo,megatexturedModel = false). - Reference Depth: The Kiscule references were found via quoted values that exactly match an Entity name. A reference using other spellings would be missing.
python tools/survey_kitmap_skeleton.py # all Gates, writes re_probes/kitmap/skeleton/, exit 1 on FAIL
python tools/survey_kitmap_skeleton.py --shift 1 # Negative Control Code: 0/37, 0/3, exit 1
English Wiki generated from the measured research notes in docs/. Evidence grades and uncertainty are preserved intentionally.
- WolfSDK Loader · DE
- WolfSDK Studio · DE
- FAQ · DE
- Mod format · DE
- Modding Guide
- Modding Surface
- CVar Catalog
- CVar Validation (old scrape)
- Texture Database
- Developer Mode
- Dev Menu
- Custom-Map Status