GameSaverRevived
Auto-saves every card pick so a crash or disconnect doesn't end the run. Bug-fix fork of otDan's GameSaver. Fixes written with AI assistance.
| Date uploaded | a day ago |
| Version | 1.1.0 |
| Download link | zenham-GameSaverRevived-1.1.0.zip |
| Downloads | 5 |
| Dependency string | zenham-GameSaverRevived-1.1.0 |
This mod requires the following mods to function
BepInEx-BepInExPack_ROUNDS
BepInEx pack for ROUNDS. Preconfigured and ready to use.
Preferred version: 5.4.1901willis81808-UnboundLib
This is a helpful utility for ROUNDS modders aimed at simplifying common tasks.
Preferred version: 3.2.14Pykess-ModdingUtils
Utilities aimed at making modding easier and more accessible
Preferred version: 0.4.8README
GameSaverRevived
Auto-saves every card pick so a crash or disconnect doesn't end the run. Bug-fix fork of otDan's GameSaver. Fixes written with AI assistance.
A bug-fix fork of otDan's GameSaver (MIT). All of the original design and UI is otDan's work. This fork exists because the save browser had stopped working: the LOAD and BACK buttons did nothing, players showed up twice, card counts were wrong, and rounds needed several clicks to select or became unselectable entirely.
AI disclosure: the bug fixes, the offline share-code system and the icon in this fork were written with AI assistance, reviewed and tested by a human. The original mod's design and UI are otDan's work. ROUNDS has no "AI Generated" category on Thunderstore, so this note is the disclosure.
Works with RoundsWithFriends and LobbyImprovements.
What it does
Every time cards are picked, the mod snapshots the whole match: every player's cards, points, rounds won, colour, and the game mode settings. If the game falls over, someone drops, or the lobby dies, the host opens a new lobby, hits LOAD, picks the round, and starts the match. Everyone gets their cards and score back and the run continues.
Saves live in BepInEx/config/Saves/. Only the host sees the LOAD button, because only the
host can drive the restore.
Sharing a save
Pick a round, hit EXPORT, and a code is copied to your clipboard. Paste it to a friend; they hit IMPORT and it appears in their browser. Whoever hosts next can then load it.
The code is the save. Nothing is uploaded, there is no account, no API key and no server that can expire or disappear, and it works offline. Encoding is deterministic, so the same save always produces the identical code and two people can compare codes to confirm they are about to load the same state.
Typical codes run 400 to 1300 characters, which fits in one Discord message. A very large lobby
with long decks can exceed the 2000-character limit, so every export is also written to
BepInEx/config/Saves/_shared/ as a text file you can attach instead. IMPORT reads whatever is
in the text box, or falls back to your clipboard if it is empty.
The original used paste.ee: it uploaded your entire saves folder and returned a five-character id, with the author's personal API token compiled into the mod. That is why a short code was possible, and also why it broke.
Installing
Install through r2modman or Thunderstore Mod Manager along with its dependencies.
Remove or disable otDan-GameSaver first. This fork keeps the original plugin GUID on
purpose so it acts as a drop-in replacement, and BepInEx resolves a GUID clash by loading only
the highest version. That means installing both is harmless but pointless, and leaving the old
one enabled while downgrading this one would silently bring the bugs back.
What was fixed
Card resolution no longer takes the whole menu down with it. One unresolvable card used to
throw out of the resolution loop. The half-filled list was then cached forever, which is why
players with 18 cards displayed 5. The same exception escaped through StartCoroutine, which
returns null when an iterator throws on its first step, and that null went into the list
that Close() walks with StopCoroutine - so LOAD and BACK threw a NullReferenceException
before doing anything, for the rest of the session. Cards now resolve one at a time behind a
try/catch, the list is only published once complete, and anything unresolvable shows as ???
instead of killing the menu.
The most common trigger was a randomly-generated card from RandomCardsGenerator. Its
GenerateRandomCard(int seed) overload passes a null player and then dereferences it, and
only survives when the seed happens to still be in that process's in-memory cache from the
original match. So the identical save could load fine on one machine and throw on another.
Selecting things works on the first click. Clicking a round used to start a builder coroutine that hid rows one per frame and, at the same time, an enabler coroutine that showed them one per frame. The two fought, which is why a round took about five clicks. Clicking an already-selected game ran the builder, which hid every round button, then returned early before anything re-showed them - so the round list went permanently dead. Building and showing are now a single coroutine per level, started only after the selection guard.
No more duplicate players. A row was created active and only tracked at the very end of a long build. If anything threw in between, the row stayed on screen but nothing had a handle on it, so it could never be hidden or destroyed - a second, card-less copy of a player. Rows are now tracked from the moment they exist and destroyed if the build fails.
BACK actually returns to the lobby. Close() did its coroutine cleanup first, so when that
threw, the lobby stayed parked off-screen with the save menu still blocking input on top of it.
Restoring the lobby now happens first, open is reset properly, and cleanup runs in a finally.
Steam ids are recorded. The lookup table was a null static that GameStart reassigned to
an empty dictionary immediately after the lobby-join RPCs that fill it and immediately
before the save that reads it - so every save file ever written recorded the "unknown"
sentinel. Players are now matched by steam id, then by Photon nickname, then by position, never
matching one live player to two saved ones.
Team scores restore correctly. The score was applied once per saved player, re-keyed onto whatever team that player is on now, so loading a 4-way free-for-all into a 2v2 threw half the scores away. Scores are now grouped and applied once per live team.
Restoring no longer hands null cards to ModdingUtils. Unresolvable cards were passed straight through, and RandomCardsGenerator prefixes that method and dereferences the card before ModdingUtils' own null check runs. They are filtered out and reported instead.
Saves are taken AFTER the pick phase, and loading drops you straight into the round. The
original saved at PickStart - before anybody had picked - so the newest save was permanently one
pick phase out of date: it was missing the cards everyone had just chosen, and restoring it put you
back at the start of a phase you had already played. Saving at PickEnd instead means a save is
exactly "everyone's decks and scores going into round N".
Loading also no longer runs the opening card pick. RWF's DoStartGame gives every player a card
before round one, unconditionally, so restoring a save used to hand everyone their saved deck plus
a bonus card and dump them in a pick screen. GameSaver now suppresses that one pick when, and only
when, a save is being restored - per-round picks are untouched.
Export/import no longer needs the internet. See Sharing a save above. Besides removing the dependency on a third party's account, this drops Newtonsoft.Json from the package entirely.
Plus: the asset bundle failing no longer poisons the whole plugin through a cached type initializer, the saving indicator is null-guarded, imported saves show up without a restart, and the lobby LOAD button is rebuilt idempotently instead of stacking a new menu on every room join.
Config
BepInEx/config/ot.dan.rounds.gamesaver.cfg
| Setting | Default | Meaning |
|---|---|---|
SaveEnabled |
true |
Write auto-saves at all. |
SaveAsHostOnly |
false |
Only save while hosting. Only the host can load, so this stops non-hosts filling their disk with saves they can never use. |
Building
Needs the .NET SDK and a ROUNDS install with the dependencies present in an r2modman profile.
dotnet build -c Release
python package.py --deploy <r2modman-profile-name>
Paths are overridable without editing the project file:
dotnet build -c Release /p:RoundsFolder="D:\Steam\steamapps\common\ROUNDS" /p:ProfileName=myprofile
RWF's PrivateRoomHandler is internal, so the build publicizes it via
BepInEx.AssemblyPublicizer.MSBuild. The original project referenced a hand-made
RoundsWithFriends-nstrip.dll that only existed on the author's machine.
Source
gitlab.com/zenham/GameSaverRevived (mirrored to github.com/zen-ham/GameSaverRevived)
Licence
MIT, same as the original. Copyright otDan and contributors.