MechanicaSaveFix, a BepInEx mod that replaces Mechanica’s save system with a single archive per world.
Demo video : https://youtu.be/NF6xDftQVgA
This isn’t my first time getting into this game! I’ve been trying things out, doing little tweaks, etc. on the game for years now. But this is my first and biggest completed project aimed at modding this game!
There are no resources online about the game’s structure, how it works, etc. So I spent way more time searching and understanding than actually coding! Working without documentation is really annoying :)
My goal here is to get a big Blåhaj! Will I pull it off? I don’t know…
The problem?
Mechanica saves every object, robot, weld, link, etc. into its own little file, stored in nested folders. A small test world already makes hundreds of folders, and large worlds reach thousands of files. On large worlds, you run into what I call “Infinite Save Loading” (ISL). The save never finishes, you have to restart the game, and you lose your progress. I suspect the number of files is the cause, but I haven’t proven it, because these freezes are very hard to reproduce.
However, during my testing, I didn’t have the issue. So at best, it improves things! Which is already pretty neat :3
What does the mod do?
The mod doesn’t rewrite the game’s save system, which wasn’t designed to be replaced. It sits on top of it:
It intercepts the game’s calls to files (File, Directory, FileInfo, DirectoryInfo) using Harmony;
It redirects them to a Virtual File System (VFS) that keeps everything in memory;
It then writes the result into a single .msa (Mechanica Save Archive) file, which is actually a zip containing the original files and folders.
The game therefore continues to believe it is writing thousands of small files, while it is actually writing only one file to disk. On the first save, a classic world is converted to .msa. The original folder is not touched, and you can extract the archive to get back a standard save.
I also rewrote the world selection menu (it lists classic folders and archives) and the world creation flow. I added a confirmation window, a config file, a backup system, and an optional update check.
Difficulties encountered:
Unfortunately, I admit, this is just a band-aid on a gaping wound. It only improves or at best delays the problem. Not to mention the fact that other things like the game’s multiplayer are still completely broken ¯_(ツ)_/¯
Version v0.11.0 introduced a system that checks the GitHub repository for new versions of the mod.As for
v0.12.0 … I ended up finding a bug that was causing certain item categories to be duplicated whenever a save was reloaded. I spent quite a bit of time tracking down the issue and fixing it, and it ended up requiring some fairly significant changes to the VFS.(Nothing too crazy, though. I basically just added explicit directory tracking so that empty directories are still properly preserved.)
Anyway, it works now! :3
There we go! The mod is technically functional and usable. All that’s left is getting other people to test it and adding a couple more things, like an update check, and that’s it (:
What’s changed:
Progress so far:
System.IO functions..msa (Mechanica Save Archive) file.I thought this step was going to be simple… Not at all!
It’s been a nightmare of an ordeal. The save selection menu’s code (its logic) was basically one flat, tangled plate of spaghetti.
The base game used a bunch of dictionaries to store data and look it back up. The functions tied to the buttons used the Rect object handed to the class to look up the data they needed. Why hand over an object to look up data through a dictionary when you can just pass the function what it needs directly…
I completely rewrote the menu’s behavior from scratch, cleanly, to support displaying and loading saves in their new archive form.
What’s changed:
Progress so far:
System.IO functions..msa (Mechanica Save Archive) file.It wasn’t as simple as expected… But it works now!
You can load a normal world, and on the first save it will be converted into a .msa archive. From then on, whenever you try to load a world that has an archived version, that version will be loaded instead of the other one.
What’s changed:
VirtualFS incorrectly normalizing the root save path, which caused it to no longer match the paths built by the game itself, breaking file lookups everywhere.QueryEntries not sanitizing multi-level directory paths, which caused Directory.GetFiles/GetDirectories to return nothing for nested folders (welds, anchors, etc.)./ and \ separators, which broke the game’s own string comparisons (e.g. RemoveOldFiles deleting everything instead of only orphaned files).DirectoryInfo/FileInfo instance methods (GetFiles, GetDirectories, CreateSubdirectory, Exists), which weren’t covered by the static File/Directory patches.VirtualFS‘s internal mode from three states (Idle/Capturing/Playback) down to two (Idle/Intercepting), since capture and playback behaved identically from the interception logic’s point of view.Save()/LoadSave() coroutine wrappers that trigger the capture/playback cycle and commit/load the .msa archive.Progress so far:
System.IO functions..msa (Mechanica Save Archive) file.I added the Harmony patches to intercept System.IO functions. It was long and repetitive 👍.
What’s changed:
src\Patches\io\Directory.cs for the directory-related function patches.src\Patches\io\File.cs for the file-related function patches.Progress so far:
System.IO functions..msa (Mechanica Save Archive) file.The next step should let me run the first tests, which will probably be followed by some bug fixes. Hopefully not 🤞.
I almost went CRAZY 😰 I ran into an architecture problem, and I was about two steps away from refactoring EVERYTHING to make it clean. I finally found a solution. It’s not the cleanest, but I didn’t have any other idea. So the world-swap function lives inside the VFS. In the end it’s just a matter of roles/responsibilities, nothing technically blocking really 😅.
What’s changed:
Progress so far:
System.IO functions..msa (Mechanica Save Archive) file.Mostly environment/tooling additions and improvements.
What’s changed:
build.yml GitHub CI, to verify that commits and PRs compile.release.yml GitHub CI, to build and attach the mod to releases.Progress so far (so no changes here ¯_(ツ)_/¯):
System.IO functions..msa (Mechanica Save Archive) file.I finished the Forward component, which faithfully reproduces every System.IO function (the functions used to interact with the computer’s file system).
Why? : I need functions with the exact same API, since I’m going to intercept the game’s disk read/write functions to stop it from writing thousands of small files and folders.
What’s changed:
VirtualFS.DeleteFile()
VirtualFS.DeleteFileNoThrow() (System.IO‘s Delete function doesn’t throw an error, unlike VirtualFS.DeleteFile(), so I made an alternative to properly mimic Delete’s behavior.Forward.cs
Progress so far:
System.IO functions..msa (Mechanica Save Archive) file.I have to admit I’ve been working on reverse-engineering / decompiling this game for a long time now, so getting set up was pretty quick.
I established the mod/patch structure, refactored it, refactored it again, until I landed on my current plan.
Right now I’ve only written VirtualFS.cs, which contains the virtual file system that will intercept System.IO requests to prevent them from writing to disk.
Next I’ll move on to writing the fake System.IO functions (write a file, read a file, etc.), the functions that will write and read the save in .zip format, the patches for the system functions, the triggers, and finally the interface.
Progress so far:
System.IO functions..msa (Mechanica Save Archive) file.Markdown checkboxes don’t work… so I’m using good old emojis instead. ¯_(ツ)_/¯
First of all, I would like to point out that I spent a huge amount of time (which is therefore not accounted for here) thinking through the structure of the project, its features, and overall architecture.
I’ve set up the command-line arguments here. In THEORY, I haven’t forgotten any. But there are no guarantees, and I might change some of the formats in the future. (I ran some tests with libraries; that time is included here.)
Here is a (simplified) diagram of the code architecture.