Modding research
Multiplayer, and why it breaks
Lords of Magic multiplayer has a twenty-five-year reputation for being touchy. Reading the game's networking code explains the reputation exactly — and turns up a diagnostic the game has always had and never showed anyone.
A caveat that applies to this whole page: none of it was established by playing a networked game. It was read out of the game's executable and its scripts on one machine. A second machine would settle several of the questions below, and has not been available.
Every player runs the whole game
Measured
There is no host that owns the truth. Each copy of the game simulates the entire world, and what crosses the network is orders — move this army, cast this spell — not the results of those orders. Both machines are expected to compute the same outcome from the same inputs.
That is a design with a single catastrophic failure mode. If the two simulations ever disagree by one value, they keep running and keep diverging, and by the time anyone notices they are playing different games. The engine knows this: it continuously compares six independent checksums between peers and reports “Divergence” when any of them disagree.
The desync post-mortem ships switched off
Measured
This is the best story in the research. The moment a divergence is detected, the engine calls a routine built to dump the whole simulation state — every player, every army, every unit — through a logging hook the shipped scripts already register. It is exactly the tool you would want, wired to exactly the right event.
It is gated behind a flag. That flag sits past the end of the initialised data in the executable, so it is zero when the game loads, and the only instruction in the entire program that writes to it writes zero. The dump can never run.
Which is, most likely, why nobody in this community has ever diffed a desync. The instrument exists, fires in the right place, and has no switch. Turning it on means changing a byte of the executable, which is outside what this research does.
A note on how easily this conclusion could have been wrong: an identical-looking measurement earlier in the same work was wrong. A value with many reads and no direct writes turned out to be a field of an object whose writer held its address in a register. The difference here is that this flag is written directly, and its neighbours are each written directly by different code — the signature of separate switches rather than one object.
The asterisk in the game list
Inferred
Before you join anything, the game builds a small tag out of two numbers: a checksum over the script content it loaded, and a sum over its own executable. It attaches that tag to the session it advertises, and it compares an advertised tag against its own when it draws the list of available games.
If they do not match, the game's name is shown with a leading asterisk. It marks; it does not refuse. So an asterisk in that list means the host is running a different build from you — a different patch, a different mod, a different version — and that is the single most common way a session ends in divergence.
Labelled inferred rather than measured for one honest reason: the comparison and the asterisk formatting were read in the code, but nobody here has seen the list render.
Everyone must run byte-identical files
Measured
Three of the six things the engine compares between peers are the executable version, the script archive and the sprite archive. A player on the unofficial bug-fix patch, a player on a balance overhaul and a player on retail have different script archives by definition, so they will be reported as disagreeing.
This is the single most actionable finding of the whole investigation, and it is unglamorous: copy the executable and the three archives from one machine to every other machine before you play. Not “install the same patch” — copy the files.
The other things that make it fragile
Measured
- It is built on DirectPlay, a Microsoft networking layer long since retired. There is no address field in the interface: sessions are found by broadcast, so both machines must be on the same local network segment. A plain routed connection over the internet will not even list the game.
- Network messages are capped at 257 bytes on a fixed buffer. That cap is why transferring a scenario to a joining player is its own multi-message protocol rather than just sending the file.
- The send path sleeps on the game thread while it retries, so a struggling link stutters the game rather than dropping packets quietly.
- Session discovery gives up after 50 milliseconds — generous on a 1997 LAN, tight on anything with a hop in it.
- The resynchronisation message the engine defines is dead code. Nothing anywhere ever sends it. Once peers diverge, there is no recovery path.
A dedicated server would not fix this
Inferred
It is tempting to think that an always-on machine hosting games would make multiplayer reliable. It would help with one real problem — being able to find a game at all, which a layer-2 VPN plus a permanently running peer genuinely solves — and it would do nothing at all about the dominant failure.
Desync is a determinism problem, not a networking problem. The packets arrived. The two simulations computed different answers from them. No amount of uptime, bandwidth or latency improvement changes that.
Where a permanent host does earn its place is as the one canonical install: the machine everyone copies their files from, so that the tag comparison above always matches.
Two things this research got wrong
We were wrong about this
An operator in the scripts named something like a network lock was initially read as evidence for a different networking model. It is not; it is a no-op without an active session, along with its whole family of related operators. The wrong reading and the reasoning that produced it are left in the source document rather than edited away.
Separately, the engine's own table of network message names is off by one. A missing comma in the source fused two names into one entry, so the table has 97 entries where the code accepts 98 — meaning every message past the fused pair is misnamed, and the last valid index reads past the end of the table entirely. Anyone debugging by message name in this game is reading the wrong names.
Still unknown
- What actually causes the divergences in practice. That needs two machines and a controlled experiment, which has been specified and not yet run.
- Whether a mismatched build is refused at join time or merely marked and allowed through.
- What is written to the application log file the shipped build clearly has a path for, and whether anything reaches it.