Skip to content

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_id to all / specific players via IDirectPlay3::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_flags retry bits 0x40 then 0x80. The receiver dedupes by (player slot, sequence) in lotr2_net_dispatch_app_message.
  • Validation: receiver checks length == lotr2_net_opcode_len[opcode] (DAT_004f0988[]) and the payload checksum before invoking the unpack handler PTR_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 = 0x28 at +0x00; caps fields checked at +0x10 (== 0x10000), +0x18 (== 0xFA/250), +0x20 (== 8) to detect an IPX-like provider. Passed to InitializeConnection.
  • 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 in lotr2_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 to CreatePlayer, which returns lotr2_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) from seq_flags & 0x3f — a stock client expects the duplicates, so an interop implementation must reproduce the triple-send and the dedupe window.
  • DPSYS_HOST transfers host authority; do not hardcode host. Session GUIDs are taken from menu parameters (host) or EnumSessions results (join).

Open questions#

  • Dump the authoritative opcode tables from a binary that matches this Ghidra export to replace source-only TBD rows.
  • 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 DPSESSIONDESC2 flags and provider GUID.