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
- 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 ifsys_langchanges. It never happens when loading a map. - A goal with plain text or a new
#str_key will display as empty.missionobj.titleis not text, but anidStrId. 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"". - The smallest safe path is to use existing Retail
missionobjDecls 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 includestrings/german.langin the chunk and typesys_lang germaninto 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):
.langfiles exist only ingameresourcesandpatch_1..4, totaling exactly 10 languages.germanandjapaneseare allowed insys_langbut do not exist anywhere.dlc_1..3contain 0.lang, 0mission, 0missionobj, and 0cfile. DLC mission texts were loaded viabase/. Therefore, Retail never loads a cfile from a DLC chunk.- Of the 338 Retail
missionobj, 55 reference title keys missing fromenglish.lang. According to the idStrId rule, their titles would be empty. They are listed inobjective_strings.jsonundernicht_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, sha256e4ecac70…c540e9. This isenglish.langfrompatch_4with 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, sha2568ad65ab7…e5814c. This is the installed dlc_0-chunk (byte-identical withre_probes/dlc4_override/chunk_22.resources, sha256cadfbfce…) withcfile:strings/german.langas the last entry. All 7,128 old lines (without offset) and payloads are unchanged.probe_datahashandprobe_idclhashare 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>::Loadfinds a cfile from a chunk mounted during map loading. Retail never does this (0 cfiles in dlc_1..3). It is plausible because theextkiscluleof 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)
- Start the game with
+com_enableDeveloperMode 1and begin the Gestapo Office mission normally. - After loading, open the console and enter
sys_lang german. - Expect in
qconsole.log:Reading strings/german.lang as UTF-8, followed by… strings readwith the same count as when starting withenglish.lang. Then, in the pause menu or at the next objective message: "Eliminate the officers" instead of "Enter the offices". - Control test: If it says
Reading strings/english.lang, thengerman.langwas not found in the mounted chunk. This is a precise negative finding for "cfile from DLC Chunk", no reason to fiddle with the container. - Revert:
sys_lang englishor 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
missionobjDecl 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 tocvartableit is not settable in Retail.