Skip to content

Kitmap Map Skeleton

LopeKinz edited this page Sep 26, 2026 · 1 revision

Skeleton for a new map in c2v1

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.

Result

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.

1. What remains

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 from sacramento_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.

2. Rules (first hit wins, each entity exactly one rule)

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.

3. The Four Decisions and Their Evidence

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.

3.1 PVS and Tome: Empty the Tome, Leave PVS Unchanged

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 (0xAA380E copies it into the length fields of the memory file). SetTome checks cmp [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 in idUmbraViewContext::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 → r8 from 0x6DC130). 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 = 1 and r_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: 0x6DC169 loads the PVS only if a Tome exists; otherwise it immediately returns false. 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". bonusmap01 is delivered in Retail entirely without .pvs (measured here).
  • The tool exists: idcl.pack_payload(tome, b"") → (b"", 0, stored), followed by idclwrite.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.

3.2 staticShadowGeom: remove

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 query 0x381F92 executes 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.

3.3 world.hkshape: Mandatory, replacing c06p2

  • Cannot be omitted. If the file is missing, 0x11A8EC0 reports "No world collision shape. Do you need to rebuild the world collision?" via 0x12386B0 with 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 cm entries. 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.
  • Replacement: The Retail map c06p2 provides a world.hkshape consisting 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 same cm version as c2v1. A world collision without game geometry is thus Retail practice; c06p2 is also the only Retail map entirely lacking idStaticInstanceEntity. 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_blockers in skeleton.json with 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.

3.4 Mission logic: remove all Kiscule entities

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.

3.5 Unchanged

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.

4. Checks

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)

5. Open and Risks

  • 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 map map_dlc_c2v1 is 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 .proc entry 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.

6. Commands

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

Clone this wiki locally