Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Latest research roundsNeueste Forschungsrunden

Custom Objective Shows in Journal but Not in the HUD Banner

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

In plain wordsIn einfachen Worten

In a custom Wolfenstein II mission, the objective appeared in the journal but the on-screen banner never showed. By reading the game's machine code, this report found a second way to announce an objective ('SetActiveObjective') that skips the one check suspected of blocking the banner. The proposed fix is to add one such script node per objective, a two-line change in the mission-building tool. This is based only on code reading; the fix has not been tried in the running game.

  • Three blocking checks were known on the normal objective-banner path; the third, a 'tracked mission' pointer, is the suspect.
  • The SetActiveObjective path respects the first two checks but never reads the third.
  • A search of the whole program found that pointer read in exactly one place, ruling out a second hard-coded setter.
  • The original game already uses SetActiveObjective repeatedly when a sibling objective finishes.
  • Fix proposed but untested in game; if the banner is still missing, a mission debug setting helps narrow it down.

In einer eigenen Mission für Wolfenstein II erschien das Ziel im Tagebuch, aber das Banner auf dem Bildschirm kam nie. Durch Lesen des Maschinencodes fand dieser Bericht einen zweiten Weg, ein Ziel anzukündigen („SetActiveObjective“), der genau die Prüfung auslässt, die das Banner vermutlich blockiert. Als Lösung wird pro Ziel ein solcher Skriptknoten ergänzt, eine Zwei-Zeilen-Änderung im Missions-Werkzeug. Das beruht nur auf Codelesen; die Lösung wurde im laufenden Spiel noch nicht ausprobiert.

  • Auf dem normalen Weg zum Ziel-Banner waren drei Sperrprüfungen bekannt; verdächtig ist die dritte, ein Zeiger auf die „verfolgte Mission“.
  • Der Weg über SetActiveObjective beachtet die ersten beiden Prüfungen, liest die dritte aber nie.
  • Eine Suche im ganzen Programm fand diesen Zeiger an genau einer Lesestelle; eine zweite fest eingebaute Setzstelle ist damit ausgeschlossen.
  • Das Originalspiel nutzt SetActiveObjective schon mehrfach, wenn ein Nachbarziel abgeschlossen wird.
  • Lösung vorgeschlagen, aber im Spiel ungetestet; fehlt das Banner weiterhin, hilft eine Missions-Debug-Einstellung bei der Eingrenzung.

Technical detailsTechnische Details

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

This note continues an earlier investigation into why a custom mission's objective entry appears correctly in the in-game journal but never triggers the HUD "banner" notification, while the same wiring works for retail missions. The earlier pass found three candidate blocking conditions ("gates") on the engine's ObjectiveStart HUD-notify path. This pass fully disassembles a second, independent dispatch path — ActionMission.SetActiveObjective — and finds it shares two of the three gates but never reads the one gate suspected of actually blocking the banner, a per-player "currently tracked mission" pointer. A whole-binary search confirms that field is read in exactly one place in the entire executable, ruling out a second hidden setter. Everything here is static disassembly of the shipped executable and decl data; it has not been confirmed by running the game.

Mission/HUD dispatch (addresses, module.bin)

ActionMission port table at 0x988ff5:

MissionStart          0x989200
MissionComplete       0x989330
MissionEnd            0x989450
ObjectiveStart         0x989690
ObjectiveComplete      0x9894e0
ObjectiveEnd           0x9897a0
SetActiveObjective     0x989890

Known gates on the ObjectiveStart → HUD path (prior round, unchanged):

  1. Gate 1 — controller-index filter in the per-player loop.
  2. Gate 2 — a global suppression byte at object+0x5a98d.
  3. Gate 3 — a per-player "currently tracked mission" pointer at player+0xbf58, read only inside ObjectiveStart's own notify chain (0xF6EA20 → 0xFACCA0).

New this pass: SetActiveObjective's own dispatch chain

Disassembled instruction-by-instruction: 0x989890 (validates mission/objectives, fetches showNotification) → 0xF6ECE0 (per-player loop, 12 slots) → 0xFAD020 (notifier). It respects gate 1 and gate 2 (the 0x5a98d check is present inside 0xFAD020 at 0xfad1b1) but contains zero references to offset 0xbf58 anywhere in any of the three functions. A whole-.text exact-displacement search for 0xbf58 across the entire mapped image finds exactly one occurrence total — the already-known ObjectiveStart-side read — which rules out a second, hard-coded setter elsewhere in the binary (it does not rule out a setter reached through the engine's generic reflected-field mechanism, which resolves offsets at runtime from a field-name string and is invisible to this kind of search).

SetActiveObjective's handler only ever fetches m_mission, m_objectives (a single-item list matching the firing objective's own ObjectiveStart node), and m_showNotification — never completeHides/keepMissionInJournal — so a new node can safely reuse the existing mission-node builder helper unchanged.

Retail precedent: one retail script node receives SetActiveObjective from two different sibling objectives' ObjectiveComplete outputs — "re-point the tracker when a sibling finishes," used repeatedly, not a one-shot pairing.

Recipe

Add one ActionMission.SetActiveObjective node per objective in the chain, wired from the same Received trigger as that objective's own ObjectiveStart, reusing the existing mission-node helper unchanged — a two-line diff inside the per-objective loop of the mission-building tool. Because the full dispatch chain above contains no reference to gate 3 at all, this fix is structurally immune to the one concrete blocking mechanism found in two rounds of disassembly, regardless of whether gate 3 was the actual cause in any specific failing run.

Dead end

The project's own Ghidra RTTI/vtable field data has no per-member offset table for the player class (class hierarchy and vtable addresses only) — this avenue for naming the +0xbf58 field by RTTI is a confirmed dead end, not merely untried.

Open questions

  • Who/what writes player+0xbf58 and a related mission-side flag bit, and whether it was actually pointing at a conflicting mission at the moment a real HUD failure was observed — not resolvable statically; would need a live memory read during a session, or tracing the reflected-field dispatch table itself.
  • Whether the drawing logic past both gates (icon/material lookups) has further conditions that could themselves no-op — not reached in either round.

In-game test (not yet run)

Apply the SetActiveObjective node change, rebuild the custom mission, and replay it to the point where the HUD banner should appear.

  • Banner appears: fix confirmed.
  • Still missing: enable the mission debug cvar, replay to the same point, and check the console log for the line that marks SetActiveObjective's own call path firing. If that line is present but no banner shows, the problem is in the notifier's drawing logic itself, past both known gates — a new disassembly pass starting there is needed. If the line is absent, the node wiring itself needs re-checking.