Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Latest research roundsNeueste Forschungsrunden

Dev Starts After Map Load (kce) Restored Through Marker Layers

Partly solvedTeilweise gelöst Original page in the GitHub wikiOriginalseite im GitHub-Wiki

In plain wordsIn einfachen Worten

Many developer menu entries of Wolfenstein II: The New Colossus used to run extra commands after the map loaded, such as a teleport or a scene start, but those commands do not exist in the retail game. This round-9 report shows by reading the code that a made-up 'marker' layer name, passed when the map starts, lets each map's own Kiscule script send the same events instead. Version 0.2.0 of the dev menu mod restores 126 of the 158 such entries, and every offline check passes. Nothing was tested in game, and one timing difference stays open.

  • The marker must go into both layer lists: the active one so the script check sees it, the fill one so the chapter lookup ignores it.
  • 63 layer checks in 12 base scripts on 12 maps restore 126 of 158 entries; 2 more are only partly served.
  • 32 are not restored: 14 DLC entries, 14 whose events have no receiver, and 4 that only had give lines.
  • Fixed on the way: 32 Fergus/Wyatt timeline entries matched no chapter part before; the timeline layers now go into both lists.
  • Open risk: the old commands ran before the map's start event, the new chain runs during it; two maps (c01p1, c09p1) gate their intro on such an event.

Viele Einträge im Entwicklermenü von Wolfenstein II: The New Colossus führten früher nach dem Laden der Karte zusätzliche Befehle aus, etwa einen Teleport oder den Start einer Szene, aber diese Befehle gibt es in der Verkaufsversion nicht. Dieser Bericht aus Runde 9 zeigt durch Codelesen, dass ein ausgedachter „Markierungs“-Layername, beim Kartenstart übergeben, das eigene Kiscule-Skript jeder Karte dieselben Ereignisse senden lässt. Version 0.2.0 der Dev-Menü-Mod stellt 126 von 158 solcher Einträge wieder her, und jede Offline-Prüfung besteht. Im Spiel wurde nichts getestet, und ein Unterschied im Zeitablauf bleibt offen.

  • Die Markierung muss in beide Layer-Listen: in die aktive, damit die Skriptprüfung sie sieht, und in die Füll-Liste, damit die Kapitelsuche sie ignoriert.
  • 63 Layer-Prüfungen in 12 Basis-Skripten auf 12 Karten stellen 126 von 158 Einträgen wieder her; 2 weitere werden nur teilweise bedient.
  • 32 werden nicht wiederhergestellt: 14 DLC-Einträge, 14, deren Ereignisse keinen Empfänger haben, und 4, die nur give-Zeilen hatten.
  • Nebenbei behoben: 32 Fergus/Wyatt-Einträge der Zeitlinien fanden vorher keinen Kapitelteil; die Zeitlinien-Layer stehen jetzt in beiden Listen.
  • Offenes Risiko: Die alten Befehle liefen vor dem Startereignis der Karte, die neue Kette läuft währenddessen; zwei Karten (c01p1, c09p1) knüpfen ihr Intro an ein solches Ereignis.

Technical detailsTechnische Details

Der technische Teil ist auf Englisch – so, wie er im Wiki steht. Unterstrichene Begriffe haben eine Erklärung auf Deutsch.

Round-9 pass on restoring the developer "starts" that ran after map load (kce) in tnc_devmenu_fix, through marker layers. Statically the idea holds, with one change: the marker must go into both si_nextlayeractive and si_filllayeractive. Version 0.2.0 of the mod restores most of the stock after-load entries, and every check passes, including two controls that must fail; one timing difference (BeginGame versus the after-load queue) stays open. Static only; not tested in game.

Static work only. Runtime image re_probes/runtime_87400/module.bin (base 0x7ff62c210000, all addresses below are RVAs), tools/decomp.py on the TNC GZF, raw disassembly via tools/tnc_runtime_code.py. No game launch, nothing written under the install or Saved Games; the apply/revert test ran on copies under D:/wolf-decomp/tmp/devstarts (deleted afterwards). Stock decls/scripts were read from the loader backup (Pristine in tools/build_devmenu_fix.py: rebuilt archives from their full copies, all 24 checked against the manifest's original size + head sha256), never from the modded live archives.

Result in one paragraph

The idea holds statically, with one change: the marker must go into both si_nextlayeractive and si_filllayeractive. FlowLayerCheck reads the map's active layer names; the part resolver ignores every active layer that is also a fill layer; an unknown layer name is never validated anywhere on this path. mods/tnc_devmenu_fix 0.2.0 restores 126 of the 158 stock after-load entries (63 FlowLayerChecks in 12 base scripts, 12 maps). Every check in tools/verify_devmenu_devstarts.py --copies passes, including two controls that must fail. One semantic difference remains open: kce ran from the after-load queue, before BeginGame. The chain now fires during BeginGame. Two maps gate their intro on a dev event and may race (see Q4).

Q1 — Which list does FlowLayerCheck's Validate read? The active layers.

  • RTTI vtable idKisculeNodeFlowLayerCheck 0x2881678; slot 13 = definition factory 0x9B7920, which registers Validate -> callback 0x9B7AB0 and the outputs "Validation succeeded"/"Validation failed".
  • 0x9B7AB0: m_layerPresent via 0x9BC010. Reflection says the field is const idDeclLayer * layerPresent, at offset 0x10 (record at 0x22E3A60). A null pointer returns with no output. Otherwise it calls 0x11B3340(gameLocal from TLS, decl->name at +8).
  • 0x11B3340: case-insensitive search of idList<idStr> at gameLocal+0x60C0 (count +0x60C8, stride 0x30).
  • Writers of +0x60C0: the spawn function 0x11B3410, called from MapInit 0x11B55F0 at 0x11B6FEB. MapInit passes a list copied from map params+0x1198 (the active list, loop 0x11B6FA7..0x11B6FDE). At its end (0x11B4E26..) the function appends every name of its active set to +0x60C0 and +0x60E0. It skips only layer decls with forceOnSpawn (the only idDeclLayer field, +0x80; all 144 retail layer decls are { }). SetTimeline 0x11ABA00 also edits these lists, only for layer/fergus and layer/wyatt. The fill list (params+0x18B8) is never read for +0x60C0.
  • Retail usage agrees: 105 of the 134 retail FlowLayerChecks are driven by BeginGame.OnBeginGameAlways.

Q2 — Unknown layer names, the resolver, and where the marker must go

  • si_nextlayeractive and si_filllayeractive are copied as plain strings. The map parser 0xA7BD10 reads them only when there is no -layername/-filllayername argument (0xA7CED9/0xA7CF0B, r8). The splitter 0xA97670 does string work only and looks up no decl. The "map layer '%s' swapped for '%s'" lookup (0xA93C40) applies only to explicit -layername arguments and never fails. There is no warning and no abort for an unknown name in either list.
  • Unknown name in the active list: the spawn set gets it, but no entity carries it (entity layers are the layers { } blocks in .entities), so spawning does not change. It ends up in +0x60C0/+0x60E0.
  • Fill list readers: the resolver 0xDF5860, the timeline getter 0x11AB970 via 0x11F6B90 (looks for layer/fergus), and save serialisation 0x11DB010. The spawn function writes it only for bake variations.
  • Resolver 0xDF5860: builds active minus fill (exact compare, 0xDF5AD4..0xDF5BFD), then 0xDF8540 requires every remaining name to be a decl name in the candidate part's base list (part+0 only). So an active-only marker would break playerDef and mainMission resolution, as suspected. A marker that is also a fill layer drops out before the comparison. Marker route: put it in both lists. FlowLayerCheck sees it (active), and the resolver does not (fill).
  • The marker is not a real decl. The kiscule reflection reader for resource pointers, 0xF26F10, finds the resource list by class name (0xABBE60, "idDeclLayer"), then calls 0xABA110(list, name, 1, 0). For a missing name that call creates and loads the resource (vtable +0xC8/+0x28). It warns or errors only through resource_errorInGame, which is 0 live. FlowLayerCheck then gets a non-null decl that carries the marker name. Residual risk: not seen at runtime; if it fails, the chain is silent. One cosmetic side path: 0xAAC010 matches active names to layer decls for the loading-screen choice.
  • Failed assumption found on the way (fixed): the previous build put the Fergus/Wyatt layers into the active list only. For 32 variation entries (layer/fergus / layer/wyatt, e.g. c03p1 "D1 - harbor - fergus") these are not in the part's base list, so 0xDF5860 matched no part. The timeline getter also reads layer/fergus from the fill list. The timeline layers now go into both lists. Check 4 shows active minus fill inside the base list for all 301 variation entries.

Q3 — SendCustomEvent versus kce

  • SendCustomEvent Exec 0x9AC5D0 calls 0xF562F0 with the manager at gameLocal+0xD0CC0, emitter = node+0x28 (its instance), the event name and global. With global=true it visits every instance and every receiver of that name (0xF62AD0). It delivers when the receiver sits in the emitter's own instance or its global byte (+0x40) is set, and only if m_enabled is true. It then sets received=1 and fromConsole=0, respects maxNotificationCount and notifies "Received".
  • A console send (null emitter) reaches only global receivers. A script-side global send reaches the same set plus non-global receivers inside the sending instance. The build checks this: no event sent from the 12 hosts has a non-global receiver in its host.
  • m_fromConsole: none of the GetCustomEvent receivers of the sent events in the affected maps' scripts has an edge from its m_fromConsole output (check 5). The fromConsole=0 difference therefore changes nothing here.

Q4 — BeginGame timing versus the after-load queue

  • notice_BeginGame (event def 0x2F68A20, registered 0x16ED20) is posted with delay 0 by 0xE77940, once per entity (flag +0x5518), from game-side think code. Delivery goes through 0xC7EA20 (event 0x209) to node handler 0x9A8510. A node fires once ("ignored BeginGameEvent as it has already triggered once", +0x40). It fires OnBeginGame if challenge-entity+0x618C is set or ignoreInitialFlag (default true). It then always fires OnBeginGameAlways.
  • At that point all kiscule instances exist, because they are spawned inside the map change, and +0x60C0 is filled at the end of 0x11B3410. Receivers and the layer list are ready.
  • The after-load queue (0xA82EC0 → common+0x10B18) is drained in idCommonLocal::Frame 0xA747F0 at 0xA74E37..0xA74F01. That is directly after the map-change block (params 0xA97B20, load 0xA849B0) and the first-frame call (0xA80AD0/0xA80E40); the lines go to the command buffer, which is drained at once. kce therefore ran before BeginGame was dispatched. Inferred: the exact first-tick order was not traced further.
  • Decision: OnBeginGame with the default ignoreInitialFlag=true, the configuration heaven_flow uses for its one-shot intro. OnBeginGameAlways is what retail uses for state restore (show/hide). Today both fire in the same call. No Delay is added: SendCustomEvent's Send output fires after delivery, which keeps the stock order.
  • Open risk: the chain now fires during BeginGame, not before it. Its order relative to other scripts' BeginGame nodes is not established statically. The signal executor 0xF53160 runs some node classes at once and queues others. Two maps gate their intro on a dev event:
    • c01p1: devstarted → heaven_flow Gate.Close. That gate guards OnBeginGame → Log → Gate.Test → play_soup.
    • c09p1: Dev_DevLoaded → event_flow gate before BossOffice_play.
    • Elsewhere the dev events only teleport or start scenes.

The 18 events not in the base scripts

events where result
devstart_afterescape, _meeting, _panzerhund (c1v1); _firstroom, _secondroom (c2v1); _greenroom, _concerthall, _lorentz (c2v2); _controlcenter, _lifesupport, _thechase (c2v3); ev_open_bridge_door (c3v3) GetCustomEvent in DLC archives: dlc_1 chunk_20, dlc_0 and dlc_2 chunk_22, dlc_3 chunk_24 (*_devstart(s).decl, mission_flow, progression, vo) not served: DLC maps, and the loader writes base/ only
player_start_wall, _maintenance, _roundhouse (c08p1, c08p2 "Lakeview") only as cb_player_start_* (sent by c08p1 gp_kiscule_campain_specifics, received by the combat scripts) stock kce had no receiver: a no-op, not restored
start_panzerhund_alley, _streets, _tunnels (c08p2) in none of the 3078 scripts, base or DLC a no-op, not restored

What was built

  • tools/build_devmenu_fix.py (docstring updated):
    • reads stock data through Pristine;
    • gives each servable after-load sequence the marker layer/wolfsdk/<events, lower case, joined by __>, e.g. layer/wolfsdk/devtel_soup__devstarted, in active and fill;
    • adds the timeline layers to fill;
    • edits one host script per map through kiscule.apply_ops: one BeginGame, plus per marker a FlowLayerCheck and a chain of SendCustomEvent(global=true) in stock order;
    • stores the edited scripts as extkisclule:<script> file assets (packed exactly like Studio's script_assets) under assets/kiscule/;
    • merges only entries whose lines are byte-identical and lists every merge. The host is the map's *devstart* script, or else the one with most receivers. It must be placed exactly once, on layers inside every restored entry's active list.
  • Also fixed:
    • devMenuSubList = NULL is now read as "no sub list". The 7 c07p1 Wyatt entries were dead sub-list links before.
    • New entry "WolfSDK: Box" in devmenuoption/main, placed before "WolfSDK: console test", as the coordinator asked (devmap game/wolfsdk/box, layer/dlc/c2v1). It is exempt from the shipped-maps filter, because the map comes from dlc_0's packagemapspec.
  • Not reproduced:
    • give weapon/player/sp/lkw|dkw (c02p2). apply_ops cannot create the ActionGiveItems list item, because the catalogue has no item template.
    • Progression.
  • Counts:
    • 95 decls, 12 scripts, 306 starts (220 before).
    • Restored: 126 entries, 63 FlowLayerChecks. Per map: c01p1 13, c02p0 4, c02p1 6, c02p2 1, c04p2 2, c05p2 8, c08p1 2, c08p2 1, c09p1 9, c09p2 7, c11p1 4, submarine/c07p1 6.
    • Partly served: 2 (c02p2 "piston room": the event is sent, give is not).
    • Not restored: 32. That is 14 DLC entries, 14 c08p1/c08p2 entries whose events have no receiver (these stay separate entries with the plain start), and 4 c02p2 entries that only have give. Of these, 5 DLC entries are still merged: c2v1 Second Room into First Room; c2v2 Concert hall and Lorentz into Green room; c2v3 Life support and The chase into Control center.

Checks (python -X utf8 tools/verify_devmenu_devstarts.py --copies): all OK

  1. The mod folder equals a fresh build (107 assets). build_devmenu_fix.py --check also passes; it failed before, because it read the modded live decls.
  2. Each of the 12 scripts:
    • pack(unpack(asset)) reproduces the asset byte for byte, and Doc.render() is stable;
    • the view diff against retail shows only added BeginGame, FlowLayerCheck and SendCustomEvent nodes, exactly one BeginGame, and edges only between added nodes;
    • no retail node, param, port or edge changed; the broken-edge count is unchanged.
    • Control: one retail GetCustomEvent eventName changed → detected.
  3. All 126 restored entries:
    • the marker appears once in the active list and once in the fill list;
    • there is exactly one FlowLayerCheck for it in the map's scripts (host replaced by the mod copy), wired from OnBeginGame;
    • its chain sends exactly the stock kce events in stock order;
    • at least one of them reaches a receiver.
    • Control: a wrong marker → detected.
  4. Resolver: active minus fill is inside the part's base layers for all 301 variation entries. Control: the timeline layers that rely on the fill exemption are present (with fill "" these 32 entries fail).
  5. No receiver of a sent event reads m_fromConsole.
  6. mod.build_payloads on the live install, read only, with the mods applied now (tnc_photo_mode, tnc_skin_eigene_pistole) plus this mod: 0 conflicts, 0 errors, and our payloads equal the files. No other tnc mod touches these assets.
  7. Copies of the 13 archives that hold the assets (pristine sources, md5 checked after copying):
    • tncpatch.Installation(copy).apply → no missing or unsupported asset. It rebuilt 8 archives: chunk_1, _3, _9, _13, gameresources, patch_1_chunkbase_6, patch_2, patch_3.
    • Every copy of every asset reads back as built.
    • revert() → all 13 archives md5-identical to the originals.
    • For the real apply: the backup needs full copies of those 8 archives, about 1.6 GB.

In-game test for the maintainer

The game must be closed. In the loader, apply the mods including "Dev Menu Map Loading" (0.2.0). Start the game and open the dev menu.

  1. Levels → c05 → c05p2 → (Fergus dev starts) → "Train". Expected: the map loads, then within the first second the player is moved to the train, as the stock telTrain did. Control in the same list: "Train Station", then "Ending" — each must end up somewhere different.
  2. Levels → c11 → c11p1 → "hugin room" (also an ungated map): the same kind of teleport.
  3. Levels → c01 → c01p1 → Fergus dev starts → "bomb". Expected: the bomb scene, and the "soup" intro does not play. If both play, or only the intro plays, that is the Q4 ordering risk. Report which one happened.
  4. Timeline fix: Levels → c03 → c03p1 → "D1 - harbor - fergus" should start with that part's player and main mission (the resolver now finds the part). Before this change it probably fell back to the defaults.
  5. "WolfSDK: Box" in the main dev menu should load the box map.

What the results mean:

  • If 1 and 2 load the map but nothing happens after loading, the marker decl path failed (FlowLayerCheck got a null decl). Tell me; the fallback is to use unused existing layer decls as markers.
  • If the console prints "Unknown command" for any dev-menu line, the old dev-menu path itself failed (see docs/tnc-devmenu-status.md).

Related: Research Report: R8 Devmenu Fix Verify, Developer Mode, Kiscule Scripting.