fix(saves): leave Selkies settings out of game saves
The streaming client in a streamed game's iframe shares the app's origin and keeps its settings in localStorage under its page address, which includes the session. Those keys changed during play, so they were attributed to the game and a new batch joined its save every session. Exclude them, and drop any already recorded when gathering a save.
This commit is contained in:
@@ -40,6 +40,12 @@ describe('game saves in the browser', () => {
|
|||||||
|
|
||||||
localStorage.setItem('games.example/game-proxy/http:/example.com/game.swf/save', 'AMF');
|
localStorage.setItem('games.example/game-proxy/http:/example.com/game.swf/save', 'AMF');
|
||||||
localStorage.setItem('flashpoint-theme-settings', '{}');
|
localStorage.setItem('flashpoint-theme-settings', '{}');
|
||||||
|
// Selkies' settings, from a streamed game's iframe on the same origin
|
||||||
|
const selkiesPrefix = `${location.origin}/stream/`.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
|
localStorage.setItem(
|
||||||
|
`${selkiesPrefix}0123456789abcdef0123456789abcdef_scaleLocallyManual`,
|
||||||
|
'true'
|
||||||
|
);
|
||||||
await writeDatabase('wildlife-rescue:/scope/', 'level 3');
|
await writeDatabase('wildlife-rescue:/scope/', 'level 3');
|
||||||
await storeSaves(GAME, { 'cookies.txt': 'YQ==' });
|
await storeSaves(GAME, { 'cookies.txt': 'YQ==' });
|
||||||
|
|
||||||
@@ -59,6 +65,21 @@ describe('game saves in the browser', () => {
|
|||||||
expect(saveHash({})).toBeNull();
|
expect(saveHash({})).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('leaves out Selkies settings a save picked up before they were excluded', async () => {
|
||||||
|
const selkiesKey =
|
||||||
|
`${location.origin}/stream/`.replace(/[^a-zA-Z0-9._-]/g, '_') + 'abc_scaleLocallyManual';
|
||||||
|
localStorage.setItem(selkiesKey, 'true');
|
||||||
|
localStorage.setItem('game-key', 'save');
|
||||||
|
const save = await collectSave({
|
||||||
|
gameId: GAME,
|
||||||
|
keys: [selkiesKey, 'game-key'],
|
||||||
|
databases: [],
|
||||||
|
syncedHash: null,
|
||||||
|
savedAt: null,
|
||||||
|
});
|
||||||
|
expect(save.localStorage).toEqual({ 'game-key': 'save' });
|
||||||
|
});
|
||||||
|
|
||||||
it('applies a save into a fresh browser, replacing what the game kept before', async () => {
|
it('applies a save into a fresh browser, replacing what the game kept before', async () => {
|
||||||
localStorage.setItem('key-a', 'old');
|
localStorage.setItem('key-a', 'old');
|
||||||
const tracker = await SaveChangeTracker.start(GAME);
|
const tracker = await SaveChangeTracker.start(GAME);
|
||||||
|
|||||||
@@ -38,8 +38,20 @@ const APP_DATABASES = new Set(['flashpoint-stream-saves', STATE_DB]);
|
|||||||
// Unity WebGL's asset cache: large and re-downloadable, not a save
|
// Unity WebGL's asset cache: large and re-downloadable, not a save
|
||||||
const IGNORED_DATABASES = new Set(['UnityCache']);
|
const IGNORED_DATABASES = new Set(['UnityCache']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selkies, the client in a streamed game's iframe, shares this origin and keys
|
||||||
|
* its settings by its page address (/stream/<session>/), sanitized like this.
|
||||||
|
*/
|
||||||
|
function streamClientPrefix(): string {
|
||||||
|
return `${window.location.origin}/stream/`.replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
function isAppKey(key: string): boolean {
|
function isAppKey(key: string): boolean {
|
||||||
return key.startsWith('flashpoint-') || key === 'activityDashboardCollapsed';
|
return (
|
||||||
|
key.startsWith('flashpoint-') ||
|
||||||
|
key === 'activityDashboardCollapsed' ||
|
||||||
|
key.startsWith(streamClientPrefix())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGameDatabase(name: string): boolean {
|
function isGameDatabase(name: string): boolean {
|
||||||
@@ -98,6 +110,7 @@ export async function collectSave(state: SaveSyncState): Promise<GameSaveData> {
|
|||||||
if (Object.keys(stream).length > 0) data.stream = stream;
|
if (Object.keys(stream).length > 0) data.stream = stream;
|
||||||
|
|
||||||
const entries = state.keys
|
const entries = state.keys
|
||||||
|
.filter((key) => !isAppKey(key))
|
||||||
.map((key) => [key, localStorage.getItem(key)] as const)
|
.map((key) => [key, localStorage.getItem(key)] as const)
|
||||||
.filter((entry): entry is readonly [string, string] => entry[1] !== null);
|
.filter((entry): entry is readonly [string, string] => entry[1] !== null);
|
||||||
if (entries.length > 0) data.localStorage = Object.fromEntries(entries);
|
if (entries.length > 0) data.localStorage = Object.fromEntries(entries);
|
||||||
|
|||||||
Reference in New Issue
Block a user