Wolfenstein Modloader Buy me a coffeeKaffee spendieren

Wiki / The New OrderThe New Order

User Interface — What Can and Cannot Be Changed

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

In plain wordsIn einfachen Worten

This page checks whether a mod could add its own in-game menu to Wolfenstein: The New Order. The menus are stored in idSWF, an undocumented binary format by id Software that no existing tool can write, so a real new menu is not feasible. What is possible is renaming existing menu entries, changing menu sounds and music, and editing loading-screen tips and tutorial texts. A test that replaced three menu strings was applied, but the change did not show up in the game.

  • The 9 menu and HUD screens use idSWF version 20, not Adobe Flash or Scaleform.
  • 7,154 text strings exist, 367 of them for the menu; replacements must keep exactly the same byte length.
  • New menu lines cannot be added; the game's code decides which lines exist.
  • The hidden developer menu (DevGUI) showed nothing in a main-menu test; it only stays on inside a level and the menu file has no known reader (code only).
  • A three-line menu text mod was applied, but it did not appear in the game.

Diese Seite prüft, ob ein Mod ein eigenes Menü in Wolfenstein: The New Order einbauen könnte. Die Menüs liegen im idSWF-Format, einem undokumentierten Binärformat von id Software, das kein vorhandenes Tool schreiben kann; ein echtes neues Menü ist daher nicht machbar. Möglich ist, vorhandene Menüeinträge umzubenennen, Menüsounds und Musik zu ändern sowie Ladebildschirm-Tipps und Tutorialtexte zu bearbeiten. Ein Test, der drei Menütexte ersetzte, wurde eingespielt, aber die Änderung war im Spiel nicht zu sehen.

  • Die 9 Menü- und HUD-Oberflächen nutzen idSWF Version 20, nicht Adobe Flash oder Scaleform.
  • Es gibt 7.154 Texte, 367 davon fürs Menü; Ersatztexte müssen exakt gleich viele Bytes lang sein.
  • Neue Menüzeilen lassen sich nicht hinzufügen; welche Zeilen es gibt, entscheidet der Programmcode.
  • Das versteckte Entwicklermenü (DevGUI) zeigte im Test am Hauptmenü nichts; es bleibt nur in einem Level an und die Menüdatei hat keinen bekannten Leser (nur Code).
  • Ein Mod mit drei geänderten Menütexten wurde eingespielt, erschien im Spiel aber nicht.

Technical detailsTechnische Details

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

Measured against the original archives in the backup using tools/probe_ui_assets.py. All numbers are derived from 14 archives containing 203,843 assets.


Short Answer

A genuine in-game menu styled like the game's main menu is not feasible. Not because the files are locked — they reside normally in the archive and can be overwritten. But because what one would need to write inside is an undocumented binary format from id Software, which appears nowhere else: idSWF, Version 20. No tool on Earth writes it, and generating it from nothing would be a reverse-engineering project of its own, whose only test would consist of starting the game and seeing if it crashes.

What is possible: rewriting every text string in the menu. Menu titles, option names, help lines, difficulty levels, journal entries — 7,154 strings, of which 367 are in the menu. This isn't a menu, but it's more than nothing: you can rename the existing menu.


1. What Actually Exists

There is no asset type swf, gui, menu, hud, or shell. The interface is distributed across three locations:

Location Content Count
Type file, generated/swf/*.bswf The surfaces themselves (Logic + Layout) 9 variants
Type file, generated/swf/*.bimage The image atlas for each surface 9 variants
Type file, strings/*.lang String tables, 6 languages 6
Type file, textures/guis/**/*.dct Collection object renderings 144
Type swfresource Decls: Sounds, Fonts, Category Titles 11
Type font Distance-Field Fonts 6
Type image / material under gui/, hud/ HUD and Menu Graphics 208 / 804

The nine surfaces:

tungshell_new      371.411 B   Main Menu (Start, Chapter, Enigma, Options)
tungpause_new      139.752 B   Pause Menu
tunginventory_new  122.155 B   Journal / Collectibles
tunghud            119.204 B   HUD
tungtutorial_new    27.958 B   Tutorial Overlays
tungload_new        14.360 B   Loading Screen
tungmodelbrowser    13.661 B   Model Browser (Extras)
tungdialog           9.241 B   Dialog Boxes
vo                   2.423 B   Subtitles

tunghud, tunginventory_new, tungmodelbrowser, tungtutorial_new, and vo appear in 13 of the 14 archives — any change must affect all copies.


2. The Format: Not Scaleform, Not SWF

The initial assumption was Scaleform. It is incorrect. The file header states:

42 53 57 14 ...    ->  "BSW" + 0x14

No FWS, no CWS, no ZWS (Adobe SWF), no GFX/CFX (Scaleform). It is idSWF, Version 20 — id Software's own Flash clone, present since Rage/Doom 3 BFG in id Tech. Confirmed by symbol names in the EXE:

idSWF, idSWFSprite, idSWFSpriteInstance, idSWFScriptFunctionAS2,
idSWFConstantPoolAS2, SWF_DICT_SHAPE/SPRITE/FONT/EDITTEXT, swfTag_t,
w:\tech5\engine\guis\swf\SWF_Load.cpp

A .bswf contains: String table (ActionScript-2 identifiers like inhibitControl, _absLeft_settings, easeInOutSine), compiled AS2 bytecode, Sprite/Shape/EditText dictionaries, and timelines. The image atlas is stored separately in .bimage — a BIM (\x09MIB, the same format as image assets; wolfsdk/bim.py already reads the header).

Consequence: No existing tool opens it. JPEXS reads SWF, the Scaleform exporter reads GFX; neither applies here. Generating a .bswf would require writing serializers for the Dictionary, timeline, and AS2 bytecode — plus a matching BIM atlas. A BIM encoder now exists in this SDK (wolfsdk/image.py, to_bim()), but only for RGBA8 and only with the replacement image as the header template; see docs/limits.md, Section 5. This does not change the effort required to create an own .bswf.


3. Do They Fall Into The Particle Trap? No — But They Have Their Own

The particle trap does not apply here. Evidence:

  • .bswf, .bimage, and .lang are ordinary archive entries of type file with their own offset, usize, and csize — exactly like any Decl.
  • There is no associated .bin under base/generated/. There are 76 files there that the game itself writes — 73 of them are particle caches, the rest are model and world caches. Nothing from generated/swf/.
  • The log (qconsole.log, 4,858 lines) contains zero lines of idFile_RingBuffer.

But there is a second storage class, and the UI sits right in the middle. Within the file type, some endings are stored as 100% uncompressed while others are stored as 100% deflate-compressed — and this is not a compression heuristic:

Ending Entries raw deflate would compress to actually
.bswf 69 69 0 23 % raw
.bimage 69 69 0 21 % raw
.lang 6 6 0 28 % raw
.dct 144 144 0 100 % raw
.png 14 14 0 100 % raw
.script 97 0 97 20 % deflate
.entities 25 0 25 7 % deflate
.txt 2 0 2 6 % deflate

.dct and .png are already compressed internally — applying Deflate to them yields no further benefit, so "raw" is the correct choice. .bswf, .bimage, and .lang would instead quarter in size. 5.6 MB of string tables lie uncompressed on disk, while 700 kB of scripts sit beside them compressed. This is not a coincidence; this is a decision: these files are apparently read via a path that requires the bytes verbatim — memory mapping or streaming rather than Inflate.

The Patcher can now do this. resources.plan_in_place() has a raw write path:

if entry.stored_raw:                       # csize == usize
    return (data, len(data)) if len(data) == entry.csize else None

A raw-stored entry thus accepts a replacement of exactly the same length, and the bytes go unchanged to their offset — no compression, no change of storage class. Archive.rebuild() receives the storage class as well (packed = data if entry.stored_raw else _deflate(data)).

The question remains: whether the Engine tolerates a raw-stored entry being switched to Deflate is not measured — but based on this table, one should not bet on it. The raw write path does not even ask that question.

This adds length fidelity: the margin is 0 Bytes, not 3 to 15. Between the payloads lies alignment padding, but the Patcher does not touch it — a shrinking csize shifts the addressing of all subsequent entries (No address, error: 487, Modding-Limits.md §4). Going longer or shorter only works via rebuild(), which then writes a 151-MB archive (chunk0) or up to 2.8 GB anew if one touches tunghud in all 13 copies.

What follows from this

If you want to change strings/english.lang, you have exactly one clean way: replace byte-length-faithfully and write raw back. That is the same trick as _same_width() in tools/build_gore_deep.py, just at the text level: "CONTINUE" can become "WEITER ", not "WEITERSPIELEN".

The write path for this exists: plan_in_place() places the bytes directly at the offset when csize == usize, instead of sending them through deflate_exact(), and marks a replacement of another length. Neither offset nor csize moves. This has already been built as mods/menue_marke — three lines of the English string table, padded on the right with spaces to the old width, 868.273 bytes like the original. It was applied and viewed in-game but did not appear.

Side note, historical: deflate_exact() on an 868-kB file would hang with a guaranteed empty result. Measured: a Deflate pass takes 0.10 s and shrinks the file from 868.273 to 246.796 Bytes. The function then attempts up to 4,608 fill variants — roughly 8 minutes — to reach exactly 868.273 Bytes. It cannot: between these two sizes there are 621 kB, and the filler material reaches at most 4.6 kB in length. The raw write path also bypasses this — it does not call deflate_exact() for such entries at all.


4. What can be changed data-drivenly

4.1 The String Table — the real gain

strings/english.lang is a plain text file (UTF-8 with BOM, CRLF) in the known Decl-near format:

// string table
//

{
    "#str_menu_continue"    "CONTINUE"
    "#str_menu_game_difficulty" "DIFFICULTY"
    "#str_menu_difficulty_veryhard" "ÜBER"
    "#str_menu_difficulty_hard_help"    "Hard difficulty setting for the experienced gamer."
    ...
}

7,154 keys, of which 367 have the prefix #str_menu_. They contain practically the entire menu: title, tabs, option lines, help text, keybinding labels, difficulty names, chapter names, dialog boxes. Six languages: English, French, Italian, Polish, Russian, Spanish. German is not present in this installation.

This allows renaming existing menu entries — for example, an option line that you don't need anyway can be renamed to "GORE-MOD ACTIVE". What you cannot do: add a line. Which lines exist is decided by the code, not the table.

4.2 The swfresource Decls

Eleven text decls, normally patchable, small (26–2,340 B). They control:

  • Menu length — each tone after every click (soundEffects)
  • Menu music — tungshell: music/menu/main_menu
  • Text effects — which outline to which font (fontEffects)
  • Representation — snapToPixel, useBlackbars
  • Category titles in the Journal — tunginventory_new with nine categoryDecls, each Material + #str_ key

This is the one place where a decl directly impacts the menu. It is more than just cosmetics.

4.3 Other on-screen text types

Type Count What
job 169 Jobs/Goals in the Journal
tutorialEvent 140 Tutorial overlays (screen, bindset, activateSound)
secrets 78 Collectibles
achievement 50 Achievements
loadingscreen 33 Loading screen tips (tips)
secrettype 11 Categories of collectibles

All text, all patchable. loadingscreen is the most convenient channel for a mod note: load_general has tips = { num = 0; } — there one could enter custom tips that appear on loading. (Untested, but it is an ordinary Decl.)

4.4 What does not work: adding lines

Verified: tungshell_new.bswf contains zero Cvar names. No trace of g_, pm_, com_, r_. Option lines are therefore not built from a table in the SWF, but generated by C++ code and passed into the SWF. The hope to bypass an unused option line by renaming a Cvar string to a custom Cvar is thus moot — the string does not exist.

In the SWF there are only 14 #str_ keys (Enigma tab, keybinding, loading indicator). Everything else comes at runtime.


5. The existing developer interface

debugmenu.txt (22,419 B) and debugmenucheck.txt (159,248 B) lie as text in the archive and are patchable.

wolfsdk/devmenu.py uses this: it attaches its own page "WOLFSDK" to debugmenu.txt — with Cheat toggles, movement speed, FOV, difficulty. The approach is flawless on the data side: tools/verify_devmenu.py checks four invariants (valid JSON, full file parses further, injection idempotent, removal restores original byte-exactly) and all hold. It is cleanly built work.

It still doesn't work. The DevGUI is not connected in the Retail build. According to docs/limits.md (Section 2): devgui can be set to 1 via +toggle devgui without a ban warning and without being deleted by the Cheat-Reset — yet F8 and F10 still did nothing (measured, at the main menu). The page exists in the file, but no consumer for debugmenu.txt was found in the executable (code only, from strings), and the game resets devgui to 0 outside a level, so the earlier reading "never rendered because the Renderer is not connected" is not established. See Modding-Limits.md §2.

The approach was therefore correctly conceived but applied at the wrong lever. It would be immediately usable if the Renderer were accessible — which fails in the Steam version due to the SteamStub encryption of the .text section.


6. Assessment: Is an In-Game Menu realistic?

Stage What Effort Verdict
1 Rewrite menu text (.lang, length-preserving) none — the raw write path is in the Patcher doable, untested in-game
2 Menu sounds, music, font effects (swfresource) trivial, existing Decl-Pipeline doable
3 Loading screen tips, tutorial text, journal entries trivial doable
4 Replace HUD/menu graphics (image, .bimage) Encoder present (only RGBA8), size forces Rebuild doable, appearance in-game unverified
5 Change menu structure (rows, pages, logic) idSWF-Serializer + AS2-Assembler no
6 Custom Menu via DevGUI Renderer not connected in Retail no

The honest answer: no. A menu that looks and behaves like the game's menu, with its own pages and its own toggles, is not accessible via data files. Both paths to it — writing a custom .bswf or using the DevGUI — are blocked, for different reasons: one by an undocumented binary format, the other by the missing Renderer.

What remains is renaming the existing menu. This is significantly less than hoped for, and we should not speak of it here as if it were more than it is. It suffices for "This installation runs with Mod X" in the main menu. It does not suffice for "Toggle mod on/off via button press".


7. Unverified Traces

Three threads have been noticed and were not pursued because the game would need to be launched for that. They are listed here so they do not get lost — not as a promise.

7.1 swf_loadBinary

The EXE knows the Cvar swf_loadBinary, description: "used to set whether to load binary swf from generated". If it is set to 0, the engine presumably loads a real Adobe .swf instead of the .bswf. Raw .swf files are not included in the archives — you would have to provide one yourself, and the SWF format is documented unlike idSWF.

Open questions, all unresolved: Is the Cvar affected by the Retail ban? Does the engine search for files on disk at all in production mode? Under what path? And even if everything aligns: you would still have to rebuild the entire main menu in AS2, otherwise it is gone.

7.2 fs_overlaypath

Description in the EXE: "(Read Only) Extra location for game files (with precedence over fs_basepath)". An overlay path with precedence would be the most convenient mod channel of all — no archives to touch, just place files alongside. Whether it still does something in the Retail build and whether it can be set via the command line is unverified.

The fact that the engine reads from disk in production mode at all is shown by the log: WARNING: Resource loaded in production mode: font : futura com book while loading font futura com book from swf tunginventory_new — seven such lines per session, for font, cm, aas, and foliageModel.

7.3 The Script Language — the most interesting thread

script/*.script are text files in the archive — 37 different ones, of which 5 are shared (in 13 archives each) and 32 card-specific, totaling 97 entries. Deflate-compressed and thus patchable via the existing pipeline. script/script_main.script says about itself: "This is the main script that is loaded before any level scripts load." It exists in all 13 card archives.

The script interface (script/script_events.script, 247 kB autogenerated typeinfo) includes, among others:

scriptEvent void   setcvar( string name, string value );
scriptEvent string getcvar( string name );
scriptEvent void   executeCommandText( string command );
scriptEvent void   drawText( string text, vector origin, float scale,
                             vector color, int align, float lifetime );
scriptEvent float  getUserButtons();          // on $player1
scriptEvent void   wait( float time );

Plus 23 BUTTON_* constants. In principle, this allows building a loop that reads key presses, draws text, and sets Cvars — a crude menu without any SWF involvement.

However, the limitations are significant and should not be downplayed:

  • drawText draws at a world coordinate, not on the screen. You would have to calculate the text position every frame in front of the player's eyes. A screen-text event did not appear in the typeinfo.
  • Whether setcvar bypasses the Retail lock is unknown. It is a direct script event, not a console path — but this has not been measured. executeCommandText, on the other hand, will likely fail against the lock just like +exec.
  • A script error is fatal: the engine compiles when loading cards and aborts on syntax errors.
  • And there remains an in-game text overlay, not a menu.

Of the three traces, this is the only one that works without a new binary format. If this project intends to pursue further, this is the thread to pull — with the expectation of an overlay, not a menu.