-
Notifications
You must be signed in to change notification settings - Fork 5
Reimplement multiplayer command ownership and document network flow #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a1f977d
8a9c79d
848b328
f13ce56
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
|
Comment on lines
+37
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Did something UCP module related slip through here? |
||
|
|
||
| ## 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. | ||
|
Comment on lines
+49
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| - `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. | ||
|
Comment on lines
+62
to
+66
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replay talk. |
||
|
|
||
| ## 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. | ||
|
Comment on lines
+68
to
+89
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This whole part: |
||
|
|
||
| ## 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). | ||
|
Comment on lines
+91
to
+98
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is likely a leftover? |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
TheRedDaemon marked this conversation as resolved.
|
||
| } | ||
|
|
||
| } // namespace Synchrony | ||
| } // namespace OpenSHC | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The translator is0x47EAF0, reimplemented by this contribution.That needs to be removed. The wiki has nothing to do with the function you currently implement.