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):
- Gate 1 — controller-index filter in the per-player loop.
- Gate 2 — a global suppression byte at
object+0x5a98d. - Gate 3 — a per-player "currently tracked mission" pointer at
player+0xbf58, read only insideObjectiveStart'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+0xbf58and 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.