diff --git a/docs/wiki.rst b/docs/wiki.rst index ce0e938e..993a3eae 100644 --- a/docs/wiki.rst +++ b/docs/wiki.rst @@ -33,7 +33,7 @@ The Game itself - AI Behavior - Graphics and Sound Systems - Modding Support -- Multiplayer Architecture +- :doc:`Multiplayer command ownership and replay boundaries ` (Coming Soon!) diff --git a/docs/wiki/multiplayer-command-flow.md b/docs/wiki/multiplayer-command-flow.md new file mode 100644 index 00000000..66e51b4d --- /dev/null +++ b/docs/wiki/multiplayer-command-flow.md @@ -0,0 +1,98 @@ +# Multiplayer command ownership and replay boundaries + +This note records original-code evidence from Crusader 1.41, SHA-256 +`3bb0a8c1e72331b3a30a5aa93ed94beca0081b476b04c1960e26d5b45387ac5a`. +Addresses refer to that executable. Generated names help navigation; the original +instructions establish behavior. No multiplayer match was run for this analysis. + +## From transport to simulation + +1. `queueCommand` (`0x489100`) builds a command in a 200-entry ring. Each entry is + `0x4F8` bytes, with tick at offset 0, sender handle at 4, category at 8, state + at 9 and parameters at 10. The handler participates in both serialization + and execution; invoking it does not necessarily mean the command executed. +2. `transmitCommand` (`0x487C50`) serializes the low **three bytes** of the tick + after the category byte. The generated `Packet` labels divide this into + `short time` and `byte unknown`, but the native copy at `0x46A830` writes all + three bytes of the timestamp. Payloads above 200 bytes normally use envelope + category 125; category 65 has a separate exception. Most commands use + guaranteed DirectPlay delivery; categories 12 and 117 have special handling. +3. `receiveAllTransmittedCommands` (`0x490690`) unwraps category 125 and zeroes a + 32-bit tick before copying those three bytes into it. DirectPlay system + messages, synchronization packets and ordinary commands take different paths. + Host migration and player removal are processed here, outside timed dispatch. +4. `scheduleReceivedCommand` (`0x480210`) invokes the handler in receive/parse + mode. Timed payloads go into the ring. Commands with signed time <= 0 copy to + the fixed parameter area and execute immediately at `0x480425`, then clear + their temporary ring entry. They never reach `processWaitingCommands`. +5. `getCommandIDFromCommandSelectionStuff` (`0x480440`) selects due, unprocessed + entries, with a batch cap of 100. It translates sender handles and stably + orders the selected entries by logical player slot. A recorder must observe + actual dispatch order; transport arrival order is insufficient. +6. `processWaitingCommands` (`0x4892F0`) translates each sender again, sets the + execution action/parameter state, calls the handler and marks the entry + processed. The translator is `0x47EAF0`, reimplemented by this contribution. + +The timestamp is thus 24-bit on the wire and 32-bit in the ring. The examined +receive routine does not reconstruct a higher epoch. Long-duration recording +and replay must preserve that distinction rather than assuming identical wire +and internal formats. This observation does not establish a reproduced wrap bug. + +## System messages bypass command dispatch + +After a successful `IDirectPlay4A::Receive`, the receiver compares the sender +with zero (`DPID_SYSMSG`). Its system-type switch begins at `0x490735` +(Extreme `0x490895`). Failed receives and ordinary nonzero senders branch away +before this point. Values below also agree with the DirectX SDK `dplay.h`: + +- `DPSYS_DESTROYPLAYERORGROUP` (`5`) reads the handle at message offset 8 and + calls the identity translator at `0x490755`. The next call is + `removePlayerFromLobby` at `0x49075B`, without reloading ECX. This is another + concrete native caller requiring this reimplementation to retain ECX. +- `DPSYS_HOST` (`0x101`) sets `isHost`, resets the hash countdown, assigns a new + `timeGetTime()` value to the autosave timer and clears both nine-entry player + timing arrays. Chat and out-of-match lobby ordering also change. A host-only + transition can therefore change native scheduling state without a changed + player roster or any timed command. +- `DPSYS_CREATEPLAYERORGROUP` (`3`) and `DPSYS_SESSIONLOST` (`0x31`) fall through + to the next receive iteration without special handling in this switch. This + describes the original routine; it does not imply that a transport or replay + implementation may disregard the broader connection lifecycle. + +Polling roster and synchronization fields at simulation boundaries cannot +establish that no system event occurred between them. Recorder diagnostics can +observe the type-switch entry before mutation, but replaying those events still +requires their semantics and timing. DirectPlay system structures can contain +process pointers; copying their bytes is not a portable replay format. + +## Why one save and a timed-command log are insufficient + +The original save section table does not cover active mode or the full network +handle array. It does cover a saved mode copy, AI-slot values and local player +slot. The single-player load-dialog path (`0x4950B0`) clears network handles and +recreates the local entry. Multiplayer playback therefore needs explicit roster +restoration and command ownership, not merely a changed mode flag or slot zero. + +`queueSynchronizedAutosaveProtocol` (`0x48C660`) uses host wall-clock elapsed time, +then queues a save command carrying the simulation tick and a unit checksum. +`checkGameSync` (`0x48CB00`) compares peer evidence and starts the resync state +machine. `recomputeHashesAndSendResync` (`0x48CC90`) hashes selected game arrays; +`sendPendingResyncCommandsInBudget` (`0x48E680`) sends mismatching sections in +bounded batches, retaining its category/item position between calls. These +include units, buildings, player data and tile-map regions. Resync replaces +simulation data; treating every immediate command as presentation-only is wrong. + +A useful recorder must account separately for timed command execution, immediate +state-changing commands, roster/host transitions and resync transfers. Reliable +transport delivery alone cannot restore missing replay state or make +frame/audio-driven RNG calls deterministic. An extension's ordinary save +integration also does not demonstrate integration with native resync transfers. + +## Validation scope + +The accompanying player-identity checker compiles and executes the actual C++ +function against the originals in both variants. It covers last-match semantics, +sentinels, missing handles, relocated receivers and the retained ECX value. +Other routines in this note were inspected in the named Ghidra project and +original assembly; they are not newly reimplemented or live-validated here. +See [reproduction instructions](../../tools/reimplementation-tests/PLAYER-IDENTITY.md). diff --git a/src/OpenSHC/Synchrony/GameSynchronyState/translateMultiplayerIDsIntoPlayerIDs.cpp b/src/OpenSHC/Synchrony/GameSynchronyState/translateMultiplayerIDsIntoPlayerIDs.cpp new file mode 100644 index 00000000..1bf55882 --- /dev/null +++ b/src/OpenSHC/Synchrony/GameSynchronyState/translateMultiplayerIDsIntoPlayerIDs.cpp @@ -0,0 +1,29 @@ +#include "../GameSynchronyState.func.hpp" + +namespace OpenSHC { +namespace Synchrony { + + // FUNCTION: STRONGHOLDCRUSADER 0x0047EAF0 + uint GameSynchronyState::translateMultiplayerIDsIntoPlayerIDs(int playerHandle) + { + uint player = 0; + if (this->currentGameMode == Game::GM_SOLITARY) { + return this->currentPlayerSlotID; + } + + if (this->currentGameMode == Game::GM_SKIRMISH_SINGLE_PLAYER) { + return this->currentPlayerSlotID; + } + + // Do not return on the first match: the native function lets the last + // matching slot win, including duplicate or sentinel-valued handles. + for (int i = 1; i < 9; ++i) { + if (this->currentPlayerFullIDArray[i] == playerHandle) { + player = i; + } + } + return player; + } + +} // namespace Synchrony +} // namespace OpenSHC diff --git a/status/addresses-SHC-3BB0A8C1.txt b/status/addresses-SHC-3BB0A8C1.txt index 10a0c2b0..dfee26ec 100644 --- a/status/addresses-SHC-3BB0A8C1.txt +++ b/status/addresses-SHC-3BB0A8C1.txt @@ -22385,7 +22385,7 @@ SHC_3BB0A8C1_0x0047E8F0 | 0.0% | Pending SHC_3BB0A8C1_0x0047EA40 | 0.0% | Pending -SHC_3BB0A8C1_0x0047EAF0 | 0.0% | Pending +SHC_3BB0A8C1_0x0047EAF0 | 100.00% | Reimplemented SHC_3BB0A8C1_0x0047EB80 | 0.0% | Pending