MULTIP.DLL is module 35, installed as MULTIPLAYER.DLL. It owns the
DirectPlay session, the local and remote player records, network messages and
the extrapolated aircraft state handed to the rest of the simulator.
MULTIP has passed x86 whole-module acceptance and startup smoke testing.
All eleven public entries, worker lifecycles, protocols, dialogs and CHAIN
callbacks are implemented and compared with retail. The latest evidence is in
the acceptance checkpoint. It is included
in MODULES; make install maps bin/MULTIP.DLL to MULTIPLAYER.DLL.
The temporary smoke test restores the previous game DLL rather than deploying
this build permanently.
This section is the handoff for a fresh context. The current project decision
for all MULTIP code is snake_case, the standard <stdint.h> types
(uint32_t, int16_t, and so on), and no types.h. This supersedes stale
instructions that request Hungarian names or the old typedefs. Preserve the
retail offsets and ABI, but use descriptive snake-case names where semantics
are known and offset-based names where they are not.
Read CLAUDE.md, README.md, docs/method.md, src/multip/AGENTS.md, then
this section and the receive/lifecycle checkpoint.
Check git status --short before editing: this checkpoint includes modified
and untracked files. Preserve the entire working tree; do not stage, commit,
push or install MULTIP. A fresh context in this workspace can use these files
immediately; a separate checkout also needs the uncommitted changes.
- Verified: all 43 top-level suites pass 20,000 scenarios each, zero
differences, plus eight dispatch-family reruns. The acceptance checkpoint
adds a mixed public lifecycle and guarded malformed packet boundaries.
The earlier host-fixture correction remains covered by
players,system,lifecycleanddispatch. - Latest acceptance work:
multip_whole.incandmultip_bounds.incin the harness, isolated mutation selectors, andtools/multip_smoke.sh. Private entries 0–269 remain unchanged; init/deinit and their two callbacks occupy 266–269, mapping retail RVAs0x1020/0x1070/0x1090/0x1220. - Mutation status: all 456 targets match exactly once and all 456 are caught at 1,200 scenarios. The command slice also passes all 14 checks with the compiler idle during watchdog measurements. Interrupted attempts and fixture failures were excluded from the completed-result ledger.
- Smoke/build: forced
make -B allis warning-free. Startup with the exact replacement passes 27/27 modules and 95 gauges; the previous DLL is restored and its checksum verified. MULTIP is now inMODULESwith the existingMULTIPLAYER.DLLmapping. - Next: MPCHAT is the next module. Further MULTIP validation can cover real provider interoperability, live multiplayer sessions and non-x86 fallbacks.
- Limits: scripted x86 comparisons and startup smoke do not validate native non-x86 behavior, real network providers or OS thread scheduling races.
Build with make multipdiff; run make multipdiff-run N=20000 for the full
checkpoint. From bin/difftest, run WINEDEBUG=-all wine multipdiff.exe 1200 init or replace init with lifecycle, dispatch, dialogs or lobby
for focused comparisons.
Run mutations from the repository root with
python3 -B tools/multip_breaks.py --slice init (or another named slice).
Run Wine tests serially: concurrent runs previously produced timeouts.
The generator reads the retail orig/Modules/multip.dll; do not depend on
scratch disassembly under /tmp surviving a restart. .text offsets are
relative to VA 0x20851000; fixture RVAs are relative to 0x20850000.
The working files are:
src/multip/multip.c,.def, and.rc: the in-progress DLL;tools/difftest/multipdiff.c,multip_send.inc,multip_receive.inc, andmultip_workers.incandmultip_directplay.inc: query, transport, controlled worker, and loader/interface-lifetime harness;tools/multip_breaks.py: isolated mutation checks for recovered owners;- the
multipdiff,multipdiff-run, and explicitbin/MULTIP.DLLtargets in the top-levelMakefile.
Do not add MULTIP to MODULES or install it into the game until all public
slots, both worker lifecycles, and the CHAIN callbacks are implemented and a
smoke test passes. These gates are now satisfied; see the acceptance checkpoint.
Additional implementation lives in multip_profile.inc, multip_session.inc,
multip_aircraft.inc, multip_records.inc, multip_tracking.inc,
multip_messages.inc, multip_players.inc, multip_callbacks.inc,
multip_motion.inc, multip_commands.inc, multip_weather.inc and
multip_tick.inc, multip_system.inc, multip_app_players.inc,
multip_updates.inc, multip_textures.inc, multip_texture_receive.inc,
multip_transfer_messages.inc, multip_texture_send.inc, multip_model_send.inc,
multip_model_cache.inc, multip_model_receive.inc, and multip_title_receive.inc
under src/multip/.
The differential fixtures with the same names live under tools/difftest/ (record enumeration shares the aircraft
copy fixture). The generated profile field order comes from
tools/gen_multip_profile.py; initialized image data and pointer fixups come
from tools/gen_multip_data.py. Neither generator embeds executable code.
The following internal paths are now implemented and compared against retail:
- Profile read/write at
.text+0x8f30/+0x93e0: 33 reads and 25 writes, with unsigned clamps and the derived packet interval. Retail spells the keyALLOW_EXTURE_RECEIVE; its player-list update key writes+0x174while validation changes+0x178. Both behaviors are retained. - Host/join and named groups at
+0x2340/+0x2380/+0x23c0/+0x2400,+0x3890/+0x3940, membership swaps at+0x97a0/+0x97f0. Enumeration treats only three specific HRESULTs as failure; membership changes ignore errors. - Session-description allocation at
+0xa7a0, capacity packing/unpacking at+0x9f40/+0xa020, name type/host encoding at+0xa050/+0xa100, local name publication at+0xa1a0, collision timer at+0xa590, and type counts at+0xa8d0. Name publication leaks its allocation on success as retail does. - Aircraft selection/release/copy at
+0x8a20/+0x8aa0/+0x8b30. Copy preserves partial reallocations, a zero-filled replacement header, and separately allocated model title. ACONTAIN uses nonzero success, host memory services use zero success. The fixtures include both ownership modes and failures. - Browser record construction/copy/free, name-list cleanup and player
enumeration at
+0xa2a0through+0xa540. An invalid final name leaves its appended record in the list; EnumPlayers errors still publish partial results. Cleanup ignores a walker error and frees through a temporary. - Track-name display and CHAIN
0x19callback at+0x96b0/+0x9710/+0x9740, selection reset/get/apply at+0x8cb0/+0xa830/+0xa6b0, view guard at+0xa680, and local model references at+0x9ea0. The callback preserves the argument as its return when it skips drawing. View application preserves the different state destinations, word writes, and copy-error boundaries. - Sixteen message constructors at
+0x5a80through+0x6060: tags1001,1008,1002,1009,1003,1004,1005,1006,100f,1010,1011,100a,100c,100e,1007,1012. Tag100ecarries a variable title, its terminator, and a visibility byte. Four set request-pending/time only after successful enqueue; four allocate an extra packet and leak it on enqueue failure. The unused tails of retail's 1000-byte stack buffers are omitted because they are never sent.
The private test table now has entries 0–265; send_internal in
tools/difftest/multip_send.inc maps each to its retail RVA. Entries 25–33
are profile and group/session helpers, 34–41 description/name/counter helpers,
42–44 aircraft ownership, 45–50 browser records/enumeration, 51–58 tracking
and selection, 59–72 message constructors, and 73–78 player ownership
helpers. Entries 79–80 are local creation/start, 81–84 are peer enumeration,
receive membership, ping and host departure; 85–87 own the session CHAIN
0x14 callback, and 88–91 own drawing/CHAIN 0x10. Entries 92–96 own
extrapolation/CHAIN 0x13, 97–103 view commands/CHAIN 0x0c, 104–106 weather
and sync messages, and 107–114 tick callbacks/CHAIN 0x02. Entries 115–119
are system notifications, 120–135 weather/player handlers, 136–140 state
updates, 141–152 texture files/catalogues, 153–162 texture reception,
163–167 multipart packet constructors, 168–177 texture sending, 178–185 model
sending, 186–189 cache files/replacement, 190–194 model reception, and
195–196 title lookup/announcements, 197–200 state constructors, and
201–205 control/publication helpers and the verified control callback.
The callback fixture drives real publication and collision helpers. Production exports
still contain only ImportTable and Linkage.
New comparison labels are profile load/save, session/group primitives,
session descriptors/name metadata, aircraft select/release,
aircraft copy/session browser records, track-name display,
application message constructors, player lifecycle helpers,
local player creation/start, session CHAIN 0x14, and
player drawing/CHAIN 0x10. They compare
ordered callbacks/imports, outputs,
partial state, heap bytes, failure leaks and queue contents as applicable.
Real critical sections/events are used for message enqueueing; the worker
suite runs last because it replaces the Win32 import cells.
Mutation checking accepts --slice profile, groups, metadata, aircraft,
records, tracking, messages, players, local, frame, and draw in addition to the original
slices. Each variant runs 1,200 scenarios so independently generated invalid/valid name
bytes reach all type characters. Constructor snapshots must be saved before
later enumeration resets the embedded list; a stride mutation exposed this
fixture gap and now fails the comparison.
The current forty-suite differential run passes 20,000 scenarios per suite
with zero differences. make multipdiff builds both production and test DLLs
without warnings; the production export table contains only ImportTable and
Linkage. These remain slice results, not whole-module acceptance. The earlier
154-mutation sweep passed; the current larger sweep status is recorded below. All five data
includes reproduce exactly from the updated generator. No crash or fixture
error counts as a catch.
Remote creation/removal and list ownership at +0x82c0/+0x84b0/+0x8930,
unknown-type initialization at +0x8980, local entry removal at +0xa8a0,
and remove-all at +0x8f00 now pass the fourteenth suite. The small name-label
model at RVA 0x2cf4 is extracted by the data generator into
multip_nametag.inc. The fixture retains moving-array banks so retail's stale
local pointer is observable, and checks per-action live-record hashes, heap
bytes/ownership, partial setup, duplicate IDs, model failures, host capacity
updates, selected-player reset, and CHAIN list publications. Local creation at +0x8820 and startup at +0x8700 are now recovered too;
see the additional boundary notes below.
Local creation/start has a separate fixture (multip_local.inc) that paints
the unwritten DPNAME reserved word. Retail reads it at entry ESP-0x400 in
+0x8820, or ESP-0x404 through +0x8700; the C bodies receive it through
small x86 entry shims. Adjacent slots are painted differently so a wrong
read cannot pass. Non-x86 entries use zero because there is no x86 caller
stack. Startup preserves the initial CHAIN list publication, local marker,
host counts (including wrap), join requests, name/capacity publication,
collision timer and second publication, with partial failures retained.
CreatePlayer's result, hosting flag and requested type are independently
seeded; correlated values initially hid every successful host path.
Peer enumeration at +0x86b0/+0x86d0 skips non-player records before inspecting
their names, stops on add failure, and returns the raw EnumPlayers HRESULT.
+0x9960 applies descriptive-receive group membership. Message 1007 at
+0x6010 updates a matching peer's +0xfc timestamp before enqueue, uses
flags zero before the guaranteed-message policy, and still sends for absent
IDs. Message 1012 at +0x6060 is the allocated host-departure broadcast.
Those paths extend the player/message fixtures.
The session CHAIN 0x14 callback at +0x2c10 and registration at
+0x2bc0/+0x2be0 are in multip_callbacks.inc. The compressed event switch
is extracted into multip_frame_actions.inc by the data generator. Its
fixture exhausts codes 0–255 plus wrapped/high codes, checking aircraft
reference refresh and title messages, collision reset, view flags, and raw
code-0x21 returns on default/flag-only paths. Registration sets +0x954
before calling CHAIN, retaining it on failure.
The CHAIN 0x10 renderer at +0x2d20/+0x2d30 and registration at
+0x2cd0/+0x2cf0 are recovered in the same source include. It captures the
list before reading the clock, initializes the previous time to now-200,
walks peers in reverse, gates on local/type/state/visibility/display flags,
uses ACONTAIN for owned models and G3D for received models, and optionally
adds the name-label vector through UTIL before drawing it. The renderer
results are ignored. Its fixture mutates state/options inside the draw stubs
to prove reloads, covers first/repeated timestamps and wrap, and self-checks
that owned, direct and label draws are all reached. A 20,000-scenario run
observed 1,878 ACONTAIN draws, 2,781 direct draws, and 2,403 labels.
The harness exits immediately on an unhandled exception, printing suite,
scenario, side and fault address. A fault or timeout remains a fixture/owner
finding and never counts as a mutation catch. Selected self-contained suites
can run alone: multipdiff.exe 1200 local, frame, draw, players, or
messages. The mutation runner uses those selectors where available.
Public init/deinit, session toggle and dialog entries are now recovered in
the later lobby/configuration and init checkpoints below.
All six callback-registration groups, aggregate start/stop, common completion
at +0x9c70 and disconnect at +0x8cf0 are now recovered and tested together.
Lobby/provider and configuration/dialog callbacks are also now recovered;
init/deinit now have integration coverage in the final checkpoint. Keep MULTIP outside MODULES until the
whole-module acceptance gates below pass.
multip_motion.inc recovers +0x3330/+0x33f0 and the CHAIN 0x13 owner
at +0x2e30/+0x2e50/+0x2e80. Its feedback retains x87 intermediate stores,
heading-dependent signed/unsigned conversion, longitude/metres conversion
at the saved latitude, and partial model updates on copy failure. The fixture
uses retail UTIL's stateless conversions. Its unsigned DIV overflow is repaired
exactly as the host does: quotient UINT32_MAX, remainder zero. This is required
for normal negative longitude steps, not a filtered input domain.
multip_commands.inc recovers +0x2f40, +0x3020, +0x3200, +0x3250
and CHAIN 0x0c at +0x2ea0/+0x2ec0/+0x2ef0. Cycling preserves retail's
empty/no-eligible spin and invalid-pointer outcomes; a main-thread watchdog
and vectored exception boundary compare these outcomes. The 20k run observes
79 spins and 78 pointer faults. Follow placement keeps its two caller-stack
padding words (entry ESP-0x24 and ESP-0x1c; through the command callback,
ESP-0x28 and ESP-0x20) via entry shims. Observer conversion preserves capacity
wrap, counts, name publication, request-pending state and failure ordering.
multip_weather.inc adds messages 1013, 1014, and 1017 at
+0x60d0/+0x6140/+0x67e0. The 28 weather fields are extracted in call order by
tools/gen_multip_weather.py into multip_weather_fields.inc. They initialize
bytes +8 through +0x257, while retail sends 0x25c bytes: the last four bytes
are caller stack at entry ESP-0xa8, preserved by a shim. The tick maintenance
caller contributes another eight bytes, so its shim reads entry ESP-0xb0.
Both fixtures paint adjacent slots differently. Sync timestamps are read after
allocation and before peer fields, and sync traffic starts with flags zero.
multip_tick.inc adds all four CHAIN 0x02 callbacks and their
+0x2460/+0x24d0 owner. Sync at +0x2590 runs after the counter exceeds the
period, includes wrapping counters, and preserves incoming EAX when disabled.
Interval accumulation at +0x2550 retains upper incoming EAX and returns the
raw x87 status word in AX; tests vary precision/rounding and include NaN and
infinite values. Maintenance at +0x25f0 decrements the track countdown and
clears weather-pending before sending. Guidance at +0x2630 plus
+0x2730/+0x2790 preserves panel word writes, ADF bearing, autopilot heading,
GLOBAL+0x84c's disabling side effect, and state reloads after range calls.
These four new suites pass 20,000 scenarios each. Extrapolation's 12, commands' 14, weather's 12 and tick's 14 deliberate errors are caught. Crashes, fixture exhaustion and timeouts are not catches.
System notifications (multip_system.inc) cover player/group creation,
player destruction/data and host migration. Destruction returns -1 even after
successful removal. Host migration publishes its flag before querying the
session and retains partial changes and leaks on failure. Application player
handlers in multip_app_players.inc preserve capacity equality checks,
reverse lookup, local-name publication for remote grants and the ignored
publication failure in the sender-ID fallback. These suites catch 8 and 12
mutations respectively.
multip_updates.inc covers messages 1000, 1017, 1018 and 1019, including
sequence wrapping, the zero-byte anchor copy, signed velocity conversions,
caller-stack padding and the running timing average. Thirteen mutations are
caught. Malformed matching lengths below 0x6c still need explicit guarded
coverage before whole-module acceptance.
multip_textures.inc covers catalogue/path/read/write/cleanup and messages
1015/1016. The fixture uses real retail G3D record stepping and a memory
filesystem; guarded path inputs produce 34,958 pointer-fault outcomes in
20,000 scenarios. All fourteen mutations are caught. Host +0x304 clears only
the list count and returns the original list pointer; it does not free or
reset the backing array.
multip_texture_receive.inc covers 101d/e/f and both linked-list levels.
The 101d wire fields are size at +0x18, texture ID at +0x1c, and segments at
+0x20. The internal add signature is owner/name/size/ID/segments. Removing a
non-head owner still replaces the global head with that owner's successor,
detaching earlier owners without freeing them. Ten mutations are caught;
the alternative unlink mutation repairs both links so it completes rather
than crashing on a dangling owner.
multip_transfer_messages.inc builds 101a/b/c/d/e, including raw fifteen-byte
names, ignored title/strcpy results, and initial packet leaks on enqueue
failure. The model header ignores its second argument and reads the current
GLOBAL aircraft total after callbacks. Twelve mutations are caught.
multip_texture_send.inc covers the file cache, recipient lists, CHAIN 2
registration and timed chunks plus the mutable 1015 request. Cached file data
is retained when a file node is removed. It subtracts 1.0 after reaching the
timer threshold; multip_model_send.inc instead resets its accumulator to
-1.0. Both compare the extended sum before the double store and retain raw
FNSTSW returns and pointer returns. Texture tests compare all fifteen name
bytes, including after NUL. The suites catch twelve and ten mutations.
multip_model_cache.inc serializes four fields in order 0x20, 0x28, 0x14,
0x1c, then header and model bytes. Path-join failure on the initial path
returns success, as retail does. Loading reallocates both buffers, closes the
file, then allocates the model's path string. Replacement retains the old
position and frees the temporary path/model/header in that order. Eight file
mutations are caught. The fixture has byte cursors and independently chosen
short-transfer stages, so later short reads/writes are reachable.
multip_model_receive.inc covers 101a/b/c and the transfer list. Tests include
duplicate announcements, out-of-order chunks, wrapped segment counts,
missing peers, failed frees and completion through cache publication and
peer replacement. Twenty thousand scenarios match, with 57 pointer-fault
outcomes from retained retail failure paths; all twelve mutations are caught.
The completion fixture currently uses an empty texture catalogue; nonempty
catalogues are covered independently by the texture suite.
The previous full checkpoint had 35 suites of 20,000 scenarios with zero
differences. The receive/lifecycle checkpoint below adds two top-level suites.
multip_title_receive.inc adds title announcements (100e), local ACONTAIN
selection, UTIL +0xa0 cache probing, cache replacement and optional 1010
requests. Its 20,000-scenario suite and seven mutations pass. Outgoing full
state and compact position constructors in multip_state_messages.inc pass
20,000 scenarios and ten mutations. They preserve both compact two-byte
padding words and the full-state packet's 0x6c-byte uninitialized tail through
painted caller-stack fixtures. multip_control_updates.inc adds control
change detection, collision cooldowns and state throttling; its helper suite
passes 20,000 scenarios and twelve mutations. The complete control callback
at +0x2870 now has a separate 20,000-scenario fixture. It drives flight-model
+0x2c/+0x30 services, raw failure returns,
signed timesteps, wrapped clock arithmetic, state publication and collisions.
Retry dispatch uses messages 1008 and 1009 for configured types 1 and 2;
the first integration comparison exposed and corrected the wrong retry tags.
The fixture varies all three x87 precision modes and four rounding modes,
paints the flight model's callee-left padding word, and compares per-action
rate/velocity state, timestamps, retries, queues, heap and GLOBAL. All thirteen
deliberate errors are caught by --slice control_callback; the serial rerun
completed after an earlier concurrent Wine timeout.
The x86 callback has the retail stack layout needed for packet padding; the non-x86 C callback is implemented but is not covered by this x86 fixture.
The full mutation sweep remains incomplete: the 324-case run stopped on a Wine timeout in the drawing slice. The current 427 mutation targets all match exactly once, but the full sweep still needs completion. The earlier 154-case sweep and individual new owner slices passed before the naming cleanup. Do not count a timeout or fixture abort as a catch.
The earlier naming audit covered 70 MULTIP source/include/resource, generator,
fixture and mutation files. The cleanup renamed 119 identifiers across 18
files and retained standard <stdint.h> types. All project-owned identifiers
are snake_case; external Windows/COM names and required exports are preserved.
src/multip/AGENTS.md records the user's naming correction for future work.
Retail is based at 0x20850000. It has 43,318 bytes of .text, 0xa00 bytes
of initialised .data, and uses the rest of the .data page for zero-filled
runtime state. ImportTable is at .data+0x000 and Linkage at +0x058.
The imports and their pointer cells are:
| Import-table offset | Pointer offset | Module |
|---|---|---|
+0x00 |
+0x04 |
host |
+0x08 |
+0x0c |
GLOBAL |
+0x10 |
+0x14 |
ACONTAIN |
+0x18 |
+0x1c |
CHAIN |
+0x20 |
+0x24 |
HG2D |
+0x28 |
+0x2c |
G3D |
+0x30 |
+0x34 |
MAIN |
+0x38 |
+0x3c |
NUMERIC |
+0x40 |
+0x44 |
UTIL |
+0x48 |
+0x4c |
WEATHERM |
| Slot | Retail address | ABI/meaning | Port |
|---|---|---|---|
+0x04 |
.text+0x0020 |
no arguments; register module and first CHAIN 0x14 callback |
stub |
+0x08 |
.text+0x0070 |
no arguments; unregister the host module handle | stub |
+0x18 |
.text+0x8e60 |
no arguments; load transport and start/join/stop a session | verified toggle |
+0x1c |
.text+0x39a0 |
no arguments; options/session dialog entry | verified dialog |
+0x20 |
.text+0x9840 |
stdcall (to_id, payload, size, flags); enqueue an application message |
exact enqueue |
+0x24 |
.text+0x9de0 |
no arguments; return {count, records} |
exact |
+0x28 |
.text+0x8680 |
stdcall (player_id); return a player record |
exact |
+0x2c |
.text+0x8690 |
no arguments; return local-player record | exact |
+0x30 |
.text+0xa560 |
no arguments; return multiplayer status bits | exact |
+0x34 |
.text+0xa5b0 |
stdcall (int32_t *result); conditional MAIN result override |
exact |
+0x38 |
.text+0xa5f0 |
stdcall (int32_t *result); selected aircraft range |
exact |
The functions are widely separated because Linkage points to public entries
among many internal MSVC functions; the byte counts printed by
dumplinkage.py are gaps between public entries, not function sizes.
Remote records are 0x100 bytes. The live count is at .data+0x660 and the
allocated array pointer at +0x678. The local record pointer is at +0xa3c.
The first fields established so far are:
| Offset | Meaning |
|---|---|
+0x00 |
name pointer; MPCHAT displays this directly |
+0x04 |
DirectPlay player ID |
+0x08 |
record type (1 and 2 are counted separately internally) |
+0x0c |
state flag; zero on a type-1 record sets status bit 2 |
+0x28 |
associated remote-aircraft state; tested by +0x34 and +0x38 |
The local-record refresh at .text+0x9ea0 also establishes the following
fields. Their deeper meaning is not yet required by the implemented slice, so
the source data is more reliable than speculative names:
| Offset | Source written by retail |
|---|---|
+0x20 |
GLOBAL[GLOBAL+0x11e0]+0x04 |
+0x24 |
GLOBAL[GLOBAL+0x11e0]+0x08 |
+0x28 |
pointer at GLOBAL+0x11e4 |
+0x2c |
GLOBAL[GLOBAL+0x11e0]+0x18 |
+0x30 |
GLOBAL[GLOBAL+0x11e0]+0x0c |
+0x34 |
literal 1 |
+0x38 |
zero-extended byte at GLOBAL+0x609 |
+0x28 tests the local record first, then searches the remote array backwards.
That reverse order is observable if duplicate IDs exist and is covered by the
differential test. +0x24 writes the count and pointer to the stable two-dword
result at .data+0xef8 and returns that result's address. DYNAMIC consumes
exactly this shape and filters records whose +0x08 is 1 and +0x0c is
zero.
The +0x30 result is a mask:
| Bit | State |
|---|---|
0 |
the DirectPlay loader succeeded (.data+0xa48) |
1 |
a session is connected (.data+0xaac) |
2 |
while connected, at least one type-1 remote record has +0x0c == 0 |
MAIN uses these bits to enable and check its multiplayer menu items.
+0x34 always returns zero. It writes -1 through its sole argument only
when a session is connected, DISTANCE_DISPLAY is enabled, the player mode is
1, the active player ID resolves, and that record's +0x28 is nonzero. MAIN's
caller makes this a result override; “collision result” is the current
interpretation, not a confirmed field name.
+0x38 also always returns zero and leaves its output unchanged unless the
session is connected, DISTANCE_DISPLAY is enabled, the player mode is
nonzero, the active ID resolves, and the record has an aircraft-state pointer
at +0x28. It asks UTIL +0x64 for the distance between GLOBAL+0x560 and
aircraft-state +0x04, using flags 3, then writes the truncated value of
distance * 0.0005396115152459395 * 10. The two multiplications and MSVCRT
_ftol truncation are reproduced with x87 operations. The current source name
M_AIRCRAFT_ACTIVE for .data+0xab4 is provisional and should eventually be
renamed to M_DISTANCE_DISPLAY; the profile-key mapping proves the latter.
The initialized .data ends at +0x9ff, but the loader maps the complete
page. Retail uses both the initialized tail and the zero-filled remainder.
Keep the offsets stable because the differential harness seeds them directly.
| Data offset | Confirmed or bounded meaning |
|---|---|
+0x174 |
session-list update time and destination passed for the player-list read |
+0x178 |
player-list update time value subsequently range-checked by retail |
+0x660 |
remote-record count |
+0x678 |
pointer to the contiguous 0x100-byte remote-record array |
+0x980 |
loaded DirectPlayLobbyCreateA export |
+0x984 |
session/open transition flag; exact public meaning unresolved |
+0x988 |
live IDirectPlay3A * |
+0x98c |
loaded DirectPlayCreate export |
+0x990 |
loaded DirectPlayEnumerateA export |
+0x998 |
configured maximum players, clamped to 1..255 (zero becomes 8) |
+0x99c |
configured maximum observers, clamped to at most 255 |
+0x9a0 |
live count of type-1 records |
+0x9a4 |
live count of type-2 records |
+0x9a8 |
player-name buffer, profile read with capacity 0x13 |
+0x9c0 |
0x50-byte session/open descriptor scratch block |
+0xa10 |
configured maximum track-name display time, clamped to 5..30 |
+0xa34 |
host module-registration handle/output |
+0xa38 |
value returned by host +0x58, used in session descriptors |
+0xa3c |
local 0x100-byte player-record pointer |
+0xa40 |
local DirectPlay player-created flag |
+0xa44 |
successful DirectPlay interface-query flag |
+0xa48 |
DirectPlay loader succeeded; status bit 0 |
+0xa5c |
player mode; zero disables selected-player lookup |
+0xa60 |
selected/active DirectPlay player ID |
+0xa6c |
CONNECTION_TYPE option |
+0xa70 |
PROTOCOL_SELECTION option |
+0xaa0, +0xaa4 |
DirectPlay group IDs used by add/remove helpers |
+0xaa8 |
HOST_SESSION option/current host-session flag |
+0xaac |
connected/open-session flag; status bit 1 |
+0xab0 |
ADF_TRACK option |
+0xab4 |
DISTANCE_DISPLAY option |
+0xab8 |
AUTOPILOT_LOCK option |
+0xabc, +0xac0 |
descriptive-message send/receive options |
+0xac4, +0xac8 |
texture send/receive options |
+0xacc, +0xad0 |
aircraft-model send/receive options |
+0xae0 |
session-name buffer, profile read with capacity 0x1d |
+0xb00 |
0x104-byte internet-address/path buffer |
+0xf00 |
GUARANTEED_MESSAGES option |
+0xf1c |
derived value 18 * maximum_track_name_display_time |
+0xf24 |
both worker subsystems started flag |
+0xf28 |
one-byte checksum/cache used by an aircraft update path |
+0xef8 |
stable {remote_count, remote_records} result block |
Offsets not listed here are not free. Large initialized descriptor tables run
through +0x93f, and transport, interpolation, queue, and dialog state occupy
much of +0x940..+0xeff. Name an unknown by offset until its reads and writes
establish its role.
The public init/deinit entries are small, but init exposes two larger callback paths and therefore must not be implemented with a dummy callback.
.text+0x0020calls host+0x298(Linkage, &data[0xa34]), saves host+0x58's result at+0xa38, and adds.text+0x0090to CHAIN list0x14..text+0x0070calls host+0x29c(data[0xa34]). It does no other teardown.- The first callback at
.text+0x0090recognizes messages0x36and0x3a. Message0x36loads the profile at.text+0x8f30, fills the 0x104-byte address/path buffer through host+0x2a0, registers the second callback, starts the lower-level subsystems through.text+0x9e00, then enters the lobby/connection path at.text+0x9990. Message0x3adeep-copies and relocates a supplied compound block, conditionally passing it to.text+0x9a20when DirectPlay is loaded. - The second callback at
.text+0x0220recognizes message0x37. It runs the disconnect path at.text+0x8cf0, saves the profile at.text+0x93e0, tears down supporting state, and calls.text+0x9e20.
Those message numbers and call boundaries are confirmed; descriptive event
names are intentionally deferred until the CHAIN producer is traced. The
init fixture now logs registration, dispatches 0x36, 0x37, and 0x3a,
and compares host/CHAIN calls, heap ownership and modified state with retail.
The profile section name is MULTIPLAYER. The loader at .text+0x8f30 and
saver at .text+0x93e0 cover these keys:
| Key | Primary data destination |
|---|---|
ALWAYS_SHOW_INITIAL_DIALOG |
+0x180 |
SESSION_NAME |
+0xae0 |
HOST_SESSION |
+0xaa8 |
PLAYER_NAME |
+0x9a8 |
GUARANTEED_MESSAGES |
+0xf00 |
EXTRAPOLATION |
+0x184 |
MAX_PLAYERS / MAX_OBSERVERS |
+0x998 / +0x99c |
TICKS_PER_SYNC / PACKETS_PER_SEC |
+0x164 / +0x1a8 |
COLLISION_SECS / FOLLOWING_MULTIPLIER |
+0x160 / +0x168 |
MAX_TRACK_NAME_DISPLAY_TIME |
+0xa10 |
MAX_SEGMENT_SIZE / MAX_SEGMENTS_PER_SEC |
+0x16c / +0x170 |
SESSION_LIST_UPDATE_TIME |
read into and validated at +0x174 |
PLAYER_LIST_UPDATE_TIME |
retail passes +0x174 to the reader, then validates +0x178 |
CONNECTION_TYPE / PROTOCOL_SELECTION |
+0xa6c / +0xa70 |
INTERNET_ADDRESS |
+0xb00 |
ALLOW_DESCRIPTIVE_SEND / RECEIVE |
+0xabc / +0xac0 |
ALLOW_TEXTURE_SEND / RECEIVE |
+0xac4 / +0xac8 |
ALLOW_PLANE_MODEL_SEND / RECEIVE |
+0xacc / +0xad0 |
ADF_TRACK / DISTANCE_DISPLAY / AUTOPILOT_LOCK |
+0xab0 / +0xab4 / +0xab8 |
ALLOW_NAME_TAG_DISPLAY / ALLOW_KEEPALIVE |
+0x188 / +0x18c |
TIME_BETWEEN_PLAYER_TYPE_QUERIES |
+0x17c |
TIME_BETWEEN_CONNECTION_ATTEMPTS |
+0x194 |
The mismatched destination and validation for PLAYER_LIST_UPDATE_TIME are
visible in the retail instructions and may be a retail bug; reproduce them
unless the host service's contract proves it writes a larger overlapping type.
The public send entry at .text+0x9840 is only a four-argument stdcall wrapper
around .text+0x87e0. The latter applies the GUARANTEED_MESSAGES option and
passes the request to .text+0x7890, which allocates a 0x20-byte queue record,
copies the payload, chooses one of two send queues, and signals an event.
.text+0x79d0 removes one queued record and calls IDirectPlay3A::Send; it
retries DPERR_BUSY (0x8877010e) without discarding the record.
The enqueue path is now ported. Its record layout is {to_id, payload, size, flags, reserved[4]} (0x20 bytes). Each queue is {count, head, tail, CRITICAL_SECTION} (0x24 bytes); its 8-byte nodes hold {record, next}.
Appending holds the queue lock while allocating and linking the node.
Retail only ORs flag bit 0 when GUARANTEED_MESSAGES == 1, and selects queue
+0xe60 for any nonzero flags, otherwise +0xe64. It does not inspect
CONNECTED. Payload allocation and copying happen even for zero length.
Record, payload, copy, or queue failures return 0xffffffff without cleaning
up earlier allocations. Node allocation failure additionally leaves the queue
lock held. A failed SetEvent also returns failure, but the record is already
queued. The port preserves these observed orderings. Host allocation flag 1
normally makes allocation exhaustion fatal; the harness injects nonzero host
returns to check the branches that remain in the retail code.
Send-one (.text+0x79d0), normal draining (+0x7970), and full flushing
(+0x7aa0) are now ported and tested against retail. Send-one removes the
queue node before calling IDirectPlay3A::Send, using the local record's
+0x04 as from-ID. Only DPERR_BUSY retries; all other HRESULTs, including
generic errors, proceed to freeing the payload and record and return success
if cleanup succeeds. Node-free failure returns before sending and leaves the
record/payload allocated; payload-free failure leaves both allocated;
record-free failure leaves the record with its payload pointer cleared.
The queue has already advanced in each case, and its lock is released even
when node freeing fails.
Normal draining empties plain queue +0xe64 before sending at most one
flagged message from +0xe60. Full flush empties both queues in that order.
A missing queue counts as empty, but send-one on a missing queue returns
failure, so a normal drain with a missing flagged queue fails whereas a full
flush can succeed. Empty, non-null queues succeed without sending.
The receive helper at .text+0x5820 calls IDirectPlay3A::Receive at
.text+0x5875. Its work record contains from-ID, to-ID, a buffer pointer and
buffer size. On
DPERR_BUFFERTOOSMALL (0x8877001e) it allocates or resizes through host
+0x20c/+0x210 and retries. A successful record is queued with the value
returned by host +0x38, after which higher-level message dispatch owns it.
Receive-one (.text+0x5820) and receive-dequeue (+0x5980) are ported.
A receive record is 0x14 bytes: {from_id, to_id, payload, size, timestamp}.
Each receive-one starts with a fresh zero-filled record and calls Receive
with flags 1 and a null buffer/zero capacity. DPERR_BUFFERTOOSMALL allocates
the first buffer and reallocates on subsequent growth, retrying until another
HRESULT arrives. A successful result sets the caller's received flag to 1,
reads host +0x38 into timestamp, and appends to queue +0xdf8.
Other receive HRESULTs, including no-message and generic errors, free the buffer (if present) and record and return zero with received=0. Allocation, reallocation, free and queue failures return -1; earlier allocations remain where retail leaves them. In particular, successful receive followed by queue append failure leaves received=1, and node allocation failure leaves the queue lock held. Receive-dequeue sets its received flag only after a successful queue take; node-free failure can return a non-null record while leaving that flag zero. Dequeued records and their payloads belong to the higher-level consumer; these helpers do not free them.
There are two independent event/thread subsystems:
| Role | Setup | Teardown | Start | Stop | Thread entry | Main unit of work | State |
|---|---|---|---|---|---|---|---|
| receive | .text+0x56a0 |
+0x56e0 |
+0x5730 |
+0x5770 |
+0x57a0 |
+0x5820 |
events +0xdf0/+0xdf4, queue +0xdf8, thread +0xdfc, id +0xe00, running +0xe04 |
| send | .text+0x7700 |
+0x7750 |
+0x77b0 |
+0x77f0 |
+0x7840 |
enqueue +0x7890, drain +0x7970, send +0x79d0 |
events +0xe58/+0xe5c, queues +0xe60/+0xe64, thread +0xe68, id +0xe6c, running +0xe70 |
Both worker lifecycles and queue construction/clear/destruction are ported and verified. Setup creates the work event before the stop event, both unnamed, auto-reset and initially unsignalled. It ignores event failures and returns only queue construction failures, without rolling back partial setup. Receive builds one queue; send builds flagged then plain queues. Teardown reverses queue construction order, then closes work and stop events, returning on the first failure. Repeated setup overwrites prior pointers and handles; teardown leaves event cells unchanged.
Start sets its running flag before CreateThread and returns zero even if
creation fails, leaving running=1. Repeated starts do nothing while that flag
is set. The security attributes at data +0x538 and +0x548 are {12, 0, 0}.
Both entries wait on stop/work handles with WaitForMultipleObjects,
wait-all=false and timeout=INFINITE. Stop clears running; work drains receive
until no message remains, or invokes normal send-drain once. Other wait
results loop again. A receive error exits with -1 without clearing running;
the send worker ignores drain failures.
Receive stop signals its stop event, ignores the result of the 100 ms wait, and retains its thread handle. Send stop first clears flagged then plain queues, signals stop, ignores the 100 ms wait result, and returns the thread handle close result. Queue clear frees the record and node for each entry, leaving record-owned payloads allocated. It advances head before freeing, retains its lock on a free failure, and clears count/tail only after complete success. Queue destruction then deletes its critical section and frees the queue, returning immediately if either clearing or freeing fails.
The combined wrappers are also ported: .text+0x9e00 sets up receive then
send, and +0x9e20 tears down send then receive. +0x9e40 starts send then
receive only while data +0xf24 is zero, and sets that shared flag after both
calls. +0x9e70 stops receive then send while the flag is nonzero, and clears
it only after both calls succeed. They do not roll back failures. Public
session entries reach the workers through completion; the init checkpoint
also covers subsystem setup/teardown through both entry callbacks.
The observed Win32 imports are InitializeCriticalSection,
DeleteCriticalSection, EnterCriticalSection, LeaveCriticalSection,
CreateEventA, SetEvent, CloseHandle, CreateThread,
WaitForSingleObject, WaitForMultipleObjects, Sleep, wsprintfA,
ShellExecuteA, and MSVCRT _ftol. ShellExecuteA is called at
.text+0x41d8 with the runtime buffer at +0xb00; a harness must log and
suppress it.
The main object at .data+0x988 is conclusively IDirectPlay3A, not a custom
COM-shaped interface. The evidence is both static and structural:
.rdata+0x070is byte-for-byteIID_IDirectPlay3Aand is passed toQueryInterfacewhile filling.data+0x988.- Calls through the object use the inherited
IDirectPlay2Avtable at the documented offsets:AddPlayerToGroup+0x0c,Close+0x10,CreateGroup+0x14,CreatePlayer+0x18,DeletePlayerFromGroup+0x1c,DestroyGroup+0x20,DestroyPlayer+0x24,EnumGroups+0x2c,EnumPlayers+0x30,EnumSessions+0x34,GetPlayerName+0x54,GetSessionDesc+0x58,Open+0x60,Receive+0x64,Send+0x68,SetPlayerName+0x78, andSetSessionDesc+0x7c. - The usual
QueryInterface(+0x00) andRelease(+0x08) calls bracket its lifetime.
The loader boundary is also known. A descriptor at .data+0x148 names the
three output cells +0x98c, +0x990, and +0x980; host +0x124 fills them
at .text+0x9cf0, and host +0x128 unloads them at .text+0x9d20.
.text+0x9d50 calls DirectPlayCreate at +0x98c, queries the temporary
object first with IID_IDirectPlay2A at .rdata+0x050 and then with
IID_IDirectPlay3A at .rdata+0x070, stores the latter at +0x988, and
releases the temporary interfaces. DirectPlayLobbyCreateA at +0x980 is
used only by the lobby/connection path after host +0x120; the observed lobby
object calls Release at vtable +0x08 and SetConnectionSettings at
+0x30.
The loader/interface-lifetime helpers above are now ported and verified. The
host boundaries are stdcall with one argument: host +0x124 at 0x413570
and host +0x128 at 0x413620 both end in ret 4. The module's descriptor
contains three addresses of output cells, with no names or terminator; host
code owns the DLL and export names. The port initializes those addresses in
multip_install to preserve relocation and the data +0x148 ABI.
Load normalizes any nonzero host result to -1. On host success, it sets loaded
flag +0xa48 to 1 only when DirectPlayEnumerateA is non-null. An absent
enumerator, or load failure, leaves a previous flag unchanged. Other exports
are not checked here. Unload releases a non-null +0x988 interface, clears its
cell, and then calls the host unloader; it leaves both loaded +0xa48 and
query-success +0xa44 flags unchanged, regardless of the host return.
Create (.text+0x9d50) takes a provider GUID pointer (nullable), clears query
flag +0xa44, and calls DirectPlayCreate(provider, &created, NULL). Factory
failure returns -1 without releasing any partially returned object. The first
query, for IID_IDirectPlay2A, also returns -1 on nonzero HRESULT without
releasing the factory reference. Both IID queries use the factory object;
the first queried interface is only retained for later release. The second
query writes directly to global +0x988. Zero sets query flag=1, but a nonzero
second-query result is not propagated: both temporary interfaces are released
(first queried, then factory), and the function returns zero. Query output
writes on failure are not repaired, and a pre-existing global interface is not
released before replacement. These are retail behavior, not cleanup fixes.
This reduces the transport model to a well-defined fake IDirectPlay3A plus
a very small fake IDirectPlayLobbyA.
The receive loop treats DPERR_BUFFERTOOSMALL (0x8877001e) as a resize and
retry, while the send loop retries DPERR_BUSY (0x8877010e). The first
fake can therefore implement only the methods
listed above, scripted sessions/players, and a deterministic receive queue.
The fake should expose call logs as data, not assertions buried inside COM methods. At minimum each scripted call records the method, object identity, player/group IDs, flags, buffer size, and relevant input bytes, then returns a queued HRESULT and outputs. It needs:
- export shims for
DirectPlayCreate,DirectPlayEnumerateA, andDirectPlayLobbyCreateA; - reference counting and both observed
QueryInterfaceIIDs; - lobby
ReleaseandSetConnectionSettingsbehavior; - deterministic ID allocation for
CreateGroupandCreatePlayer; - scripted
EnumSessions,EnumGroups, andEnumPlayerscallbacks; - mutable player names and session descriptors for the get/set pairs;
- group membership for
AddPlayerToGroup,DeletePlayerFromGroup,DestroyGroup, andDestroyPlayer; - a receive queue capable of the buffer-too-small first pass;
- a send script capable of one or more busy results before success;
OpenandClosestate with injectable failures at each stage.
Use the real retail DLL on one side and the same fake object/script on the OpenFS98 side. Normalize allocated pointers, COM object addresses, handles, thread IDs, and callback addresses before comparing results.
make multipdiff-run defaults to 20,000 deterministic randomized scenarios.
It loads retail and OpenFS98 together, seeds equivalent local and remote
records plus fake GLOBAL and UTIL imports, and compares normalized results for
the recovered +0x24 through +0x38 slice: the result block, local and remote
lookups, reverse duplicate-ID selection, missing IDs, all three status bits,
active/inactive collision overrides, every active-range guard, the UTIL call
shape, and the range conversion. The current result is 20,000 scenarios with
zero differences.
A separate 20,000-scenario run covers +0x20 enqueue. Each scenario sends six
messages through real Win32 critical sections and an auto-reset event backed
by a deterministic fake host heap. It compares return values, per-send event
signals, ordered allocation/copy calls, both queue chains, payload bytes,
reserved fields, leaked partial records, and lock recursion counts. Inputs
cover zero-length payloads, flags 0/1/2/random, guaranteed option values 0..3,
connected/disconnected state, missing queues, invalid event handles, and
injected failures at each host-call position after two initial sends. Caller
buffers are overwritten after each send to check that queued data is copied.
A third 20,000-scenario suite invokes send-one/drain/flush directly, after
enqueueing zero to six messages. A fake IDirectPlay3A::Send at vtable +0x68
logs object identity, from/to IDs, flags, sizes, payload bytes and scripted
HRESULTs, interleaved with host allocation/copy/free calls. The suite compares
live allocations, remaining queue chains, return values and queue counts
after each drain step, including repeated flushes, multiple messages in each
queue, zero to three busy retries, success and generic errors, missing queues,
and failures at node, payload and record frees. Full-flush cases begin with
both queues populated to check ordering independently of normal draining.
Retail's private helpers are called at loaded-image RVAs 0x89d0, 0x8970
and 0x8aa0; receive-one and dequeue use 0x6820 and 0x6980.
make multipdiff builds a separate MULTIP_test.dll with
MULTIP_DIFFTEST, exposing the multip_test_entries address table. The normal
bin/MULTIP.DLL exports only ImportTable and Linkage; no public slots are
added to reach internal helpers. Staging copies use temporary files and atomic
renames so rebuilding a fixture cannot truncate a DLL already mapped by Wine.
make multip-send-breaks builds temporary source mutations and DLLs in /tmp.
The unmodified baseline must pass, then nine mutations must produce completed
delivery comparisons with differences: busy retry, ignored HRESULT, send
arguments, payload cleanup, the plain drain loop, the flagged drain limit,
dequeue tail, full-flush ordering, and node-free failure. Crashes, timeouts and
fixture errors do not count as catches.
A fourth 20,000-scenario suite covers receive-one/dequeue with three receive
attempts and four dequeue attempts per scenario. The fake Receive logs the
input IDs, capacity, flags and buffer identity as well as output IDs, required
sizes, HRESULTs and payload bytes. Cases include immediate zero-byte success,
no-message/generic errors, one to four buffer-growth passes, moving realloc
with preserved contents, timestamp values, missing queues, and injected
allocation/reallocation/free failures. Snapshots compare queue counts after
each receive, dequeue pointers and flags, the complete call transcript, live
allocations, leaked partial records, and held locks. The fake heap records
freed storage for comparison but does not reuse it within a scenario.
make multip-breaks includes the forty-two transport/worker/DirectPlay mutations
and the additional owner checks described above; the send-only
alias is make multip-send-breaks and the receive-only alias is
make multip-receive-breaks. The six receive mutations cover retry, flags,
timestamp, successful-receive flag, error cleanup, and dequeue flag behavior.
make multip-worker-breaks checks sixteen lifecycle mutations: event order,
repeated start, thread creation failure result, security attributes, receive
handle retention, shutdown wait duration, send handle close, send queue clear,
ignored send errors, receive-until-empty, running flag after receive failure,
record freeing, held locks on clear failure, critical-section deletion, and
the shared start/stop flag.
A fifth 20,000-scenario suite covers worker lifecycles. It patches the two DLLs' Win32 import cells with deterministic event, thread, wait, close and critical-section services. Thread creation records the callback; callbacks run synchronously under scripted waits, sometimes when the stop call waits for completion. Completed callbacks are not rerun. Twenty-four action slots mix individual and combined setup/start/stop/teardown, repeated setup/start, restart after completion, queued work, repeated stops and partial teardown.
Each side receives independent but equivalent fake handles and heap records.
The suite compares a unified ordered Win32/host/DirectPlay call transcript,
worker state after every action (including shared +0xf24), queue headers,
locks, live and leaked allocations, event signalling, handle lifetime and
callback results. Inputs inject event/thread/SetEvent/CloseHandle and host
allocation/free failures, failed/timeout waits, transport errors and busy
retries. Failure schedules are independent of deferred execution so worker
error paths are reached. This proves controlled callback behavior and ABI;
it does not exercise OS thread scheduling races.
A sixth 20,000-scenario suite covers DirectPlay loader/interface lifetime at
private RVAs 0xacf0, 0xad20 and 0xad50, exposed as test-table indexes
22–24. Eight actions mix repeated load, create/query, unload, and create after
unload. Four fake COM identities distinguish factory, first query, second
query and pre-existing global interfaces. The suite compares ordered host and
COM calls, exact IID/provider bytes, output-cell addresses, state visible
inside callbacks, per-action module/status snapshots, and reference counts
(including leaked or overwritten references).
Scripts cover nullable provider GUIDs, each export-presence combination, zero/positive/error HRESULTs, partial outputs on failed calls, query failure that clears or preserves its destination, and arbitrary Release returns. Factory availability is seeded independently before calling create, so a missing factory export is compared as loader state rather than executed as null code. The original lifetime fake is separate from the send/receive fixtures. The receive/lifecycle fixture now combines session, player, group and transport contracts; the later lobby fixture also exercises creation and connection through completion and disconnect. No real DirectPlay DLL or network is used.
make multip-directplay-breaks checks eleven mutations: loader export
selection, loaded-flag retention, export descriptor order, unload flag and
release-state ordering, first-query leak, second-query target and error return,
temporary release order, factory outer argument, and query-flag reset. As with
the other slices, a mutation counts only when its comparison completes with
differences; a crash or fixture error is not a catch.
This loader/lifetime slice is not a whole-module 20k result. Receive dispatch,
session integration, lobby/configuration callbacks and public +0x18/+0x1c
are verified in the later checkpoints below. Init/deinit and their entry
callbacks are implemented in the final checkpoint. Worker lifecycles are verified through
controlled internal calls and the later completion/lobby fixtures.
Run the current checkpoint with:
make multipdiff-run
make multipdiff-run N=20000
make multip-breaksThe first command already defaults to 20,000; the explicit form is useful in logs. The expected differential checkpoint lines are:
MULTIP +0x24..+0x38: 20000 scenarios, 0 differ
MULTIP +0x20 enqueue: 20000 scenarios, 0 differ
MULTIP send-one/drain/flush: 20000 scenarios, 0 differ
MULTIP receive-one/dequeue: 20000 scenarios, 0 differ
MULTIP DirectPlay loader/lifetime: 20000 scenarios, 0 differ
MULTIP profile load/save: 20000 scenarios, 0 differ
MULTIP session/group primitives: 20000 scenarios, 0 differ
MULTIP session descriptors/name metadata: 20000 scenarios, 0 differ
MULTIP aircraft select/release: 20000 scenarios, 0 differ
MULTIP aircraft copy/session browser records: 20000 scenarios, 0 differ
MULTIP track-name display: 20000 scenarios, 0 differ
MULTIP application message constructors: 20000 scenarios, 0 differ
MULTIP player lifecycle helpers: 20000 scenarios, 0 differ
MULTIP local player creation/start: 20000 scenarios, 0 differ
MULTIP session CHAIN 0x14: 20000 scenarios, 0 differ
MULTIP player drawing/CHAIN 0x10: 20000 scenarios, 0 differ
MULTIP aircraft extrapolation/CHAIN 0x13: 20000 scenarios, 0 differ
MULTIP view commands/CHAIN 0x0c: 20000 scenarios, 0 differ
MULTIP weather/sync messages: 20000 scenarios, 0 differ
MULTIP tick callbacks/CHAIN 0x02: 20000 scenarios, 0 differ
MULTIP worker lifecycles: 20000 scenarios, 0 differ
multip_dispatch.inc implements the receive callback (+0x47f0), top-level
sender split (+0x4890), DirectPlay system dispatch (+0x48e0), application
dispatch (+0x4960), and reverse-order unknown-peer expiry (+0x89a0).
The application switch reaches all 31 handlers in 1000–101f; 100b and
unknown tags forward {sender, packet, size} to CHAIN 0x1d/5, ignoring its
return. tools/gen_multip_dispatch.py extracts and checks the retail jump
table before emitting named C/assembly calls, rather than executable bytes.
There is no added minimum-packet-size check. Only 1017 receives the timestamp.
The x86 app dispatcher retains its 12-byte frame and handler nesting, including
1013 weather-reply and 1019 position padding below the entry stack. The top
sender dispatcher normalizes nonzero handler results to -1; the app/system
switches preserve their raw returns.
Receive pumping expires unknown peers before dequeuing. It frees payload then record only after successful dispatch. A NULL payload leaks its record; failed dispatch, node free, payload free or record free preserves retail's partial state. The callback propagates host-free failures unchanged.
multip_lifecycle.inc adds receive/control registration (+0x27c0/+0x2810),
aggregate registration/removal (+0x8220/+0x8270), common completion
(+0x9c70) and disconnect (+0x8cf0). Owner flags survive partial failures,
registration order is preserved, and there is no added rollback. Completion
starts workers, registers callbacks, creates/publishes the local player,
applies receive membership, sets the started-local flag, enumerates peers,
then sends the initial ping/type queries. Disconnect broadcasts host departure,
flushes sends, clears receive records, closes its progress display, removes
callbacks, stops workers, resets selection, destroys players, closes DirectPlay
and unloads it. Its positive signed delay is now - start + 3000, preserving
retail's addition of elapsed time. Progress calls are void and DestroyPlayer errors are
ignored; Close's raw HRESULT is retained unless a later unload fails.
Handlers 1005, 1006, 1016 and system 31 now use that disconnect path. Rejected 1005 calls DirectPlay Close before clearing the connected flag; its following disconnect then does nothing. Their confirmation ordering and error boundaries are compared with active and already disconnected sessions.
The dispatch fixture runs the existing player, timing/state, weather, system,
model/texture receive, title and model-send scenarios through the recovered
switches. The lifecycle fixture runs repeated registration/removal, completion,
queued receive processing, expiry, notification-triggered disconnect and
repeated close over deterministic COM, CHAIN and worker services. It compares
ordered calls, callback identities, packet hashes, return values, flags, GLOBAL,
heap ownership and queue states. Failure injection reaches late teardown;
clock steps include zero and wrap, with positive and negative signed sleep
delays.
The completion entry keeps retail's nesting so local startup reads its
reserved DPNAME word after earlier CHAIN/worker calls. The fixture paints the
slot, changes it during registration, and paints its neighbors differently.
No real threads, networking or sleep are used by this integrated fixture.
Failed completion can leave no local player: subsequent send flushing can
fault in retail, and those faults are compared as outcomes before the scenario
ends. Fixture errors and process crashes remain verification failures.
The integrated fixture exposed a host-stub error: HOST +0x300 at 0x410210
computes an unsigned element index, then 0x410470 unconditionally decrements
the count and skips copying when the signed index is at or beyond the new end.
It does not reject a stale out-of-range record pointer. The player fixture now
models that behavior; MULTIP's stale-local-pointer behavior is retained.
make multipdiff-run N=20000 passes all 37 top-level suites with zero
differences; the dispatch suite additionally repeats eight handler families
through their real switches. --slice dispatch catches all four mutations;
--slice lifecycle catches all nineteen, including both completion stack-word
checks. The full 382-target mutation sweep remains pending.
The private test entries 206–220 cover these owners, handlers and switches.
The non-x86 C fallbacks are implemented but are not validated by the x86 fixture.
This receive checkpoint is internal integration, not whole-module acceptance.
The later dialog checkpoint enables public +0x18/+0x1c; the init checkpoint
enables +0x04/+0x08.
The transport units and worker lifecycles in stages 1–4 are complete, including
queue construction/clear/destruction and the combined subsystem wrappers.
DirectPlay lifetime, profiles, host/join/group primitives, session descriptions,
player creation/removal, and all six session callback owners described above
are also verified, including receive/control ownership at +0x27c0, receive
dispatch, and integration of all callback owners and workers in common
completion +0x9c70 and disconnect +0x8cf0. Lobby/provider and
configuration/dialog wiring for public +0x18/+0x1c is now recovered in the
checkpoint below. Init/deinit and their callbacks follow in the final
checkpoint. The loader flag alone does
not prove the factory is available, and create's first query failure leaks a
reference; retain those boundaries in integrated cases.
The next public-entry boundaries are confirmed in .text+0x8e60: load first;
when connected, UTIL +0xa4 supplies confirmation before disconnect
+0x8cf0. The disconnected path first calls +0x3d60; data +0x1a4 == 0
unloads and == 3 returns 1. Otherwise data +0xaa8 chooses host helper
+0x3890 or join helper +0x2340, followed by common completion +0x9c70.
Disconnect includes CHAIN messages, send flush, receive queue clear, worker
stop, player destruction, IDirectPlay3A::Close, and unloader calls. These
are now also exercised through the public session toggle and its
configuration flow in the lobby fixture.
Disconnect's wait is Sleep((int32_t)(now - start + 3000)) when that signed
value is positive: retail adds elapsed time. Preserve that arithmetic.
The view-command callback at +0x2ef0 dispatches 0x1016c–0x1016f into
+0x3200/+0x2f40/+0x3020/+0x3250; the cycle helpers can loop forever when
no record is eligible and can read record zero even when count is zero.
Their existing command fixture compares fault/hang outcomes rather than
filtering those inputs.
Each stage should extend the harness before the corresponding Linkage slot is enabled. Keep the test output labeled with the exact covered slice until the last stage.
- Build the fixtures. Add the host heap/memory services, deterministic
events, CHAIN registration/dispatch, call logging, the DirectPlay export
loader cells, and the fake
IDirectPlay3A. HookShellExecuteAso retail can never launch anything. - Port the queue primitives and transport units. Start with receive
+0x5820, send enqueue+0x7890, send drain+0x7970, and send-one+0x79d0. Test allocation/reallocation failures, empty queues,DPERR_BUFFERTOOSMALL, repeatedDPERR_BUSY, success, and generic errors. - Port public send
+0x20. Cover both guaranteed-message settings, both queues, zero and nonzero payload lengths, disconnected state, and failure propagation. MPCHAT is the easiest real caller to inspect for argument meaning. - Port worker setup/start/stop. Test idempotent starts and stops, event creation failure at each index, thread creation failure, ordered draining, and the retail 100 ms shutdown wait.
- Port DirectPlay object/session helpers and public
+0x18. Cover loader failure, interface-query failure, host and join paths, session enumeration, group/player creation, open/close, every cleanup edge, and repeated calls while already connected or disconnected. - Port player/message dispatch. Recover every on-wire message tag and length check before naming structures. Cover joins, leaves, type changes, names, aircraft updates, multipart model/texture traffic, malformed input, duplicate IDs, and reverse lookup behavior.
- Port public dialog
+0x1cand profile round-trip. Model host dialog0x110b, UTIL+0x8c/+0x90, enumeration callbacks, and the suppressedShellExecuteApath. - Port init/deinit and both CHAIN callbacks. Only now enable
+0x04and+0x08, because their callbacks can reach every subsystem above. - Promote the harness to whole-module coverage. Then, and only then,
change the output from a slot range to
MULTIP whole module.
A final 20,000-scenario claim means all eleven public Linkage entries are non-stubs and invoked, plus asynchronous work is drained after every action. The scenario generator must mix:
- fresh init, repeated init-like messages, orderly shutdown, and injected failures at every host allocation, event, thread, and DirectPlay call;
- disconnected, hosting, joining, connected, and closing session states;
- zero, one, maximum, duplicate-ID, observer, and player record sets;
- all DirectPlay enumeration and name/session descriptor callbacks;
- empty, single, and multiple receive/send queues, including retry HRESULTs;
- every recovered application-message type, malformed sizes, and multipart boundaries;
- all profile booleans and boundary values, including retail clamps;
- pointer-normalized public returns, state snapshots, ordered host/CHAIN/COM call logs, sent payload bytes, and queued/received payload bytes.
Before installation, also require a clean make all, git diff --check, the
whole-module differential run with zero differences, and a game smoke test.
Only after those pass should MULTIP be added to MODULES; its installed name
must remain MULTIPLAYER.DLL through the existing Makefile mapping.
multip_providers.inc recovers DirectPlay provider enumeration (.text+0x41f0),
its five-argument callback (+0x4210), and catalogue cleanup (+0x42a0).
Private test entries 221–223 map to cleanup, callback and enumeration respectively.
These are configuration +0x3d60 dependencies; their original checkpoint kept
public entries guarded until the later lobby/configuration work below.
The callback appends a zeroed 28-byte record before allocating a zero-filled
name of strlen(description) + 1 bytes. Allocation failure returns false and
leaves that record appended. String and 16-byte GUID copy failures are ignored;
major/minor version writes still happen and the callback returns true. The
callback ignores its context.
Cleanup never advances its record pointer: it frees the first name, then
rechecks that now-null field for every remaining iteration. Other names leak.
A failed free returns -1 before clearing the list count. Enumeration consumes
that cleanup failure as success (zero), without calling DirectPlayEnumerateA.
Otherwise any nonzero enumeration HRESULT, including positive values, becomes
-1. HOST +0x304 clears only the count and retains the backing array.
The synthetic providers fixture compares five successive actions, ordered
host calls, callback identity/context, returns, descriptor state, GUID/version
bytes, name heap bytes and ownership, and partial records. It varies empty and
multi-provider catalogues, null first names hiding later names, empty and long
descriptions, positive/error HRESULTs and allocation/free/copy failures. Known
retail outcomes independently assert the first-name leak, swallowed cleanup
failure, version fields and positive-HRESULT normalization. List append is a
successful fixed-capacity host model; fatal host growth failures and real
DirectPlay/network behavior are outside this slice.
make multipdiff-run N=20000 passes all 38 top-level suites with zero
differences, including the retail boundary assertions in the provider fixture.
All eleven provider mutations are caught at 1,200 scenarios. The production
DLL builds without warnings and still exports only ImportTable and Linkage;
git diff --check passes. This remains slice coverage, not whole-module
acceptance. MULTIP has not been installed or added to MODULES.
Focused verification: make multipdiff, then from bin/difftest,
WINEDEBUG=-all wine multipdiff.exe 20000 providers. Mutation checks:
python3 -B tools/multip_breaks.py --slice providers. The earlier 382-target
full mutation sweep remains unfinished; the provider mutations extend that
set to 393 targets at that checkpoint. The later lobby/configuration work below
recovers the remaining browser/dialog owners and enables public +0x18/+0x1c.
multip_configuration.inc recovers setup, validation, provider selection,
reset, acceptance, options and external launch for dialog 0x110a
(.text+0x3d60–+0x41e0). multip_browser.inc covers session discovery,
matching/stale-record pruning, nested player lists, deep-copy callbacks and
cleanup (+0x42f0–+0x47ec). multip_options.inc recovers public +0x1c
and dialog 0x110b (+0x39a0–+0x3d57). The shared player-details dialog
0x110c, including capacity edits, periodic refresh, removal confirmation and
cleanup (+0x7b10–+0x805d), is in multip_details.inc. The callback pointers
in generated data are bound to these owners at DLL attachment.
multip_lobby.inc recovers startup with host-supplied or explicit connection
settings, lobby creation/application, host/join and interface connection
(+0x9990–+0x9c60). multip_toggle.inc implements public session toggle
+0x18 (+0x8e60). Both public entries are enabled. The later init checkpoint
enables init/deinit as well; MULTIP remains outside MODULES.
Preserved behavior includes:
- Configuration setup/dialog failures return cancellation (zero) without cleanup. Cancellation clears host/type state. Accepted launch mode calls ShellExecuteA only for mode 2 and ignores its result. Reverse name cleanup decrements the published count before freeing and ignores browser-clear errors.
- Session refresh marks old records stale, reinitializes matching records without freeing their previous name/list, and publishes only after successful EnumSessions. TIMEOUT returns false without inspecting a descriptor. Failed record setup returns -1, which DirectPlay treats as a true callback result. Marker/prune walker errors are consumed. Browser cleanup ignores walker errors, then frees its backing array through a temporary and resets fields only on success.
- Options save publishes the local name before swapping receive-group membership. Both name validations and both numeric parses run where retail runs them; invalid fields retain their individual control error codes. The weather-button entry retains the four caller-stack bytes transmitted at the end of message 1014.
- Player-details refresh uses unsigned elapsed time and a strict greater-than
comparison to
period * 1000, including wrap. Removing a player sends only after confirmation; early send/free errors leak the allocated strings. Success moves the next refresh time forward by 2000 ms. Capacity edits preserve the minimum-two clamp and per-control parse results. - Lobby creation and SetConnectionSettings HRESULTs are ignored. Host takes priority when both connection bits are set. Connect's second QueryInterface failure is consumed and leaves the queried flag unchanged; its temporary interface is still released. Later failure leaks the lobby interface. Explicit-connection startup returns -1 even after successful completion.
- Public toggle loads first, confirms before disconnect, and retains retail's mix of cancellation, zero, one and failure returns. It also proceeds from a nonzero configuration failure according to the retained type/host fields.
The integrated toggle fixture found an ordering gap in the previously recovered
multip_session_host: retail reads the interface's vtable before writing the
final capacity and name-pointer fields. A null interface faults before those
writes. The owner now preserves that ordering with a volatile vtable read.
Scenarios 7419 and 13537 exposed this gap in the 20,000-case lobby run.
The dialogs fixture drives the real callback blocks over deterministic host,
UTIL exchange and NUMERIC boundaries, session/player enumerators, and a stable
heap. It visits all three dialogs, repeated refresh/prune, valid/invalid fields,
allocation/copy/free failures, capacity publication, weather traffic and player
removal. ShellExecuteA is patched on both DLLs before any callback runs, so the
fixture cannot launch an external program. The only real UI-formatting import
used by the fixture is wsprintfA.
The lobby fixture drives all six lobby owners and public +0x18 through real
completion, options +0x1c and repeated disconnect. COM, CHAIN, thread creation,
events, waits and Sleep are scripted; no network, OS worker threads or real
sleep are used. Lobby, temporary connected and queried global interfaces have
distinct fixture identities, and settings start with nonzero flags independent
of MULTIP's configured session flags. Open failures are scripted independently
of memory/callback failures. Distinct deep stack words and a word changed by registration
check completion's unwritten DPNAME field through host/join/apply/start/toggle
nesting. Heap ownership/bytes, input session settings, queue metadata, returns,
module state and ordered calls are compared after actions. A deliberate null-interface
host call also targets the capacity/name write ordering directly, and refresh
inputs are aimed at each side's published timer boundary. Coverage assertions
require all dialog families, pruning, shell interception, player removal,
lobby creation/release and painted completion words to occur.
The host list stand-in follows 0x4102a0/0x410470: iteration increments the
nesting depth and defers removals, then applies queued indexes in reverse even
when a visitor returned an error. Growth uses the descriptor's actual growth
step. Valid memory-copy/set/string operations write before reporting injected
failure, preventing artificial uninitialized stack strings. List growth remains
fatal rather than a fallible callback boundary.
Faults caused by retained partial records are compared as outcomes. A failed
local CreatePlayer can leave +0x0c clear and its name pointing at .data+0x9a8;
disconnect then tries to free module memory. That exact invalid-free boundary is
modeled without calling the process allocator. Other invalid fixture frees
remain harness errors. These x86 comparisons do not validate native fallbacks,
real DirectPlay providers, Windows dialog rendering, or OS thread scheduling.
make multipdiff-run N=20000 passes all 40 top-level suites, zero
differences. The new dialog suite observes 19,219 configuration dialogs,
56,406 options dialogs, 37,203 player-details dialogs, 18,867 prune calls,
36,959 player-removal confirmations and 22,484 intercepted shell calls.
The lobby suite observes 3,317 lobby creations, 2,526 releases, 10,657 checked
completion stack words and 457 public-toggle completions. The 980 dialog and
1,809 lobby fault outcomes are compared, not filtered away. Production and
fixture DLLs compile without warnings; the production export table still has
only ImportTable and Linkage. git diff --check passes.
All 21 dialog mutations and all 13 lobby/toggle mutations are caught at 1,200 scenarios each. All 427 targets match exactly once; the full 427-target sweep remains unfinished. The mutation runner uses each new slice's own unmodified baseline, while the full regression still exercises all owners.
Focused commands from bin/difftest: WINEDEBUG=-all wine multipdiff.exe 20000 dialogs and WINEDEBUG=-all wine multipdiff.exe 20000 lobby. Mutations run from
the repository root with python3 -B tools/multip_breaks.py --slice dialogs
or --slice lobby. Run Wine serially.
multip_init.inc implements public init +0x04 (.text+0x20), deinit
+0x08 (+0x70), and their two CHAIN callbacks (+0x90, +0x220). All
eleven public entries now have recovered owners. At this checkpoint MULTIP
remained outside MODULES; the later acceptance checkpoint completes the
remaining gates and promotes it.
Init ignores the host registration result, saves the application GUID pointer, and returns CHAIN registration's raw result. Only a zero registration result clears the three DirectPlay export addresses. Deinit returns the raw host unregistration result and performs no session cleanup.
The first callback returns an unrecognized event's code. Event 0x36 loads
the profile, ignores the resource-path result, merges flag 0x40 when
+0x18c is nonzero, resets four startup fields, registers the shutdown
callback, constructs the worker subsystems and starts the lobby path. Each
failure retains the work already done; repeated startup can retain old queues.
Event 0x3a reads the compound connection size at payload +0x110, copies
the bytes at +0x114, and relocates session, session-name, name-descriptor,
short-name, long-name and address offsets against the allocation. It clears
the session password pointer. Offset zero is still relocated. Allocation,
copy and free results remain raw. Copy failure leaks the block; a loaded
DirectPlay instance also retains it because explicit lobby startup always
returns -1, even after successful completion. No pointer validation was added.
The second callback handles 0x37 by disconnecting, saving the profile,
deleting catalogued texture files and tearing down workers, stopping at the
first failure. Other events preserve incoming EAX. The x86 wrappers retain
the retail frame depth and use the first callback's caller-owned code cell
as allocation scratch. The portable fallback cannot express incoming EAX
and is not validated by these x86 comparisons.
The init fixture drives registration, repeated startup, compound connections,
unrecognized events and repeated shutdown. It compares returns, ordered calls,
module state, heap bytes, liveness, retained allocations and fault outcomes.
Host/CHAIN returns include positive and negative values independent of memory
failure injection. A mutation removing the worker-setup error branch initially
survived the generic failure schedule. Targeted failures now cover each startup
queue allocation, and that mutation is caught.
Scripted texture paths and deletion failures test shutdown ordering without
filesystem writes. Win32 events and critical sections are deterministic stand-ins;
thread creation, waits, COM and ShellExecuteA reuse the controlled lobby backend.
Painted stack words check completion through both entry-callback paths. Faults
from null events and malformed relative offsets remain compared outcomes.
Run the focused fixture from bin/difftest with WINEDEBUG=-all wine multipdiff.exe 20000 init; run its mutations from the repository root with
python3 -B tools/multip_breaks.py --slice init. Run Wine serially.
make multipdiff-run N=20000 passes all 41 top-level suites, zero
differences. The init suite observes 6,248 startup registrations, 301,415
integer-profile writes, 6,877 compound copies, 3,569 checked completion stack
words and 1,075 queue allocation failures. All 3,755 fault outcomes are
compared. All 23 init mutations are caught at 1,200 scenarios each;
all 450 mutation targets match exactly once. The complete 450-target
sweep remains unfinished. Both DLL builds are warning-free, production
exports remain ImportTable and Linkage, and git diff --check passes.
The harness now combines the specialized protocol, profile, player, dialog, worker and dispatch suites with two additional acceptance suites:
wholeretains one module state across public init, CHAIN startup, host/join completion, peer additions, all public queries, sending, receiving, options, repeated startup, public toggle, shutdown and deinit. Eligible receive/dispatch/send work runs synchronously after every action. Failures retain their queues, leaks and partial state; the scheduler does not repair those outcomes. It compares ordered calls, normalized pointers, heap bytes, module state, and complete sent packet bytes, including retry attempts.boundsdrives message1000both directly and through the actual application dispatcher, with matching lengths0..0x6b,0x6c,0x6d,0xffffffffand0x80000000. Readable pages followed byPAGE_NOACCESSexpose wrapped copy lengths and bound real copy accesses. Both source and destination boundaries are exercised, along with missing peers/models, stale/equal/new sequences and mismatching lengths. Partial writes, requested counts, sequence state and guard faults are compared. Retail's unchecked subtraction remains intact.
The 20,000-case mixed trace passes with zero differences: 235,815 actions, 128,462 send attempts, 11,892 received packets and 5,880 traces reaching all eleven public entries. Coverage assertions require disconnected, hosting, joining and closing states and at least eight player records. Repeated startup can retain eleven records, matching retail. Its 551 fault outcomes are compared. The boundary suite also passes 20,000 scenarios: 1,090 wrapped copies and 1,100 guard faults. The complete initialized-pointer catalogue was audited: all 31 code targets have recovered bindings.
The complete regression passes all 43 top-level suites at 20,000 scenarios
each, plus eight dispatch-family reruns, and reports MULTIP whole module: 20000 scenarios per suite, 0 differences.
Mutation slices now select their own fixtures instead of running unrelated
suites after a deliberate error. Each selected slice first checks an unmodified
baseline. Standalone receive installs its own allocator callbacks, and the
group/metadata fixtures initialize their own COM objects; they no longer
depend on preceding suites. An early full sweep exposed these dependencies
and an unrelated lifecycle heap exhaustion; neither was counted as a catch.
--start-index supports resuming an explicitly reviewed contiguous prefix
without treating interrupted attempts as completed comparisons.
The baseline DLL is built once. Two compiler jobs prepare independent mutant
DLLs; installation and Wine comparisons remain serial, with the previous
process finished before the next DLL is copied into the test directory.
make multip-smoke builds the module, temporarily swaps it into the staged
game as Modules/MULTIPLAYER.DLL, and uses the existing startup assertions.
It additionally requires the smoke log to confirm the exact OpenFS98 MULTIP
build, and restores the previous DLL with a bytewise check on exit. The smoke
run passed: 27/27 modules, 95 gauges, SEHFIX present, with the replacement
MULTIP confirmed. The prior DLL's SHA-256 was also verified after restoration.
The other installed game modules were retained; a forced rebuild changes PE
timestamps, so differing DLL bytes alone do not establish retail provenance.
The generic smoke diagnostic now states that the exact build was not tested
instead of claiming the installed DLL must be retail.
make -B all completes without warnings. make multip-acceptance reproduces
the full 20,000-case harness, all mutation checks, whitespace checks and the
temporary game smoke test in sequence. Focused selectors are whole and
bounds; their new mutation slices contain four and two checks respectively.
Run Wine serially. These checks do not validate non-x86 fallbacks, real provider
interoperability or OS thread scheduling races.
The complete 456-target mutation sweep passes, with each target matched exactly once and every comparison finishing normally with differences. The reviewed first 37 catches were preserved across a deliberate interruption; the resumed run completed the remaining 419 in corpus order, including the six new checks. The combined ledger was checked against the exact ordered corpus. All 14 command mutations were additionally rechecked with compiler jobs idle during watchdog measurements. Crashes, exhausted fixtures and interrupted attempts did not count.
With the acceptance gates passed, MULTIP is included in MODULES. The install
mapping remains MULTIP.DLL to MULTIPLAYER.DLL. Startup smoke verifies this
replacement temporarily and restores the exact previous DLL; normal deployment
is through make install. Non-x86 behavior, OS scheduling races and live
provider interoperability remain outside the automated claim.
After promotion, make all rebuilds MULTIP without warnings and the install
mapping check returns MULTIPLAYER.DLL. The rebuilt production .text is
byte-identical to the accepted implementation; the source edit only updates
the status comment. Startup smoke was repeated with this final production
build and again passed 27/27 modules, 95 gauges, with the exact replacement
confirmed and the original game DLL's SHA-256 verified after restoration.
Production exports remain only ImportTable and Linkage. The 14 quiet command
rechecks reproduce the same differing-scenario counts as the full sweep.
git diff --check passes. Changes remain uncommitted.