NFMEditor.exe — .NFM Multiplayer/Server Config Editor (deep-dive)¶
Reference record; not an oracle target. See the editor-tools overview.
1. Binary facts¶
| File | reference\NFMEditor.exe |
| Size | 471,040 B (0x73000) |
| SHA-256 | 173db0b5f7c7000febf86e7c21519c24f8e11cdab41730d049a1fd22aaf3aa21 |
| Toolchain | MSVC6, Linker 6.0, TimeDateStamp 2003-02-15 19:57:40Z |
| Machine / Subsystem | x86 (0x014C) / GUI (2) |
| ImageBase | 0x00400000 |
| Sections | .text RVA 0x1000 VSize 0x5CFDE · .rdata RVA 0x5E000 · .data RVA 0x6E000 VSize 0x775B0 · .rsrc RVA 0xE6000 |
| Entry | 0x0045BE5F |
| Imports | MFC42 (138), MSVCRT (66), KERNEL32 (84), USER32 (63), GDI32 (12), ADVAPI32 (12), ole32 (3), MSACM32 (7), VERSION (3), WINMM (5) |
| Rich header | XOR key 0x72BAA77B, DanS @ 0x80 (Linker 6.00 SP6 + C7 toolset) |
| Functions | 1,129 (Ghidra) |
MFC42 is imported dynamically (no static MFC in .text), unlike the game EXE.
The tool must run from the game install root — it loads GameOS resources and
Language.dll from assets\binaries\, and reads/writes
resource\Missions\*.nfm / resource\UserMissions\*.nfm.
2. Purpose¶
NFMEditor edits the .nfm "network/multiplayer mission" files that define a
map's multiplayer rulesets and scenarios (see docs/editor_tools/README.md for
the file's place in the game data set). Each map in resource\Missions\ /
resource\UserMissions\ has one .nfm describing:
[info]— author / blurb / link / long display name,[ruleset]— the list of game-type rules that may be launched on the map, each rule = atype=$(<GameType>)macro entry + optionalteam/alignment,[scenarios]— named scenario blocks (Name={ ... }), each with a[server]sub-section of per-scenario settings (game type, teams, kill/respawn limits, night/weather/radar, tonnage/C-Bill caps, game length, script name) and optionalteam_N/bot_Nsub-blocks for per-team and per-bot constraints.
It also edits the Server Cycle List (servercycle.txt, via -cycle <file>):
a rotation of map=Name.nfm::ScenarioName entries the game server walks to cycle
maps.
The .nfm/servercycle.txt text format is GameOS's generic "info file"
parser (a C-preprocessor-like key/value grammar) — see §4.
3. Layout — .text regions¶
| Range | Size | Content |
|---|---|---|
| 0x401000–0x4072xx | ~25 KB | Editor app code (dialog, .nfm load/save, cycle list) |
| 0x408xxx–0x44Exxx | ~290 KB | Bundled GameOS framework (same engine code as MW4Mercs.exe) |
| 0x44F000–0x458xxx | ~40 KB | The "info file" parser/serializer library (pure, reusable) |
| 0x459000–0x45BE00 | ~12 KB | MFC glue + GameOS helpers + Ordinal thunks (0x45BBxx–0x45BExx) |
| 0x45EA00+ | — | Class vtables in .rdata (PTR_LAB_0045ea00 = app vtable) |
The Ghidra inventory names most MFC imports by Ordinal_XXXX (jmp-thunk table at
0x45BB80+); e.g. Ordinal_2864 = the frame/app singleton accessor, Ordinal_6907
= list-control "set item text", Ordinal_3996 = list-control "insert column",
Ordinal_3302 = list-control "get item text", Ordinal_2642 = MFC "invalidate"
family, Ordinal_6199 = string lowercase, Ordinal_3998 = list "insert item".
4. The "info file" format (GameOS generic key/value text)¶
.nfm, servercycle.txt, Content\GameTypes.h (the include), and the
resource\missions\*.txt scenario-text files are all read by one parser family.
Ground truth: the shipped files (see §5/§6) plus the parser below.
4.1 Grammar¶
Read line by line (\r\n or \n), 512-byte line buffer, 0x200 max per line:
- Comments:
//to end of line;/* ... */block (multi-line; error"There is a missing */ in %s"on EOF)./* */is stripped inline. - Line-continuation/escape: a line beginning with
@has the@removed (used to force literal interpretation / preserve case on an otherwise lowercased line). Lines that do not start with@are lowercased (_strlwr) unless!preservecase=trueis in effect. - Whitespace: leading/trailing spaces and tabs on a line are trimmed; keys are right-trimmed.
[SectionName]— section header (name trimmed). A]-less header is rejected (incomplete).key=value— key/value pair in the current section. Keys may repeat (all occurrences are kept; lookups return the first, case-insensitive).Name={— a block:Namebecomes a key whose value is a nested section list. Contents are parsed recursively until a lone}line closes the block. Serializer re-emitsName={+ nested sections +}. A missing}at EOF is an error ("There is a missing } in %s").!directive— control directives (see 4.2).#define name value— C-preprocessor-style macro definition (this is howContent\GameTypes.hworks).$(name)— macro reference, expanded recursively at parse time (the$(StockBattle)in the shipped files becomes0). Anykey=valueline whose value contains$(is macro-expanded. Recursive expansion is cycle-guarded.- Bare lines — a line with no
=and no[creates a key with an empty value (harmless).
Macros are resolved from the macro table (name → value). #define lines add
to it; unknown $(...) references resolve to the empty string.
4.2 Directives (!...)¶
| Directive | Effect |
|---|---|
!include=<path> |
Parse <path> as another info file into the same tree; relative paths resolve against the including file's directory. Quoted paths allowed. Include files are deduped via an include cache (a file is parsed once). |
!concatenate=true\|yes\|1 |
Toggle concatenation mode (parser flag). |
!preservecase=true\|yes\|1 |
Toggle case preservation (default: keys/values lowercased). The shipped .nfm files set !preservecase=true. |
!<name>=<value> |
Define a custom macro/directive name → value. |
4.3 In-memory tree¶
- Root/tree object:
+0x00first-section,+0x05concatenate flag,+0x06preservecase flag,+0x0cinclude list,+0x44sorted section list (doubly linked, head at+0x44),+0x48count,+0x4cparser context. Created byFUN_00451390, freed byFUN_00451510. - Section node (0x20 B,
FUN_004524b0):+0x00vtable,+0x04parent,+0x14name,+0x18key list. Sections are kept sorted by name. - Key node (0x1C B,
FUN_00453c40):+0x00vtable,+0x04owning section,+0x14key name,+0x18value string (or block). Every parse creates a new node (duplicate keys retained);FUN_00453b50returns the first case-insensitive match. - Block contents are a nested section list hung off the block key (assembled by
FUN_00454f20, serialized byFUN_00451b40).
4.4 Parser/serializer function map (the reusable library)¶
| VA | Size | Role |
|---|---|---|
| 0x00451390 | 171 | Parse-file entry: open + parse + missing/empty check ("%s is missing or empty!") |
| 0x00450960 | 295 | File open/read wrapper (mode 0 read / 1 write) |
| 0x00450900 / 0x00450940 | 60/23 | File wrapper ctor / close |
| 0x004515c0 | 107 | Parse setup → calls parse loop |
| 0x00451630 | 1293 | Parse loop (line reader, comment/@/trim handling) |
| 0x00451c40 | 653 | Line handler: [section], key=value, key={ block recursion |
| 0x00451ed0 | 1098 | ! directive handler (include/concatenate/preservecase/custom) |
| 0x00452320 | 139 | #define handler |
| 0x00451270 | 285 | $(macro) resolver (recursive, cycle-guarded) |
| 0x004510e0 | 397 | Macro-table define |
| 0x004523b0 / 0x00452490 | 147/31 | Section lookup / find-or-create (block mode) |
| 0x004524b0 | 292 | Create/insert section node |
| 0x00453c40 | 116 | Append key node (duplicates allowed) |
| 0x00453b50 | 62 | Key lookup (first match, case-insensitive) |
| 0x00453d30 / 0x00453df0 | 80/83 | Read string / int value (required flag → "%s: [%s]%s is a required entry!") |
| 0x00453dc0 / 0x00453e80 | 40/25 | Set string / int value |
| 0x004544d0 / 0x00454d80 | 83/416 | Read sub-tree (block contents) |
| 0x00454f20 | 277 | Block attach/serialize (Name={ … }) |
| 0x00451b90 | 84 | Serialize entry: write tree to its file |
| 0x00451b40 | 67 | Recursive section serializer |
| 0x00453a80 | 205 | Section serializer ([name]\r\n, blank line after) |
| 0x00454610 | 161 | Key serializer (key / key=value\r\n) |
| 0x004578a0 | 174 | Include-file registration |
| 0x00450310 / 0x004505b0 | 558/54 | FindFirstFile/FindNextFile-style directory scan wrapper |
Output is written with \r\n line endings; a blank line separates sections.
5. The .nfm format spec¶
Ground truth = the 21 shipped files (resource\MISSIONS\*.nfm) + the editor's
reader (FUN_00404f00) + writer (FUN_004060b0).
5.1 Top-level layout¶
!include=Content\GameTypes.h
!preservecase=true
[info]
author=...
info=... (e.g. "Standard Mission")
link=... (e.g. www.microsoft.com/games/mechwarrior4)
longname=... (display name, e.g. "Alpine")
[ruleset]
type=$(StockBattle) <- one line per allowed rule
type=$(StockTeamBattle)
team=8 <- optional per-rule team count
alignment=6 <- optional per-rule alignment
[scenarios]
ScenarioName={
[server]
ruleset=$(StockDestruction)
name=... (optional)
teamAllowed=0|1
teamcount=<0..8>
scenariotext=resource\missions\<file>.txt (optional)
... more optional keys (5.3) ...
team_0={ teamdata{ ... } } (optional, see 5.4)
bot_0={ botdata{ ... } } (optional, see 5.5)
}
The shipped files use a tab to indent the [server] block's keys (the
parser trims whitespace, so indentation is cosmetic).
5.2 Game-type macros (Content\GameTypes.h)¶
!include=Content\GameTypes.h is a preprocessor file, not a tree section:
#define lines feed the macro table. Values:
| Macro | Value | Macro | Value | |
|---|---|---|---|---|
$(StockBattle) |
0 | $(CustomBattle) |
6 | |
$(StockTeamBattle) |
1 | $(CustomTeamBattle) |
7 | |
$(StockDestruction) |
2 | $(CustomDestruction) |
8 | |
$(StockTeamDestruction) |
3 | $(CustomTeamDestruction) |
9 | |
$(StockMissionPlay) |
4 | $(CustomMissionPlay) |
10 | |
$(StockCampaign) |
5 | $(CustomCampaign) |
11 | |
$(CustomUndefined) |
12 | |||
$(SinglePlayer) |
13 |
GameTypeCount = CustomUndefined + 1 = 13 (the editor's per-type arrays are
sized 13; index 13/SinglePlayer is a separate flag). Note the editor's
display strings are the $(...) forms — the shipped defianceholds.nfm uses
ruleset=$(StockMissionPlay) inside [server], which resolves to 4.
5.3 [server] keys, types and validation ranges (from the editor)¶
| Key | Type | Range / values | Written if |
|---|---|---|---|
ruleset |
int | 0..12 (game-type index) | always |
name |
string | — | always (map name) |
teamAllowed |
int | 0 or 1 | always |
teamcount |
int | 1..8 (if teamAllowed); else forced 0/0 | always |
scenariotext |
string | path (e.g. resource\missions\des.txt) |
non-empty |
killLimit |
int | 0 or 1 | non-empty |
killLimitNumber |
int | 1..99 (default 10) |
killLimit==1 |
respawnLimit |
int | 0 or 1 | non-empty |
respawnWaveNumber |
int | 1..9 (default 1) |
respawnLimit==1 |
isNight |
int | 0 or 1 | non-empty |
weather |
int | 0 or 1 | non-empty |
radarmode |
int | 0..2 | always (0 default) |
onlyStockMech |
int | 0 or 1 | non-empty |
maximumtonnage |
int | 25..100 | non-empty |
maximumCBills |
int | 2,000,000..34,999,999 | non-empty |
gameLength |
int | 1..120 (minutes) | non-empty |
scriptName |
string | custom script name | non-empty |
(The [server] keys are case-insensitive — the shipped files mix teamAllowed
and teamcount casing; the writer emits teamcount lower-case.)
5.4 team_N block (teamdata sub-key)¶
Written per team index N while teamAllowed==1:
team_0={
teamdata{
maximumTonnage=25..100
minimumTonnage=25..100
maximumTotalTonnage=100..1600
maximumCBills=2000000..35000000
maximumTotalCBills=2000000..500000000
maxPlayersOnTeam=1..16
skin=<single char '0'-'9' or 'a'-'z'>
alignment=0..7
}
}
Each field is emitted only when set (a non-default value). Constraint checked: max ≥ min (else both invalidated).
5.5 bot_N block (botdata sub-key)¶
bot_0={
botdata{
botCount=1..14
team=<1..teamcount> (only meaningful when teamAllowed==1)
maximumTonnage=25..100
minimumTonnage=25..100
maximumLevel=0..9
minimumLevel=0..9
}
}
5.6 [ruleset] details¶
Repeated type=$(...) entries are the mechanism for a multi-rule map: each
type= line is one allowed game type. Per-rule optional keys follow their
type line: team=<1..8> (team count) and alignment=<0..6>. The stock
alpine.nfm declares two rule groups (destruction + team-destruction, and battle
+ team-battle). The editor's per-type arrays (+0x4e8 flags, +0x554 team
count, +0x588 alignment) mirror these. If the single-player flag is set, the
writer emits a type=$(SinglePlayer) line in [ruleset].
5.7 Round-trip behavior¶
- Load (
FUN_00404f00): parse<dir>\<map>.nfm, populate ruleset flags + scenario list + per-scenario server/team/bot rows; out-of-range values are clamped or dropped (defaults shown above). - Save (
FUN_004060b0): rebuild the tree from the UI and serialize. Only non-default fields are written (except the always-written keys). The output is normalized:!include/!preservecasefirst, then[info],[ruleset],[scenarios]. The original file's own formatting/comment/order is not preserved (parse→modify→re-serialize).
6. The Server Cycle List (servercycle.txt)¶
- CLI:
NFMEditor.exe -cycle <file>(also-cycle <file>with a trailing space).FUN_00401210validates the argument ("INVALID PARAMETER - No parameter","INVALID PARAMETER - No space after"); default file isservercycle.txt(FUN_004012c0, InitInstance). - Format (ground truth: the shipped
ServerCycle.txt):
[cycle]
map=Shoreline.nfm::Shoreline3
map=Alpine.nfm::Alpine4
...
Each entry: map=<mapfile>::<ScenarioName> (scenario = the [scenarios]
block name in the map's .nfm). The editor list shows two columns (map name,
scenario/game-type) joined with ::.
- Save: FUN_00402720 (invoked from the OnClose handler 0x00402E80, which
prompts "Do you want to save the Server Cycle List?"). On exit with an
empty list: "The current Server Cycle List is empty. ... will stay at its
last saved state.". Success message: "Server Cycle List Saved.".
- Row population from an .nfm is FUN_004023a0 (reads a scenario's
[server] ruleset/teamAllowed/teamcount into one list row).
7. Editor UI structure (main dialog object offsets)¶
The main window object (thiscall param_1 throughout the handlers) layout:
| Offset | Control / field |
|---|---|
| +0x20 / +0x24 | parent handles (frame access via Ordinal_2864) |
| +0x2c0 | Server-cycle list control (columns: map, scenario) |
| +0x340 | Missions/UserMissions folder combo (0=resource\Missions\, 1=resource\UserMissions\) |
| +0x380 | Map listbox (populated from *.nfm by FUN_00401e00) |
| +0x3c0 | Rules list (18 columns, see below) |
| +0x180 | Bot list (7 columns) |
| +0x400 | longname edit |
| +0x480 | info edit |
| +0x3a8 | "has current file" flag |
| +0x4dc | app icon (ID 0x80) |
| +0x4e8 | 13 × game-type enabled flag (one per GameTypeCount) |
| +0x51c | 13 × per-type custom script-name list head |
| +0x554 | 13 × per-type team count (default 8) |
| +0x588 | 13 × per-type alignment (default 6) |
| +0x550 | SinglePlayer flag (game-type index 13) |
List columns (from FUN_00404b60):
- Rules (+0x3c0, 18): Rule Name, Rule Set, Scenario Name, Team Allowed, Team Count, Scenario Text File Name, Kill Limit Allowed, Kill Limit Count, # of Waves Allowed, # of Waves Count, Night, Weather, Radar Mode, Stock 'Mechs Only, Max Tonnage, Max C-Bills, Game Length, Custom Script Name.
- Teams (+0x380, 9): Team #, Max Tonnage, Min Tonnage, Max Total Tonnage, Max C-Bills, Max Total C-Bills, Max # of Players, Skin, Alignment.
- Bots (+0x180, 7): Rule #, # of Bots, Team #, Max Tonnage, Min Tonnage, Max AI Level, Min AI Level.
Handlers: FUN_00404b60 = New/Open (sets columns, default file new.nfm or
loads current via FUN_00404f00); FUN_00404f00 = load; FUN_004060b0 =
save; FUN_00406ee0/00406fe0 = populate team/bot lists; FUN_00406eb0/00406fb0
= per-scenario team/bot data accessors; FUN_00407120/00407160 = ruleset
helpers; 0x00402E80 = OnClose (cycle-list save prompt).
8. Bundled GameOS framework (0x408xxx–0x44Exxx)¶
The tool links a large slice of the GameOS engine (the same framework the game EXE uses), which is why it must run from the game root:
- DirectSound init/caps dump (
FUN_0041a0b0, 16.7 KB — the bigSECONDARYMONO/SECONDARYSTEREO/MaxHwMixing*/FreeHw3D*report builder over theDAT_00470810caps table), GameOS memory manager (heap tagsDAT_004e4a38/004e4b48, "GameOS_Memory*", "GameOS Parent Heap"), file/mapped-file I/O (CreateFileMappingA,UnmapViewOfFile), - Direct3D/DirectDraw/DirectInput/DirectPlay wrapper error tables
(D3DERR_, DDERR_, DIERR_, DPERR_), texture codecs (BMP/TGA/JPG loading,
mipmap rules — the "Only power of 2..." diagnostics), registry helpers
(
\Config\Platform,Config Manager\Enum), AGP miniport detection, Language.dll/blade.dll/gundll.dll/dbghelp.dll/dsetup.dll/ijl10.dllloader strings,GameOS.vxd, MAPI (MAPISendMail), a SQL connection string (DRIVER={SQL Server};...),VideoCard.txtdump.
This is the same subsystem family as the game EXE's bundled engine code; the
.nfm reader/writer and the info-file library (§4) are the parts unique to this
tool's workflow (the game EXE reads .nfm with its own copy of the same info
parser — see src\mw4mercs).
9. Notable strings (VA → value)¶
0x46E108 servercycle.txt · 0x46E61C/0x46E634 resource\Missions\*.nfm /
resource\UserMissions\*.nfm · 0x46EBC4 new.nfm · 0x46E1D4 NFM File ·
0x46EEA0 Content\GameTypes.h · 0x46EEB4 !include · 0x46EEC8
!preservecase · 0x46EE74/0x46EE88 resource\Missions\ /
resource\UserMissions\ · 0x46E738 cycle · 0x46E744 map · 0x46E740
:: · 0x46E764 Do you want to save the Server Cycle List? · 0x46E7B4
The current Server Cycle List is empty. ... · 0x46E74F Server Cycle List
Saved. · 0x46ED4C Not enough memory to view the current NFO file. (sic)
Field strings ([server]/blocks): ruleset 0x46EC24/0x46EC68 · scenarios
0x46EC54 · server 0x46EC60 · teamAllowed 0x46EC78 · teamCount
0x46EC84 · scenarioText 0x46EC90 · killLimit 0x46ECA0 ·
killLimitNumber 0x46ECAC · respawnLimit 0x46ECBC ·
respawnWaveNumber 0x46ECCC · isNight 0x46ECE0 · weather 0x46ECE8 ·
radarMode 0x46ECF0 · onlyStockMech 0x46ECFC · maximumTonnage
0x46ED0C · maximumCBills 0x46ED1C · gameLength 0x46ED2C ·
scriptName 0x46ED38 · team_%d 0x46ED7C · teamdata 0x46ED84 ·
maximumTotalTonnage 0x46EDB0 · maximumTotalCBills 0x46EDD4 ·
maxPlayersOnTeam 0x46EDE8 · skin 0x46EDFC · alignment 0x46EE04 ·
bot_%d 0x46EE10 · botdata 0x46EE18 · botCount 0x46EE20 · team
0x46EE2C · maximumLevel 0x46EE54 · minimumLevel 0x46EE64 · info
0x46EED8 · author 0x46EEE0 · link 0x46EEF0 · longname 0x46EEF8 ·
type 0x46EF0C · name 0x46EF6C · $(SinglePlayer) 0x46EF34
10. Key data-structure take-aways¶
.nfmandservercycle.txtshare one grammar: the GameOS info file format (sections,key=value,Name={blocks,#define/!directives,$(macro)expansion,////* */comments).- The format is case-insensitive by default;
.nfmfiles opt out with!preservecase=true. Keys are lowercased on read otherwise. - Duplicate keys are meaningful — a map's
[ruleset]is a list of repeatedtype=lines; the parser retains every occurrence and readers take the first match. - Game types are a 13-entry enum (0..12, 13 = SinglePlayer) defined by
Content\GameTypes.h; the editor's state arrays are sized exactly 13. - The writer normalizes output and only emits non-default optional fields —
a hand-edited
.nfm's formatting/comments are not preserved on save.