Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / Custom maps and missionsEigene Karten und Missionen

Mission Text Sources: Where Goal and HUD Text Comes From and What a DLC Folder Can Change

SolvedGelöst Original page in the GitHub wikiOriginalseite im GitHub-Wiki

In plain wordsIn einfachen Worten

This page traces how Wolfenstein II: The New Colossus shows objective text on the HUD. The game loads its language table once at startup, before any DLC content is mounted, so a mod in a DLC folder cannot replace or add objective text; new text keys or plain text show up empty. The safe option is to reuse existing objectives whose English titles fit; the page lists 30 checked ones. A workaround with a modified German language file plus a console command was built, but not yet confirmed in the game.

  • The language table is loaded at startup and only reloaded when the sys_lang setting changes, never when a map loads.
  • Objective titles are text IDs looked up at load time; unknown keys or plain text become 0 and display as empty.
  • 30 existing retail objectives were checked, e.g. 'Kill all enemies', 'Search for Intel' and 'Find exit'. None of the 338 retail titles mentions 'survive' or 'waves'.
  • A German language file with four changed lines was built and reproduces all 10 retail language files byte-for-byte in format; using it needs 'sys_lang german' in the console. Not yet confirmed in the game.
  • Checks: 122 of 122 and 8 of 8 pass.

Diese Seite verfolgt, wie Wolfenstein II: The New Colossus Zieltexte im HUD anzeigt. Das Spiel lädt seine Sprachtabelle einmal beim Start, bevor DLC-Inhalte eingebunden werden. Ein Mod im DLC-Ordner kann Zieltexte deshalb weder ersetzen noch ergänzen; neue Textschlüssel oder reiner Text erscheinen leer. Der sichere Weg ist, vorhandene Ziele mit passendem englischem Titel wiederzuverwenden; die Seite listet 30 geprüfte. Ein Umweg über eine geänderte deutsche Sprachdatei plus Konsolenbefehl wurde gebaut, ist im Spiel aber noch nicht bestätigt.

  • Die Sprachtabelle wird beim Start geladen und nur neu geladen, wenn sich die Einstellung sys_lang ändert – nie beim Laden einer Karte.
  • Zieltitel sind Text-IDs, die beim Laden nachgeschlagen werden; unbekannte Schlüssel oder reiner Text werden zu 0 und erscheinen leer.
  • 30 vorhandene Ziele aus dem Verkaufsspiel wurden geprüft, z. B. „Kill all enemies“, „Search for Intel“ und „Find exit“. Keiner der 338 Titel enthält „survive“ oder „waves“.
  • Eine deutsche Sprachdatei mit vier geänderten Zeilen wurde gebaut; ihr Format stimmt mit allen 10 Sprachdateien des Spiels byte-genau überein. Du brauchst dafür „sys_lang german“ in der Konsole. Im Spiel noch nicht bestätigt.
  • Prüfungen: 122 von 122 und 8 von 8 bestanden.

Technical detailsTechnische Details

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

Track missions/text, 2026-09-25. Files: tools/probe_mission_text.py and re_probes/missions/text/ (objective_strings.json, german.lang.cfile, german_overrides.json, chunk_22.resources, run.log, container.log, and this report). Only the game was read against, as well as re_probes/runtime_87400/module.bin. The game was not started, and nothing was installed.

python tools/probe_mission_text.py              # code + data + list + german: 122 Checks, ~10 s
python tools/probe_mission_text.py container    # Chunk copy + german.lang: 8 Checks, ~1 min

Result in Three Sentences

  1. A DLC chunk cannot simply replace or supplement goal text. The language table is loaded exactly once at startup from gameresources + patch_1..4. This happens before any DLC chunk is mounted. It is only reloaded if sys_lang changes. It never happens when loading a map.
  2. A goal with plain text or a new #str_ key will display as empty. missionobj.title is not text, but an idStrId. The Decl reader looks up the key in the currently loaded (English) table during parsing. If it does not find it, it writes 0, and 0 is displayed as "".
  3. The smallest safe path is to use existing Retail missionobj Decls whose English text fits. Below are 30 checked goals. They are entered in Kiscule (ActionMission.objectives) by Decl name, and no new entries are needed in the container. Custom (German) texts can only go via a detour: You include strings/german.lang in the chunk and type sys_lang german into the console in-game. This path is built and structurally verified, but unconfirmed at runtime.

1. The Chain, Measured

Kiscule ActionMission (objectives = ["cobalt/c2v1/mission_c2v1_main_obj_01_find"])
   │  Decl-Name → idResourceList<missionobj> (Retail: all 338 in gameresources/patch_1..3)
   ▼
missionobj  = Class idDeclObjectiveMerc (0x3dc6108 → 0x2542c08, Field Table 0x22806c0)
   title  : idStrId (+0x88, 4 Byte)   desc : idStrId (+0x0c)   parentMission, material
   │  Decl Reader 0x1281060: FindIndex(Language Table 0x2fe02b0, "#str_…")
   │     found → Field = ID (FNV-1a-Hash of the lowercased key)
   │     not found / plain text → Field = 0
   ▼
Display 0x121d720: ID → Value from CURRENTLY loaded table, unknown → ""
   ▲
Language Table: idLangDict::Load 0x121c260, from strings/<sys_lang>.lang (cfile)

Loader 0xa6de20 has exactly three call sites in the entire .text:

Caller When Evidence
idCommonLocal::Init 0xa66425 Once at startup, after "time 70". The start mount 0xabd110 → 0xabdaa0 (only gameresources + patch_N) has already run by then. Anchor, Caller list complete
idCommonLocal::Frame 0xa7663c Only if the MODIFIED bit (0x40000) is set in sys_lang shr eax, 0x12 / btr ecx, 0x12
reloadLanguage 0xa6e1d0 Console command, not exposed (not callable in Retail) cmdtable/commands.json

LoadNamedResourceBlock (0xac1730), the location that mounts the DLC chunk when loading a map, does not call the loader. The loader searches for strings/<sys_lang>.lang first as a loose file. For this to work, com_loadlooseLanguages must be set, and that is a CHEAT and not exposed, so it is false. Afterwards, it searches via idResourceList<cfile>::Load in everything that is currently mounted. If the file is missing, it sets sys_lang back to english. idLangDict::Load replaces the table instead of mixing it (pointers and count are zeroed). If two keys result in the same ID, it reports "Computed a duplicate ID" and aborts (int3 after the error call).

Regarding which container is decisive for text: the base archives at startup. The DLC chunk only plays a role if sys_lang is switched while it is mounted.

Retail confirms this from the data side (daten):

  • .lang files exist only in gameresources and patch_1..4, totaling exactly 10 languages. german and japanese are allowed in sys_lang but do not exist anywhere.
  • dlc_1..3 contain 0 .lang, 0 mission, 0 missionobj, and 0 cfile. DLC mission texts were loaded via base/. Therefore, Retail never loads a cfile from a DLC chunk.
  • Of the 338 Retail missionobj, 55 reference title keys missing from english.lang. According to the idStrId rule, their titles would be empty. They are listed in objective_strings.json under nicht_verwenden_leerer_titel (do not use empty titles).

2. What a DLC Chunk Can and Cannot Do

Idea Result Reason
Override strings/english.lang in the chunk no The table is already loaded when the map loads and is not re-read. Even after a sys_lang switch, it remains unclear whether the chunk or patch_4 wins.
New #str_ key (in any .lang) no The Decl parses against the table loaded at that time. The key is missing there, so ID 0, resulting in empty text.
Plain text as title = "Eliminiere die Offiziere" no idStrId, FindIndex fail, ID 0, empty (anchor 0x12810fd/0x1281124)
strings/german.lang in the chunk plus sys_lang german in console built, runtime open This is the only free slot, and no Retail container competes for it. Existing key IDs remain unchanged; the value is re-read on every display.
Reuse existing Retail missionobj yes, safe They are located in the start mount and exist for every map. The fact that a target appears under a foreign parentMission is proven by the screenshot (anatomy E13).

3. The Safe Way: 30 Retail Targets

Every line has been verified: The Decl exists in the start mount, its title key provides exactly this text in english.lang, and image material lies as a material Decl also in the start mount (two targets have none). Full data with description text, archive, and material is available in objective_strings.json.

# Purpose English Title missionobj-Decl Title Key
1 eliminate Kill all enemies cobalt/c1v1/mission_c1v1_main_obj_06_kill #str_mission_cobalt_c1v1_main_obj_06_kill_name
2 eliminate Kill them all c08p2/mission_c08p2_main_rendezvous_obj_05_lakeview #str_mission_c08p2_main_rendezvous_obj_05_lakeview_name
3 eliminate Kill Everyone cobalt/c1v3/mission_c1v3_main_obj_11_kill #str_mission_cobalt_c1v3_main_obj_11_kill_name
4 eliminate Kill the Witnesses cobalt/c1v3/mission_c1v3_main_obj_07_killwitnesses #str_mission_cobalt_c1v3_main_obj_07_killwitnesses_name
5 eliminate Fight c06p3/mission_c06p3_main_monster_obj_02_kill #str_mission_c06p3_main_obj_02_kill_name
6 eliminate Wreak Havoc cobalt/c1v1/mission_c1v1_main_obj_12_havoc #str_mission_cobalt_c1v1_main_obj_12_havoc_name
7 eliminate (c2v1) Kill Ubercommander Hans cobalt/c2v1/mission_c2v1_main_obj_03_kill_hans #str_mission_cobalt_c2v1_main_obj_03_kill_name
8 find Search for Intel cobalt/c3v1/mission_cobalt_c3v1_main_obj_05_command #str_mission_cobalt_c3v1_main_obj_05_command_name
9 find Search the office cobalt/c0v2/mission_c0v2_main_obj_03_search_office #str_mission_cobalt_c0v2_obj_03_folder_name
10 find (Documents) Find New Orleans Dossier c07p2/mission_c07p2_main_dossier_obj_04_dossier #str_mission_c07p2_main_dossier_obj_04_dossier_name
11 find Investigate what's going on c10p1/mission_c10p1_main_lostatsea_obj_01_investigate #str_mission_c10p1_main_01_obj_01_investigate_name
12 find (c2v1) Locate Hans' office cobalt/c2v1/mission_c2v1_main_obj_02_locate #str_mission_cobalt_c2v1_main_obj_02_locate_name
13 find Find the command center c07p2/mission_c07p2_main_dossier_obj_03_command #str_mission_c07p2_main_dossier_obj_03_command_name
14 hold/survive Stall Nazis c03p3/mission_c03p3_main_manhattan_obj_01_stalling #str_mission_c03p3_main_manhattan_obj_01_stalling_name
15 hold/survive Assist the resistance c08p1/mission_c08p1_main_horton_obj_06_siege #str_mission_c08p1_main_horton_obj_06_siege_name
16 hold/survive Distract the Nazis c08p2/mission_c08p2_main_rendezvous_obj_01_streets #str_mission_c08p2_main_rendezvous_obj_01_streets_name
17 Sabotage Destroy radio c02p2/mission_c02p2_main_obj_02_destroyradio #str_mission_c02p2_main_01_obj_01b_destroyradio_name
18 Sabotage Release the door locks cobalt/c1v1/mission_c1v1_main_obj_03_openhangardoors #str_mission_cobalt_c1v1_main_obj_03_openhangardoors_name
19 Sabotage Override the Blast Doors c07p2/mission_c07p2_main_dossier_obj_blastdoor_01 #str_mission_c07p2_main_dossier_obj_blastdoors_01_name
20 Sabotage Unlock Cell Block cobalt/c1v2/mission_c1v2_main_obj_12_unlockcells #str_mission_cobalt_c1v2_main_obj_12_unlock_name
21 enter (c2v1) Enter the offices cobalt/c2v1/mission_c2v1_main_obj_01_find #str_mission_cobalt_c2v1_main_obj_01_find_name
22 enter Break In cobalt/c1v3/mission_c1v3_main_obj_01_breakin #str_mission_cobalt_c1v3_main_obj_01_breakin_name
23 exit (c2v1) Escape cobalt/c2v1/mission_c2v1_main_obj_04_kill_all #str_mission_cobalt_c2v3_main_obj_07_name
24 exit Find exit c03p1/mission_c03p1_main_manhattan_obj_04_tunnels #str_mission_c03p1_main_manhattan_obj_04_tunnels_name
25 exit Find a way out cobalt/c1v1/mission_c1v1_main_obj_07_wayout #str_mission_cobalt_c1v1_main_obj_07_wayout_name
26 exit Get out of here cobalt/c1v1/mission_c1v1_main_obj_04_reachresistancehq #str_mission_cobalt_c1v1_main_obj_04_reachresistancehq_name
27 exit Escape outside c05p2/mission_c05p2_main_destroyoberkommando_obj_06_escape #str_mission_c05p2_main_destroyoberkommando_obj_06_escape_name
28 exit Head for the surface cobalt/c3v1/mission_cobalt_c3v1_main_obj_07_storage #str_mission_cobalt_c3v1_main_obj_07_storage_name
29 exit Find Another Way Through cobalt/c3v1/mission_cobalt_c3v1_main_obj_02_subpen #str_mission_cobalt_c3v1_main_obj_02_subpen_name
30 exit Reach the rooftop c08p2/mission_c08p2_main_rendezvous_obj_06_rooftop #str_mission_c08p2_main_rendezvous_obj_06_rooftop_name

Matching the four user wishes:

  • "Eliminate the officers": #1 Kill all enemies or #3 Kill Everyone.
  • "Find the documents": #10 Find New Orleans Dossier or #8 Search for Intel.
  • "Survive three waves": #14 Stall Nazis, #6 Wreak Havoc or #2 Kill them all.
  • "Reach the exit": #24 Find exit, #25 Find a way out or #23 Escape.

Negative finding: None of the 338 Retail target titles contain "survive", "wave", or "documents". Also, among all 23,224 strings, there is no short "Survive …". The only hit is a Doom rest in #str_menu_root_coop_help.

HUD frame texts are set by the game itself; they do not belong to the target. All following candidates exist. Which pair the HUD actually uses is unproven. #str_new_objective = "NEW OBJECTIVE", #str_objective_completed = "OBJECTIVE COMPLETED", #str_notification_newobj = "New objective:", #str_notification_completedobj = "Objective completed:", #str_notification_completedmission = "MISSION COMPLETED!".

4. The experiment for own German texts: strings/german.lang

Two files are built:

  • german.lang.cfile, 478,384 bytes, sha256 e4ecac70…c540e9. This is english.lang from patch_4 with four changed values. These are exactly the keys that c2v1s targets (and the wave-Kiscule of the Kiscule-track for obj_01) already use:
Key Retail neu
#str_mission_cobalt_c2v1_main_obj_01_find_name Enter the offices Eliminiere die Offiziere
#str_mission_cobalt_c2v1_main_obj_02_locate_name Locate Hans' office Finde die Dokumente
#str_mission_cobalt_c2v1_main_obj_03_kill_name Kill Ubercommander Hans Überlebe drei Wellen
#str_mission_cobalt_c2v3_main_obj_07_name Escape Erreiche den Ausgang (also applies in c2v2/c2v3)

The file has the same format as Retail: Kraken LEVEL_NORMAL, declen = plaintext length, AES-128-CBC and HMAC-SHA256 with the name strings/german.lang in the key. This path reproduces all 10 Retail .lang bytes identically, with their Salt and IV. The key set is identical (23,224). Thus no ID changes, and a duplicate-ID crash is excluded. Only the four lines differ; the rest is byte-identical. The Ü already appears in Retail texts ("Übercommander"), so the glyph is present.

  • chunk_22.resources, 7,129 entries, sha256 8ad65ab7…e5814c. This is the installed dlc_0-chunk (byte-identical with re_probes/dlc4_override/chunk_22.resources, sha256 cadfbfce…) with cfile:strings/german.lang as the last entry. All 7,128 old lines (without offset) and payloads are unchanged. probe_datahash and probe_idclhash are green. The entry for plaintext is decrypted from the container. To apply this to the Kiscule-chunk: python tools/probe_mission_text.py container re_probes/missions/kiscule/chunk_22.resources.

Own texts set: python tools/probe_mission_text.py deutsch --set "#str_…=Text" …. Only existing keys are allowed, and no ", \, tab or line break. A new key is rejected (Section 2).

What remains open at runtime (the user tests, the master installs):

  • whether idResourceList<cfile>::Load finds a cfile from a chunk mounted during map loading. Retail never does this (0 cfiles in dlc_1..3). It is plausible because the extkisclule of the chunks load via the same resource manager.
  • whether the language switch mid-map has side effects. Language output: according to vo_localizeLangs_ROW ("other language will use english") falls back to English VO for German. Whether a runtime switch reloads VO is unknown.
  • which HUD texts are redrawn: The first objective message appears ~4.6 s after start and thus before every console command. Objectives that start or are completed after the switch should show the new text.

sys_lang is exposed (settable in Retail), but not ARCHIVE. After every restart, the language reverts to english. If started without a mounted chunk, german cannot be found and resets to english. Reverting means: sys_lang english or a restart.

Runbook (only if the Master installed the Chunk)

  1. Start the game with +com_enableDeveloperMode 1 and begin the Gestapo Office mission normally.
  2. After loading, open the console and enter sys_lang german.
  3. Expect in qconsole.log: Reading strings/german.lang as UTF-8, followed by … strings read with the same count as when starting with english.lang. Then, in the pause menu or at the next objective message: "Eliminate the officers" instead of "Enter the offices".
  4. Control test: If it says Reading strings/english.lang, then german.lang was not found in the mounted chunk. This is a precise negative finding for "cfile from DLC Chunk", no reason to fiddle with the container.
  5. Revert: sys_lang english or restart.

5. Tests

run.log: 122/122, container.log: 8/8.

Group Count What
Code Anchors 51 + Control Test Instructions at fixed RVAs with resolved RIP targets: Loader, Init/Frame, Table Replacement, Duplicate-ID fatal, idStrId Reader (0 on failure), Display ("" for unknown ID). Same anchors shifted by +1 hit 0/51.
Completeness 8 Caller of the Loader and Parser throughout .text, function pointers, absolute pointers. Control Test: 0xa6de21 has no callers.
Snapshot Data 11 cfile list, resource manager vtable, four cvars (EXPOSE/CHEAT/ARCHIVE), sys_lang value list, missionobj → idDeclObjectiveMerc, title/desc = idStrId
Data 3 Location of .lang, retail DLC without text and without cfile, 10 languages
List 36 30 objectives individually, material, HUD candidates, negative finding word choice, 55 empty titles. Control Test: false text or missing Decl are rejected.
german 12 10/10 Retail-.lang Round-Trip, Diff, key set, line comparison, glyphs, pack format, decryption. Control Tests: false name, flipped byte, " in value, new key.
container 8 +1 entry, comp 0, payload, plaintext from container, 7128 old lines and payloads, 1 appended hash, probe_datahash, probe_idclhash

6. Open

  • Runtime of the german.lang path (Section 4). Everything else is statically proven.
  • Whether a new missionobj Decl in the Chunk (Title = any existing key from all 23,224, e.g., #str_useable_exitlevel = "Exit level") is loaded. Retail DLC chunks contain none. This would be the next stage beyond the 338 Retail objectives, but was not built.
  • g_debugMissions (proposed by anatomy as runtime aid) is CHEAT and not exposed, so according to cvartable it is not settable in Retail.