Carry Flash saves between Ruffle and the original Flash Player #8

Merged
branderson merged 3 commits from flash-save-sync into main 2026-10-14 17:26:20 -07:00
Collaborator

Summary generated by LLM agent

Flash game saves now carry over between Ruffle and the original Flash Player (streamed). Until now each player had its own saves.

How the two store saves

I confirmed this by loading a hand-built test SWF in Ruffle 0.5 through the /game-proxy/ path, and reading back what it wrote. Ruffle stores each SharedObject in localStorage as a base64-encoded .sol file, the same format Flash Player writes. Only the names differ:

SharedObject Ruffle (localStorage key) Flash Player (stream save file)
default path <site>/game-proxy/http://<host>/<path>/<name> Flash_Player/#SharedObjects/<folder>/<host>/<path>/<name>.sol
localPath "/" <site>//<name> Flash_Player/#SharedObjects/<folder>/<host>/<name>.sol

Changes

  • lib/saves/flashSaves.ts translates between the two, given the game's proxied SWF URL.

  • StreamPlayer, for Flash games Ruffle can play:

    • when the stream starts, the game's Ruffle saves are translated into Flash Player files and added to the files it starts with;
    • while it plays, the saves it changes are copied back into Ruffle's localStorage.

    Since that copy is in localStorage, the existing account save sync picks it up too.

  • Saves under [[DYNAMIC]]: a SWF that a wrapper loads from memory saves under its loader's path plus [[DYNAMIC]]/<n>. MochiAds does that in Ruffle but not in the standalone Flash Player, so Bloons keeps its progress at bloons.swf/[[DYNAMIC]]/1/bloonsProfile in Ruffle and at bloons.swf/bloonsProfile in Flash Player. Testing on flashbox found this: the translated file reached Flash Player byte-for-byte, but Flash Player read the plain path. Saves differing only by that step are now one save:

    • the stream starts with Ruffle's copy at every path it has for that save, using the dynamic copy when Ruffle has both;
    • only saves the stream changed are copied back, to every Ruffle key for that save.
  • Root-path saves: saves with a localPath of / are shared by every Ruffle game on the site. Only those the save tracker has attributed to this game carry over.

  • Which copy wins: the stream starts with Ruffle's copy of each save. That's safe because Ruffle's copy is kept current in both directions:

    • before Ruffle starts a game, it's given any stream saves it doesn't have yet, so progress made only in the stream is loaded rather than replaced by a fresh save the game writes on first start;
    • while the stream plays, its saves are copied into Ruffle as they change.

    An earlier version kept the stream's copy on a game's first switch. Testing on flashbox showed why that was wrong: an old, empty stream save of Bloons replaced real Ruffle progress. A replaced stream save is still in the account's backups.

  • Hook fix: moving the Flash content type check above GamePlayer's early return also fixes an existing conditional hook, the rules-of-hooks lint error.

  • Docs: the game saves doc explains this.

Verified on flashbox

The translated names match what Flash Player writes exactly, for Bloons including its [[DYNAMIC]]/1/ subpath, and Flash Player reused the existing #SharedObjects folder. One question remains: whether Flash Player picks up the DOTCOMAR folder created for a game that has never been streamed. That needs a Flash game played in Ruffle first and then streamed for the first time.

Testing

  • Frontend: 172 tests pass, 9 new. They use the exact keys and .sol values Ruffle wrote in the experiment and cover:
    • building the naming context;
    • Ruffle saves to Flash Player files, with root saves only when attributed;
    • Flash Player files to Ruffle keys, ignoring other hosts and non-save files;
    • starting a stream with Ruffle's saves in the right folder;
    • Ruffle's copy replacing a stale stream copy;
    • stream-only saves reaching Ruffle before it starts, without overwriting Ruffle's own;
    • Bloons' actual saves: Ruffle's dynamic progress replacing Flash Player's fresh plain profile, and Flash Player progress reaching Ruffle's dynamic path;
    • only changed saves copied back;
    • copying back into localStorage.
  • Typecheck passes, and lint has one fewer error than main.

Known gap

Progress made only in the stream before this change, in a wrapper game where Ruffle already has its own copy of the save, still loses to Ruffle's copy on the next switch. Stream progress made from now on is copied into Ruffle as it happens, so this is limited to old saves. Those are still in the account's save backups.

**Summary generated by LLM agent** Flash game saves now carry over between Ruffle and the original Flash Player (streamed). Until now each player had its own saves. ## How the two store saves I confirmed this by loading a hand-built test SWF in Ruffle 0.5 through the `/game-proxy/` path, and reading back what it wrote. Ruffle stores each SharedObject in localStorage as a base64-encoded `.sol` file, the same format Flash Player writes. Only the names differ: | SharedObject | Ruffle (localStorage key) | Flash Player (stream save file) | |---|---|---| | default path | `<site>/game-proxy/http://<host>/<path>/<name>` | `Flash_Player/#SharedObjects/<folder>/<host>/<path>/<name>.sol` | | `localPath "/"` | `<site>//<name>` | `Flash_Player/#SharedObjects/<folder>/<host>/<name>.sol` | ## Changes - `lib/saves/flashSaves.ts` translates between the two, given the game's proxied SWF URL. - `StreamPlayer`, for Flash games Ruffle can play: - when the stream starts, the game's Ruffle saves are translated into Flash Player files and added to the files it starts with; - while it plays, the saves it changes are copied back into Ruffle's localStorage. Since that copy is in localStorage, the existing account save sync picks it up too. - **Saves under `[[DYNAMIC]]`:** a SWF that a wrapper loads from memory saves under its loader's path plus `[[DYNAMIC]]/<n>`. MochiAds does that in Ruffle but not in the standalone Flash Player, so Bloons keeps its progress at `bloons.swf/[[DYNAMIC]]/1/bloonsProfile` in Ruffle and at `bloons.swf/bloonsProfile` in Flash Player. Testing on flashbox found this: the translated file reached Flash Player byte-for-byte, but Flash Player read the plain path. Saves differing only by that step are now one save: - the stream starts with Ruffle's copy at every path it has for that save, using the dynamic copy when Ruffle has both; - only saves the stream changed are copied back, to every Ruffle key for that save. - **Root-path saves:** saves with a `localPath` of `/` are shared by every Ruffle game on the site. Only those the save tracker has attributed to this game carry over. - **Which copy wins:** the stream starts with Ruffle's copy of each save. That's safe because Ruffle's copy is kept current in both directions: - before Ruffle starts a game, it's given any stream saves it doesn't have yet, so progress made only in the stream is loaded rather than replaced by a fresh save the game writes on first start; - while the stream plays, its saves are copied into Ruffle as they change. An earlier version kept the stream's copy on a game's first switch. Testing on flashbox showed why that was wrong: an old, empty stream save of Bloons replaced real Ruffle progress. A replaced stream save is still in the account's backups. - **Hook fix:** moving the Flash content type check above `GamePlayer`'s early return also fixes an existing conditional hook, the rules-of-hooks lint error. - **Docs:** the game saves doc explains this. ## Verified on flashbox The translated names match what Flash Player writes exactly, for Bloons including its `[[DYNAMIC]]/1/` subpath, and Flash Player reused the existing `#SharedObjects` folder. One question remains: whether Flash Player picks up the `DOTCOMAR` folder created for a game that has never been streamed. That needs a Flash game played in Ruffle first and then streamed for the first time. ## Testing - Frontend: 172 tests pass, 9 new. They use the exact keys and `.sol` values Ruffle wrote in the experiment and cover: - building the naming context; - Ruffle saves to Flash Player files, with root saves only when attributed; - Flash Player files to Ruffle keys, ignoring other hosts and non-save files; - starting a stream with Ruffle's saves in the right folder; - Ruffle's copy replacing a stale stream copy; - stream-only saves reaching Ruffle before it starts, without overwriting Ruffle's own; - Bloons' actual saves: Ruffle's dynamic progress replacing Flash Player's fresh plain profile, and Flash Player progress reaching Ruffle's dynamic path; - only changed saves copied back; - copying back into localStorage. - Typecheck passes, and lint has one fewer error than `main`. ## Known gap Progress made only in the stream before this change, in a wrapper game where Ruffle already has its own copy of the save, still loses to Ruffle's copy on the next switch. Stream progress made from now on is copied into Ruffle as it happens, so this is limited to old saves. Those are still in the account's save backups.
dejima-agent added 1 commit 2026-10-14 16:14:29 -07:00
Ruffle keeps SharedObjects in localStorage and the streamed Flash Player
as .sol files, in the same format under different names. Starting the
stream now adds the game's Ruffle saves, translated to Flash Player's
paths, and stream save updates are copied back into Ruffle's storage.
Root-path saves carry over only when this game made them. A game's
first switch keeps the stream's copy of a save both sides already had.
dejima-agent added 1 commit 2026-10-14 16:45:16 -07:00
The stream starts with Ruffle's copy of each save, but Ruffle starts
first, and a game with progress only in the stream would write a fresh
save there that then replaced it. Before Ruffle starts a game it now
gets the stream's saves it doesn't have yet. This replaces the
first-switch rule, which kept a stale stream save over Ruffle progress.
Moving the Flash content type above GamePlayer's early return also
fixes a conditional hook.
dejima-agent added 1 commit 2026-10-14 17:04:19 -07:00
A SWF a wrapper loads from memory saves under [[DYNAMIC]]/<n>, and
MochiAds does that in Ruffle but not in the standalone Flash Player, so
Bloons kept its progress at different paths in each. Saves differing
only by that step are now one save: the stream starts with Ruffle's
copy (the dynamic one when both exist) at every path it has for it, and
only saves the stream changed are copied back, to every Ruffle key for
them.
branderson approved these changes 2026-10-14 17:26:16 -07:00
branderson merged commit e697638827 into main 2026-10-14 17:26:20 -07:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: branderson/dotcom-arcade#8