Skip to content

Mission Anatomy

LopeKinz edited this page Sep 26, 2026 · 1 revision

Mission Anatomy: How Wolfenstein II Builds a Mission (c2v1 "Gestapo Office")

Track missions/anatomy, 2026-09-24. Files: tools/probe_mission_anatomy.py, re_probes/missions/anatomy/ (anatomy.json, graphs/*.txt, run.log, this report). All data was extracted read-only from the Retail installation, the played bundle re_probes/dlc4_override/chunk_22.resources, and the runtime dump re_probes/runtime_87400/module.bin. The game was not launched.

python tools/probe_mission_anatomy.py      # ~90 s, 55 Checks (7 Negative Controls), Return 1 on FAIL

Result in One Sentence

A mission is not a standalone format. It consists of a mission Decl, several missionobj Decls (each with title and description as #str_ keys), and a Kiscule Script that registers, starts, completes, and (unused in Retail) fails via ActionMission nodes. The entire process is triggered by world events (Triggers, Use, Shoot, PickUp), connected by global Custom Events. The goal "Enter the offices" from the screenshot is traced node-by-node (Section 2). This same pattern applies to DLC map c3v1 and campaign map c02p1. All 55 checks are green. Runtime evidence exists for the start chain (Screenshot); everything else is structurally documented but unverified at runtime.

1. The Chain from Chapter Selection to HUD Text

chaptervariation dlc/c2v1  (patch_3; id 230)
 ├─ parts[0].map         = game/dlc/c02/c2v1
 ├─ parts[0].layers      = [layer/dlc/c2v1]            ← unlocks Entities
 ├─ parts[0].spawnSpot   = c2v1_gameplay_player_start_1  (does not exist in the map, E14)
 └─ parts[0].mainMission = cobalt/c2v1/mission_c2v1_main
        │
c2v1.entities: entity { layers { "layer/dlc/c2v1" }  entityDef sacramento_gameplay_kiscule_c2v1
                        class idKisculeEntity, edit.externalKisculeDecl = …/sacramento_gameplay_kiscule_c2v1.decl }
        │
extkisclule …/c2v1_gameplay/kiscules/sacramento_gameplay_kiscule_c2v1.decl   (122 nodes)
        │  ActionMission(mission, objectives[], showNotification, completeHides)
        ▼
missionobj cobalt/c2v1/mission_c2v1_main_obj_01_find   (patch_3)
   title  = #str_mission_cobalt_c2v1_main_obj_01_find_name
   desc   = #str_mission_cobalt_c2v1_main_obj_01_find_summary
   parentMission = cobalt/c2v2/mission_c2v2_main        ← Retail copy error, does not affect (E13)
   material = textures/guis/mission_fc/c2v1/c2v1_mission_obj_1
        ▼
strings/english.lang (patch_4, cfile): "Enter the offices"
  • Layer determines which scripts run. 31 of the 33 Kiscule Entities from c2v1 are active in the Story run (M01). The other two (sacramento_gameplay_kiscule_challenge and …combat_sim_screen_fx_893872923) reside in layer/dlc/c2v1_challenge1 and only run in the Challenge Variation (E11). Entities without a layers block always run.
  • mission Decl (edit): title, description, missionFlags (TYPE_MAIN, TYPE_OPTIONAL, TYPE_DISTRICT, TYPE_HIDEONCOMPLETE …), material, duration. c2v1: "Assassinate Jack's torturer", TYPE_MAIN.
  • missionobj Decl knows only title, desc, material, and parentMission across all 338 Retail Decls (D01). There is no counter field, and no mission or goal text in english.lang has a counter placeholder (D02).
  • Both Decl types reside in gameresources/patch_N and are therefore mounted for every map. The Decl dialect is written and read bit-exactly: 624/624 copies of mission/missionobj/chaptervariation run through Parser → Emitter (R02, including Retail manual work: spaces after ;, empty {}).

2. "Enter the offices": Start and End, Node by Node

All nodes are located in sacramento_gameplay_kiscule_c2v1.decl. The chains are exactly fixed in the Probe (E01–E03). A renamed event (N01) or a dangling edge (N04) causes failure.

START (Runtime occupied: Screenshot) BeginGame[1620406561] (ignoreInitialFlag) └─OnBeginGame→ Delay[1525383468] 500 ms └─Finished→ ActionMission[1476098740].MissionStart mission_c2v1_main, objectives = obj_01…obj_04, showNotification=false → Engine 0xF6F660 "Mission added" + 0xF6E8F0 "Adding objective … to mission" (X07) └─MissionStart→ Delay[671776109] 4100 ms └─Finished→ SendCustomEvent[1671718539] "start_obj_01" (global) ⇢ GetCustomEvent[886242348] "start_obj_01" └─Received→ ActionMission[260028346].ObjectiveStart objectives=[obj_01_find], showNotification=TRUE → Engine 0xF6EA20 "Start objective …" / "Show hud objective …" (X04) → HUD: „NEW OBJECTIVE" + „Enter the offices" (~4.6 s after map start)

END idTrigger sacramento_gameplay_triggers_obj_1_complete (layer/dlc/c2v1) Position (-4.75, -193.75, -0.95), Form cm "maps/game/dlc/c02/c2v1/.hkshape": Box 4.32 × 0.32 × 7.08 m, local to origin, contact bit 0x4000 (E07, E08) = the floor at the ventilation grate opening in the lobby (world marker objective_prone_grate at (-3, -193.7)) ⇢ EntityReference[1300045830] → TriggerNotification[1728583878].listenedEntities classFilter "idPlayer", maxNotificationCount 1 └─Touching→ SendCustomEvent[1488908889] "end_obj_01" ⇢ GetCustomEvent[169323261] └─Received→ ActionMission[1301606007].ObjectiveComplete objectives=[obj_01_find], showNotification=false → Engine 0xF6EDA0: Flag |=1, notice_CompleteObjective (X03) └─ObjectiveComplete→ SendCustomEvent[1690454534] "start_obj_02" ⇢ GetCustomEvent[1129312168] └─Received→ ActionMission[1187802266].ObjectiveStart "Locate Hans' office", showNotification=true

Machine-readable with all parameters: anatomy.json → enter_the_offices (start_trace, complete_trace, graph with 24 nodes/24 edges from chapter variation to HUD).

3. All four objectives of c2v1

Objective (missionobj) HUD Title (patch_3) Start End
obj_01_find Enter the offices BeginGame → 500+4100 ms → start_obj_01 Trigger …triggers_obj_1_complete → end_obj_01
obj_02_locate Locate Hans' office start_obj_02 from completion of obj_01 Gate (once) from Trigger …obj_2_complete or hans_notice_player (Office Script: Hans notices player) or Hans dead → end_obj_02
obj_03_kill_hans Kill Ubercommander Hans start_obj_03 from completion of obj_02 AiKilledNotification Hans → 2000 ms → end_obj_03
obj_04_kill_all Escape (patch_2 said "Leave no witnesses") Completion of obj_03 → world marker removed → ActionRemove → start_obj_04 UseNotification on sacramento_gameplay_prop_use_animated_exit_level → end_obj_04
  • Example: one MissionStart node registers all objectives silently; each objective has an ObjectiveStart node with a HUD message and an ObjectiveComplete node without. Transitions occur via global Custom Events start_obj_NN / end_obj_NN.
  • "Counter Update" does not exist as a Decl or HUD field (D01, D02). Counting is done in the script. Retail example (P01, 27 cases): AiKilledNotification with a list of N enemies and maxNotificationCount −1 or ≥ N → Counter(endCount=N) → Reached End. This implies: the notification fires per kill. SetActiveObjective (3 Retail usages, c02p1) sets the active objective anew; log "Set active on missionIndex… objectiveIndex…".
  • Failure: ActionMission has inputs ObjectiveEnd and MissionEnd. In code, both call 0xF6F210, set flag |=2, and send notice_FailedObjective or notice_FailedMission (X02). MissionNotification has outputs ObjectiveFailed and MissionFailed (X06). Retail wires none of these: 0 of 2977 scripts (S01, M03). Missions fail in-game only through player death (Checkpoint). What a failed objective does on the HUD is unknown.
  • Dead end in Retail script: SendCustomEvent[1660263701] "end_level" has no input; c2v1_combat_target_ai_spawn_7 is wired nowhere (A01).

4. Mission End: Exit → Chronicle → Next Map

UseNotification[1195927002] on sacramento_gameplay_prop_use_animated_exit_level
   (idProp2 prop/use_animated, CLIPMODEL_BOX 0,2 m, flags.hide=true until world marker shows it,
    LOOT_TOUCH_USE_TIMED 1000 ms, textOverride #str_useable_fc_c2v3_escape = "Escape (Exit level)")
 └─Used→ SendCustomEvent "end_obj_04" ⇢ GetCustomEvent[711716177]
    └─→ ActionMission[1709067129].ObjectiveComplete + .MissionComplete   (Flag |=1, notice_CompleteMission)
        └─→ SendCustomEvent[618596321] "end_level" ⇢ GetCustomEvent[188420141]
            └─→ Delay[16548334] 10 ms → ActionChronicle[1306067857].End (completed=true, returnToSas=false)
                └─End→ ActionNextMap[1488751441].NextMap (mode MODE_AUTO, mapName "")

E03 checks this chain exactly. MODE_AUTO without map names presumably means "next part according to variation" (nextVariationId = 232). The chapter completion display belongs to ActionChronicle. Both are unseen at runtime. Counter-examples end differently: in c3v1, Level-Exit → Bink → ActionChronicle(returnToSas=true), and only a custom Engine event MapMovementNotification.ChronicleToSAS triggers ActionNextMap. In c02p1, UseNotification (Level-Exit) → ActionConsumeInventory → ActionNextMap.

5. Enemies: Trigger → Spawner → Group → Alarm

Spawner (idTarget_AISpawn) triggered by Targets
…spawn_firstarea Trigger …trigger_firstarea → ActionTrigger 5 × soldier_light, Group FirstArea
…spawn_secondarea_initial Trigger …secondarea_initial(1) → ActionRemoveAIFromVolume (Empty Lobby) → 300 ms → ActionTrigger Human + 7 × soldier_heavy, SecondArea
…spawn_secondarea_labs Trigger …secondarea_labs (+ Cleanup) 4, ThirdArea, maxActiveAIs 13
…spawn_commanderarea_initial Trigger …commanderarea_initial → 2 × Volume empty → 300 ms 6 including Hans, Commander
…commanderarea_secondfloor / …thirdfloor Script and directly via idTrigger.edit.targets (A04) 6 or 1
…spawn_5, …spawn_6 alarmSpawner of both idAlarmSystem Spawn-Scenepoints → hidden AI (Reinforcement on Alarm)
…spawn_7 nothing (A01) 1 Elite
  • Spawned enemies stand ready but hidden (flags.hide = true) in the map. The spawner names them in edit.targets or names idScenePoint_Spawn, which in turn name AI templates.
  • idEncounterGroupMgr c2v1_combat_ai_groupmanager_1: Group commanders, SecondArea, FirstArea, ThirdArea. Commander and ThirdArea each have an idAlarmSystem (A03). The affiliation is stored in the enemy itself (groupName in the aiEditable block). An alarm system names its idAlarm boxes (targets) and its reinforcement spawner (alarmSpawner).
  • "Room empty" is measured by Retail with AIVolumeCountNotification (onlyAlive=true, min 0, max 0, 42 of 54 cases, P02). c2v1 uses it once, in the VO script (c2v1_snd_vo_volume_default_first_room).
  • Side effect in the played bundle (O03): The three lobby soldiers converted to Elite (soldier_light_1/3/5) have lost their groupName = "FirstArea". The spawner has none, and retype() takes over the body entirely. For waves in custom missions, the group must be retained during conversion.

6. Doors and Locks

c2v1 has no script-controlled door. The 45 "doors" are idProp2 with prop/doors/locked (5) or prop/doors/unavaliable (40), each an invisible CLIPMODEL_BOX use point in front of a static door geometry. None of them is addressed by a script (A05). Locking and unlocking runs in Retail via ActionActivatable (Enable/Disable of Triggers and Use-Props), ActionShowHide/ActionRemove, and Gate nodes. c02p1 shows key logic with PickupNotification → show Use-Prop → UseNotification → Gate. idKisculeNodeDoorNotification exists in the Exe but is not used in any Retail script.

7. What Engine Code Says (module.bin, read only)

Finding Address Check
ActionMission registers exactly 7 inputs: MissionStart/Complete/End, ObjectiveStart/Complete/End, SetActiveObjective Port table 0x988FF5 X01, N07
MissionStart sets up the mission and adds objectives, without HUD 0xF6F660, 0xF6E8F0 X07
ObjectiveStart → "Start objective %s on mission %s", "Show hud objective %s on mission %s" 0xF6EA20 X04
ObjectiveComplete / MissionComplete → Flag ` =1, notice_CompleteObjective/notice_CompleteMission` 0xF6EDA0
ObjectiveEnd / MissionEnd → Flag ` =2, notice_FailedObjective/notice_FailedMission` 0xF6F210
The log lines hang on g_debugMissions (intValue RVA 0x2FAF9C0) 4 comparisons in 0xF6EA20 X04
The DevMenu command kce <event> (Custom Event from console) is missing in the Retail Exe – X05

The HUD widget (idMenuWidget_ObjectiveNotifier, texts #str_new_objective = "NEW OBJECTIVE", #str_objective_completed = "OBJECTIVE COMPLETED") is a guess based on RTTI and strings. The code path up to 0xF6EA20 is confirmed.

Test aid for the user: g_debugMissions 1 in the console, if you allow setting the value, should write "Start objective …", "Show hud objective …", and "Complete objective …" into qconsole.log. This is the direct runtime proof of a custom mission.

8. Counterexamples: other DLCs and Campaign

Map Variation / Layer Mission Objectives End
c3v1 (dlc_3) dlc/c3v1 / layer/dlc/c3v1 "Escape Anchorage Naval Base" 6: Exit the Docks, Find Another Way Through, Search for Intel, Take the Elevator, Ascend Elevator Shaft, Steal a Truck Use gp_c3v1_levelexit → MissionComplete, parallel → Bink film → ActionChronicle(returnToSas=true); then engine event MapMovementNotification.ChronicleToSAS → NextMap
c02p1 (Campaign) c02/campaign_fergus / campaign, fergus, c02p1, code "Under siege" 6 (of which obj_04_cutwall is registered but never started in the loaded scripts) Use …interact_use_level_exit → ActionConsumeInventory (Laser rifle) → NextMap

The same extraction as for c2v1 confirms the pattern in both maps: a silent MissionStart lists all objectives, each ObjectiveStart displays a HUD message, pure ObjectiveComplete nodes display none, and every objective has a Decl and an English title (G-c3v1, G-c02p1, G-c2v1).

9. Building Blocks for Custom Missions (Documented in Retail)

Catalog of all 47 relevant node types with parameters and fixed connections: anatomy.json → building_blocks, measured across 2977 scripts in 210 node types.

User Request Retail Prototype Chain
"Eliminate the Officers" P01 (27×) AiKilledNotification(listenedEntities = Officers, maxNotificationCount = N) → Counter(endCount N) → Reached End → SendCustomEvent end_obj → ActionMission.ObjectiveComplete
"Find the Documents" c02p1 PickupNotification, c07p2 "Find New Orleans Dossier" PickupNotification (or UseNotification on a Use-Prop) → ObjectiveComplete
"Survive three waves" c2v1 waves, P02 Wave k: ActionTrigger(targetEntities = Spawner k); End: AIVolumeCountNotification(onlyAlive, 0..0) or Kill-Counter → Delay → Wave k+1; Counter(endCount 3) → Objective Complete
"Reach the Exit" c2v1 Exit World marker on prop/use_animated (CLIPMODEL_BOX, no new cm needed) → UseNotification → MissionComplete → ActionChronicle → ActionNextMap
Objective Text anatomy.json → reusable_objectives 283 existing missionobjs with English titles, e.g., "Kill them all", "Kill all enemies", "Kill Everyone", "Search for Intel", "Find New Orleans Dossier", "Find exit", "Escape"
Failure Only Code (X02) ActionMission.ObjectiveEnd – never used in Retail, runtime unknown

The shortest build path without new archive entries, derived and not built:

  1. Script: The payload of the existing entry extkisclule maps/game/dlc/c02/c2v1_gameplay/kiscules/sacramento_gameplay_kiscule_c2v1.decl is replaced in the Override-Chunk. The Kiscule entity already indicates this, the name remains. This is the same payload swap as with the .entities entry in build_dlc4_override.py. Kiscule text and containers are bit-exactly writable for all 569 DLC scripts (R01), while the 2408 base scripts according to docs/kiscule.md.

  2. Objectives/Text: Existing mission-/missionobj-Decls reference them. They reside in the always-mounted base archives. That an objective with a foreign parentMission is displayed under another mission is documented by the screenshot (E13).

  3. Triggers: The 48 existing c2v1 triggers are used. Their forms are local boxes (T02). A new idTrigger entity with the clipModelName of an existing trigger at a different position is structurally plausible, but without a Retail prototype: all 1773 Retail triggers have their own form, 0 shared, 0 × CLIPMODEL_BOX (T01). Alternatively, a new cm via tools/probe_cmfmt.py --box, also untested at runtime.

  4. Enemies: Existing spawners and hidden AIs are retyped (retype) while retaining groupName (O03), or new AI entities are added.

New objective texts require new #str_ entries in strings/english.lang. This is an encrypted cfile, where the name is part of the key, and there is no German .lang. Additionally, new missionobj entries are required in the container. Both constitute independent work (Track missions/text, Add-Entry path).

10. Open, Unconfirmed at Runtime

  • Everything except the start chain of obj_01 (Screenshot) is only structurally supported. This applies to the end sequence, obj_02–04, mission end, waves, and alarms.
  • What an ObjectiveEnd (Failure) on the HUD indicates and whether something follows afterwards.
  • Whether a Trigger-Form can hang between two Entities (Form reuse).
  • Which player start takes effect. The variation requires c2v1_gameplay_player_start_1, which does not exist (E14). In the played bundle, the two starts are swapped. If the player starts at the top of the office, the obj_01-Trigger (Lobby Ventilation) is behind them, and "Enter the offices" remains open until they pass through there.
  • MODE_AUTO/nextVariationId as the target of ActionNextMap, and how the Chronicle display looks at the end.
  • The semantics of Custom Events with global = false cannot be seen from the data. The model assumes "only in the same script". In c2v1, all events are global.

11. Tests (55, including 7 negative controls, all green)

ID Statement
M01 c2v1: 31 active scripts from 33 Kiscule-Entities, 0 dead edges
E01–E03 Start-, End-, and End chain exact (Node IDs and Types)
E04–E06 HUD flags, silent MissionStart with 4 objectives, schedule 500/4100 ms
E07–E08 Trigger-Entity, Layer, cm-Form (local box, bit 0x4000)
E09–E14 Variation, Layer, Kiscule-Entity, Challenge inactive, title "Enter the offices", parentMission copy error, missing spawnSpot
M02–M03 4 objectives complete, no failure wired
A01–A05 9 Spawners (6 script, 2 alarm, 1 dead), first wave exact, 4 groups/2 alarms, 2 triggers with targets, doors
X01–X07 Engine: Port table, Failure-, End-, Start-, and MissionStart-path, g_debugMissions, kce missing, MissionNotification outputs
G-* Patterns and map ends in c2v1, c3v1, c02p1
O01–O03 played bundle: scripts byte-identical, referenced entities present, 3 lost groups
P01–P02 Retail pattern Kill-Counter (27/27) and "Room empty" (42/54)
S01, R01, R02 2977 scripts, End-Ports 0×; 569/569 DLC-scripts and 624/624 Decls bit-exact
D01–D03 missionobj fields, no counter placeholders, 283 reusable objectives
T01–T02 1773 Retail Triggers without BOX/shared form; c2v1 forms local
N01–N07 Negative controls: Event renamed, String missing, Trigger renamed, Edge detached, Decl indentation, Kiscule byte, Port table shifted – all rejected

Clone this wiki locally