CoopFoundry Multiplayer

v0.7.13

Co-op for 8 to eight captains on one island: Steam invitations and relay, direct IP/LAN, automatic save transfer, joining a game in progress, and synchronization that repairs its

Stable 5 days ago CC BY-SA 4.0 No source
Source code
No source provided
Website
https://ko-fi.com/H2H717LY5N
Tags
Content Quality of Life UI
Installation: Close the game and copy the contents of the archive's `CoopFoundry` folder into the *Captain of Industry* installation folder on each machine. The archive includes BepInEx 5.4.23.5 because CoopFoundry must load before the main menu is built; the game's built-in mod selector loads too late to provide a login screen.


One island, up to eight captains.

CoopFoundry brings cooperative multiplayer to Captain of Industry. The host picks a save, says how many players the island will hold, and invites friends through the Steam overlay. Each one accepts, connects through Steam's relay, and receives the island automatically before play begins. No port to open, no session code to type, no server to rent. Direct IP and LAN connections are there too, and they carry the island a great deal faster.

Every machine runs the same simulation in lockstep. You each keep your own camera and your own tools: one of you can lay out a smelting line while another runs mining contracts across the map. Everything anyone builds appears for everyone, in the same order, on the same tick.

Somebody who has to leave can come back, and a friend can join a game already under way. They are handed the island as it stands and replay what they missed at speed, while everybody else carries on playing.




Capture d'écran 2026-09-29 114155.png


Features
  • Private Steam lobby for two to eight players, overlay invitations, relay transport, nothing to configure.
  • Direct IP connection without a session code, and automatic LAN lobby discovery.
  • Encrypted and authenticated session traffic.
  • Automatic save transfer with integrity verification before play starts.
  • Join a game in progress, and come back after a disconnection, without anyone else being made to wait.
  • Other gameplay mods are supported, as long as every player has the same ones. The check runs during the handshake, before a save is sent, and names what to compare.
  • Everything shared: building, demolition, conveyors and pipes, vehicles, terrain designations, research, storage and priority settings.
  • Host-controlled pause and game speed, so nobody is left behind.
  • Autosaving performed by every machine at the same instant, following the host's own interval — including having it switched off.
  • Press F9 to point at whatever you are looking at. Everyone else gets a marker and a button to go and see it.
  • Self-healing synchronization: the simulations are compared every frame, backed by a full deterministic check of terrain, entities, vehicles, products, population, workers, unity and research. If one player drifts, that player alone is put back on the host's state — the others are not interrupted.
  • Reconnection, manual recovery checkpoints, latency display, in-game text chat, and a French/English interface.
  • Installs and uninstalls in two clicks, so you can switch between a co-op island and a modded solo island. It records every file it writes and removes exactly those; a file changed since the install is reported and left alone.

Good to know
  • The host's game is the reference. There is no dedicated server yet; an unattended host is being worked on.
  • Captain of Industry 0.8.7d is the tested version. Others show a warning but are allowed to connect. Every player needs the same CoopFoundry version and the same DLC files.
  • Mods are supported but not endorsed: a mod that changes the simulation changes it for everyone. Having the same files on both sides removes the crudest cause of a desync, not every cause. If one appears after you add a mod, remove that mod before looking anywhere else.
  • CoopFoundry no longer replaces the Harmony library other mods use, which was what stopped modded saves from loading when it was installed. It now ships the same build as every other Captain of Industry mod.
  • Steam's relay carries the first save far more slowly than a direct connection — minutes rather than seconds on a large island. That pace is Steam's, not the mod's, and the screen shows the progress throughout. A direct IP connection is noticeably smoother.
  • Direct TCP uses port 28080 by default and may require port forwarding. Steam needs no open port.
  • Every frame waits for every player, so the slowest connection sets the pace when time is accelerated. On a good link, full speed.
  • Played through with three players. More is allowed and should work, but has not yet been played through.
  • Playing with other mods is new in this version and has been tested, not played through. Treat it as such.
  • As with any mod that touches saves, start on a copy of an island you would miss.

If you'd like to support the project and help keep me motivated to continue development, feel free to drop a tip here:
☕ https://ko-fi.com/H2H717LY5N
Thank you for your support!


📢 CoopFoundry: dedicated server status update

Oct 16, 2026

Hi everyone,

Since we announced the dedicated server, we’ve tested it at length on a real game, and several problems have come up. For now, they’re holding back the rest of the project. Here’s where things stand, in full transparency:

⚠️ What’s wrong

  • Desyncs with mods. Some mods don’t behave exactly the same on the server and on the players’ machines, especially CargoHelicopter, Cheat++ and Gameplay++. This causes regular desyncs and reloads, mostly at ×2 and ×3. Fixing it partly depends on the authors of these mods, so we’re reaching out to them for help.
  • Smoothness. To reach ×2 and ×3, the simulation was advancing in bundles of several steps. That caused small display glitches (resources popping off a belt for a frame, flickering) and lag when building.
  • “Pirates!” message. On the dedicated server, the game’s anti-piracy protection triggered even though our copies are legitimately purchased. The server was running without a Steam session, which is probably why it couldn’t verify the licence. We will never bypass this protection: we’re working on getting the server to run within the rules.

✅ What we’ve done: version 0.7.13

  • Smooth simulation is back as the default, like in 0.7.1: clean visuals and responsive building. Fast mode is still available for those who want it.
  • New panel (F10) with tabs Join / Host / Dedicated servers / About, bigger and easier to read.
  • CoopFoundry button you can drag anywhere, placed bottom right by default.
  • All the reconnection and mod compatibility fixes are kept.

🔜 What’s next
The dedicated server is still available, but it’s experimental. For a worry-free game, we recommend hosting from the game (Steam or direct IP). We’ll pick the dedicated server back up once the mod and licence issues are sorted.

Thanks to Atomic for the hours of testing, and thank you all for your patience 🙏
If you run into a problem, send your session log: its location is shown in the About tab.

— Knight-HD

Stable

Oct 05, 2026

The mod is now completely stable—zero desyncs! The only remaining weakness is when pausing the simulation: there's still a very slight risk of a desync, but it's really minor. I managed to play for 4 hours straight on a late-game save without a single desync.

Important Update & Info

Sep 26, 2026
  • Steam Connection vs. Direct IP: The Steam connection works completely fine! However, I strongly recommend using Direct IP connection. Due to Steam API limitations, using Direct IP allows saves to transfer much faster and results in smoother overall game performance.

  • Mod Compatibility: For now, I haven't tested compatibility with other mods yet. If you try it alongside other mods, please let me know how it goes—I’d love to get your feedback!

Thanks for testing!

v0.7.13

Latest
Game version
0.8.0 - 0.8.7d re-verified
Released
Oct 16, 2026
File size
3.9 MB
License
CC BY-SA 4.0
Save-game: Add · Remove
Changelog
- Smooth simulation is back, and is the default. One simulation step per frame, as in 0.7.1: no
more goods popping off their belt for a frame, no flicker, and building answers at once. The
price is that x2 and x3 are not always reached. The fast mode (0.7.2 to 0.7.12) stays available:
F10 > Host > "Smooth simulation", or pace = fast in server.cfg. Only the host's choice counts.
- The panel has tabs - Join, Host, Dedicated servers, About - and is larger, with bigger text.
About gives the version, author, tester, your equipment line and the session log's location.
- The CoopFoundry button is now always shown, bottom right by default, and can be dragged anywhere;
its place is remembered. About > "Put the button back at the bottom right" resets it.
- Install 0.7.13 everywhere (players and server).

0.7.12
- Dedicated server: the game is now paused while a player loads the world (joining in progress or
being put back in step), and resumes by itself once everyone is ready. A slow loader no longer
comes back too far behind to catch up and gets reloaded again and again.
- Install 0.7.12 everywhere (players and server).

0.7.11
- Dedicated server: floating-point maths now runs exactly as in the game (same Mono options).
- CoopAddons VehiclesIgnoreTransports 1.0.3: vehicles are remembered as soon as they are added.
- Install 0.7.11 everywhere (players and server).

0.7.10
- Dedicated server: a player who had reconnected was cut off by their very first action. Every
player coming back gets a new number, and anything above 1 was refused as an invalid command.
- A frame carrying more than 128 actions (a drag over a long row of buildings) was sent, then
refused on arrival, which ended the session. Up to the full 4096 now go through.
- Joining a game in progress no longer ends in a needless reload: commands played while nobody was
in step were kept waiting for an answer that would never be checked, and the first check after
the catch-up mistook that for a desync.
- Install 0.7.10 everywhere (players and server).

0.7.9
- Dedicated server: games no longer start paused for good. A server nobody joined for five
minutes gave up waiting and reopened; on reopening it read its freshly loaded (and therefore
still) island as "the host paused the game", and started the next game paused - which nobody on
a dedicated server can undo from the game. Waiting for players no longer times out, and the
pause state is only taken from a game actually in progress.
- Pressing play while the shared game is paused now asks the host to resume ("/resume"), when the
host lets players change the speed. Typing /resume in the chat works too.
- Reconnecting or joining a game in progress: the player was refused with "A simulation step was
lost or repeated" as soon as two frames of several steps arrived together, which is exactly what
catching up looks like. Frame order is now checked against each frame's last step.
- No protocol change, but install 0.7.9 everywhere (players and server).

0.7.8
- Smooth at every speed. A frame could carry several simulation steps (0.7.2), and they were
played back to back in a single update of the game: the picture jumped several steps and then
stood still until the next frame - the faster the game, the bigger the jumps. A frame's steps
are now played one by one at the game's own rhythm (a step every 100 ms divided by the speed),
on the players' machines and on a host playing from the game. A player catching up still plays
them at once.
- Frames are kept to one step whenever the link lets the game reach nine tenths of the chosen
speed; only a slow link gets more, and they are spread out too. The session log's "Rythme" line
gives the round-trip cost and the steps per frame chosen.
- No protocol change: 0.7.7 and 0.7.8 can play together, but install 0.7.8 everywhere.

0.7.7
- Dedicated server with Gameplay++: the transport bridges were never switched on on the server.
Gameplay++ looks up a Unity object just before turning them on; without the engine that lookup
failed, the mod swallowed the error, and goods crossed bridges on every player's island but not
on the server's. Trucks then got different jobs on each side about ten seconds after every load.
The server now answers such lookups as a fresh game would ("no such object") and checks, in its
log, that the bridges are on.
- The server kept a different game name after every recovery reload, and Gameplay++ files its
bridge and parking settings by game name. It now uses the same name as the players' games.
- CargoHelicopter's patches on trucks, which the mod applies from a Unity component, are now
applied on the server as well.
- Gameplay++ Parking HQ, excavator parking and heliport: their once-a-second rounds now follow the
island's own clock, and their per-vehicle memories are emptied on every machine at the same
moment (load and every shared save), instead of depending on when each machine opened the island.
- Server: when data_root is set, the settings mods keep in the profile live there too.
- Checked: 2000 frames on an island with Gameplay++, Cheat++, WindPower, CargoHelicopter,
InfiniteGroundwater and KeybindFramework, server and player, without a single recovery.

0.7.6
- Dedicated server: it no longer dies when a player joins an island with Cheat++ or WindPower.
Interface objects these mods half-build without the engine were cleaned up later on the
garbage collector's thread, and an error there ended the process. The server now treats such an
error the way the game itself does: reported, not fatal.
- The CoopFoundry panel is larger (600 x 800 at most) and keeps its size: a server's line shows
its name, players, state and "[password]" on the button, with the version and mods in small
print underneath, instead of one button as wide as the whole mod list.

0.7.5
- WindPower is now identical on every machine. Its wind was random three times over (a starting
direction from Unity's generator, a drift from a clock-seeded generator, and a turbine rota that
restarted at every load), so any island with turbines desynchronised within seconds, with or
without a server. The wind is now a function of the simulation step: the same gentle drift, the
same for everybody, however and whenever the island was opened.
- The dedicated server runs islands with Cheat++ and WindPower. Pieces of the game's interface
these mods ask for get empty stand-ins on the server, and their windows are skipped there; their
simulation runs as in the game. The server also checks the island before opening, and refuses
to open, naming the mod, if it cannot run it - instead of sending it to a player who then waited
paused until disconnected.
- The installer inside the archive is rebuilt with every package (0.7.3 and 0.7.4 shipped the
0.7.2 installer).
- Known: Cheat++'s "keep Unity full" tops Unity up every second on each machine's own clock, which
can still desynchronise a co-op game; leave it off when playing together.

0.7.4
- Favourite servers. F10 > DEDICATED SERVERS: type an address and port, press "Add this address to
favourites", and the mod asks that server itself, every time the panel opens, whether it is
there - name, players, speed, pause, password, mods - with no Steam and no web site. One click
joins. The local-network search shows the same description instead of a bare address.
- A server answers these questions on its UDP port (the same number as its TCP port): open UDP as
well as TCP in the firewall and on the router for favourites to see it from the Internet. A
question smaller than its answer is ignored, so the port cannot be used to amplify traffic.
- A game hosted from the panel answers too, as "Game of <Steam name>".

0.7.3
- Dedicated servers can be found from the game: F10 > DEDICATED SERVERS > "Look for servers" lists
the servers announced on Steam and the games on the local network, with their name, players,
speed, password and mods. One click joins. A server is listed when its operator fills in
public_address and steam_list; the list is kept by Steam itself (a public lobby used as a notice
board), so nothing else is contacted. steam_list is off by default: it keeps the server's Steam
account "in game", and Steam lets an account play on one machine at a time.
- The dedicated server no longer opens a Steam session at start-up, so the account running it can
still play on another machine. Steam is asked about DLCs only when the hosted save uses one, for a
few seconds while loading.
- Password field. A protected server asks for it, and a player hosting from the game can set one
too: the others must type the same. The password itself is never saved nor sent - both sides
derive the session key from it, and a wrong one is refused before any game data is exchanged.
- Players can change the speed. The host (or the server's players_can_change_speed) allows it; the
game's own speed buttons then send a request, and the host applies the change for everyone at
the same frame. Pausing stays with the host, because opening the Escape menu pauses too.
- Dedicated server: mods reading Unity's clock (Gameplay++, CargoHelicopter, KeybindFramework) now
load, the server supplying the clock. "CoopFoundryServer.exe --check-load" tests an island and its
mods without waiting for a player. Cheat++ (needs the game's interface) and WindPower (Unity's
random numbers) still cannot run on a dedicated server.
- Protocol 20: both players must update.

0.7.2
- The game now runs at the speed the host chose. A frame used to carry one simulation step, and a
frame is a full round trip between the players - both games' frame rates plus the network, about
a tenth of a second - so the shared game advanced at speed one whatever was selected. A six-hour
session measured nine to ten steps a second with speed three requested, where thirty were due.
Each frame now carries as many steps as its round trip lasts, measured continuously: one step
when the link is quick, three at speed three over a typical connection, never more than sixteen.
The session log says when the pace changes ("Rythme : ...").
- What this costs: at high speed, several steps are drawn at once, so vehicles move in small jumps
rather than perfectly smoothly. A machine that cannot simulate fast enough is still the limit,
exactly as in single player.

0.7.1
- Steam now shows friends that you are playing CoopFoundry: "View game info" reads, for example,
"CoopFoundry — hosting a co-op game (2/2 players)". The headline under the game's name in the
friends list cannot be changed by a mod - it comes from text only the publisher can declare.
- A Steam session can be joined straight from the friends list: when the host has room, friends
get Steam's own "Join game" button, without needing an invitation. The players of one session
are grouped together in the friends list. Direct-IP sessions show the status only, so the host's
address is never published.
- The Steam lobby is now visible to the host's friends instead of to invited players only, which
is what makes "Join game" work. It still carries no address, and the mod still checks version
and equipment before anyone can enter.

0.7.0
- Some mods keep part of the world outside the save. Gameplay++ writes the goods crossing its bridges
to text files under "Mori++ Saved settings" in the player's profile folder, and the simulation
reads them back; Cheat++ keeps its cheat switches there too. Two players with identical saves,
mods and versions were therefore not playing the same island: the host's bridges carried water the
other player's did not, and the water totals parted company two thousand frames in. That was the
cause of the recoveries at frame 2000 seen in the last sessions.
- Those files now travel with every save, host to players, and are put in place before the island
is loaded - at the start of a session, after a recovery, and for someone joining late. Nothing in
the mod knows about a particular mod: any folder in the profile that the game does not own is
treated this way, so mods not yet installed are covered. Interface preferences (the Overlord
folder) are never touched. The player's own files are set aside first and put back when the
session ends, or at the next start if the game was closed in the middle.
- Every vehicle's state (position, heading, speed, destination, navigation, load) is now kept for
the last 64 steps, and written next to the other diagnostics when a divergence is detected
("-history.tsv"). Comparing the two peers' files shows which vehicle changed first and in which
field, which a single snapshot after the fact cannot.
- Protocol 19: both players must update.

0.6.5
- The deterministic check now writes down the thing it actually decided on. Until now the only
artefact kept beside a recovery was a save-shaped capture, and a save carries the serializer's own
type tables - two peers running an identical world produce saves differing by a few hundred bytes
in a run of type names, which proves nothing either way. The projection the decision is taken on
is now written next to it, so a difference can be found rather than inferred.
- That projection is divided into named sections - HEAD, TERR, PEOP, PROD, RSCH, ENTS - so comparing
two peers lands in a named part instead of at an offset. Entities, terrain and pathfinding already
had their own dumps; products, research and population had none, and a difference in those was
indistinguishable from a difference anywhere else. That is exactly where the last two recoveries
of a four-hour session came from, and it could not be narrowed down any further with what was kept.

0.6.4
- The desynchronizations seen while playing with other mods were not desynchronizations. The
deterministic check serialised its values through the game's own BlobWriter, which does not only
write what it is given: it also emits the serializer's type and reference tables, and those
describe the process, not the simulation. Two peers running an identical world produced digests
differing by 317 bytes inside a run of type names, with the 28 million bytes before and the 16
million after byte-for-byte equal - identical entities, identical terrain, identical pathfinding
state. The mod declared a desynchronization on that and reloaded every player from the host, every
few minutes. Each of those reloads moves vehicles mid-journey, so the false alarm went on to
produce the real vehicle divergences it then reported. Every value in the check is now written by
hand as integers, and nothing that is not simulation state reaches it.
- This is also why it appeared the day sessions with mods began, and why it should not come back for
mods not yet written: every mod adds types to those tables, and the check no longer looks at them.
- The check is much cheaper as a side effect. It was serialising forty-four megabytes per sample,
most of it serializer bookkeeping.
- Both players must update together: the digest format changed, so a 0.6.3 peer and a 0.6.4 peer
would disagree on every frame.

0.6.1
- Network errors are explained instead of quoted. Every message the connection screen showed was the
framework's own text, taken straight from the exception and never passed through the mod's own
table - so a host whose listening address was not one of their machine's got "the requested address
is not valid in its context", in whatever language Windows is in. It now says that the listening
address is your own machine and not the one you are joining, and that 0.0.0.0 accepts every
adapter. Thirteen other messages were going out the same way and now go through the table too.
- A second, older installation is reported at startup, in the BepInEx log. CoopFoundry lives in two
folders - BepInEx for the connection screen, the game's Mods folder for the mod site - and the
game runs whichever it loads first, which is the one in Mods. Updating only the BepInEx half left
the old version running with no sign of it: the newer half never starts, so its own warning about
this never appears. The check now runs from the half that was updated, before the race is decided.
- The game's Mods folder is found three ways rather than one, and failing to find it is no longer
silent. The mod list is part of the compatibility check, so a machine that cannot read that folder
is refused by everyone with nothing on screen to explain it. It now says so, and distinguishes
"no mods" from "could not look".

0.6.0
A minor version rather than a patch, because what changed is what CoopFoundry can be installed next
to. Not 1.0: there is still no dedicated server, and playing with other mods is one day old.

- CoopFoundry installs and uninstalls itself. CoopFoundryInstaller.exe finds the game, writes only
BepInEx and the three Doorstop files, sets aside anything it has to replace, and records every path
with its hash. Uninstalling undoes exactly that record: a file changed since the install is
reported and left alone rather than deleted. Switching between a co-op island and a modded solo
island is now two clicks. The manual folder installs exactly the same files for anyone who would
rather not run an unsigned program.
- The archive no longer contains anything that can land next to the game executable. Previous
versions shipped the mod's documentation, manifest.json and a copy of Harmony in the same folder
players were told to copy over their installation, so changelog.txt - the file the game reads its
own version from - was replaced by ours. The packaging scripts now refuse to build an archive
carrying a name the game ships.
- An install from an older version is detected and cleaned up, including the files earlier versions
left at the top of the game folder with no record of them, and the installer offers to have Steam
restore changelog.txt.
- Other gameplay mods are supported. CoopFoundry used to refuse to start a session at all when any
third-party mod was loaded - "this experimental version only accepts the game and its official
DLCs" - which no amount of Harmony work could get past, because the refusal was ours. The reasoning
behind it was sound: a mod changes what the simulation does, and two players running different ones
drift apart silently, which is the worst failure this mod can have. The remedy was too blunt. What
has to hold is that both players run the SAME mods, and that is now enforced where it belongs, at
the handshake: the fingerprint covers every assembly in the game's Mods folder, so a difference is
refused by name before a save is sent. A mod both players share is no more dangerous than the game
itself; one they do not share can no longer reach the simulation. The connection screen lists the
game mods it found, because the mod nobody remembers installing is the one that will differ.

- Other mods are no longer broken by having CoopFoundry installed. The game loads each mod folder's
own 0Harmony.dll in turn, and because those assemblies are not strong-named, the first one loaded
answers for every mod after it. Every other Captain of Industry mod ships Harmony 2.4.2 -
byte for byte the same file - while CoopFoundry shipped HarmonyX 2.9, the fork BepInEx uses. So
whenever load order put CoopFoundry first, every later mod ran against a Harmony older than the one
it was built against, and methods added since were simply missing. That is what stopped a modded
save from loading, and why removing CoopFoundry fixed it. CoopFoundry now ships the same 2.4.2 as
everyone else, so whichever copy wins no longer matters.
- The mod is built against Harmony 2.4.2 for the same reason, and is checked against both builds: it
has to keep working under the HarmonyX that BepInEx loads as well as the 2.4.2 the game loads,
since which one it gets is not ours to decide. The one call that differed between them, UnpatchSelf,
is gone - it existed only in the fork.
- A player missing one of the game's own mods is now told before anything is sent. The compatibility
fingerprint covered CoopFoundry's files and the DLCs but not the Mods folder, so somebody without
the right mod received the whole save, waited through the transfer, and was then thrown out by the
game with "Failed to load mod 'x': Mod 'x' was not found" - an error from the game about something
CoopFoundry could have seen first. The Mods folder is now part of the fingerprint, and the refusal
says what to compare, in order: the CoopFoundry version, the game mods, the DLCs. Two players whose
mods differ could never have shared an island anyway; now they learn it in a second rather than
after a transfer.

- BepInEx's own copy is untouched and stays HarmonyX: it does not run on anything else. Five other
ways out were tried and measured first, and all of them fail - a newer HarmonyX needs MonoMod
packages BepInEx does not ship, a newer MonoMod removes API BepInEx still calls, Harmony 2.4.2 is
missing seven types BepInEx needs, and both BepInEx 6 pre.2 and its latest development build carry
the same old HarmonyX. Only the mod folder's copy ever had to change.

0.5.59
- The Steam self-check now sends the save a second time with Steam squeezed down to what a real relay
does. A connection to your own identity is short circuited locally: it runs at memory speed, the send
buffer never fills, and the one branch that has already broken a real session - the wait when Steam
answers that its buffer is full - never runs. A transfer that succeeds under those conditions proves
nothing about it.
- So Steam's own throttles are turned down for that phase: the rate to what a real session was measured
at, the buffer back to Steam's default instead of the widened one, and a relay's worth of latency and
packet loss. This is not the relay and is not claimed to be; it is the conditions the relay creates,
which is what the code was written for. The settings are global, so they are put back afterwards.
- Every wait on a full buffer is now counted, and the result says how many there were. If it comes back
zero the phase says plainly that it did not reach the code it exists to reach, instead of passing
quietly - a test that cannot fail is not evidence.

0.5.58
- The Steam self-check now sends a real save, the one chosen just above it in the menu. That is the
slowest and heaviest part of a real Steam session, and nothing had ever put a save through that
transport at all.
- It uses the game's own sending code and the game's own receiver rather than a copy of either. The
reading of the file, the hashing, the block size, the way the file is shared with the game, the
staging and the final SHA-256 check are all the real ones - a copy is the one thing certain not to
catch a mistake in the original. Reaching the end means the file that arrived is byte for byte the
file that left; the copy is then deleted.
- Sending a save was pulled out of the sending loop into a piece both can call, which is what made
that possible without duplicating it.

0.5.57
- The Steam self-check now tests what was actually new. Passing it with one connection and a
twenty-seven byte message proved the handshake, and nothing about the two things most likely to be
wrong: holding several players at once, and carrying a save. It runs in three named phases so a
failure says which part failed - one connection, then two at the same time each sending something
different to prove their messages do not cross, then eight megabytes in one-megabyte pieces to prove
that a payload past Steam's own message ceiling is cut up, handed over, waited out when the send
buffer fills, and put back together unchanged.
- The check cannot hang any more. A routing fault would show up as a message that never arrives, and
the read waiting for it would have waited for ever with the button still saying "running". After
ninety seconds the connections are closed, which makes every blocked read give up and report.
- The speed it prints is loopback speed, with no relay hop and no bandwidth limit. It is not what a
friend will see, and the result says so.

0.5.56
- A button that checks the Steam connection on its own, with nobody else and no second copy of the
game. It opens a relay socket, connects to your own Steam identity on a port of its own, runs the
real encrypted handshake and sends a message back and forth. It is how the fault that made every
Steam session impossible for fifteen releases would have been caught: no test could reach that code
and no session could be run without a second account that owns the game.
- One process rather than two, deliberately. Two copies of the game on one PC share one Steam client
and therefore one identity, so both would be listening as the same person on the same port and
nothing would say which of them a connection belongs to.
- The result says which kind of failure it is. If the connection never arrives, Steam may simply
decline to route a call to your own identity, and then the test says nothing about the mod. If it
arrives and something later fails, the fault is the mod's. The two are not reported as the same
thing.
- What it cannot check: the lobby and the friend invitation, which need a second person, and anything
about how a real link behaves.

0.5.55
- The menu keeps its background once you touch it. A style carries eight backgrounds, not one, and
IMGUI chooses between them: a window that has been clicked is drawn "on", a control under the pointer
"hover", the one holding keyboard focus "focused". Only "normal" was ever set, so the other seven fell
back to the game's own skin - which is why the panel was properly dark until the first click and then
became a transparent window with a white outline until it was closed and reopened. Every state of
every style is now painted.
- The menu also survives a save being loaded. Its one-pixel textures were created without the flag that
keeps Unity from destroying them on a scene load, while the styles holding them lived on, so the
styles ended up pointing at textures that no longer existed. They are kept now, and the styles are
rebuilt if they are ever lost anyway.

0.5.54
- Steam connections work again. They had been broken since the rework for several players, and no
session log since then shows a single successful Steam handshake. The relay used to be held on the
peer object built after authentication, while the only thing that collected Steam's messages walked
the list of peers - so during the handshake nothing fetched the very bytes the handshake was waiting
to read, and it could never finish. The relay is held from the moment the door opens, and pumped from
then on.
- A Steam host can now hold several players. The transport held exactly one, and said so in its own
code, turning any second caller away with "accepts one client". The door and the connections are now
separate things: the door accepts, and each player gets their own stream. Everything the mod gained
for several players - the lobby, joining in progress, catching up, recovering one player alone - now
works over Steam as well as over a direct address.
- A Steam player who drops out can come back. The Steam socket is no longer closed when the room
fills; it simply turns callers away, so there is no port to give back and none to fight over on the
way in again. Reopening a freed place costs nothing.
- One Steam player failing to authenticate no longer ends the accept loop, so a host keeps taking
players for the rest of the session.
- The menu says that Steam is slower. It sends a save at a few hundred kilobytes a second where a
direct connection manages tens of megabytes, which is Steam's relay and not something the mod can
fix. It is said where it is decided, in the invite section, and again on screen while somebody is
actually waiting - with "several minutes" instead of "about a minute" when the wait is Steam's.
- The menu is reorganised. The number of players and the join-in-progress switch had no heading and sat
between the save and the two ways of connecting, reading as though they belonged to the save; they
are now their own section, GAME SETTINGS, with each choice explaining what it will do. The two ways
of connecting are named for what they are and say what they cost. Buttons say which of the two they
act on.
- The status dot now has a colour that means something - green playing, amber waiting, red stopped.
Both branches of that line used to print the same grey dot. Under it, whether you are hosting or
joining, and over which of the two connections.

0.5.53
- A ping now appears on screen, with its "Go there" button, whether or not the mod's menu is open. It
lived only inside that menu - and a player plays with the menu closed, so nobody ever saw that
somebody had pointed at something. The whole purpose of the key is to say "over here" in the moment,
and that moment does not survive asking somebody to open a menu to hear about it.
- The band stays for twelve seconds and counts down, while the menu keeps the ping for two minutes as
before. A message worth interrupting a view for a few seconds is not worth sitting across it for two
minutes.
- Your own ping says "you are pointing at", rather than telling you what you just did in the third
person.

0.5.52
- F9 now finds the camera. The game keeps its camera state in Mafi.Core.UiState and the mod looked for
it in Mafi.Core, so it was never found - and a silent catch turned "I looked in the wrong place" into
"nothing to point at". Everything else the feature needs was where it was expected.
- That whole path says why it failed instead of failing quietly. Four different causes used to produce
one blank refusal; each now names itself in the log, and the menu no longer claims there was nothing
to point at when the truth is that it could not look.
- A successful point is recorded too, with the tile it sent.

0.5.51
- The host no longer decides that a player is back in step because it has finished sending to them.
Having sent somebody every frame is not the same as their having played them: the host declared a
player caught up the moment the last frame left the wire, thirteen seconds before they had actually
replayed it, and then began waiting for acknowledgements that were still answering old frames and so
were being discarded. Nothing confirmed the frames it was waiting on. A backlog long enough to take
over thirty seconds to replay - a longer absence, or a larger island - would have reached the silence
limit and stopped the session for everyone. Being back in the present is something only the player
can report, so it is now read from their acknowledgements.
- The log says which of the two happened: that every frame has been sent to somebody, and separately
that they have reached the present.

0.5.50
- A band across the top of the screen now says what the mod is doing whenever it makes somebody wait,
whether or not its menu is open. A player joining a game receives it, loads it, then replays the
frames they missed - about ten seconds of it, measured twice in the session logs - during which the
game races ahead and answers poorly. Without a word on screen that reads exactly like a crash, and
nobody investigating a crash goes looking through menus.
- While catching up, the band counts down: how far through it is, and how many frames are left, so it
is visibly working rather than merely claiming to be.
- The other players get their own line - somebody is loading, or somebody is catching up - and are told
they can carry on playing, because they can.
- A finished catch-up now records how long it took and how many frames it covered.

0.5.49
- A player joining a game in progress was paused, with no way out of it. Loading a save leaves the
simulation held still, and the order to resume had been given on frame one, long gone. Their very
first frame could not advance a single step, so they reported being one step behind, were recovered,
loaded again and sat paused again - the loop the last three sessions kept showing. The pause state is
now said again on the frame such a player must first play, and it travels to everybody, so it stays
one command on every machine at one frame. This was the real reason joining in progress failed; the
previous release's explanation of it was wrong.
- A request for a diagnostic reading arriving while a machine is loading no longer ends its session.
There is nothing to take a reading of, and the state the request was about has been replaced anyway.
- A held simulation is no longer asked eight times to advance, nor reported as an anomaly for refusing.
It is meant not to advance.
- A place freeing up behind a door that already stood open says so, rather than reporting that the door
is stuck.

0.5.48
- A player joining a game in progress arrived one simulation step behind, every time, and was sent
away to be recovered again for ever. The cause: the game's runner does not count updates, it banks
the time it is given and takes a step once the bank passes a threshold. On a freshly loaded game the
bank is empty and one update sized to reach that threshold lands exactly on it rather than past it,
so nothing moves and a step is silently lost. That cost nothing while every machine loaded at the
same moment and lost the same step; it broke the instant one player loaded alone, because the host's
bank was warm and theirs was not. A step is now asked for until it happens rather than assumed.
- Frames kept for a player who is behind are now found by their number instead of by counting from the
oldest one. A frame can cover more than one simulation step, so those numbers ascend without being
consecutive, and counting from the oldest picked the wrong frame and could read past the end of the
list.
- An unexpected fault now writes down where it happened, not only what it was. "Index was out of
range" can come from any of a dozen lines, and choosing between them by reasoning costs a whole test
session.

0.5.47
- A player being put back in step, or arriving late, no longer ends the game for everyone. They
answer for the frames they are working through one at a time, and the host refused those answers
because it was not waiting for them: the session stopped with "unexpected acknowledgement" the
instant they played their first recovered frame. Such answers cannot be checked against anything -
by the time the host is at frame 725 it no longer holds what frame 337 produced - so they are
counted and let go, and verification resumes by itself once the player is in step again.
- Taking a copy of the world for one player no longer puts every other player out of step. Writing a
save changes the simulation, which is why both machines are made to save on the same frame; but the
copy sent to somebody joining or recovering was taken the moment they asked, so the host alone made
that change and drifted away from everyone still playing. Putting one player right put all the
others wrong. The copy now waits for the next frame, where it is announced to everybody and the
host's copy is that frame's save.
- A machine that has asked to be put right no longer dies of the frames still on their way to it. It
had already stopped playing, so the next frame to arrive was refused as a protocol error, and the
reason reported was that refusal rather than the disagreement that caused it.
- The disagreement itself is now written down. A simulation step differing from the host's was
detected, acted upon and never mentioned, which is why the previous session's logs named a symptom
three messages downstream of the cause.
- Catch-up now traces what it asks of the simulation and what the simulation does, for the first
frames of each episode, and says so loudly if the two ever differ.
- A room closed at launch because it is full no longer claims that nobody can return. It reopens as
soon as a place frees up, which is what it already did.

0.5.46
- Rejoining a game in progress now actually works. Three separate faults stood in the way, and each
one alone was enough to make it impossible.
- The door was slammed shut the instant a game began. A conditional close had been added above the
old unconditional one without the old line being removed, so the setting that was meant to keep
the room open was overruled one line later. Whatever a host chose, nobody could come in.
- Joining in progress is now allowed by default. It was off, on the reasoning that an unexpected
guest mid-session is a surprise. But the same switch decides whether somebody who has dropped out
may come back, and being unable to return to the game you were just playing is not a surprise
worth protecting anyone from. A host who wants a sealed room can still say so.
- A player arriving at a game that had already been put right a few times was turned away. Their own
count of resynchronisations starts at one and the host's does not, and the check insisted the two
be a step apart at most. Any state from the present or later is now accepted; only a genuinely
overtaken one is refused.
- A dedicated server keeps its door open unless told otherwise, with --coop-closed. It is left
running so that people can come and go, which is the whole point of one.
- A player admitted mid-game was killed two seconds later. They were handed a copy of the world and
then, before they could possibly have loaded it, sent the frames that followed — statements about
an island they did not yet have. They could only refuse, with "unexpected simulation step", and the
session ended. Frames are now held back until they say they have the world.
- The logs now say why a door is shut rather than leaving its silence to be interpreted. A room
closed at launch, a place freed that could not be reopened, a place freed behind a closed door, and
a player still loading each announce themselves.
- Building the mod no longer installs it where the game cannot see it. The install step targeted a
folder under AppData that does not exist, so it wrote into nothing while the game went on loading
the previous build. Every build now compares what it has just compiled against what the game will
actually load, and says so plainly when they differ.

0.5.45
- Fixes two reasons a player still could not rejoin.
- Opening a session used to leave the previous one listening. Nothing complained at the time; the
port simply refused to be taken again later, when the host tried to reopen its door for somebody
rejoining — an error arriving so far from its cause that it looked like a different bug. The
previous connection is now let go before a new one is opened, and reopening retries for a second
rather than giving up on the first refusal.
- Addressing a player who has just left no longer ends the session. The connection layer notices a
departure before the game has finished tidying up after them, so for a moment the game still has
them on its list and still speaks to them. That is an ordinary race, not a fault — and treating it
as one ended the session with "player not found" every single time anybody disconnected. A host
alone in its room can speak to nobody without that being a failure either.
- Three more tests, one for each: a port that comes back, a place that frees up, and a message to
somebody who has gone.

0.5.44
- A player can rejoin a game they have just left. The host closes its door when the room is full,
which is right, but nothing ever opened it again — so a freed place could never be taken. It now
reopens the moment somebody leaves.
- A host no longer ends the session when its last player leaves. That was the two-player assumption
still talking: with a room and a door, a host with nobody in it is a host waiting again, and the
simulation simply pauses until somebody arrives. It is also what a dedicated server spends most
of its time doing.
- Added tests for both: a place that frees up can be taken again, and a host outlives its players.

0.5.43
- Players can now join a game already in progress. They are handed the island as it stands and then
everything that has happened since, which they play back until they are in the present. Nobody
else stops, or even notices. Off by default — an unexpected guest in the middle of a session is a
surprise rather than a feature — and switched on next to the player count.
- Added pings. F9 points at whatever you are looking at, and everyone else is told where, with a
button to go and look. It travels as a courtesy: if a queue is busy the ping is dropped rather
than delaying anything, and it touches nothing the simulation depends on.
- Added the makings of a dedicated server: a copy of the game that opens an island, waits for
players and arbitrates for them with nobody sitting at it, reopening the lobby by itself if the
session ever ends. Launch with --coop-dedicated, and optionally --coop-save=NAME, --coop-port=,
--coop-bind=, --coop-players= and --coop-join-in-progress.
Whether the game will also run without drawing anything is a separate question, and one to be
answered by launching it rather than by guessing: the simulation is started by a scene that loads
assets. If it will, these options make a true headless server; if it will not, they still give a
machine that hosts without anyone playing on it.

0.5.42
- A player who falls out of step no longer stops everyone else. The host writes a save, sends it to
that player alone, and keeps playing; they load it and then replay everything that happened while
they were away, faster than real time, until they are back in the present. Nobody else notices.
When there is nobody in particular to blame, everyone is still put back together as before.
- Frames are no longer thrown at everyone at the same moment. The host keeps what it has sent, and
each player is given what they personally still need — which is one frame for anyone keeping up,
and a whole backlog for anyone who is not. Players who are behind are left out of the wait for
acknowledgements, so being behind never holds the others still.
- A backlog is replayed within a budget of twenty-five milliseconds per update, so the game keeps
drawing and answering while it catches up. The panel says how far behind it is.
- A save now carries the frame it was taken at, so a player who receives one mid-game takes up the
count where the host left it instead of starting again from the first frame.
- Network protocol raised to 18.

0.5.40
- Groundwork for more than two players. The connection layer now holds several players at once
instead of exactly one: it tells their messages apart by who sent them, can reach one of them on
its own, and survives losing one without taking the others down with it. Nothing visible changes
yet — the host still takes one player, because the layer above it still keeps a single
acknowledgement and a single set of commands — but a third player, joining a game already running
and recovering one player without stopping the rest all rest on exactly these four things.
- The connection layer now writes to the session log: who connected and from where, who was lost and
why, how long a save took to reach each player. Diagnosing a session that goes wrong should not
need a second session.

0.5.39
- Finishes what 0.5.38 started. Acting on a lot of buildings at once passes through six separate
limits on how many commands may be queued, sent, read, tracked and reported, and 0.5.38 raised
only one of them — so the same drag stopped the session again a few steps further along. All six
are now derived from a single pair of values: what one network frame can carry, in commands and in
bytes. Whatever does not fit waits for the next frame.
- One of those limits would have refused a frame the sender was now willing to build, which would
have ended sessions for a different reason. It is set from the same value as the sender's.
- Added a test that checks the limits still agree with each other. Raising one without the others is
what caused this, twice.

0.5.38
- A single action on a lot of buildings at once no longer ends the session. Dragging across a row of
them to toggle construction priority produces one command per building, and once that passed a
hundred and twenty-eight in the same moment the session stopped outright. It is ordinary play, and
it now simply sends what fits in one network frame and the rest on the next, a tenth of a second
later. What may wait is measured in bytes against what a frame can actually carry, rather than in
a count chosen years ago.
- Anything still waiting to be sent is dropped when the host's state is reloaded, since it belongs
to a world that no longer exists.

0.5.37
- Found what has been causing the desyncs, and it is saving the game.
Saving is not a read-only act. The game's pathfinding manager listens for it, and when it hears it
it throws away whatever path search is in flight, pushes that task back to the front of its queue
to start again from the beginning, and clears its record of which vehicles had failed to navigate.
That is a change to the simulation — and the game decides when to make it from the wall clock, not
from the simulation, so two machines made it at two different simulation steps. From that moment
their vehicles were searching for different things.
It fits everything that was measured: desyncs arriving just after a save, a divergence that always
turned out to be a vehicle heading somewhere else, and recoveries five and a half minutes apart in
a game whose autosave interval is five minutes.
- The game no longer decides *when* an autosave happens during a session, only *whether* it is due.
It still applies the host's own autosave interval, and still respects their having turned
autosaving off altogether — the mod reads none of that, it simply defers the save the game asked
for by one frame so that both machines make it between the same two simulation steps. Neither runs
Autosaving is not lost, only moved.
The host's setting governs for both players, because only one clock can decide.
- Fixed the freeze this caused on its first outing. The first version held both games still until a
requested save had been written, so that neither could run a simulation step across the moment the
save changes the pathfinding state. But the game dispatches a pending save from its sync phase,
and that phase runs only as part of a simulation step — so waiting for the save to finish before
running another step meant waiting for something that could never happen, and the game stopped
dead the first time it tried to save. That wait was also unnecessary: the sync phase is part of the
step machinery, so once both machines ask for the save after the same batch of steps, both act on
it at the same following step anyway.
- The synchronised save is the game's own autosave, triggered the way the game triggers it. That
matters for more than the file name: a save marked as an autosave goes into the game's rotation
and is cleaned up in its turn. Asking for one through the public entry point would have written a
*manual* save instead — still a real save, but one the game never deletes, and a nine megabyte
file every five minutes that nothing removes fills a disk over a long session.
- If the mod ever finds it cannot save, it hands autosaving straight back to the game and says so in
the log. A session that recovers every so often is a nuisance; a session with no automatic save at
all is a lost game if anything crashes.
- Network protocol raised to 17.

0.5.34
- A divergence report now shows the differences that mattered, worst first. It used to show the
first six it came across, and since they arrive sorted by entity id that meant the six
lowest-numbered vehicles — never the one that actually forced the recovery. Five hours of logs
reported a vehicle facing ninety degrees the wrong way and a truck carrying a different load
without either ever appearing in the list. The report also says how many other vehicles differed.

0.5.33
- Fixes sessions being ended by a command that had not actually gone wrong. Commands are checked by
asking the game whether the two results are equal, and for a result that is a collection rather
than a number that question is answered by comparing object identity. The host's own result and
the one it has just decoded from the network are never the same object, so such a command reported
a difference every single time it was used, however identical the results were. Removing surface
designations does this: three recoveries in three minutes, and then the session stopped.
- A reported difference is now confirmed against the serialized bytes before it is believed.
Identical bytes mean identical results, whatever the comparison says about the two objects holding
them. A genuine difference still produces different bytes and still triggers a recovery, so this
removes a false alarm without hiding anything.
- Results that are collections are now described by what they hold. The report used to print only a
type name, so a message saying two results differed showed the same text twice and said nothing.
- A difference that cannot be reconciled now names the vehicle and the field behind it. The first
session with this in place answered the question immediately: in both cases it was the load of a
single truck.

0.5.32
- The pathfinding step budget is now the same on both machines every frame. The game works it out as
a base amount plus twenty-five steps for every search waiting in the queue, and those queue lengths
were measured differing between the two players — three against zero, giving budgets of 203850
against 69000. A step-limited search stops where its budget runs out and picks its destination from
the candidates it managed to reach, so different budgets send the same vehicle to different tiles.
The queue term is removed; the rest of the loop is unchanged.
What it costs: when many vehicles ask for a path at once the queue no longer buys extra steps, so
paths come back a little slower under load. If anything needed for this is missing the patch is not
installed at all, and if it ever fails the game's own code runs instead — the session log says which.
- Fixed a heading difference being measured the long way round the circle. A vehicle at 359.9 degrees
was reported as forty-eight degrees away from one at 0.1 degrees instead of a fifth of a degree,
which made the worst-case figures in the log far larger than anything that had happened. The
recovery decision never used that figure, so nothing behaved differently — the reports were wrong,
not the behaviour.
- A difference that cannot be reconciled now says which vehicle and which of its fields made it so,
instead of only that one did.

0.5.31
- The mod now speaks English as well as French. Everything a player reads is translated: the
multiplayer panel, every status line, every connection notice, and every message that can stop a
session. The mod follows the language the game is running in where it can read it, and falls back
to English, which is what players who are not French were seeing French for.
- Added a language switch at the bottom of the panel, so nobody is stuck with a wrong guess. The
choice is remembered between sessions.
- The panel no longer prints the whole technical reason for a recovery at the player. It used to be
several hundred characters of hashes, step budgets and per-vehicle gaps; it now says how many
times the games drifted apart and points at the session log, where that detail belongs.
- Session logs stay in French. They are a diagnostic artefact read by whoever is fixing the mod, and
keeping them in one language keeps them comparable between sessions.
- Each translation is written beside the sentence it belongs to rather than behind a key in a table,
so a translation cannot drift away from its original or be left pointing at something that no
longer exists.

0.5.30
- Save transfers should be considerably faster. Measured on a real session, 8.7 MiB took 34 seconds,
about 0.3 MiB/s — neither the disk nor the link, but Steam's own pacing. Its reliable send buffer
defaults to 512 KiB, and this transport hands over a block and then waits when that buffer is
full, so a buffer that small keeps running dry and Steam's bandwidth estimate never climbs. The
buffer is now four megabytes and the ceiling is raised so the estimate can rise if the connection
can take it. The floor is deliberately left alone: forcing a minimum rate above what a connection
can carry does not make it faster, it makes it lose packets.
- A session no longer ends because the other player kept playing. The host holds the other player's
actions until it can schedule them, and the limit on that queue was a hundred and twenty-eight —
a number an ordinary session reaches, because a recovery takes the better part of a minute and
nobody stops playing during it. Reaching it ended the session outright. The limit is now four
thousand and reaching it takes the client's state from the host instead, which is what happens
after a recovery anyway.
- The transfer timings are in the log: how long the reference save took to write, how long it took
to reach the other machine and at what rate, and how long each side spent reloading it.

0.5.29
- Fixes a save refusing to load with "Sharing violation", which could stop the game initialising
entirely. A save being sent was held open for the whole length of the transfer and shared for
reading only. The game opens a save for reading and writing, and Windows refuses that outright
while another handle permits reads alone — so for as long as a transfer lasted, that save could
not be loaded by anyone. This came in with 0.5.27, which moved the wait for the transfer to finish
inside the block that held the file.
- The handle is now released the moment the bytes have been read, and while it is held the file is
shared for writing and deleting as well. The mod can no longer be the reason a save refuses to
open, whatever else is happening.
- Fixed the transfer timing added in 0.5.27, which was wrong: it stopped counting when the last
block left the queue rather than when it had been handed to the transport, and on a real relay
that is where nearly all of the time goes. It reported a nine megabyte transfer as taking a tenth
of a second.
- Added a test that opens the save the way the game does, repeatedly, throughout a transfer. It
reproduces the failure exactly, down to the message the other player was shown.

0.5.28
- Fixes the transfer failure in 0.5.27. Steam refuses a single message beyond its own maximum and
answers InvalidParam, which is fatal and indistinguishable from a real protocol error, so the
session ended mid-transfer and the other player was told the connection had been cut. The one
megabyte blocks introduced in 0.5.27 were past that maximum.
- The Steam transport now cuts an oversized write into segments Steam will accept instead of handing
it over whole and being refused. It is a stream and the reader reassembles whatever it is given,
so this is invisible to everything above it.
- Save blocks are 256 KiB, which travels whole in a single Steam message even without that
splitting. Still four times fewer authenticated frames, network messages and queue slots than the
64 KiB of 0.5.26, without depending on the segmentation to be correct.
- Added a test that these limits stay consistent with each other. Raising one without the other is
exactly the mistake that caused this, and it is cheap to catch here rather than in a session.

0.5.27
- Saves are now sent in blocks of one megabyte instead of sixty-four kilobytes. Each block costs one
authenticated frame — an HMAC over its bytes, one network message, one queue slot — so a save of
nine megabytes used to cost a hundred and thirty-seven of everything and now costs nine. What
actually paces the transfer is Steam's own reliable send buffer, so this cuts the overhead around
the transfer rather than the transfer itself.
- Compression was measured and rejected: the game already compresses its saves. Deflate at every
level returns a file one percent smaller, for a quarter of a second of work each way. There is
nothing there to win.
- A recovery now says where its time goes, because until now that was guesswork: how long the
reference save took to write, how long it took to reach the other machine and at what rate, and
how long each side spent reloading it. Both machines log their own figures.
- The send queue can no longer be filled by a transfer at all — a megabyte per slot means five
hundred and twelve slots hold a maximum-size save — so the test for that now stalls the receiver
to fill the queue directly, which is a more honest test of the same contract.

0.5.26
- The destination divergence is fixed, and confirmed by measurement. The 0.5.24 change reports a
constant for the recomputed clearance chunk count, and the logs now read chunksRecalcules=0 on
both machines in every sample, so the Harmony patch really did take effect. In the two divergences
of the latest session the destination tile, the load, the heading, the speed and the steering were
all identical — where every earlier case had the two machines sending a vehicle to two different
tiles. Recoveries fell from four in thirty minutes to two in thirty-three.
- What remains is a different thing entirely, and much smaller: of some four thousand entities, a
single truck differed in a single field, its navigation state. One machine had begun its next path
search, the other had not yet. It went one way the first time and the other way the second, which
is what a phase offset looks like rather than a decision.
- A phase offset of one step is not a diverged simulation, but the hash could not tell the difference
and a whole island was reloaded for it. The host now asks for the detail before deciding. On a
confirmed difference the client sends one row per vehicle — position, heading, load, spawn and
navigation state — and the host measures the gap.
- A gap that nothing can reconcile, a vehicle one side does not have or a different load, is still a
divergence and the world is still replaced. A gap that is nothing but a fraction of a step of
position, with the same destination and the same load, is logged, watched and allowed to close by
itself. It is escalated the moment it grows past a sixteenth of a tile or outlives 1200 frames, so
tolerating it can delay a recovery but never replace one. An unanswered request ends in the
recovery it was trying to avoid.
- The log now names the vehicles and what differs about each, so a divergence no longer has to be
reconstructed by comparing dumps afterwards.
- Removed the targeted vehicle repair. It was measured over a full session at three attempts and
nothing held, has been switched off since 0.5.21, and the measurement above explains why: it wrote
position and heading while the difference was in the pathfinding phase behind them.
- Network protocol raised to 16.

0.5.25
- Fixes the connection failure in 0.5.24: the host died with "network queue saturated" about a tenth
of a second after the two players connected, and the client saw the session end mid-message
because the host had gone. It was not the new protocol — authentication had already succeeded.
- A save is queued for sending as 64 KiB chunks, and the queue held 128 of them. An island of a few
thousand entities makes a save of some eight megabytes, which is more than 128 chunks, so the
transfer filled the queue on its own. The first control message that then wanted a slot was
refused outright and took the session with it. The island in this save had grown past that point,
which is why it had started failing every single time rather than occasionally.
- The queue now holds 512 messages, deep enough for a whole transfer, and a message that finds no
free slot waits for one instead of being refused. A full queue means the sending loop has not
caught up yet, which is ordinary back-pressure during a transfer; it is only a failure if it never
clears. Latency replies, whose loss costs nothing but a number on a screen, are now dropped rather
than allowed to end a session.
- Added a test that fills the queue for real and checks a control message still gets through. It
refuses to pass if the queue did not actually fill, so it cannot quietly stop testing anything.

0.5.24
- The cause of the vehicle divergences is found, and it is not the vehicles. Every one of the four
divergences in the last session was the same thing: the two machines had sent one vehicle to two
different destination tiles. One case makes it beyond argument — of 4485 entities, a single field
differed in the whole dump. An excavator at an identical position to the last digit, identical
heading, speed zero, steering zero, identical job state, and a destination tile one tile apart.
- The destination is not computed, it is drawn. The pathfinder collects the candidate tiles the
search reached and picks one by index from a random generator the game creates as Unrestricted,
which is to say deliberately outside what its own determinism validator checks. Which candidates
the search reached depends on where it ran out of budget.
- That budget is charged 25 steps for every clearance chunk the search had to recompute. Chunk
recomputation is a lazy cache, so the charge depends on what happens to be cached, and that is
not the same on two machines: the client rebuilds its world from the host's save at every
recovery, and the two players look at, hover over and build in different places. A difference of
cache warmth — a mouse in a different part of the map — decides where a search stops, and
therefore which tile it comes back with.
- The recomputed chunk count now reports a constant while a session is running, which makes that
charge zero on both peers. It is the only value two machines can agree on without sharing a
cache. A search may use its full budget in a cache-cold moment instead of being cut short: more
work in a frame, never a different answer.
- The three inputs to that budget — waiting tasks, steps remaining and recomputed chunks — are now
logged for both peers side by side on every report, with the differing ones marked, so the next
session confirms it directly rather than by inference.
- Network protocol raised to 15.

0.5.23
- The search counter synchronised in 0.5.22 was not the cause. Measured over a twelve-minute
session it needed correcting exactly once, and the divergence that followed had both machines
reporting the identical counter value. The counter drift was a marker, not the mechanism.
- What the same measurement did show: a single truck, one single differing field. Position,
heading, load, navigation state and speed identical on both machines; steering angle at full left
lock on one and dead straight on the other. Both speed and steering are the output of a smoothed
driver that ramps towards a requested value a little each step rather than taking it at once, so
a single step where the two peers asked for different things leaves a difference that then takes
many steps to ramp back out.
- Those two smoothed values no longer force a recovery. They are reported on their own instead,
with every position, heading and load in agreement. A difference that matters moves a vehicle,
and moving a vehicle is still caught immediately.
- Added a driving-intent check: destination, arrival tolerance, whether the destination is final,
driving direction, remaining road segments and navigation state. None of it is smoothed, so a
difference there is a real difference of decision and must come before any difference of motion.
Reported, never acted upon. This is what will say whether the peers disagree about where a
vehicle is going, or agree and steer differently anyway.
- The entity dumps carry the same intent fields, and every recovery now logs the running counts of
driving, intent and pathfinder reports.
- Network protocol raised to 14.

0.5.22
- Likely cause of the vehicle divergences found and addressed. The same tree planter was recorded
on both machines at an identical position with an identical load, steered by the same amount in
opposite directions: +1.5 degrees against -1.5, its heading exactly 3 degrees apart. That is not
one peer running late, it is the two choosing differently. The pathfinder picks among equally
good destinations with a random generator it reseeds for every search from the start and goal
tiles and from its search counter; the tiles hash deterministically, so the counter is the only
seed input that can drift, and the generator it feeds is created by the game as Unrestricted,
which is to say deliberately outside what the engine's own determinism validator checks. Nothing
kept the two peers agreeing on it.
- The host now publishes that counter with every frame and the client adopts it, but only while no
search is in flight so that a running task is never reseeded underneath itself. The log counts
the corrections applied.
- Fixed the counter always reading as -1 in the previous build: the pathfinder is exposed as a
field, and it was being looked up as a property.
- Network protocol raised to 13.

0.5.21
- The targeted repair introduced in 0.5.20 is switched off. It was attempted three times in a full
session and held none of them: writing the authoritative position and heading is not enough,
because the steering machinery behind those fields has diverged as well. The attempt only
postponed each reload, so it stays in the code and out of the way until there is evidence it can
help. The fallback worked exactly as designed and no session was lost.
- The pathfinder's search counter is now measured every frame and recorded in the snapshots. The
pathfinder reseeds both of its random generators for every search, from the start and goal tiles
and from that counter. The tiles hash deterministically, so the counter is the one seed input
that can drift, and once it does every later search is seeded differently, explores differently
and returns a different route. That feedback loop is what makes two peers spend measurably
different amounts of search effort — the snapshots showed remaining budgets parting company by
2901 and by 103 with both queues empty, which no reordering of the queue can explain.
- Network protocol stays at 12. Both players need this version.

0.5.20
- A confirmed vehicle difference is now repaired in place before any reload is considered. The host
sends the authoritative position and heading of every vehicle and the client applies them, which
takes a frame instead of the better part of half a minute. Only those two fields are written: the
steering machinery behind them is left alone rather than half-written into an inconsistent state.
The repair is then judged over 600 frames — if the vehicles agree throughout it held, and if they
part company again the difference is deeper than position and heading and a full reload follows.
A repair can therefore only save time, never hide a difference.
- The log counts repairs attempted against repairs that held, beside the reload count.
- Diagnostic snapshots list the pathfinder's waiting queue with each request's priority and the
vehicle it belongs to. The manager derives that priority from the current step plus the queue's
own length, so a single extra request reorders the queue and shifts everything after it; this
list is what should show the first insertion where the two peers part company, which is the one
thing still missing to fix the cause rather than its effect.
- Network protocol raised to 12 for the repair message.

0.5.19
- Moved the driving flag to the pathfinding signal, beside the navigation state it was separated
from in 0.5.18. A truck standing still was reported as driving on one peer and idle on the other
while its position, heading, speed, steering angle and load were identical to the last decimal —
the flag flips when a route search completes, so it carried the pathfinder's phase and nothing a
player could see. It forced a full recovery for a difference with no consequence.
- Network protocol stays at 11. Both players need this version.

0.5.18
- Fixed a session ending on "Accusé de réception inattendu" moments after a recovery started. A
recovery clears the running state, but the other player has usually already sent a message about
the frame that was in flight, and that message was then treated as a protocol violation. Frames,
acknowledgements, resynchronization requests and load confirmations belonging to a superseded
epoch are now dropped, which is what they describe: a world that no longer exists. This is why
recoveries were ending sessions rather than repairing them.
- A vehicle difference no longer turns the next player command into a recovery. Command results
and entity counts are already compared on every frame, so a command that behaved differently is
caught on the frame it ran; forcing a full reload instead converted differences that were about
to resolve themselves into interruptions. In the last session the difference was 9 frames old,
of the 40 it was allowed, and the previous one had healed in 5.
- Navigation state moved from the vehicle signal to the pathfinding signal, where it belongs. It
flips exactly when a route search completes, so it carried the pathfinder's phase rather than
anything with a visible effect, and left in the vehicle signal it put back the very noise that
0.5.16 separated out. Position, heading, speed, steering angle and load still trigger recovery.
- Network protocol stays at 11. Both players need this version.

0.5.17
- Fixed a session ending itself after three recoveries spread over half an hour of otherwise
healthy play. The loop guard only cleared its counter after five consecutive full state checks,
and 0.5.12 had spaced those checks four times further apart, so clearing it came to require ten
thousand clean frames inside a single epoch. A session logged recoveries eleven minutes apart,
with dozens of matching checks between them, and was stopped anyway. The guard now measures how
quickly recoveries repeat rather than how many have happened: three within three minutes is a
loop and still stops, anything further apart is an isolated incident and the count restarts.
- The host can reopen a Steam lobby after a session has stopped. Reconnection only ever had a TCP
path, so a Steam session that stopped could not be resumed at all. The client rejoins by
accepting a new invitation, which is the only route Steam offers once a connection is gone, and
the panel now says so instead of leaving the client without an option.

0.5.16
- The pathfinder's internal phase no longer triggers recovery on its own. Adding it to the state
check in 0.5.14 made the check fire on a difference with no visible effect: a seventy-minute
session recorded thirteen vehicle differences, of which nine resolved themselves within fifteen
frames. The snapshots explained why — the two peers were one search cycle apart, the remaining
budget differing by exactly 3000, or by 25 per queued vehicle, which are the manager's own
constants. That is the pathfinder catching up, not a divergence. It is now reported in the log
and never used by itself to replace a player's world.
- The grace window drops from 120 frames to 40. Every difference that resolved itself did so within
fifteen frames, so the rest of the window only delayed the recoveries that were going to happen.
A confirmed difference now interrupts after about two seconds instead of six.
- Network protocol raised to 11 for the separate pathfinding fingerprint.

0.5.15
- A vehicle-only difference no longer interrupts the session on the spot. It is given 120 frames to
resolve itself; if it does, play continues and the log records that it healed. Structural checks
are untouched and still recover on the frame they fire: entity counts, command results and the
full state check all run every frame during that window, and a command from either player ends
the window immediately so that nothing is ever executed in two different worlds.
- Diagnostic snapshots are still written on the first frame a difference appears, before anything
is given a chance to settle, so they describe the divergence itself rather than what it became.
- Snapshots now carry the pathfinder's state in readable form — queued vehicles, remaining search
budget, current task — beside the hashed version, so the two files can be compared directly.
The last session narrowed every divergence to one excavator whose only differing field was its
navigation state, so this is what a real fix will be built from.
- The log reports how many differences healed by themselves against how many had to be recovered,
which is the measure of whether the grace window is worth keeping.
- Network protocol stays at 10. Both players need this version.

0.5.14
- The per-frame check now covers the shared pathfinder's own progress: how many vehicles are queued
for a route, which one is being solved, and how much of its search budget is left. The previous
build narrowed every divergence to a single excavator whose entire state matched except its
navigation state, Idle against PathFinding, meaning a route search completed one simulation step
apart on the two machines. That timing is held by the pathfinding manager and appears nowhere in
entity state, so it is read directly.
- Network protocol stays at 10. Both players need this version.

0.5.13
- The per-frame vehicle fingerprint now covers speed, steering angle, navigation state and whether
the vehicle is driving, not just position, heading and load. Two sessions localised every
divergence to a single excavator standing on an identical tile with a heading that differed by
one whole steering increment, six degrees, which means one peer took a steering decision one
simulation step before the other. Hashing the decision itself reports that step instead of the
step after it.
- Entity snapshots record the same driving state for every vehicle, plus the excavator's own state
machine, so comparing the two files shows what the two peers decided rather than only the result.
- Network protocol stays at 10; no message layout changed. Both players still need this version
because the state check covers more.

0.5.12
- Every acknowledgement now carries a fingerprint of every moving entity: position, heading and
load. Vehicles are the only part of the simulation observed to drift on their own, and the full
check ran far too rarely to say when a drift began. There are only dozens of vehicles, so this
costs almost nothing and pins a divergence to a single frame and a single vehicle.
- The full deterministic check now runs every 2000 frames instead of every 500. It walks the whole
terrain and freezes both games for roughly 400 ms, which is what players felt as a periodic
hitch; entity counts and vehicle fingerprints are verified on every frame and catch drift long
before it does, so the expensive check no longer needs to be frequent.
- Network protocol raised to 10.

0.5.11
- Vehicle cargo joins the deterministic state check. What a truck is carrying is authoritative
simulation state and was invisible to the check until now, which is how two peers could end up
with identical terrain heights, surfaces and flags but different material layers: vehicles
holding different loads dump different material on the ground. Divergences in logistics are now
caught where they start instead of once they have reached the terrain.
- Every detection path now writes diagnostic snapshots. The per-frame entity-count check added in
0.5.10 recovered without leaving any trace, which is exactly the case where knowing which entity
differs is the whole point.
- Entity snapshots record each vehicle's load and each moving entity's heading alongside its
position, so comparing the two files explains the difference rather than only locating it.
- Network protocol stays at 9. No message changed, but the state check covers more, so both players
must run this version.

0.5.10
- Divergences are now detected on the frame they happen. Every acknowledgement carries the peer's
entity count, which is an O(1) read, and a mismatch triggers recovery immediately instead of
waiting for the next deterministic check up to five hundred frames later. A ninety-minute session
showed why this was needed: one BuildTransportCmd created two conveyor segments on the host and
one on the client, yet returned the same entity id on both, so the result comparison saw nothing
and the difference only surfaced at the next checkpoint.
- Accelerated time no longer makes vehicles stutter. Speed was expressed as more simulation steps
packed into one network frame, but GameRunner interpolates a single simulation update at a time,
so those steps were drawn at once and only the last one was smoothed. Speed now shortens the
interval between frames instead, leaving steps per frame governed by latency alone, so the
picture is as smooth at speed three as at speed one. A slow link limits the speed actually
reached rather than degrading the rendering.
- Network protocol raised to 9 for the entity count in the acknowledgement.

0.5.9
- Removed the teammate cursor and map ping added in 0.5.7. Position packets were sent ten times a
second as soon as a peer finished loading, while the other peer could still be loading its copy
of the save and not consuming anything. Twenty-one seconds of that filled the client's Steam
receive queue and dropped a session whose two states were in fact identical. Presence was a
convenience; a reliable connection is not.
- The Steam receive queue is bounded by bytes rather than by message count, and a burst no longer
ends the session on its own. A 256-message cap is reached by a few seconds of small packets but
says nothing about memory, which is what actually needed limiting.
- Network protocol stays at 8. The two removed message kinds were the last ones in the enumeration,
so the numbering of every other message is unchanged.

0.5.8
- Fixed the stuttering vehicles introduced with the speed work in 0.5.7. GameRunner interpolates
moving entities between simulation steps using a parameter derived from its own clock, and that
clock only advances inside Update. The mod called only the input and render passes while waiting
for the other player, so the parameter stayed frozen and vehicles jumped from one simulation
position to the next while everything static kept rendering smoothly. Waiting frames now call
Update with a delta small enough to stay below GameRunner's sim-update threshold: the renderer
interpolates, the simulation does not advance. The step is checked before and after, and
interpolation disables itself with a log line if the simulation ever moves there.
- GameRunner's opportunistic idle simulation work is skipped while a session is active, so those
waiting frames cannot touch the simulation at all. It is a UI-side optimisation only.
- Teammate markers were projected twice too far. The axes were right but a Unity unit is half a
tile, which the calibration line caught on the first session: halving the camera position put it
exactly the 74.9 tiles from the pivot that its orbit radius predicted. The scale is now measured
against the camera's orbit rather than assumed, and the markers switch themselves off instead of
being drawn in the wrong place if the measurement does not agree.
- Network protocol is unchanged at 8, but the mod binary fingerprint differs, so both players must
install this version.

0.5.7
- Game speed now works. Every game.Run is exactly one simulation step: GameRunner only advances the
simulation once the accumulated delta reaches one sim update of 100 ms, and the mod was feeding a
fixed 50 ms, so every other Run did nothing and the speed multiplier had no effect at all. The
host now expresses its chosen speed as steps per frame and sends that count in the Frame packet,
so the client advances exactly as far as the host regardless of its own setting.
- Teammate presence: each player's mouse position is shared as a coloured marker on the other
player's screen, and V drops a map ping that both players see for eight seconds. Presence never
enters the command pipeline nor the deterministic digest, so it cannot cause a divergence; a
failure to read the cursor disables the marker and is logged rather than ending the session.
The session log carries a one-line calibration check that states whether the tile projection
agrees with the game's own camera state.
- Network protocol raised to 8: the Frame packet now carries up to sixteen steps instead of four,
and two presence message kinds were added.

0.5.6
- Fixed the command-phase boundary introduced in 0.5.4, which caused repeated desynchronizations.
GameRunner.Update only calls CollectCommands on the frames where it actually advances the
simulation. On the other frames the authoritative batch was still queued when the phase boundary
ran, so the mod claimed it as simulation-derived and replayed it one frame later on that peer
alone. In a two-machine session 128 of 132 claimed commands were in fact the authoritative batch,
and every recovery showed the host holding more entities than the client. Injected commands are
now tracked by reference and left untouched.
- Player commands scheduled while a simulation step was in flight were still being swallowed after
the step, which is how a vehicle depot placed by the host reached no one and forced a recovery.
The post-step sweep is removed: whatever the player schedules stays in the queue and is collected
and transmitted normally on the next frame.
- Steam relay: a save transfer pushes 64 KiB blocks faster than Steam's bounded reliable send
buffer drains, and Steam answered LimitExceeded. That was treated as fatal, so the host dropped
the session and the client reported "Attempted to read past the end of the stream". It is
back-pressure and is now handled by flushing and waiting for room, with a thirty-second ceiling.
- Transport errors reached the panel as raw framework text in the wrong language. A client whose
host had just died mid-transfer was told "Attempted to read past the end of the stream", which
names neither the cause nor a remedy. Failures are now translated into an actionable sentence —
disconnection, refused connection, wrong port, integrity check, timeout — while the original type
and message go to the session log. Wire.Exact also reports how many bytes it actually received.
- The multiplayer panel reported the mod version as the game version. The published package
installs its own changelog.txt, which can overwrite the game's file the version was read from.
A line that is not in the game's format is now ignored in favour of the Mafi.Core assembly
version, and the comparison no longer depends on the suffix letter.

0.5.5
- Fixed the game falling back to English whatever language was selected. BepInEx runs plugins before
the game starts, and the mod installed its Harmony patches from Awake. Preparing those patches ran
the class constructors of the patched UI types, which call Loc.Str and populate
LocalizationManager.s_enUsData. The game then refuses to load its translation file
("Some translation was already requested. Translation file must be loaded first!") and drops the
selected language.
- The patches are now installed on the first Update instead, still several seconds before the main
menu is built, so the multiplayer button and every integration point are unaffected. The
FileSystemHelper patch stays early on purpose: it is only applied when a second test instance
needs its data root redirected.
- Added a diagnostic: the session log reports how many strings the game had already materialised
when the mod took control and when the patches went in, so this regression cannot come back
unnoticed.
- Packaging: entries are written explicitly with forward slashes. Compress-Archive and
ZipFile::CreateFromDirectory both emit backslash separators on Windows PowerShell 5.1, which
breaks the single-root-folder rule the upload validator applies.

0.5.4
- Fixed the main cause of buildings existing on one machine only. GameRunner.Update runs
performInputUpdate in the middle of a simulation step, so a build committed during that step was
collected as a simulation-derived command and replayed locally instead of being sent to the peer.
Session logs show the effect: 1373 entities on the client against 1372 on the host, then
"BuildTransportCmd: host 4073, client 4074" as the entity-id counters drift apart.
- The boundary between simulation-derived commands and player commands is now taken just before
performInputUpdate. Player actions travel over the network again; only commands the simulation
itself schedules stay local.
- Detailed per-command logging: type, owner, id, sim step, payload size and payload fingerprint,
plus prototypes and transforms for BatchCreateStaticEntitiesCmd and the accepted/refused result
reported from both peers.
- Protocol stays at 7; the mod binary fingerprint already prevents a 0.5.3 peer from connecting to
a 0.5.4 one.

0.5.3
- Fixed building commands, including Fluid Storage placement, being accepted while both simulations were already structurally divergent.
- Entity-count or terrain differences now trigger immediate host recovery instead of waiting for a second checkpoint.
- A pending digest mismatch now acts as a command barrier: recovery occurs before either player's next gameplay command can run in divergent worlds.
- Digest acknowledgements now carry entity-count and terrain metadata; network protocol increased to 7.
- This corrective build was prepared without running tests, at the tester's request.

0.5.2
- Fixed grouped building placement producing a different subset of buildings on the initiating peer.
- Every local gameplay command is now executed from the same immutable serialized snapshot sent to the other player, covering all building families that use mutable placement commands.
- The original UI command receives the processed result so placement tools and command callbacks continue normally.
- This corrective build was prepared without running tests, at the tester's request.

0.5.1
- Reworked the multiplayer panel with the dark, high-contrast CoopFoundry layout: cyan section headings, dark input fields, blue-gray buttons, compact save selection, and clearer Steam/IP sections.
- A state-digest mismatch now requires confirmation at the next deterministic checkpoint before automatic recovery. Transient one-off differences no longer force an immediate reload; command-result or step mismatches remain immediate.
- Desynchronization diagnostics now include a readable entity inventory alongside each raw state capture.
- Network protocol increased to 6; version 0.5.1 cannot connect to 0.5.0.

0.5.0
- Game versions other than 0.8.7c now show a compatibility warning instead of disabling the mod or rejecting the connection. The CoopFoundry protocol and mod/DLC fingerprint checks remain strict.
- Added private two-player Steam lobbies, the Steam friends invitation overlay, and Steam relay transport.
- Steam guests now accept the invitation and connect automatically without entering an IP address, port, or code.
- Direct IP and LAN connections now negotiate their encryption key automatically and no longer ask either player for a session code.
- Simplified the multiplayer menu around Steam hosting, direct IP hosting, direct IP joining, and LAN discovery.
- Pause and speed controls are now host-authoritative. Opening the Escape menu on the client no longer pauses the shared simulation or leaves a recovery save paused.
- Steam invitations received while the game is already open and +connect_lobby launches are handled.
- Added a small BepInEx bootstrap so the multiplayer button is available on the main menu; the official game loader only instantiates third-party mods after a save is selected.
- Passed 28 protocol/file tests, the in-engine serialization checks, and a two-instance local run with save transfer, building, demolition, a CharcoalMaker, and host recovery.
- Renamed the public mod and Hub identifier to CoopFoundry.
- This version changes the network protocol to 5 and is not compatible with previous CoopCaptain builds.
- Known limitations: two players only, no dedicated server, and no compatibility guarantee with other gameplay mods.

0.4.0
- Prepared the mod metadata for the official loader; later testing showed that third-party mods are initialized too late for a main-menu connection screen.
- Direct IP hosting, LAN discovery, encrypted session invitations, and two-player chat.
- Automatic host save transfer with integrity verification and a loading barrier.
- Host-authoritative commands, adaptive network batches, reconnect, and recovery checkpoints.
- Synchronization checks for command results, terrain, entities, vehicles, products, population, workers, unity, and research.
- Automatic host recovery and detailed local diagnostics after confirmed divergence.
- Known limitations: two players only, no Steam invitations, no dedicated server, and no compatibility guarantee with other gameplay mods.
Loading forum…

0.7.13

- Smooth simulation is back, and is the default. One simulation step per frame, as in 0.7.1: no
more goods popping off their belt for a frame, no flicker, and building answers at once. The
price is that x2 and x3 are not always reached. The fast mode (0.7.2 to 0.7.12) stays available:
F10 > Host > "Smooth simulation", or pace = fast in server.cfg. Only the host's choice counts.
- The panel has tabs - Join, Host, Dedicated servers, About - and is larger, with bigger text.
About gives the version, author, tester, your equipment line and the session log's location.
- The CoopFoundry button is now always shown, bottom right by default, and can be dragged anywhere;
its place is remembered. About > "Put the button back at the bottom right" resets it.
- Install 0.7.13 everywhere (players and server).

0.7.12
- Dedicated server: the game is now paused while a player loads the world (joining in progress or
being put back in step), and resumes by itself once everyone is ready. A slow loader no longer
comes back too far behind to catch up and gets reloaded again and again.
- Install 0.7.12 everywhere (players and server).

0.7.11
- Dedicated server: floating-point maths now runs exactly as in the game (same Mono options).
- CoopAddons VehiclesIgnoreTransports 1.0.3: vehicles are remembered as soon as they are added.
- Install 0.7.11 everywhere (players and server).

0.7.10
- Dedicated server: a player who had reconnected was cut off by their very first action. Every
player coming back gets a new number, and anything above 1 was refused as an invalid command.
- A frame carrying more than 128 actions (a drag over a long row of buildings) was sent, then
refused on arrival, which ended the session. Up to the full 4096 now go through.
- Joining a game in progress no longer ends in a needless reload: commands played while nobody was
in step were kept waiting for an answer that would never be checked, and the first check after
the catch-up mistook that for a desync.
- Install 0.7.10 everywhere (players and server).

0.7.9
- Dedicated server: games no longer start paused for good. A server nobody joined for five
minutes gave up waiting and reopened; on reopening it read its freshly loaded (and therefore
still) island as "the host paused the game", and started the next game paused - which nobody on
a dedicated server can undo from the game. Waiting for players no longer times out, and the
pause state is only taken from a game actually in progress.
- Pressing play while the shared game is paused now asks the host to resume ("/resume"), when the
host lets players change the speed. Typing /resume in the chat works too.
- Reconnecting or joining a game in progress: the player was refused with "A simulation step was
lost or repeated" as soon as two frames of several steps arrived together, which is exactly what
catching up looks like. Frame order is now checked against each frame's last step.
- No protocol change, but install 0.7.9 everywhere (players and server).

0.7.8
- Smooth at every speed. A frame could carry several simulation steps (0.7.2), and they were
played back to back in a single update of the game: the picture jumped several steps and then
stood still until the next frame - the faster the game, the bigger the jumps. A frame's steps
are now played one by one at the game's own rhythm (a step every 100 ms divided by the speed),
on the players' machines and on a host playing from the game. A player catching up still plays
them at once.
- Frames are kept to one step whenever the link lets the game reach nine tenths of the chosen
speed; only a slow link gets more, and they are spread out too. The session log's "Rythme" line
gives the round-trip cost and the steps per frame chosen.
- No protocol change: 0.7.7 and 0.7.8 can play together, but install 0.7.8 everywhere.

0.7.7
- Dedicated server with Gameplay++: the transport bridges were never switched on on the server.
Gameplay++ looks up a Unity object just before turning them on; without the engine that lookup
failed, the mod swallowed the error, and goods crossed bridges on every player's island but not
on the server's. Trucks then got different jobs on each side about ten seconds after every load.
The server now answers such lookups as a fresh game would ("no such object") and checks, in its
log, that the bridges are on.
- The server kept a different game name after every recovery reload, and Gameplay++ files its
bridge and parking settings by game name. It now uses the same name as the players' games.
- CargoHelicopter's patches on trucks, which the mod applies from a Unity component, are now
applied on the server as well.
- Gameplay++ Parking HQ, excavator parking and heliport: their once-a-second rounds now follow the
island's own clock, and their per-vehicle memories are emptied on every machine at the same
moment (load and every shared save), instead of depending on when each machine opened the island.
- Server: when data_root is set, the settings mods keep in the profile live there too.
- Checked: 2000 frames on an island with Gameplay++, Cheat++, WindPower, CargoHelicopter,
InfiniteGroundwater and KeybindFramework, server and player, without a single recovery.

0.7.6
- Dedicated server: it no longer dies when a player joins an island with Cheat++ or WindPower.
Interface objects these mods half-build without the engine were cleaned up later on the
garbage collector's thread, and an error there ended the process. The server now treats such an
error the way the game itself does: reported, not fatal.
- The CoopFoundry panel is larger (600 x 800 at most) and keeps its size: a server's line shows
its name, players, state and "[password]" on the button, with the version and mods in small
print underneath, instead of one button as wide as the whole mod list.

0.7.5
- WindPower is now identical on every machine. Its wind was random three times over (a starting
direction from Unity's generator, a drift from a clock-seeded generator, and a turbine rota that
restarted at every load), so any island with turbines desynchronised within seconds, with or
without a server. The wind is now a function of the simulation step: the same gentle drift, the
same for everybody, however and whenever the island was opened.
- The dedicated server runs islands with Cheat++ and WindPower. Pieces of the game's interface
these mods ask for get empty stand-ins on the server, and their windows are skipped there; their
simulation runs as in the game. The server also checks the island before opening, and refuses
to open, naming the mod, if it cannot run it - instead of sending it to a player who then waited
paused until disconnected.
- The installer inside the archive is rebuilt with every package (0.7.3 and 0.7.4 shipped the
0.7.2 installer).
- Known: Cheat++'s "keep Unity full" tops Unity up every second on each machine's own clock, which
can still desynchronise a co-op game; leave it off when playing together.

0.7.4
- Favourite servers. F10 > DEDICATED SERVERS: type an address and port, press "Add this address to
favourites", and the mod asks that server itself, every time the panel opens, whether it is
there - name, players, speed, pause, password, mods - with no Steam and no web site. One click
joins. The local-network search shows the same description instead of a bare address.
- A server answers these questions on its UDP port (the same number as its TCP port): open UDP as
well as TCP in the firewall and on the router for favourites to see it from the Internet. A
question smaller than its answer is ignored, so the port cannot be used to amplify traffic.
- A game hosted from the panel answers too, as "Game of <Steam name>".

0.7.3
- Dedicated servers can be found from the game: F10 > DEDICATED SERVERS > "Look for servers" lists
the servers announced on Steam and the games on the local network, with their name, players,
speed, password and mods. One click joins. A server is listed when its operator fills in
public_address and steam_list; the list is kept by Steam itself (a public lobby used as a notice
board), so nothing else is contacted. steam_list is off by default: it keeps the server's Steam
account "in game", and Steam lets an account play on one machine at a time.
- The dedicated server no longer opens a Steam session at start-up, so the account running it can
still play on another machine. Steam is asked about DLCs only when the hosted save uses one, for a
few seconds while loading.
- Password field. A protected server asks for it, and a player hosting from the game can set one
too: the others must type the same. The password itself is never saved nor sent - both sides
derive the session key from it, and a wrong one is refused before any game data is exchanged.
- Players can change the speed. The host (or the server's players_can_change_speed) allows it; the
game's own speed buttons then send a request, and the host applies the change for everyone at
the same frame. Pausing stays with the host, because opening the Escape menu pauses too.
- Dedicated server: mods reading Unity's clock (Gameplay++, CargoHelicopter, KeybindFramework) now
load, the server supplying the clock. "CoopFoundryServer.exe --check-load" tests an island and its
mods without waiting for a player. Cheat++ (needs the game's interface) and WindPower (Unity's
random numbers) still cannot run on a dedicated server.
- Protocol 20: both players must update.

0.7.2
- The game now runs at the speed the host chose. A frame used to carry one simulation step, and a
frame is a full round trip between the players - both games' frame rates plus the network, about
a tenth of a second - so the shared game advanced at speed one whatever was selected. A six-hour
session measured nine to ten steps a second with speed three requested, where thirty were due.
Each frame now carries as many steps as its round trip lasts, measured continuously: one step
when the link is quick, three at speed three over a typical connection, never more than sixteen.
The session log says when the pace changes ("Rythme : ...").
- What this costs: at high speed, several steps are drawn at once, so vehicles move in small jumps
rather than perfectly smoothly. A machine that cannot simulate fast enough is still the limit,
exactly as in single player.

0.7.1
- Steam now shows friends that you are playing CoopFoundry: "View game info" reads, for example,
"CoopFoundry — hosting a co-op game (2/2 players)". The headline under the game's name in the
friends list cannot be changed by a mod - it comes from text only the publisher can declare.
- A Steam session can be joined straight from the friends list: when the host has room, friends
get Steam's own "Join game" button, without needing an invitation. The players of one session
are grouped together in the friends list. Direct-IP sessions show the status only, so the host's
address is never published.
- The Steam lobby is now visible to the host's friends instead of to invited players only, which
is what makes "Join game" work. It still carries no address, and the mod still checks version
and equipment before anyone can enter.

0.7.0
- Some mods keep part of the world outside the save. Gameplay++ writes the goods crossing its bridges
to text files under "Mori++ Saved settings" in the player's profile folder, and the simulation
reads them back; Cheat++ keeps its cheat switches there too. Two players with identical saves,
mods and versions were therefore not playing the same island: the host's bridges carried water the
other player's did not, and the water totals parted company two thousand frames in. That was the
cause of the recoveries at frame 2000 seen in the last sessions.
- Those files now travel with every save, host to players, and are put in place before the island
is loaded - at the start of a session, after a recovery, and for someone joining late. Nothing in
the mod knows about a particular mod: any folder in the profile that the game does not own is
treated this way, so mods not yet installed are covered. Interface preferences (the Overlord
folder) are never touched. The player's own files are set aside first and put back when the
session ends, or at the next start if the game was closed in the middle.
- Every vehicle's state (position, heading, speed, destination, navigation, load) is now kept for
the last 64 steps, and written next to the other diagnostics when a divergence is detected
("-history.tsv"). Comparing the two peers' files shows which vehicle changed first and in which
field, which a single snapshot after the fact cannot.
- Protocol 19: both players must update.

0.6.5
- The deterministic check now writes down the thing it actually decided on. Until now the only
artefact kept beside a recovery was a save-shaped capture, and a save carries the serializer's own
type tables - two peers running an identical world produce saves differing by a few hundred bytes
in a run of type names, which proves nothing either way. The projection the decision is taken on
is now written next to it, so a difference can be found rather than inferred.
- That projection is divided into named sections - HEAD, TERR, PEOP, PROD, RSCH, ENTS - so comparing
two peers lands in a named part instead of at an offset. Entities, terrain and pathfinding already
had their own dumps; products, research and population had none, and a difference in those was
indistinguishable from a difference anywhere else. That is exactly where the last two recoveries
of a four-hour session came from, and it could not be narrowed down any further with what was kept.

0.6.4
- The desynchronizations seen while playing with other mods were not desynchronizations. The
deterministic check serialised its values through the game's own BlobWriter, which does not only
write what it is given: it also emits the serializer's type and reference tables, and those
describe the process, not the simulation. Two peers running an identical world produced digests
differing by 317 bytes inside a run of type names, with the 28 million bytes before and the 16
million after byte-for-byte equal - identical entities, identical terrain, identical pathfinding
state. The mod declared a desynchronization on that and reloaded every player from the host, every
few minutes. Each of those reloads moves vehicles mid-journey, so the false alarm went on to
produce the real vehicle divergences it then reported. Every value in the check is now written by
hand as integers, and nothing that is not simulation state reaches it.
- This is also why it appeared the day sessions with mods began, and why it should not come back for
mods not yet written: every mod adds types to those tables, and the check no longer looks at them.
- The check is much cheaper as a side effect. It was serialising forty-four megabytes per sample,
most of it serializer bookkeeping.
- Both players must update together: the digest format changed, so a 0.6.3 peer and a 0.6.4 peer
would disagree on every frame.

0.6.1
- Network errors are explained instead of quoted. Every message the connection screen showed was the
framework's own text, taken straight from the exception and never passed through the mod's own
table - so a host whose listening address was not one of their machine's got "the requested address
is not valid in its context", in whatever language Windows is in. It now says that the listening
address is your own machine and not the one you are joining, and that 0.0.0.0 accepts every
adapter. Thirteen other messages were going out the same way and now go through the table too.
- A second, older installation is reported at startup, in the BepInEx log. CoopFoundry lives in two
folders - BepInEx for the connection screen, the game's Mods folder for the mod site - and the
game runs whichever it loads first, which is the one in Mods. Updating only the BepInEx half left
the old version running with no sign of it: the newer half never starts, so its own warning about
this never appears. The check now runs from the half that was updated, before the race is decided.
- The game's Mods folder is found three ways rather than one, and failing to find it is no longer
silent. The mod list is part of the compatibility check, so a machine that cannot read that folder
is refused by everyone with nothing on screen to explain it. It now says so, and distinguishes
"no mods" from "could not look".

0.6.0
A minor version rather than a patch, because what changed is what CoopFoundry can be installed next
to. Not 1.0: there is still no dedicated server, and playing with other mods is one day old.

- CoopFoundry installs and uninstalls itself. CoopFoundryInstaller.exe finds the game, writes only
BepInEx and the three Doorstop files, sets aside anything it has to replace, and records every path
with its hash. Uninstalling undoes exactly that record: a file changed since the install is
reported and left alone rather than deleted. Switching between a co-op island and a modded solo
island is now two clicks. The manual folder installs exactly the same files for anyone who would
rather not run an unsigned program.
- The archive no longer contains anything that can land next to the game executable. Previous
versions shipped the mod's documentation, manifest.json and a copy of Harmony in the same folder
players were told to copy over their installation, so changelog.txt - the file the game reads its
own version from - was replaced by ours. The packaging scripts now refuse to build an archive
carrying a name the game ships.
- An install from an older version is detected and cleaned up, including the files earlier versions
left at the top of the game folder with no record of them, and the installer offers to have Steam
restore changelog.txt.
- Other gameplay mods are supported. CoopFoundry used to refuse to start a session at all when any
third-party mod was loaded - "this experimental version only accepts the game and its official
DLCs" - which no amount of Harmony work could get past, because the refusal was ours. The reasoning
behind it was sound: a mod changes what the simulation does, and two players running different ones
drift apart silently, which is the worst failure this mod can have. The remedy was too blunt. What
has to hold is that both players run the SAME mods, and that is now enforced where it belongs, at
the handshake: the fingerprint covers every assembly in the game's Mods folder, so a difference is
refused by name before a save is sent. A mod both players share is no more dangerous than the game
itself; one they do not share can no longer reach the simulation. The connection screen lists the
game mods it found, because the mod nobody remembers installing is the one that will differ.

- Other mods are no longer broken by having CoopFoundry installed. The game loads each mod folder's
own 0Harmony.dll in turn, and because those assemblies are not strong-named, the first one loaded
answers for every mod after it. Every other Captain of Industry mod ships Harmony 2.4.2 -
byte for byte the same file - while CoopFoundry shipped HarmonyX 2.9, the fork BepInEx uses. So
whenever load order put CoopFoundry first, every later mod ran against a Harmony older than the one
it was built against, and methods added since were simply missing. That is what stopped a modded
save from loading, and why removing CoopFoundry fixed it. CoopFoundry now ships the same 2.4.2 as
everyone else, so whichever copy wins no longer matters.
- The mod is built against Harmony 2.4.2 for the same reason, and is checked against both builds: it
has to keep working under the HarmonyX that BepInEx loads as well as the 2.4.2 the game loads,
since which one it gets is not ours to decide. The one call that differed between them, UnpatchSelf,
is gone - it existed only in the fork.
- A player missing one of the game's own mods is now told before anything is sent. The compatibility
fingerprint covered CoopFoundry's files and the DLCs but not the Mods folder, so somebody without
the right mod received the whole save, waited through the transfer, and was then thrown out by the
game with "Failed to load mod 'x': Mod 'x' was not found" - an error from the game about something
CoopFoundry could have seen first. The Mods folder is now part of the fingerprint, and the refusal
says what to compare, in order: the CoopFoundry version, the game mods, the DLCs. Two players whose
mods differ could never have shared an island anyway; now they learn it in a second rather than
after a transfer.

- BepInEx's own copy is untouched and stays HarmonyX: it does not run on anything else. Five other
ways out were tried and measured first, and all of them fail - a newer HarmonyX needs MonoMod
packages BepInEx does not ship, a newer MonoMod removes API BepInEx still calls, Harmony 2.4.2 is
missing seven types BepInEx needs, and both BepInEx 6 pre.2 and its latest development build carry
the same old HarmonyX. Only the mod folder's copy ever had to change.

0.5.59
- The Steam self-check now sends the save a second time with Steam squeezed down to what a real relay
does. A connection to your own identity is short circuited locally: it runs at memory speed, the send
buffer never fills, and the one branch that has already broken a real session - the wait when Steam
answers that its buffer is full - never runs. A transfer that succeeds under those conditions proves
nothing about it.
- So Steam's own throttles are turned down for that phase: the rate to what a real session was measured
at, the buffer back to Steam's default instead of the widened one, and a relay's worth of latency and
packet loss. This is not the relay and is not claimed to be; it is the conditions the relay creates,
which is what the code was written for. The settings are global, so they are put back afterwards.
- Every wait on a full buffer is now counted, and the result says how many there were. If it comes back
zero the phase says plainly that it did not reach the code it exists to reach, instead of passing
quietly - a test that cannot fail is not evidence.

0.5.58
- The Steam self-check now sends a real save, the one chosen just above it in the menu. That is the
slowest and heaviest part of a real Steam session, and nothing had ever put a save through that
transport at all.
- It uses the game's own sending code and the game's own receiver rather than a copy of either. The
reading of the file, the hashing, the block size, the way the file is shared with the game, the
staging and the final SHA-256 check are all the real ones - a copy is the one thing certain not to
catch a mistake in the original. Reaching the end means the file that arrived is byte for byte the
file that left; the copy is then deleted.
- Sending a save was pulled out of the sending loop into a piece both can call, which is what made
that possible without duplicating it.

0.5.57
- The Steam self-check now tests what was actually new. Passing it with one connection and a
twenty-seven byte message proved the handshake, and nothing about the two things most likely to be
wrong: holding several players at once, and carrying a save. It runs in three named phases so a
failure says which part failed - one connection, then two at the same time each sending something
different to prove their messages do not cross, then eight megabytes in one-megabyte pieces to prove
that a payload past Steam's own message ceiling is cut up, handed over, waited out when the send
buffer fills, and put back together unchanged.
- The check cannot hang any more. A routing fault would show up as a message that never arrives, and
the read waiting for it would have waited for ever with the button still saying "running". After
ninety seconds the connections are closed, which makes every blocked read give up and report.
- The speed it prints is loopback speed, with no relay hop and no bandwidth limit. It is not what a
friend will see, and the result says so.

0.5.56
- A button that checks the Steam connection on its own, with nobody else and no second copy of the
game. It opens a relay socket, connects to your own Steam identity on a port of its own, runs the
real encrypted handshake and sends a message back and forth. It is how the fault that made every
Steam session impossible for fifteen releases would have been caught: no test could reach that code
and no session could be run without a second account that owns the game.
- One process rather than two, deliberately. Two copies of the game on one PC share one Steam client
and therefore one identity, so both would be listening as the same person on the same port and
nothing would say which of them a connection belongs to.
- The result says which kind of failure it is. If the connection never arrives, Steam may simply
decline to route a call to your own identity, and then the test says nothing about the mod. If it
arrives and something later fails, the fault is the mod's. The two are not reported as the same
thing.
- What it cannot check: the lobby and the friend invitation, which need a second person, and anything
about how a real link behaves.

0.5.55
- The menu keeps its background once you touch it. A style carries eight backgrounds, not one, and
IMGUI chooses between them: a window that has been clicked is drawn "on", a control under the pointer
"hover", the one holding keyboard focus "focused". Only "normal" was ever set, so the other seven fell
back to the game's own skin - which is why the panel was properly dark until the first click and then
became a transparent window with a white outline until it was closed and reopened. Every state of
every style is now painted.
- The menu also survives a save being loaded. Its one-pixel textures were created without the flag that
keeps Unity from destroying them on a scene load, while the styles holding them lived on, so the
styles ended up pointing at textures that no longer existed. They are kept now, and the styles are
rebuilt if they are ever lost anyway.

0.5.54
- Steam connections work again. They had been broken since the rework for several players, and no
session log since then shows a single successful Steam handshake. The relay used to be held on the
peer object built after authentication, while the only thing that collected Steam's messages walked
the list of peers - so during the handshake nothing fetched the very bytes the handshake was waiting
to read, and it could never finish. The relay is held from the moment the door opens, and pumped from
then on.
- A Steam host can now hold several players. The transport held exactly one, and said so in its own
code, turning any second caller away with "accepts one client". The door and the connections are now
separate things: the door accepts, and each player gets their own stream. Everything the mod gained
for several players - the lobby, joining in progress, catching up, recovering one player alone - now
works over Steam as well as over a direct address.
- A Steam player who drops out can come back. The Steam socket is no longer closed when the room
fills; it simply turns callers away, so there is no port to give back and none to fight over on the
way in again. Reopening a freed place costs nothing.
- One Steam player failing to authenticate no longer ends the accept loop, so a host keeps taking
players for the rest of the session.
- The menu says that Steam is slower. It sends a save at a few hundred kilobytes a second where a
direct connection manages tens of megabytes, which is Steam's relay and not something the mod can
fix. It is said where it is decided, in the invite section, and again on screen while somebody is
actually waiting - with "several minutes" instead of "about a minute" when the wait is Steam's.
- The menu is reorganised. The number of players and the join-in-progress switch had no heading and sat
between the save and the two ways of connecting, reading as though they belonged to the save; they
are now their own section, GAME SETTINGS, with each choice explaining what it will do. The two ways
of connecting are named for what they are and say what they cost. Buttons say which of the two they
act on.
- The status dot now has a colour that means something - green playing, amber waiting, red stopped.
Both branches of that line used to print the same grey dot. Under it, whether you are hosting or
joining, and over which of the two connections.

0.5.53
- A ping now appears on screen, with its "Go there" button, whether or not the mod's menu is open. It
lived only inside that menu - and a player plays with the menu closed, so nobody ever saw that
somebody had pointed at something. The whole purpose of the key is to say "over here" in the moment,
and that moment does not survive asking somebody to open a menu to hear about it.
- The band stays for twelve seconds and counts down, while the menu keeps the ping for two minutes as
before. A message worth interrupting a view for a few seconds is not worth sitting across it for two
minutes.
- Your own ping says "you are pointing at", rather than telling you what you just did in the third
person.

0.5.52
- F9 now finds the camera. The game keeps its camera state in Mafi.Core.UiState and the mod looked for
it in Mafi.Core, so it was never found - and a silent catch turned "I looked in the wrong place" into
"nothing to point at". Everything else the feature needs was where it was expected.
- That whole path says why it failed instead of failing quietly. Four different causes used to produce
one blank refusal; each now names itself in the log, and the menu no longer claims there was nothing
to point at when the truth is that it could not look.
- A successful point is recorded too, with the tile it sent.

0.5.51
- The host no longer decides that a player is back in step because it has finished sending to them.
Having sent somebody every frame is not the same as their having played them: the host declared a
player caught up the moment the last frame left the wire, thirteen seconds before they had actually
replayed it, and then began waiting for acknowledgements that were still answering old frames and so
were being discarded. Nothing confirmed the frames it was waiting on. A backlog long enough to take
over thirty seconds to replay - a longer absence, or a larger island - would have reached the silence
limit and stopped the session for everyone. Being back in the present is something only the player
can report, so it is now read from their acknowledgements.
- The log says which of the two happened: that every frame has been sent to somebody, and separately
that they have reached the present.

0.5.50
- A band across the top of the screen now says what the mod is doing whenever it makes somebody wait,
whether or not its menu is open. A player joining a game receives it, loads it, then replays the
frames they missed - about ten seconds of it, measured twice in the session logs - during which the
game races ahead and answers poorly. Without a word on screen that reads exactly like a crash, and
nobody investigating a crash goes looking through menus.
- While catching up, the band counts down: how far through it is, and how many frames are left, so it
is visibly working rather than merely claiming to be.
- The other players get their own line - somebody is loading, or somebody is catching up - and are told
they can carry on playing, because they can.
- A finished catch-up now records how long it took and how many frames it covered.

0.5.49
- A player joining a game in progress was paused, with no way out of it. Loading a save leaves the
simulation held still, and the order to resume had been given on frame one, long gone. Their very
first frame could not advance a single step, so they reported being one step behind, were recovered,
loaded again and sat paused again - the loop the last three sessions kept showing. The pause state is
now said again on the frame such a player must first play, and it travels to everybody, so it stays
one command on every machine at one frame. This was the real reason joining in progress failed; the
previous release's explanation of it was wrong.
- A request for a diagnostic reading arriving while a machine is loading no longer ends its session.
There is nothing to take a reading of, and the state the request was about has been replaced anyway.
- A held simulation is no longer asked eight times to advance, nor reported as an anomaly for refusing.
It is meant not to advance.
- A place freeing up behind a door that already stood open says so, rather than reporting that the door
is stuck.

0.5.48
- A player joining a game in progress arrived one simulation step behind, every time, and was sent
away to be recovered again for ever. The cause: the game's runner does not count updates, it banks
the time it is given and takes a step once the bank passes a threshold. On a freshly loaded game the
bank is empty and one update sized to reach that threshold lands exactly on it rather than past it,
so nothing moves and a step is silently lost. That cost nothing while every machine loaded at the
same moment and lost the same step; it broke the instant one player loaded alone, because the host's
bank was warm and theirs was not. A step is now asked for until it happens rather than assumed.
- Frames kept for a player who is behind are now found by their number instead of by counting from the
oldest one. A frame can cover more than one simulation step, so those numbers ascend without being
consecutive, and counting from the oldest picked the wrong frame and could read past the end of the
list.
- An unexpected fault now writes down where it happened, not only what it was. "Index was out of
range" can come from any of a dozen lines, and choosing between them by reasoning costs a whole test
session.

0.5.47
- A player being put back in step, or arriving late, no longer ends the game for everyone. They
answer for the frames they are working through one at a time, and the host refused those answers
because it was not waiting for them: the session stopped with "unexpected acknowledgement" the
instant they played their first recovered frame. Such answers cannot be checked against anything -
by the time the host is at frame 725 it no longer holds what frame 337 produced - so they are
counted and let go, and verification resumes by itself once the player is in step again.
- Taking a copy of the world for one player no longer puts every other player out of step. Writing a
save changes the simulation, which is why both machines are made to save on the same frame; but the
copy sent to somebody joining or recovering was taken the moment they asked, so the host alone made
that change and drifted away from everyone still playing. Putting one player right put all the
others wrong. The copy now waits for the next frame, where it is announced to everybody and the
host's copy is that frame's save.
- A machine that has asked to be put right no longer dies of the frames still on their way to it. It
had already stopped playing, so the next frame to arrive was refused as a protocol error, and the
reason reported was that refusal rather than the disagreement that caused it.
- The disagreement itself is now written down. A simulation step differing from the host's was
detected, acted upon and never mentioned, which is why the previous session's logs named a symptom
three messages downstream of the cause.
- Catch-up now traces what it asks of the simulation and what the simulation does, for the first
frames of each episode, and says so loudly if the two ever differ.
- A room closed at launch because it is full no longer claims that nobody can return. It reopens as
soon as a place frees up, which is what it already did.

0.5.46
- Rejoining a game in progress now actually works. Three separate faults stood in the way, and each
one alone was enough to make it impossible.
- The door was slammed shut the instant a game began. A conditional close had been added above the
old unconditional one without the old line being removed, so the setting that was meant to keep
the room open was overruled one line later. Whatever a host chose, nobody could come in.
- Joining in progress is now allowed by default. It was off, on the reasoning that an unexpected
guest mid-session is a surprise. But the same switch decides whether somebody who has dropped out
may come back, and being unable to return to the game you were just playing is not a surprise
worth protecting anyone from. A host who wants a sealed room can still say so.
- A player arriving at a game that had already been put right a few times was turned away. Their own
count of resynchronisations starts at one and the host's does not, and the check insisted the two
be a step apart at most. Any state from the present or later is now accepted; only a genuinely
overtaken one is refused.
- A dedicated server keeps its door open unless told otherwise, with --coop-closed. It is left
running so that people can come and go, which is the whole point of one.
- A player admitted mid-game was killed two seconds later. They were handed a copy of the world and
then, before they could possibly have loaded it, sent the frames that followed — statements about
an island they did not yet have. They could only refuse, with "unexpected simulation step", and the
session ended. Frames are now held back until they say they have the world.
- The logs now say why a door is shut rather than leaving its silence to be interpreted. A room
closed at launch, a place freed that could not be reopened, a place freed behind a closed door, and
a player still loading each announce themselves.
- Building the mod no longer installs it where the game cannot see it. The install step targeted a
folder under AppData that does not exist, so it wrote into nothing while the game went on loading
the previous build. Every build now compares what it has just compiled against what the game will
actually load, and says so plainly when they differ.

0.5.45
- Fixes two reasons a player still could not rejoin.
- Opening a session used to leave the previous one listening. Nothing complained at the time; the
port simply refused to be taken again later, when the host tried to reopen its door for somebody
rejoining — an error arriving so far from its cause that it looked like a different bug. The
previous connection is now let go before a new one is opened, and reopening retries for a second
rather than giving up on the first refusal.
- Addressing a player who has just left no longer ends the session. The connection layer notices a
departure before the game has finished tidying up after them, so for a moment the game still has
them on its list and still speaks to them. That is an ordinary race, not a fault — and treating it
as one ended the session with "player not found" every single time anybody disconnected. A host
alone in its room can speak to nobody without that being a failure either.
- Three more tests, one for each: a port that comes back, a place that frees up, and a message to
somebody who has gone.

0.5.44
- A player can rejoin a game they have just left. The host closes its door when the room is full,
which is right, but nothing ever opened it again — so a freed place could never be taken. It now
reopens the moment somebody leaves.
- A host no longer ends the session when its last player leaves. That was the two-player assumption
still talking: with a room and a door, a host with nobody in it is a host waiting again, and the
simulation simply pauses until somebody arrives. It is also what a dedicated server spends most
of its time doing.
- Added tests for both: a place that frees up can be taken again, and a host outlives its players.

0.5.43
- Players can now join a game already in progress. They are handed the island as it stands and then
everything that has happened since, which they play back until they are in the present. Nobody
else stops, or even notices. Off by default — an unexpected guest in the middle of a session is a
surprise rather than a feature — and switched on next to the player count.
- Added pings. F9 points at whatever you are looking at, and everyone else is told where, with a
button to go and look. It travels as a courtesy: if a queue is busy the ping is dropped rather
than delaying anything, and it touches nothing the simulation depends on.
- Added the makings of a dedicated server: a copy of the game that opens an island, waits for
players and arbitrates for them with nobody sitting at it, reopening the lobby by itself if the
session ever ends. Launch with --coop-dedicated, and optionally --coop-save=NAME, --coop-port=,
--coop-bind=, --coop-players= and --coop-join-in-progress.
Whether the game will also run without drawing anything is a separate question, and one to be
answered by launching it rather than by guessing: the simulation is started by a scene that loads
assets. If it will, these options make a true headless server; if it will not, they still give a
machine that hosts without anyone playing on it.

0.5.42
- A player who falls out of step no longer stops everyone else. The host writes a save, sends it to
that player alone, and keeps playing; they load it and then replay everything that happened while
they were away, faster than real time, until they are back in the present. Nobody else notices.
When there is nobody in particular to blame, everyone is still put back together as before.
- Frames are no longer thrown at everyone at the same moment. The host keeps what it has sent, and
each player is given what they personally still need — which is one frame for anyone keeping up,
and a whole backlog for anyone who is not. Players who are behind are left out of the wait for
acknowledgements, so being behind never holds the others still.
- A backlog is replayed within a budget of twenty-five milliseconds per update, so the game keeps
drawing and answering while it catches up. The panel says how far behind it is.
- A save now carries the frame it was taken at, so a player who receives one mid-game takes up the
count where the host left it instead of starting again from the first frame.
- Network protocol raised to 18.

0.5.40
- Groundwork for more than two players. The connection layer now holds several players at once
instead of exactly one: it tells their messages apart by who sent them, can reach one of them on
its own, and survives losing one without taking the others down with it. Nothing visible changes
yet — the host still takes one player, because the layer above it still keeps a single
acknowledgement and a single set of commands — but a third player, joining a game already running
and recovering one player without stopping the rest all rest on exactly these four things.
- The connection layer now writes to the session log: who connected and from where, who was lost and
why, how long a save took to reach each player. Diagnosing a session that goes wrong should not
need a second session.

0.5.39
- Finishes what 0.5.38 started. Acting on a lot of buildings at once passes through six separate
limits on how many commands may be queued, sent, read, tracked and reported, and 0.5.38 raised
only one of them — so the same drag stopped the session again a few steps further along. All six
are now derived from a single pair of values: what one network frame can carry, in commands and in
bytes. Whatever does not fit waits for the next frame.
- One of those limits would have refused a frame the sender was now willing to build, which would
have ended sessions for a different reason. It is set from the same value as the sender's.
- Added a test that checks the limits still agree with each other. Raising one without the others is
what caused this, twice.

0.5.38
- A single action on a lot of buildings at once no longer ends the session. Dragging across a row of
them to toggle construction priority produces one command per building, and once that passed a
hundred and twenty-eight in the same moment the session stopped outright. It is ordinary play, and
it now simply sends what fits in one network frame and the rest on the next, a tenth of a second
later. What may wait is measured in bytes against what a frame can actually carry, rather than in
a count chosen years ago.
- Anything still waiting to be sent is dropped when the host's state is reloaded, since it belongs
to a world that no longer exists.

0.5.37
- Found what has been causing the desyncs, and it is saving the game.
Saving is not a read-only act. The game's pathfinding manager listens for it, and when it hears it
it throws away whatever path search is in flight, pushes that task back to the front of its queue
to start again from the beginning, and clears its record of which vehicles had failed to navigate.
That is a change to the simulation — and the game decides when to make it from the wall clock, not
from the simulation, so two machines made it at two different simulation steps. From that moment
their vehicles were searching for different things.
It fits everything that was measured: desyncs arriving just after a save, a divergence that always
turned out to be a vehicle heading somewhere else, and recoveries five and a half minutes apart in
a game whose autosave interval is five minutes.
- The game no longer decides *when* an autosave happens during a session, only *whether* it is due.
It still applies the host's own autosave interval, and still respects their having turned
autosaving off altogether — the mod reads none of that, it simply defers the save the game asked
for by one frame so that both machines make it between the same two simulation steps. Neither runs
Autosaving is not lost, only moved.
The host's setting governs for both players, because only one clock can decide.
- Fixed the freeze this caused on its first outing. The first version held both games still until a
requested save had been written, so that neither could run a simulation step across the moment the
save changes the pathfinding state. But the game dispatches a pending save from its sync phase,
and that phase runs only as part of a simulation step — so waiting for the save to finish before
running another step meant waiting for something that could never happen, and the game stopped
dead the first time it tried to save. That wait was also unnecessary: the sync phase is part of the
step machinery, so once both machines ask for the save after the same batch of steps, both act on
it at the same following step anyway.
- The synchronised save is the game's own autosave, triggered the way the game triggers it. That
matters for more than the file name: a save marked as an autosave goes into the game's rotation
and is cleaned up in its turn. Asking for one through the public entry point would have written a
*manual* save instead — still a real save, but one the game never deletes, and a nine megabyte
file every five minutes that nothing removes fills a disk over a long session.
- If the mod ever finds it cannot save, it hands autosaving straight back to the game and says so in
the log. A session that recovers every so often is a nuisance; a session with no automatic save at
all is a lost game if anything crashes.
- Network protocol raised to 17.

0.5.34
- A divergence report now shows the differences that mattered, worst first. It used to show the
first six it came across, and since they arrive sorted by entity id that meant the six
lowest-numbered vehicles — never the one that actually forced the recovery. Five hours of logs
reported a vehicle facing ninety degrees the wrong way and a truck carrying a different load
without either ever appearing in the list. The report also says how many other vehicles differed.

0.5.33
- Fixes sessions being ended by a command that had not actually gone wrong. Commands are checked by
asking the game whether the two results are equal, and for a result that is a collection rather
than a number that question is answered by comparing object identity. The host's own result and
the one it has just decoded from the network are never the same object, so such a command reported
a difference every single time it was used, however identical the results were. Removing surface
designations does this: three recoveries in three minutes, and then the session stopped.
- A reported difference is now confirmed against the serialized bytes before it is believed.
Identical bytes mean identical results, whatever the comparison says about the two objects holding
them. A genuine difference still produces different bytes and still triggers a recovery, so this
removes a false alarm without hiding anything.
- Results that are collections are now described by what they hold. The report used to print only a
type name, so a message saying two results differed showed the same text twice and said nothing.
- A difference that cannot be reconciled now names the vehicle and the field behind it. The first
session with this in place answered the question immediately: in both cases it was the load of a
single truck.

0.5.32
- The pathfinding step budget is now the same on both machines every frame. The game works it out as
a base amount plus twenty-five steps for every search waiting in the queue, and those queue lengths
were measured differing between the two players — three against zero, giving budgets of 203850
against 69000. A step-limited search stops where its budget runs out and picks its destination from
the candidates it managed to reach, so different budgets send the same vehicle to different tiles.
The queue term is removed; the rest of the loop is unchanged.
What it costs: when many vehicles ask for a path at once the queue no longer buys extra steps, so
paths come back a little slower under load. If anything needed for this is missing the patch is not
installed at all, and if it ever fails the game's own code runs instead — the session log says which.
- Fixed a heading difference being measured the long way round the circle. A vehicle at 359.9 degrees
was reported as forty-eight degrees away from one at 0.1 degrees instead of a fifth of a degree,
which made the worst-case figures in the log far larger than anything that had happened. The
recovery decision never used that figure, so nothing behaved differently — the reports were wrong,
not the behaviour.
- A difference that cannot be reconciled now says which vehicle and which of its fields made it so,
instead of only that one did.

0.5.31
- The mod now speaks English as well as French. Everything a player reads is translated: the
multiplayer panel, every status line, every connection notice, and every message that can stop a
session. The mod follows the language the game is running in where it can read it, and falls back
to English, which is what players who are not French were seeing French for.
- Added a language switch at the bottom of the panel, so nobody is stuck with a wrong guess. The
choice is remembered between sessions.
- The panel no longer prints the whole technical reason for a recovery at the player. It used to be
several hundred characters of hashes, step budgets and per-vehicle gaps; it now says how many
times the games drifted apart and points at the session log, where that detail belongs.
- Session logs stay in French. They are a diagnostic artefact read by whoever is fixing the mod, and
keeping them in one language keeps them comparable between sessions.
- Each translation is written beside the sentence it belongs to rather than behind a key in a table,
so a translation cannot drift away from its original or be left pointing at something that no
longer exists.

0.5.30
- Save transfers should be considerably faster. Measured on a real session, 8.7 MiB took 34 seconds,
about 0.3 MiB/s — neither the disk nor the link, but Steam's own pacing. Its reliable send buffer
defaults to 512 KiB, and this transport hands over a block and then waits when that buffer is
full, so a buffer that small keeps running dry and Steam's bandwidth estimate never climbs. The
buffer is now four megabytes and the ceiling is raised so the estimate can rise if the connection
can take it. The floor is deliberately left alone: forcing a minimum rate above what a connection
can carry does not make it faster, it makes it lose packets.
- A session no longer ends because the other player kept playing. The host holds the other player's
actions until it can schedule them, and the limit on that queue was a hundred and twenty-eight —
a number an ordinary session reaches, because a recovery takes the better part of a minute and
nobody stops playing during it. Reaching it ended the session outright. The limit is now four
thousand and reaching it takes the client's state from the host instead, which is what happens
after a recovery anyway.
- The transfer timings are in the log: how long the reference save took to write, how long it took
to reach the other machine and at what rate, and how long each side spent reloading it.

0.5.29
- Fixes a save refusing to load with "Sharing violation", which could stop the game initialising
entirely. A save being sent was held open for the whole length of the transfer and shared for
reading only. The game opens a save for reading and writing, and Windows refuses that outright
while another handle permits reads alone — so for as long as a transfer lasted, that save could
not be loaded by anyone. This came in with 0.5.27, which moved the wait for the transfer to finish
inside the block that held the file.
- The handle is now released the moment the bytes have been read, and while it is held the file is
shared for writing and deleting as well. The mod can no longer be the reason a save refuses to
open, whatever else is happening.
- Fixed the transfer timing added in 0.5.27, which was wrong: it stopped counting when the last
block left the queue rather than when it had been handed to the transport, and on a real relay
that is where nearly all of the time goes. It reported a nine megabyte transfer as taking a tenth
of a second.
- Added a test that opens the save the way the game does, repeatedly, throughout a transfer. It
reproduces the failure exactly, down to the message the other player was shown.

0.5.28
- Fixes the transfer failure in 0.5.27. Steam refuses a single message beyond its own maximum and
answers InvalidParam, which is fatal and indistinguishable from a real protocol error, so the
session ended mid-transfer and the other player was told the connection had been cut. The one
megabyte blocks introduced in 0.5.27 were past that maximum.
- The Steam transport now cuts an oversized write into segments Steam will accept instead of handing
it over whole and being refused. It is a stream and the reader reassembles whatever it is given,
so this is invisible to everything above it.
- Save blocks are 256 KiB, which travels whole in a single Steam message even without that
splitting. Still four times fewer authenticated frames, network messages and queue slots than the
64 KiB of 0.5.26, without depending on the segmentation to be correct.
- Added a test that these limits stay consistent with each other. Raising one without the other is
exactly the mistake that caused this, and it is cheap to catch here rather than in a session.

0.5.27
- Saves are now sent in blocks of one megabyte instead of sixty-four kilobytes. Each block costs one
authenticated frame — an HMAC over its bytes, one network message, one queue slot — so a save of
nine megabytes used to cost a hundred and thirty-seven of everything and now costs nine. What
actually paces the transfer is Steam's own reliable send buffer, so this cuts the overhead around
the transfer rather than the transfer itself.
- Compression was measured and rejected: the game already compresses its saves. Deflate at every
level returns a file one percent smaller, for a quarter of a second of work each way. There is
nothing there to win.
- A recovery now says where its time goes, because until now that was guesswork: how long the
reference save took to write, how long it took to reach the other machine and at what rate, and
how long each side spent reloading it. Both machines log their own figures.
- The send queue can no longer be filled by a transfer at all — a megabyte per slot means five
hundred and twelve slots hold a maximum-size save — so the test for that now stalls the receiver
to fill the queue directly, which is a more honest test of the same contract.

0.5.26
- The destination divergence is fixed, and confirmed by measurement. The 0.5.24 change reports a
constant for the recomputed clearance chunk count, and the logs now read chunksRecalcules=0 on
both machines in every sample, so the Harmony patch really did take effect. In the two divergences
of the latest session the destination tile, the load, the heading, the speed and the steering were
all identical — where every earlier case had the two machines sending a vehicle to two different
tiles. Recoveries fell from four in thirty minutes to two in thirty-three.
- What remains is a different thing entirely, and much smaller: of some four thousand entities, a
single truck differed in a single field, its navigation state. One machine had begun its next path
search, the other had not yet. It went one way the first time and the other way the second, which
is what a phase offset looks like rather than a decision.
- A phase offset of one step is not a diverged simulation, but the hash could not tell the difference
and a whole island was reloaded for it. The host now asks for the detail before deciding. On a
confirmed difference the client sends one row per vehicle — position, heading, load, spawn and
navigation state — and the host measures the gap.
- A gap that nothing can reconcile, a vehicle one side does not have or a different load, is still a
divergence and the world is still replaced. A gap that is nothing but a fraction of a step of
position, with the same destination and the same load, is logged, watched and allowed to close by
itself. It is escalated the moment it grows past a sixteenth of a tile or outlives 1200 frames, so
tolerating it can delay a recovery but never replace one. An unanswered request ends in the
recovery it was trying to avoid.
- The log now names the vehicles and what differs about each, so a divergence no longer has to be
reconstructed by comparing dumps afterwards.
- Removed the targeted vehicle repair. It was measured over a full session at three attempts and
nothing held, has been switched off since 0.5.21, and the measurement above explains why: it wrote
position and heading while the difference was in the pathfinding phase behind them.
- Network protocol raised to 16.

0.5.25
- Fixes the connection failure in 0.5.24: the host died with "network queue saturated" about a tenth
of a second after the two players connected, and the client saw the session end mid-message
because the host had gone. It was not the new protocol — authentication had already succeeded.
- A save is queued for sending as 64 KiB chunks, and the queue held 128 of them. An island of a few
thousand entities makes a save of some eight megabytes, which is more than 128 chunks, so the
transfer filled the queue on its own. The first control message that then wanted a slot was
refused outright and took the session with it. The island in this save had grown past that point,
which is why it had started failing every single time rather than occasionally.
- The queue now holds 512 messages, deep enough for a whole transfer, and a message that finds no
free slot waits for one instead of being refused. A full queue means the sending loop has not
caught up yet, which is ordinary back-pressure during a transfer; it is only a failure if it never
clears. Latency replies, whose loss costs nothing but a number on a screen, are now dropped rather
than allowed to end a session.
- Added a test that fills the queue for real and checks a control message still gets through. It
refuses to pass if the queue did not actually fill, so it cannot quietly stop testing anything.

0.5.24
- The cause of the vehicle divergences is found, and it is not the vehicles. Every one of the four
divergences in the last session was the same thing: the two machines had sent one vehicle to two
different destination tiles. One case makes it beyond argument — of 4485 entities, a single field
differed in the whole dump. An excavator at an identical position to the last digit, identical
heading, speed zero, steering zero, identical job state, and a destination tile one tile apart.
- The destination is not computed, it is drawn. The pathfinder collects the candidate tiles the
search reached and picks one by index from a random generator the game creates as Unrestricted,
which is to say deliberately outside what its own determinism validator checks. Which candidates
the search reached depends on where it ran out of budget.
- That budget is charged 25 steps for every clearance chunk the search had to recompute. Chunk
recomputation is a lazy cache, so the charge depends on what happens to be cached, and that is
not the same on two machines: the client rebuilds its world from the host's save at every
recovery, and the two players look at, hover over and build in different places. A difference of
cache warmth — a mouse in a different part of the map — decides where a search stops, and
therefore which tile it comes back with.
- The recomputed chunk count now reports a constant while a session is running, which makes that
charge zero on both peers. It is the only value two machines can agree on without sharing a
cache. A search may use its full budget in a cache-cold moment instead of being cut short: more
work in a frame, never a different answer.
- The three inputs to that budget — waiting tasks, steps remaining and recomputed chunks — are now
logged for both peers side by side on every report, with the differing ones marked, so the next
session confirms it directly rather than by inference.
- Network protocol raised to 15.

0.5.23
- The search counter synchronised in 0.5.22 was not the cause. Measured over a twelve-minute
session it needed correcting exactly once, and the divergence that followed had both machines
reporting the identical counter value. The counter drift was a marker, not the mechanism.
- What the same measurement did show: a single truck, one single differing field. Position,
heading, load, navigation state and speed identical on both machines; steering angle at full left
lock on one and dead straight on the other. Both speed and steering are the output of a smoothed
driver that ramps towards a requested value a little each step rather than taking it at once, so
a single step where the two peers asked for different things leaves a difference that then takes
many steps to ramp back out.
- Those two smoothed values no longer force a recovery. They are reported on their own instead,
with every position, heading and load in agreement. A difference that matters moves a vehicle,
and moving a vehicle is still caught immediately.
- Added a driving-intent check: destination, arrival tolerance, whether the destination is final,
driving direction, remaining road segments and navigation state. None of it is smoothed, so a
difference there is a real difference of decision and must come before any difference of motion.
Reported, never acted upon. This is what will say whether the peers disagree about where a
vehicle is going, or agree and steer differently anyway.
- The entity dumps carry the same intent fields, and every recovery now logs the running counts of
driving, intent and pathfinder reports.
- Network protocol raised to 14.

0.5.22
- Likely cause of the vehicle divergences found and addressed. The same tree planter was recorded
on both machines at an identical position with an identical load, steered by the same amount in
opposite directions: +1.5 degrees against -1.5, its heading exactly 3 degrees apart. That is not
one peer running late, it is the two choosing differently. The pathfinder picks among equally
good destinations with a random generator it reseeds for every search from the start and goal
tiles and from its search counter; the tiles hash deterministically, so the counter is the only
seed input that can drift, and the generator it feeds is created by the game as Unrestricted,
which is to say deliberately outside what the engine's own determinism validator checks. Nothing
kept the two peers agreeing on it.
- The host now publishes that counter with every frame and the client adopts it, but only while no
search is in flight so that a running task is never reseeded underneath itself. The log counts
the corrections applied.
- Fixed the counter always reading as -1 in the previous build: the pathfinder is exposed as a
field, and it was being looked up as a property.
- Network protocol raised to 13.

0.5.21
- The targeted repair introduced in 0.5.20 is switched off. It was attempted three times in a full
session and held none of them: writing the authoritative position and heading is not enough,
because the steering machinery behind those fields has diverged as well. The attempt only
postponed each reload, so it stays in the code and out of the way until there is evidence it can
help. The fallback worked exactly as designed and no session was lost.
- The pathfinder's search counter is now measured every frame and recorded in the snapshots. The
pathfinder reseeds both of its random generators for every search, from the start and goal tiles
and from that counter. The tiles hash deterministically, so the counter is the one seed input
that can drift, and once it does every later search is seeded differently, explores differently
and returns a different route. That feedback loop is what makes two peers spend measurably
different amounts of search effort — the snapshots showed remaining budgets parting company by
2901 and by 103 with both queues empty, which no reordering of the queue can explain.
- Network protocol stays at 12. Both players need this version.

0.5.20
- A confirmed vehicle difference is now repaired in place before any reload is considered. The host
sends the authoritative position and heading of every vehicle and the client applies them, which
takes a frame instead of the better part of half a minute. Only those two fields are written: the
steering machinery behind them is left alone rather than half-written into an inconsistent state.
The repair is then judged over 600 frames — if the vehicles agree throughout it held, and if they
part company again the difference is deeper than position and heading and a full reload follows.
A repair can therefore only save time, never hide a difference.
- The log counts repairs attempted against repairs that held, beside the reload count.
- Diagnostic snapshots list the pathfinder's waiting queue with each request's priority and the
vehicle it belongs to. The manager derives that priority from the current step plus the queue's
own length, so a single extra request reorders the queue and shifts everything after it; this
list is what should show the first insertion where the two peers part company, which is the one
thing still missing to fix the cause rather than its effect.
- Network protocol raised to 12 for the repair message.

0.5.19
- Moved the driving flag to the pathfinding signal, beside the navigation state it was separated
from in 0.5.18. A truck standing still was reported as driving on one peer and idle on the other
while its position, heading, speed, steering angle and load were identical to the last decimal —
the flag flips when a route search completes, so it carried the pathfinder's phase and nothing a
player could see. It forced a full recovery for a difference with no consequence.
- Network protocol stays at 11. Both players need this version.

0.5.18
- Fixed a session ending on "Accusé de réception inattendu" moments after a recovery started. A
recovery clears the running state, but the other player has usually already sent a message about
the frame that was in flight, and that message was then treated as a protocol violation. Frames,
acknowledgements, resynchronization requests and load confirmations belonging to a superseded
epoch are now dropped, which is what they describe: a world that no longer exists. This is why
recoveries were ending sessions rather than repairing them.
- A vehicle difference no longer turns the next player command into a recovery. Command results
and entity counts are already compared on every frame, so a command that behaved differently is
caught on the frame it ran; forcing a full reload instead converted differences that were about
to resolve themselves into interruptions. In the last session the difference was 9 frames old,
of the 40 it was allowed, and the previous one had healed in 5.
- Navigation state moved from the vehicle signal to the pathfinding signal, where it belongs. It
flips exactly when a route search completes, so it carried the pathfinder's phase rather than
anything with a visible effect, and left in the vehicle signal it put back the very noise that
0.5.16 separated out. Position, heading, speed, steering angle and load still trigger recovery.
- Network protocol stays at 11. Both players need this version.

0.5.17
- Fixed a session ending itself after three recoveries spread over half an hour of otherwise
healthy play. The loop guard only cleared its counter after five consecutive full state checks,
and 0.5.12 had spaced those checks four times further apart, so clearing it came to require ten
thousand clean frames inside a single epoch. A session logged recoveries eleven minutes apart,
with dozens of matching checks between them, and was stopped anyway. The guard now measures how
quickly recoveries repeat rather than how many have happened: three within three minutes is a
loop and still stops, anything further apart is an isolated incident and the count restarts.
- The host can reopen a Steam lobby after a session has stopped. Reconnection only ever had a TCP
path, so a Steam session that stopped could not be resumed at all. The client rejoins by
accepting a new invitation, which is the only route Steam offers once a connection is gone, and
the panel now says so instead of leaving the client without an option.

0.5.16
- The pathfinder's internal phase no longer triggers recovery on its own. Adding it to the state
check in 0.5.14 made the check fire on a difference with no visible effect: a seventy-minute
session recorded thirteen vehicle differences, of which nine resolved themselves within fifteen
frames. The snapshots explained why — the two peers were one search cycle apart, the remaining
budget differing by exactly 3000, or by 25 per queued vehicle, which are the manager's own
constants. That is the pathfinder catching up, not a divergence. It is now reported in the log
and never used by itself to replace a player's world.
- The grace window drops from 120 frames to 40. Every difference that resolved itself did so within
fifteen frames, so the rest of the window only delayed the recoveries that were going to happen.
A confirmed difference now interrupts after about two seconds instead of six.
- Network protocol raised to 11 for the separate pathfinding fingerprint.

0.5.15
- A vehicle-only difference no longer interrupts the session on the spot. It is given 120 frames to
resolve itself; if it does, play continues and the log records that it healed. Structural checks
are untouched and still recover on the frame they fire: entity counts, command results and the
full state check all run every frame during that window, and a command from either player ends
the window immediately so that nothing is ever executed in two different worlds.
- Diagnostic snapshots are still written on the first frame a difference appears, before anything
is given a chance to settle, so they describe the divergence itself rather than what it became.
- Snapshots now carry the pathfinder's state in readable form — queued vehicles, remaining search
budget, current task — beside the hashed version, so the two files can be compared directly.
The last session narrowed every divergence to one excavator whose only differing field was its
navigation state, so this is what a real fix will be built from.
- The log reports how many differences healed by themselves against how many had to be recovered,
which is the measure of whether the grace window is worth keeping.
- Network protocol stays at 10. Both players need this version.

0.5.14
- The per-frame check now covers the shared pathfinder's own progress: how many vehicles are queued
for a route, which one is being solved, and how much of its search budget is left. The previous
build narrowed every divergence to a single excavator whose entire state matched except its
navigation state, Idle against PathFinding, meaning a route search completed one simulation step
apart on the two machines. That timing is held by the pathfinding manager and appears nowhere in
entity state, so it is read directly.
- Network protocol stays at 10. Both players need this version.

0.5.13
- The per-frame vehicle fingerprint now covers speed, steering angle, navigation state and whether
the vehicle is driving, not just position, heading and load. Two sessions localised every
divergence to a single excavator standing on an identical tile with a heading that differed by
one whole steering increment, six degrees, which means one peer took a steering decision one
simulation step before the other. Hashing the decision itself reports that step instead of the
step after it.
- Entity snapshots record the same driving state for every vehicle, plus the excavator's own state
machine, so comparing the two files shows what the two peers decided rather than only the result.
- Network protocol stays at 10; no message layout changed. Both players still need this version
because the state check covers more.

0.5.12
- Every acknowledgement now carries a fingerprint of every moving entity: position, heading and
load. Vehicles are the only part of the simulation observed to drift on their own, and the full
check ran far too rarely to say when a drift began. There are only dozens of vehicles, so this
costs almost nothing and pins a divergence to a single frame and a single vehicle.
- The full deterministic check now runs every 2000 frames instead of every 500. It walks the whole
terrain and freezes both games for roughly 400 ms, which is what players felt as a periodic
hitch; entity counts and vehicle fingerprints are verified on every frame and catch drift long
before it does, so the expensive check no longer needs to be frequent.
- Network protocol raised to 10.

0.5.11
- Vehicle cargo joins the deterministic state check. What a truck is carrying is authoritative
simulation state and was invisible to the check until now, which is how two peers could end up
with identical terrain heights, surfaces and flags but different material layers: vehicles
holding different loads dump different material on the ground. Divergences in logistics are now
caught where they start instead of once they have reached the terrain.
- Every detection path now writes diagnostic snapshots. The per-frame entity-count check added in
0.5.10 recovered without leaving any trace, which is exactly the case where knowing which entity
differs is the whole point.
- Entity snapshots record each vehicle's load and each moving entity's heading alongside its
position, so comparing the two files explains the difference rather than only locating it.
- Network protocol stays at 9. No message changed, but the state check covers more, so both players
must run this version.

0.5.10
- Divergences are now detected on the frame they happen. Every acknowledgement carries the peer's
entity count, which is an O(1) read, and a mismatch triggers recovery immediately instead of
waiting for the next deterministic check up to five hundred frames later. A ninety-minute session
showed why this was needed: one BuildTransportCmd created two conveyor segments on the host and
one on the client, yet returned the same entity id on both, so the result comparison saw nothing
and the difference only surfaced at the next checkpoint.
- Accelerated time no longer makes vehicles stutter. Speed was expressed as more simulation steps
packed into one network frame, but GameRunner interpolates a single simulation update at a time,
so those steps were drawn at once and only the last one was smoothed. Speed now shortens the
interval between frames instead, leaving steps per frame governed by latency alone, so the
picture is as smooth at speed three as at speed one. A slow link limits the speed actually
reached rather than degrading the rendering.
- Network protocol raised to 9 for the entity count in the acknowledgement.

0.5.9
- Removed the teammate cursor and map ping added in 0.5.7. Position packets were sent ten times a
second as soon as a peer finished loading, while the other peer could still be loading its copy
of the save and not consuming anything. Twenty-one seconds of that filled the client's Steam
receive queue and dropped a session whose two states were in fact identical. Presence was a
convenience; a reliable connection is not.
- The Steam receive queue is bounded by bytes rather than by message count, and a burst no longer
ends the session on its own. A 256-message cap is reached by a few seconds of small packets but
says nothing about memory, which is what actually needed limiting.
- Network protocol stays at 8. The two removed message kinds were the last ones in the enumeration,
so the numbering of every other message is unchanged.

0.5.8
- Fixed the stuttering vehicles introduced with the speed work in 0.5.7. GameRunner interpolates
moving entities between simulation steps using a parameter derived from its own clock, and that
clock only advances inside Update. The mod called only the input and render passes while waiting
for the other player, so the parameter stayed frozen and vehicles jumped from one simulation
position to the next while everything static kept rendering smoothly. Waiting frames now call
Update with a delta small enough to stay below GameRunner's sim-update threshold: the renderer
interpolates, the simulation does not advance. The step is checked before and after, and
interpolation disables itself with a log line if the simulation ever moves there.
- GameRunner's opportunistic idle simulation work is skipped while a session is active, so those
waiting frames cannot touch the simulation at all. It is a UI-side optimisation only.
- Teammate markers were projected twice too far. The axes were right but a Unity unit is half a
tile, which the calibration line caught on the first session: halving the camera position put it
exactly the 74.9 tiles from the pivot that its orbit radius predicted. The scale is now measured
against the camera's orbit rather than assumed, and the markers switch themselves off instead of
being drawn in the wrong place if the measurement does not agree.
- Network protocol is unchanged at 8, but the mod binary fingerprint differs, so both players must
install this version.

0.5.7
- Game speed now works. Every game.Run is exactly one simulation step: GameRunner only advances the
simulation once the accumulated delta reaches one sim update of 100 ms, and the mod was feeding a
fixed 50 ms, so every other Run did nothing and the speed multiplier had no effect at all. The
host now expresses its chosen speed as steps per frame and sends that count in the Frame packet,
so the client advances exactly as far as the host regardless of its own setting.
- Teammate presence: each player's mouse position is shared as a coloured marker on the other
player's screen, and V drops a map ping that both players see for eight seconds. Presence never
enters the command pipeline nor the deterministic digest, so it cannot cause a divergence; a
failure to read the cursor disables the marker and is logged rather than ending the session.
The session log carries a one-line calibration check that states whether the tile projection
agrees with the game's own camera state.
- Network protocol raised to 8: the Frame packet now carries up to sixteen steps instead of four,
and two presence message kinds were added.

0.5.6
- Fixed the command-phase boundary introduced in 0.5.4, which caused repeated desynchronizations.
GameRunner.Update only calls CollectCommands on the frames where it actually advances the
simulation. On the other frames the authoritative batch was still queued when the phase boundary
ran, so the mod claimed it as simulation-derived and replayed it one frame later on that peer
alone. In a two-machine session 128 of 132 claimed commands were in fact the authoritative batch,
and every recovery showed the host holding more entities than the client. Injected commands are
now tracked by reference and left untouched.
- Player commands scheduled while a simulation step was in flight were still being swallowed after
the step, which is how a vehicle depot placed by the host reached no one and forced a recovery.
The post-step sweep is removed: whatever the player schedules stays in the queue and is collected
and transmitted normally on the next frame.
- Steam relay: a save transfer pushes 64 KiB blocks faster than Steam's bounded reliable send
buffer drains, and Steam answered LimitExceeded. That was treated as fatal, so the host dropped
the session and the client reported "Attempted to read past the end of the stream". It is
back-pressure and is now handled by flushing and waiting for room, with a thirty-second ceiling.
- Transport errors reached the panel as raw framework text in the wrong language. A client whose
host had just died mid-transfer was told "Attempted to read past the end of the stream", which
names neither the cause nor a remedy. Failures are now translated into an actionable sentence —
disconnection, refused connection, wrong port, integrity check, timeout — while the original type
and message go to the session log. Wire.Exact also reports how many bytes it actually received.
- The multiplayer panel reported the mod version as the game version. The published package
installs its own changelog.txt, which can overwrite the game's file the version was read from.
A line that is not in the game's format is now ignored in favour of the Mafi.Core assembly
version, and the comparison no longer depends on the suffix letter.

0.5.5
- Fixed the game falling back to English whatever language was selected. BepInEx runs plugins before
the game starts, and the mod installed its Harmony patches from Awake. Preparing those patches ran
the class constructors of the patched UI types, which call Loc.Str and populate
LocalizationManager.s_enUsData. The game then refuses to load its translation file
("Some translation was already requested. Translation file must be loaded first!") and drops the
selected language.
- The patches are now installed on the first Update instead, still several seconds before the main
menu is built, so the multiplayer button and every integration point are unaffected. The
FileSystemHelper patch stays early on purpose: it is only applied when a second test instance
needs its data root redirected.
- Added a diagnostic: the session log reports how many strings the game had already materialised
when the mod took control and when the patches went in, so this regression cannot come back
unnoticed.
- Packaging: entries are written explicitly with forward slashes. Compress-Archive and
ZipFile::CreateFromDirectory both emit backslash separators on Windows PowerShell 5.1, which
breaks the single-root-folder rule the upload validator applies.

0.5.4
- Fixed the main cause of buildings existing on one machine only. GameRunner.Update runs
performInputUpdate in the middle of a simulation step, so a build committed during that step was
collected as a simulation-derived command and replayed locally instead of being sent to the peer.
Session logs show the effect: 1373 entities on the client against 1372 on the host, then
"BuildTransportCmd: host 4073, client 4074" as the entity-id counters drift apart.
- The boundary between simulation-derived commands and player commands is now taken just before
performInputUpdate. Player actions travel over the network again; only commands the simulation
itself schedules stay local.
- Detailed per-command logging: type, owner, id, sim step, payload size and payload fingerprint,
plus prototypes and transforms for BatchCreateStaticEntitiesCmd and the accepted/refused result
reported from both peers.
- Protocol stays at 7; the mod binary fingerprint already prevents a 0.5.3 peer from connecting to
a 0.5.4 one.

0.5.3
- Fixed building commands, including Fluid Storage placement, being accepted while both simulations were already structurally divergent.
- Entity-count or terrain differences now trigger immediate host recovery instead of waiting for a second checkpoint.
- A pending digest mismatch now acts as a command barrier: recovery occurs before either player's next gameplay command can run in divergent worlds.
- Digest acknowledgements now carry entity-count and terrain metadata; network protocol increased to 7.
- This corrective build was prepared without running tests, at the tester's request.

0.5.2
- Fixed grouped building placement producing a different subset of buildings on the initiating peer.
- Every local gameplay command is now executed from the same immutable serialized snapshot sent to the other player, covering all building families that use mutable placement commands.
- The original UI command receives the processed result so placement tools and command callbacks continue normally.
- This corrective build was prepared without running tests, at the tester's request.

0.5.1
- Reworked the multiplayer panel with the dark, high-contrast CoopFoundry layout: cyan section headings, dark input fields, blue-gray buttons, compact save selection, and clearer Steam/IP sections.
- A state-digest mismatch now requires confirmation at the next deterministic checkpoint before automatic recovery. Transient one-off differences no longer force an immediate reload; command-result or step mismatches remain immediate.
- Desynchronization diagnostics now include a readable entity inventory alongside each raw state capture.
- Network protocol increased to 6; version 0.5.1 cannot connect to 0.5.0.

0.5.0
- Game versions other than 0.8.7c now show a compatibility warning instead of disabling the mod or rejecting the connection. The CoopFoundry protocol and mod/DLC fingerprint checks remain strict.
- Added private two-player Steam lobbies, the Steam friends invitation overlay, and Steam relay transport.
- Steam guests now accept the invitation and connect automatically without entering an IP address, port, or code.
- Direct IP and LAN connections now negotiate their encryption key automatically and no longer ask either player for a session code.
- Simplified the multiplayer menu around Steam hosting, direct IP hosting, direct IP joining, and LAN discovery.
- Pause and speed controls are now host-authoritative. Opening the Escape menu on the client no longer pauses the shared simulation or leaves a recovery save paused.
- Steam invitations received while the game is already open and +connect_lobby launches are handled.
- Added a small BepInEx bootstrap so the multiplayer button is available on the main menu; the official game loader only instantiates third-party mods after a save is selected.
- Passed 28 protocol/file tests, the in-engine serialization checks, and a two-instance local run with save transfer, building, demolition, a CharcoalMaker, and host recovery.
- Renamed the public mod and Hub identifier to CoopFoundry.
- This version changes the network protocol to 5 and is not compatible with previous CoopCaptain builds.
- Known limitations: two players only, no dedicated server, and no compatibility guarantee with other gameplay mods.

0.4.0
- Prepared the mod metadata for the official loader; later testing showed that third-party mods are initialized too late for a main-menu connection screen.
- Direct IP hosting, LAN discovery, encrypted session invitations, and two-player chat.
- Automatic host save transfer with integrity verification and a loading barrier.
- Host-authoritative commands, adaptive network batches, reconnect, and recovery checkpoints.
- Synchronization checks for command results, terrain, entities, vehicles, products, population, workers, unity, and research.
- Automatic host recovery and detailed local diagnostics after confirmed divergence.
- Known limitations: two players only, no Steam invitations, no dedicated server, and no compatibility guarantee with other gameplay mods.
This mod depends on

This mod has no dependencies.

Mods that depend on this

No other mods depend on this mod yet.