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:
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.
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.
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.
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
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.solfile, the same format Flash Player writes. Only the names differ:<site>/game-proxy/http://<host>/<path>/<name>Flash_Player/#SharedObjects/<folder>/<host>/<path>/<name>.sollocalPath "/"<site>//<name>Flash_Player/#SharedObjects/<folder>/<host>/<name>.solChanges
lib/saves/flashSaves.tstranslates between the two, given the game's proxied SWF URL.StreamPlayer, for Flash games Ruffle can play: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 atbloons.swf/[[DYNAMIC]]/1/bloonsProfilein Ruffle and atbloons.swf/bloonsProfilein 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:Root-path saves: saves with a
localPathof/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:
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#SharedObjectsfolder. One question remains: whether Flash Player picks up theDOTCOMARfolder 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
.solvalues Ruffle wrote in the experiment and cover: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.