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
idKisculeNodeFlowLayerCheck0x2881678; slot 13 = definition factory 0x9B7920, which registersValidate-> callback 0x9B7AB0 and the outputs "Validation succeeded"/"Validation failed". - 0x9B7AB0:
m_layerPresentvia 0x9BC010. Reflection says the field isconst 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_nextlayeractiveandsi_filllayeractiveare 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-layernamearguments 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 readslayer/fergusfrom 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
globalbyte (+0x40) is set, and only ifm_enabledis 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_fromConsoleoutput (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 firesOnBeginGameif challenge-entity+0x618C is set orignoreInitialFlag(default true). It then always firesOnBeginGameAlways.- 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
Sendoutput 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_flowGate.Close. That gate guards OnBeginGame → Log → Gate.Test →play_soup. - c09p1:
Dev_DevLoaded→ event_flow gate beforeBossOffice_play. - Elsewhere the dev events only teleport or start scenes.
- c01p1:
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'sscript_assets) underassets/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.
- reads stock data through
- Also fixed:
devMenuSubList = NULLis 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_opscannot 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
- The mod folder equals a fresh build (107 assets).
build_devmenu_fix.py --checkalso passes; it failed before, because it read the modded live decls. - Each of the 12 scripts:
pack(unpack(asset))reproduces the asset byte for byte, andDoc.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.
- 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.
- 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).
- No receiver of a sent event reads m_fromConsole.
mod.build_payloadson 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.- 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.
- 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
telTraindid. Control in the same list: "Train Station", then "Ending" — each must end up somewhere different. - Levels → c11 → c11p1 → "hugin room" (also an ungated map): the same kind of teleport.
- 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.
- 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.
- "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.