Game versioning in the reference (MW4Mercs.exe)¶
How the reference build identifies its version and how that identity is used to
keep multiplayer clients on the same version. All addresses are reference VAs;
function names are the oracle's func_<VA> names. This is reference (oracle)
RE.
TL;DR¶
- One version value, stored in two live hardcoded copies:
"50.07.01.2105"(.rdata0x0076CB3C, loaded at startup intog_game_version0x00858924, and0x00773040, read by the join handshake and saved-data paths). This is the official Point Release 1 (PR1) build. - Hosts advertise it as the DirectPlay session attribute
GameVersion(and inside the session config blob as the\vdirective). - The server browser compares each enumerated session's
GameVersionagainst the local one with whole-string lexicographic compares (_stricmpin the filter,_mbscmpin the sort — no numeric parsing; older = string less than local, newer = greater). Three UI toggles (curveroff/newveroff/oldveroff) and a 6-modeversionsortcontrol filter and order the list; the UI shows the selected server's version asReferenceVersionnext toLocalGameVersion. - The Zone matchmaking layer has its own protocol-version gate; its
failure string is
"Incompatible Protocol version mismatch."(0x007A04DC, ZoneTech error0xC1120C05). - There is an in-game join handshake (net event
0x0A,FUN_0053F210, see §7): the joiner sends a packed 64-bit version code (derived from"50.07.01.2105") and an 8-byte content checksum; the receiver compares both against its own computation and answers0x0B(reject + status) or0x0C(accept + checksum echo the joiner re-verifies inFUN_0053F9B0). Mismatches logPlayer_Reject\t%d\t%d(0x0077B730) with status4(content),5(version),6(banned IP) or8(duplicate player GUID). The content checksum covers the code bytes of the 15 resource-loader functions plus one mission archive entry (see §8). - The connect path's DirectPlay application GUID (
0x007A2E30={36E95EE0-8577-11CF-960C-0080C7534E82}) is game identity only, identical across builds. The advertisement + browser filter/sort is advisory; the join handshake is the actual same-build gate. - The handshake and the variant self-digest are the game's two integrity checks; see §8 and §9 for what each one covers, and §11 for the observed limitations.
1. The version value¶
| Item | Address | Notes |
|---|---|---|
| Build version string | 0x0076CB3C |
"50.07.01.2105" |
| Local version global | 0x00858924 |
char* in BSS, zero-initialized |
| Option/config init | FUN_00407BE0 |
0x00407BE0: sets g_858924 = 0x76CB3C |
| Log/registry init | FUN_00737020 |
0x00737020: calls FUN_00407BE0, then a fallback |
FUN_00737020 (called from the startup/log init FUN_00709120 and
FUN_007374B0) calls the big option-init FUN_00407BE0, which assigns
g_game_version = "50.07.01.2105". Only if that pointer were still null/empty
would FUN_00737020 build a date-derived fallback
wsprintfA(0x008AA390, "00.00.%02d.%02d00", month, day) (format 0x007A6958)
and point g_858924 at it — dead in the normal path.
The version string doubles as a per-version registry value name: the
registry key is "Software\Microsoft\Microsoft Games\%s" (format 0x007A6918)
and FUN_007086D0 / FUN_00708880 read/write a value named by g_858924
(window size, perf counter, exe date). So each build keeps its own settings.
1.1 The four fields (and the two live copies)¶
The string is four .-separated decimal fields of 2+2+2+4 digits
(50 07 01 2105). Nothing in the game assigns meaning to them
individually — the fallback format "00.00.%02d.%02d00" (month in field 3,
day×100 in field 4) is the only hint at the intended shape, and the shipped
family shows the convention:
| Binary | Version | Note |
|---|---|---|
MW4Mercs.exe |
50.07.01.2105 |
this target |
MW4Ed2.exe |
50.06.10.0801 |
mission editor (older) |
autoconfig_MERCS.exe |
50.06.09.3002 |
graphics config tool |
(50 = MW4 family, field 2 = title/generation, field 3 = minor series,
field 4 = build number; see docs/editor_tools/ for the tool versions.)
There are two live hardcoded copies, both read-only literals, read by different subsystems:
| Copy | Address | Consumers |
|---|---|---|
g_858924 target |
0x0076CB3C |
UI (LocalGameVersion), browser filter/sort, session advertisement, gundll wide conversion, registry key, log init |
DAT_00773040 |
0x00773040 |
join handshake version code (FUN_00535F30), GameSpy gamever (FUN_004D8A80, FUN_004FF490), log header (FUN_00563800), saved-game version compare (FUN_00616830), stats (FUN_00564AA0, FUN_00565E00), mission/save data (FUN_00614A80, FUN_00615100, FUN_00615FF0, FUN_0061A510) |
A third copy is the unreferenced what-string
"@(#) Microsoft MechWarrior IV: Mercenaries Build 50.07.01.2105"
(0x007B4640, no pointers into it). Both live copies have ~2 bytes of
zero-padding slack before the next literal (usable string length ≈ 14 chars +
NUL), so an in-place overwrite is possible; changing only one of them makes
the advertised version and the join version code disagree.
Where the player sees it¶
The multiplayer UI (bound in FUN_00404D60 / FUN_00404AC5) has three
version fields:
| UI field name | Name string | Backing global | Meaning |
|---|---|---|---|
GameVersion |
0x0076C218 |
0x007B7C70 |
version of the session currently selected/received (FUN_00401E50 GetGameData) |
ReferenceVersion |
0x0076C260 |
0x007B7E44 |
version of the selected server, filled on demand |
LocalGameVersion |
0x0076C274 |
0x00858924 |
this client's own version |
ReferenceVersion is filled by the UI event GetReferenceVersion
(0x0076C4E0); its handler at 0x004010B0 (inside func_00401006.c) takes
the browser's current selection (FUN_006F6350, which returns the selected
entry or NULL), reads its GameVersion attribute (FUN_006F5B80) and
strncpys it (0xFF bytes) into 0x007B7E44. It is informational — the player compares the two strings on the
server-info panel.
The version also goes out in:
- the log header (
Game_Versionblock at0x0077EA3x, writerFUN_00563800— game name/version/author/server info), and - the gundll (GunDll) interface init (
FUN_006FCF00/FUN_00701030convertg_858924to wide and pass it to the gundll vtable at0x008589A4).
2. How it is advertised (host side)¶
The session config is a backslash-directive blob parsed by FUN_00705D80
(0x00705D80):
\g<name> -> session+0x1878 (game name)
\m<map> -> session+0x1834 (map type)
\v<ver> -> session+0x18B8 (GAME VERSION)
\a / \h / \r -> session+0x18F8 / +0x1938 / +0x1978 (UnlimitedAmmo / NoHeat / NoRespawn)
\p<xy> -> session+0xC34 + idx*0x40 (per-player name, count at +0x1874)
\c<xy> -> session+0x1234 + idx*0x40 (per-player clan)
Two registration paths publish the DirectPlay session attributes:
FUN_00705FE0(0x00705FE0, "game creation / server info registration") copies the lobby record's config blob (lobby+0x30, 3000 bytes) intosession+0x7C, parses it withFUN_00705D80, then setsGameVersion = session+0x18B8— i.e. the advertised version is whatever the host's\vdirective says. It also setsMod Name,Product Name,IpAddr,ServerPort,GName, player/bot counts,MaxPlayers,MapType,PasswordProtected,Instance,Handle,Flags,Protocol,GameType,Update Time,ExpectRefresh="false",UnlimitedAmmo,NoHeat,NoRespawn, and one entry per player.FUN_00705F9Cis the sibling variant of the same routine.FUN_007008E0(0x007008E0,IGunHost::AddPlayer) setsGameVersion = g_858924directly (wide), alongsideMaxPlayers,PasswordProtected(L"No"/L"Yes"),Instance,MapType("-"),Flags,GameType("0").
Instance is the game-instance GUID (a 16-byte game ID formatted with
"{%08X-%04X-%04X-%02X%02X-%02X%02X%02X%02X%02X%02X}", 0x007A1120), not a
version derivative. GameVersion is the only build-version field on the wire
(a plain string).
3. Client-side matching (server browser)¶
Session attributes are read with FUN_006F5B80(obj, "GameVersion") and
compared as whole strings — _stricmp (0x0076B2AC, case-insensitive) in
the filter, _mbscmp (0x0076B300, case-sensitive) in the sort. There is
no numeric/semantic version parsing anywhere: "older" means the entry's
version string is lexicographically less than the local g_858924 string,
"newer" means greater (argument order in the calls is (entry, local)).
That works only because the shipped string uses fixed-width zero-padded fields
(50.07.01.2105); an unpadded field breaks the order ("50.7.x" > "50.10.x"
lexicographically, although 50.7 < 50.10 numerically), and the filter
(_stricmp) and sort (_mbscmp) even disagree on case.
3.1 The filter callback¶
FUN_006F6EB0 (0x006F6EB0, a method of the filter object vtable at
0x0079FD24) implements three version predicates against g_858924, enabled
by bytes of the filter object:
| Filter byte | Predicate (_stricmp(entry, local), entry = session GameVersion) |
|---|---|
+0x40 (curveroff) |
equal (== 0) |
+0x41 (newveroff) |
entry newer (entry > local, > 0) |
+0x42 (oldveroff) |
entry older (entry < local, < 0) |
(The UI names agree: newveroff = hide newer, oldveroff = hide older.)
The callback is invoked with a mode (1–4) that encodes the ZoneTech filter
engine's AND/OR polarity: mode 1 rejects when the predicate holds, mode 2
accepts when it fails, mode 3 accepts when it holds, mode 4 rejects when it
fails; unset bytes simply fall through. A missing GameVersion attribute is
treated as predicate-false. The remaining filter bytes cover the non-version
filters (ping/gametype/players/clan/available/active).
3.2 The UI filter parameters¶
FUN_00402660 (0x00402660, SetFilterParam) / FUN_004024B2 (0x004024B2,
GetFilterParam) map UI script parameter names to those globals:
| Parameter | String | Global | Filter byte |
|---|---|---|---|
curveroff |
0x0076B890 |
0x007B7C60 |
+0x40 (equal) |
newveroff |
0x0076B878 |
0x007B7C61 |
+0x41 (entry newer) |
oldveroff |
0x0076B884 |
0x007B7C62 |
+0x42 (entry older) |
versionsort |
0x0076B86C |
0x007B7C64 (1–6) |
sort mode |
All default to 0 (no version filtering, no version sort). A bad versionsort
value asserts with "Invalid version sorting filter setting '%s'. Should be a
number between 1 and 6." (0x0076B94C).
FUN_004022E0 (0x004022E0, ApplyFilters) pushes the three bytes onto the
filter object through the trivial setters FUN_006F73B0 / FUN_006F73D0 /
FUN_006F73F0 (+0x40/+0x41/+0x42) and the sort mode through
FUN_006F79E0.
3.3 The version sort¶
FUN_00401140(0x00401140, theSortGameListUI callback) dispatches on the sort-key name; when the key is"GameVersion"(0x0079FA48) it clampsg_007B7C64to 0–6 and passes it toFUN_006F7BF0.FUN_006F7BF0(0x006F7BF0) partitions the list into three groups by_mbscmp(entry_version, g_858924)(case-sensitive): entry < local → older, entry == local → same, entry > local → newer — the same lexicographic comparison as the filter, in the opposite call order and case-sensitively. The mode (1–6) then selects the resulting order/grouping of those three groups; mode 0 leaves the list unsorted by version.FUN_006F7B10(0x006F7B10) is the attribute-change callback: when an entry'sGameVersion(orFlags) attribute arrives/updates, it re-runs theFUN_006F7BF0reorder.
3.4 List ingestion gate¶
Before an entry is even added to the list, FUN_006FFC90 (0x006FFC90),
FUN_006FEAB0 (0x006FEAB0) and FUN_006FF640 (0x006FF640) require the
entry's Protocol attribute (0x0079FAD4) to equal "0" (0x0079FB30) and
its ExpectRefresh attribute (0x0079FAFC) to equal "false". The Protocol
attribute is the lobby/browser protocol marker (set from session+0x78 in
FUN_00705F9C), not the build version.
4. The lobby (ZoneTech) protocol gate¶
The matchmaking layer's error-code→string map is FUN_006FCB40
(0x006FCB40). Its ZoneTech error family 0xC1120xxx includes:
0xC1120C05→"Incompatible Protocol version mismatch."(0x007A04DC)
That is the hard "your client's protocol/version does not match the lobby"
rejection — the closest thing to a version gate in the matchmaking path. The
rest of the family (0x007A03E8–0x007A0B8C) covers session/GUID/lobby
errors such as "The requested component version in not available."
(0x007A0B8C) and "Client is disconnected from the server." (0x007A0424).
5. What is and is not version-gated¶
- The connect path
FUN_00703E60(0x00703E60) verifies the DirectPlay application GUID at0x007A2E30={36E95EE0-8577-11CF-960C-0080C7534E82}and rejects a mismatch. This identifies the game (MW4 Mercs) and is the same for every build, so it does not separate versions. - The advertised
GameVersionattribute is not what the join handshake checks. The handshake's version code is computed from the second hardcoded copyDAT_00773040(see §1.1), not from the advertised string nor fromg_858924; so a host can advertise an arbitraryGameVersionyet the in-session check still validates the real build (and vice versa). - The browser filter/sort is advisory. A client can reach a session whose
advertised version differs (join directly, or disable the browser version
filters); the lobby protocol check and the join handshake (see §7) are the
enforcement, and the UI deliberately surfaces both versions
(
ReferenceVersionvsLocalGameVersion) so the player can decide. - What is not gated at all: content outside the 15 hashed loader functions and the mission entry, the peer's actual variant files (only a local self-digest, see §9), the peer's identity/IP fields (self-reported), and anything after join (no runtime re-validation, see §10).
6. Related version checks (not multiplayer)¶
For contrast, the binary has many other version checks that are unrelated to client compatibility:
- Data-format magic+version asserts for MLR/ERF/OBB/GFX/GAF assets —
e.g. MLR
"MLR#"+ version0x12(FUN_00421E60/FUN_00421EC0), ERF (FUN_004AF200), OBB (FUN_004FEC10), with strings like"Application must be rebuilt to use this version of MLR data"(0x0076DD24). - Saved-game version (
FUN_00616830, message"You are loading a game saved by a newer game version. The saved game might not work correctly.",0x0078C310) — single player. - Diagnostics:
GetDLLVersions(0x007A4750) reads file versions viaVERSION.dll(GetFileVersionInfoA,0x007B1318);"Executable version"/"DirectX version"/"GameOS build version"report block (0x007A4A59+). - gundll/gun-auth receives the version string at interface init
(
FUN_006FCF00/FUN_00701030). - DRM/anti-tamper (not multiplayer):
FUN_0052A460is the SafeDisc CD check (CdaSys*,"ScsHD.csa", anti-tamper verification);FUN_0052AAB0calls the registry/date checksFUN_00536060/FUN_00536190("Date/time validation");FUN_005FCC80is a local XOR-checksum/flag check. These are copy-protection/clock-tamper tripwires, unrelated to client compatibility.
7. Join-time validation — net event 0x0A (FUN_0053F210)¶
This is the actual same-build/content gate, and it happens in-session (after the DirectPlay connect).
7.1 The message path¶
The in-game net dispatcher FUN_00546C60 (0x00546C60) switches on an event id
(jump table 0x00547074, index = eventId - 0x0A). The relevant events:
| Event | Handler | Role |
|---|---|---|
0x0A |
FUN_0053F210 |
player join/registration message — validate + accept/reject |
0x0B |
FUN_0053F910 |
join reject reply — sets local session state (this+0x3514 = 2/3/4/5) |
0x0C |
FUN_0053F9B0 |
join accept reply — joiner re-verifies the echoed checksum; mismatch sets state 3 |
Flow:
- The joiner's sender
FUN_0053FA70(0x0053FA70) builds the0x0Apayload (content checksum viaFUN_004D2AB0+FUN_0062DDC0at0x53FAC2/0x53FAEE, version code via theFUN_00535F30family at0x53FB0C, plus the player record), callsFUN_0053F210on its own copy (loopback,0x540021), then transmits. - Every receiving peer runs
FUN_0053F210, which recomputes the local reference values intoDAT_007DDD50/54(content checksum),DAT_007DDD58/5C(FUN_00535B40= 0,0 — reserved),DAT_007DDD60/64(version code) and setsDAT_007DDD48 = 1(join-validation-in-progress). - The result goes back as
0x0B(reject) with a 4-byte status, or0x0C(accept) with an 8-byte re-derived checksum pair;FUN_0053F9B0recomputes the pair locally and aborts the session (state 3) on mismatch, so the check is mutual.
7.2 The 0x0A payload as read by FUN_0053F210¶
| # | Field | Size | Destination / check |
|---|---|---|---|
| 1 | content checksum | 8 | vs DAT_007DDD50/54 → status 4 |
| 2 | version code | 8 | vs DAT_007DDD60/64 → status 5 |
| 3 | flag bytes | 3×1 | scratch (one becomes slot +0x353F on accept) |
| 4 | name length + string | 4 + n | player slot +0x358C |
| 5 | dword | 4 | slot +0x3590 |
| 6 | flag1 | 1 | slot +0x355D → triggers GUID1 search |
| 7 | flag2 | 1 | slot +0x355E → triggers GUID2 search |
| 8 | GUID1 | 8 | slot +0x3594 |
| 9 | GUID2 | 8 | slot +0x359C |
| 10 | data16 |
16 | sender's index word in dword 0 (seed for the accept echo) |
| 11 | val5 |
4 | ban-list third key |
| 12 | data16b |
16 | ban-list IP key |
| 13 | content checksum again | 8 | re-checked vs DAT_007DDD50/54 → status 4 |
Then:
- GUID searches (only when flag1/flag2 are set): scan the 0x2038-stride player table for a slot with the same GUID1/GUID2 (skipping the joining player's own slot, and slot 1 when a global flag is set); a match sets status 8.
- Ban check
FUN_005450B0(skipped for the local player index): the 16-bytedata16/data16b(IP address words) andval5are looked up in the three lists atthis+0x239D54/58/5C(counts) /+0x239D60/64/68(data); found → status 6. - The status is logged via
FUN_00563550with"Player_Reject\t%d\t%d"(0x0077B730) →FUN_00563C80, and returned as message0x0B. - On accept (
status == 0) the slot fields+0x35B4,+0x35B8,+0x353F,+0x35DC(16 bytes),+0x35EC,+0x5558(16 bytes) are filled, the MOTD chunks at+0x309Care re-broadcast (thisvtable slot+0x30), andFUN_00536DF0/FUN_00543EC0/FUN_00543BC0run for non-self players.
7.3 The ban lists¶
FUN_00544B60 (0x00544B60) rebuilds the three lists from the game config
(ban.txt): keys ban (0x0077B7B4), ngban (0x0077B7B8), ispban
(0x0077B7C8), values parsed with "%d.%d.%d.%d" (0x0077B744) into 16-byte
entries (first two lists) / 4-byte entries (third list), initialized from the
all-zero 16-byte constant 0x007AFAF0. The lists are (re)built by
FUN_00544B60 (called from FUN_00536840 / FUN_005449F0, i.e. when the
config/MOTD side reloads) and torn down by FUN_0052FA70.
8. The content checksum — what the peers must match¶
DAT_007DDD50/54 = FUN_004D2AB0(entry) ^ FUN_0062DDC0(entry), where
entry = [game_object + 0x1804] (the mission/archive entry index).
| Part | Function | Hash input | Output |
|---|---|---|---|
| mission entry | FUN_004D2AB0 → FUN_00414A70 |
MD5 stream: 4-byte entry index + the archive entry's bytes | digest[0] ^ digest[8] (8 bits, edx = 0) |
| loader code | FUN_0062DDC0 |
MD5 stream: code bytes of the 15 resource loaders (addresses 0x007B55F8[i], sizes 0x007F2CE0[i]) + 4-byte entry index |
d0^d1, d2^d3 (64 bits) |
The 15 hashed loader regions (sizes computed by FUN_0062D5AB /
FUN_0062D67B into 0x007F2CE0..0x007F2D18; addresses from the pointer table
0x007B55F8):
| # | Address | Size | # | Address | Size |
|---|---|---|---|---|---|
| 0 | 0x0070E380 |
0x0B30 |
8 | 0x005ECBC0 |
0x1C50 |
| 1 | 0x004E5FD0 |
0x20B0 |
9 | 0x005ABD30 |
0x10D0 |
| 2 | 0x004C8E00 |
0x0160 |
10 | 0x005A61D0 |
0x0780 |
| 3 | 0x006212E0 |
0x3490 |
11 | 0x005EF590 |
0x03D0 |
| 4 | 0x005B6C20 |
0x0A90 |
12 | 0x0069D490 |
0x1410 |
| 5 | 0x00548F60 |
0x12820 |
13 | 0x00637F60 |
0x2DF0 |
| 6 | 0x005F2B00 |
0x4150 |
14 | 0x0062D750 |
0x06E0 |
| 7 | 0x00581480 |
0x0FA60 |
(Entry 14 is the region that contains the checksum helpers themselves,
FUN_0062DCD0…FUN_0062DE30.)
DAT_007DDD58/5C = FUN_00535B40 (0x00535B40, xor eax,eax; xor edx,edx;
ret) = 0,0 — a reserved/disabled third checksum slot.
The version code (DAT_007DDD60/64) is FUN_00535F30 (0x00535F30): it
tokenizes g_858924 on '.', atois the components c0..c3 and packs
(((c0*100 + c1)*100 + c2)*10000 + c3) as a 64-bit value (through the
__allmul-style FUN_0075DC60). For "50.07.01.2105" that is 5007012105.
Notes:
- Both hashes are unkeyed MD5 over public data — the only "secret" is the algorithm, which ships in the binary.
- The mission-entry contribution is 8 bits; the loader-code part is 64 bits.
- The accept path re-derives the checksum with the peer's entry index
(
data16[0] ^ 0) and echoes the pair in0x0C;FUN_0053F9B0recomputes and compares, so the check is mutual. FUN_0053F210itself computes the pair twice (top of function and again on accept);FUN_004D2AB0/FUN_0062DDC0are also called by the senderFUN_0053FA70and by the joiner's verifierFUN_0053F9B0.
9. Variant (mech) file integrity and loadout validation¶
9.1 The variant self-digest¶
When a player-creation message is processed (FUN_00534200 / FUN_00533C30),
the receiver:
- reads the variant name from the message,
- loads
Resource\VariantsMercs\<name>.mw4from its own local database (path format0x0077B368, fetchFUN_004D2E00; mech resource id viaFUN_004D2CB0), - reads the 16-byte digest stored inside the resource (
FUN_0041E720, size 0x10), - recomputes
MD5(0xA1252388 + resource data)(FUN_0070E390/E3C0/E460) — the same salted digest the variant loaderFUN_0060C12D(0x0060C12D) computes and appends (FUN_0041F030) when building the resource, repe cmpsds the two digests; mismatch sets the per-player flag+0x357C = 1(and+0x3578holds the loaded mech resource pointer).
Consequences of the flag (both act on the local player's own slot — the mechanism is self-enforcement, not peer enforcement):
FUN_005330C0aborts the local player's mission entry (state+0x3534→ 0, cleanup).FUN_005405B5pushes event code1into the player-event queue at+0x204D2C(count+0x205524); the queue is consumed through the accessor helpers inFUN_00513B60.
Scope: this is a local self-consistency check of the receiver's own copy
of the named variant — it catches hand-edited files whose stored digest was not
updated. It does not compare the sender's file against the receiver's (the
name is the only variant data on the wire), so two clients with different
VariantsMercs files still pass and simulate different mechs.
9.2 Loadout legality¶
FUN_005FEEB0 → FUN_00601EF0 validates weapon/armor slot counts against the
chassis (mount groups, 0x429 mech-type gate), and FUN_00676EA0 validates
the GameData distributions (ArmDistributive / LegDistributive /
SideFrontDistributive / CenterFrontDistributive / CenterRearDistributive
in 0..1.0, PointsPerFerroTon / PointsPerReflectiveTon /
PointsPerReactiveTon > 0). These run on the local copy only; they bound what
a legal mech is but are not cross-checked with peers.
10. Trust model — what actually happens to a modded client¶
| Scenario | Result |
|---|---|
| Different binary build / patched loader code / different mission entry | join rejected, status 4, Player_Reject |
| Different hardcoded version string | join rejected, status 5 |
IP listed in ban.txt (ban/ngban/ispban) |
join rejected, status 6 |
| Duplicate player GUID | join rejected, status 8 |
| Variant file edited, stored digest not fixed | slot flagged +0x357C, mission entry aborted, event queued (self-enforcement) |
Variant file edited, digest recomputed (salt 0xA1252388 is public) |
passes — nothing cross-checks the file |
| Patch outside the 15 loader regions (damage/armor math, variant parser, validators) | passes — not hashed |
| Replay/spoof the stock checksum + version pair | passes — the pair is a pure function of shipped data, no nonce or secret |
Spoof the IP fields in 0x0A |
ban list bypassed |
| Memory patch after join | invisible — no re-validation during play |
Runtime is peer-to-peer with no server authority: the net event table (0x0A–
0x31, FUN_00546C60) contains join/team/decal/mission/state handlers but
no armor or damage message; the integrity checks are concentrated at
join/load time, and each client simulates the shared world from its own local
data.
11. Observed limitations¶
- No secret, no challenge. The handshake is a self-report of a constant: the expected pair is a pure function of the shipped binary and mission data. A client can compute or replay it regardless of the code it actually runs. Without a host nonce (and a keyed response) the check cannot attest to the sender's binary.
- Tiny hashed surface. Only the 15 resource-loader functions and one mission archive entry are covered. The simulation, weapon/armor math, hit detection, damage application, the loadout validators, the variant parser, and the netcode itself are not.
- Weak mission-content contribution.
FUN_00414A70folds the mission digest to 8 bits (digest[0] ^ digest[8]), so the content side of the checksum has ≤ 256 effective values; the strength rests on the loader-code hash alone. - Variant digest is local-only. The salt is a constant in the binary, the digest is stored in the file it protects, and the check compares the receiver's own file with itself. A client that recomputes the digest is not detected.
- Ban lists are IP-only and self-reported. The
0x0AIP fields come from the peer; the ban list is a localban.txt, with no server-side or shared enforcement. - Identity is self-reported. GUID1/GUID2 (the player identity used for the duplicate check) arrive in the message; a client can present arbitrary values.
- No runtime re-validation. Once in, nothing re-checks the peer; a
patched process passes forever. The
+0x357Cflag is computed locally and is self-enforced. - Divergence is undetected. Because variant data is not cross-checked, two clients can simulate different mechs and the game only notices when visible state conflicts.
- Reporting is opaque. Failures surface as
Player_Reject <index> <code>; there is no user-facing "modified files" message (the variant flag becomes event code 1 in an internal queue with no dedicated string).