DirectPlay session and application messaging#
Summary#
Lords of the Realm 2 multiplayer is built on DirectPlay 3 (DPLAYX.DLL,
imported by ordinal: ordinal 1 = DirectPlayCreate, ordinal 2 =
DirectPlayEnumerate). The client enumerates a service provider, creates an
IDirectPlay3 object, initializes a connection, hosts or joins a session,
creates a player, then exchanges a custom application-message stream over
IDirectPlay3::Send / Receive. Generic system messages are documented in
dplay-system-messages.md. Game state sync rides on
the custom frame documented in ../structs/lotr2_net_msg.h
and cataloged in dplay-app-messages.md.
Every claim below is inferred from client code (decompiled
src/LORDS2.EXE.c); none is yet observed on wire.
Evidence#
| Claim | Level |
|---|---|
| DirectPlay 3 via DPLAYX.DLL ordinals 1/2 | inferred from client code |
| IDirectPlay3 vtable offsets (Send/Receive/Open/etc.) | inferred from client code |
| Custom 4-byte app-message header | inferred from client code |
| Reliable triple-send + slot dedupe | inferred from client code |
| System-message dwType mapping (DPSYS_*) | inferred from client code |
| Session GUIDs sourced from menu / EnumSessions | inferred from client code |
Session context#
DirectPlay phases and the functions that drive them:
flowchart TD
A["lotr2_net_start_session<br/>FUN_004d243e"] --> B["lotr2_net_setup_session<br/>FUN_004d266d"]
B --> C["lotr2_net_show_select_sp_dialog<br/>FUN_004d2a1e"]
C --> D["lotr2_net_create_dplay3<br/>FUN_004d2d1f<br/>DirectPlayCreate + QI IDirectPlay3"]
B --> E["InitializeConnection<br/>vtable +0x38, DAT_00503700"]
B --> F["lotr2_net_show_host_or_join_dialog<br/>FUN_004d2a48"]
F -->|host| G["lotr2_net_show_session_name_dialog<br/>FUN_004d2a99"]
G --> H["Open create, flags=2<br/>vtable +0x60, DAT_005036b0"]
F -->|join| I["lotr2_net_show_browse_sessions_dialog<br/>FUN_004d3120"]
I --> J["EnumSessions vtable +0x34<br/>cb lotr2_net_enum_sessions_callback"]
J --> K["Open join, flags=1"]
B --> L["CreatePlayer Nobleman<br/>vtable +0x18 -> local_player_id"]
A -->|client| M["send opcode 1, wait handshake"]
M --> N["lotr2_net_poll_messages<br/>FUN_004d36f0"]
N -->|app| O["lotr2_net_dispatch_app_message<br/>FUN_00448362"]
N -->|system| P["lotr2_net_handle_system_message<br/>FUN_004480ef"]
O -->|handshake| Q["lotr2_net_on_join_handshake<br/>sets lotr2_net_handshake_done"]
Prerequisites for the data path: connection initialized, session opened, and a
player created (lotr2_net_local_player_id valid).
IDirectPlay usage#
The IDirectPlay3 object is the global lotr2_net_dplay3 (was DAT_004ff400).
Observed vtable offsets:
| Offset | API | Call site (symbol // was) |
|---|---|---|
| +0x08 | Release | lotr2_net_shutdown // FUN_004d23d0 |
| +0x10 | Close | lotr2_net_shutdown // FUN_004d23d0 |
| +0x18 | CreatePlayer | lotr2_net_setup_session // FUN_004d266d |
| +0x24 | DestroyPlayer | lotr2_net_leave_session // FUN_004d25c6 |
| +0x34 | EnumSessions | lotr2_net_dlg_browse_sessions // FUN_004d3120 |
| +0x38 | InitializeConnection | lotr2_net_setup_session // FUN_004d266d |
| +0x60 | Open | lotr2_net_setup_session / lotr2_net_dlg_browse_sessions |
| +0x64 | Receive | lotr2_net_poll_messages // FUN_004d36f0 |
| +0x68 | Send | lotr2_net_dp_send // FUN_004d39d5 |
| +0x7c | SetPlayerData | lotr2_net_set_player_data // FUN_004d260d |
DirectPlayCreate (ordinal 1) + QueryInterface(IID_IDirectPlay3,
lotr2_net_iid_dplay3) build the object; DirectPlayEnumerate (ordinal 2)
populates the service-provider list in lotr2_net_dlg_select_sp.
Messages#
Application frame (all game opcodes)#
See ../structs/lotr2_net_msg.h. 4-byte header
(opcode, length, checksum, seq_flags) + payload, total <= 0x104.
- Direction: peer (sent from
lotr2_net_local_player_idto all / specific players viaIDirectPlay3::Send). - Trigger: any game event needing sync; built by
lotr2_net_send_message. - Reliability: opcode 10 is sent once (unreliable); every other opcode is
sent three times with
seq_flagsretry bits0x40then0x80. The receiver dedupes by(player slot, sequence)inlotr2_net_dispatch_app_message. - Validation: receiver checks
length == lotr2_net_opcode_len[opcode](DAT_004f0988[]) and the payloadchecksumbefore invoking the unpack handlerPTR_LAB_004f0820[opcode].
The full 90-slot opcode catalog is in
dplay-app-messages.md. The generated source table is
_generated/net_opcode_table.md. The local
bin/LORDS2.EXE did not pass anchor validation against this Ghidra export, so
unknown rows are marked TBD until a matching binary table can be dumped.
DirectPlay system messages#
When Receive reports sender id 0 (the DirectPlay system player),
lotr2_net_poll_messages reads dwType at payload offset 0 and routes to
lotr2_net_handle_system_message:
| dwType | DirectPlay constant | Action |
|---|---|---|
| 0x03 | DPSYS_CREATEPLAYERORGROUP | log; new player id from DAT_0052d498 |
| 0x05 | DPSYS_DESTROYPLAYERORGROUP | remove player slot; pre-game sends opcode 3 |
| 0x07 | DPSYS_ADDPLAYERTOGROUP | log |
| 0x21 | DPSYS_DELETEPLAYERFROMGROUP | log |
| 0x31 | DPSYS_SESSIONLOST | log (session teardown) |
| 0x101 | DPSYS_HOST | host migration -> sets lotr2_net_is_host = 1 |
| 0x102 | DPSYS_SETPLAYERORGROUPDATA | log |
| 0x103 | DPSYS_SETPLAYERORGROUPNAME | log |
A message whose sender equals lotr2_net_local_player_id is logged as a
self-echo and ignored.
See dplay-system-messages.md for SDK payload
layouts and per-message compatibility notes.
DirectPlay structures used at setup (standard SDK layouts)#
These are stock DirectX structs, documented here only by the offsets the client
touches (not re-declared in docs/structs/).
- Connection / caps
DAT_00503700(size 0x28):dwSize = 0x28at +0x00; caps fields checked at +0x10 (== 0x10000), +0x18 (== 0xFA/250), +0x20 (== 8) to detect an IPX-like provider. Passed toInitializeConnection. - DPSESSIONDESC2
DAT_005036b0(size 0x50):dwSize = 0x50,dwFlags(0x44 host create),guidInstance/guidApplication(from menu params),dwMaxPlayers = 5,lpszSessionNameA("Lords2 Session1"default or the name entered inlotr2_net_dlg_session_name). Host opens with flag 2; the join path opens the EnumSessions-selected descriptor with flag 1. - DPNAME (size 0x10, stack-built in
lotr2_net_setup_session):dwSize = 0x10,lpszShortNameA = "Nobleman"; passed toCreatePlayer, which returnslotr2_net_local_player_id.
Compatibility notes#
- All multi-byte integers are little-endian (x86).
- App header is 4 bytes and excludes itself from
length; checksum covers the payload only. Total frame must stay <= 0x104 (260) bytes. - Reliable delivery is achieved by the client sending each frame three times and
deduping on
(slot, sequence)fromseq_flags & 0x3f— a stock client expects the duplicates, so an interop implementation must reproduce the triple-send and the dedupe window. DPSYS_HOSTtransfers host authority; do not hardcode host. Session GUIDs are taken from menu parameters (host) orEnumSessionsresults (join).
Open questions#
- Dump the authoritative opcode tables from a binary that matches this
Ghidra export to replace source-only
TBDrows. - Service-provider GUID(s) actually selected (modem / serial / IPX / TCP).
- Exact checksum algorithm width (8-bit additive assumed).
- Gameplay meaning of most opaque app opcodes beyond join/roster/heartbeat.
Captures needed#
- LAN capture between two stock clients (join + a few turns) to confirm framing, checksum, and the reliable retransmit pattern on the wire.
- A lobby/host trace to confirm the
DPSESSIONDESC2flags and provider GUID.