feat: shared volume settings with a per-game override
- Music, sound and voice volumes are shared by all games by default; a game that
has never been launched with its own volumes plays with the shared ones
- The sound section of the player settings has a "own volume for this game" switch:
turning it on starts from the current values and then the sliders change only this
game, turning it off removes the override and returns to the shared volumes
- The override belongs to the game (game_id from /api/versions/{id}), so forks and mods
share it; it falls back to the version id when the id is unavailable
- Reading speed and auto delay stay shared
- volume-store.ts keeps the storage logic (damaged data ignored, values clamped,
unavailable storage tolerated) and has its own tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
52f6e7c18f
commit
a93f33c93d
5 files changed
+181
-12
No files matched your search
@@ -0,0 +1,58 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "node:test";
|
||||
import { build } from "esbuild";
|
||||
|
||||
const compiled = await build({ entryPoints: [fileURLToPath(new URL("../src/player/volume-store.ts", import.meta.url))],
|
||||
bundle: true, write: false, format: "esm", platform: "node", logLevel: "silent" });
|
||||
const { VolumeStore } = await import(`data:text/javascript;base64,${Buffer.from(compiled.outputFiles[0].text).toString("base64")}`);
|
||||
|
||||
function storage(initial = {}) {
|
||||
const data = new Map(Object.entries(initial));
|
||||
return {
|
||||
data,
|
||||
getItem: (k) => data.get(k) ?? null,
|
||||
setItem: (k, v) => void data.set(k, v),
|
||||
removeItem: (k) => void data.delete(k),
|
||||
};
|
||||
}
|
||||
|
||||
test("a game that was never launched has no volumes of its own, so it plays with the shared ones", () => {
|
||||
const store = new VolumeStore("pgvn:audio:game:7", storage());
|
||||
assert.equal(store.read(), null);
|
||||
});
|
||||
|
||||
test("own volumes are kept per game and do not touch other games", () => {
|
||||
const backing = storage();
|
||||
new VolumeStore("pgvn:audio:game:7", backing).write({ music: 0.2, sound: 0.3, voice: 1 });
|
||||
assert.deepEqual(new VolumeStore("pgvn:audio:game:7", backing).read(), { music: 0.2, sound: 0.3, voice: 1 });
|
||||
assert.equal(new VolumeStore("pgvn:audio:game:8", backing).read(), null);
|
||||
});
|
||||
|
||||
test("clearing returns the game to the shared volumes", () => {
|
||||
const backing = storage();
|
||||
const store = new VolumeStore("k", backing);
|
||||
store.write({ music: 0, sound: 0, voice: 0 });
|
||||
store.clear();
|
||||
assert.equal(store.read(), null);
|
||||
assert.equal(backing.data.size, 0);
|
||||
});
|
||||
|
||||
test("damaged or out-of-range data is ignored or limited, and a missing key means shared volumes only", () => {
|
||||
assert.equal(new VolumeStore("k", storage({ k: "{not json" })).read(), null);
|
||||
assert.equal(new VolumeStore("k", storage({ k: JSON.stringify({ music: "loud", sound: 1, voice: 1 }) })).read(), null);
|
||||
assert.deepEqual(new VolumeStore("k", storage({ k: JSON.stringify({ music: 5, sound: -1, voice: 0.5 }) })).read(),
|
||||
{ music: 1, sound: 0, voice: 0.5 });
|
||||
const none = new VolumeStore(null, storage());
|
||||
assert.equal(none.available, false);
|
||||
none.write({ music: 1, sound: 1, voice: 1 });
|
||||
assert.equal(none.read(), null);
|
||||
});
|
||||
|
||||
test("an unavailable storage does not break playing", () => {
|
||||
const broken = { getItem: () => { throw new Error("blocked"); }, setItem: () => { throw new Error("blocked"); },
|
||||
removeItem: () => { throw new Error("blocked"); } };
|
||||
const store = new VolumeStore("k", broken);
|
||||
assert.equal(store.read(), null);
|
||||
assert.doesNotThrow(() => { store.write({ music: 1, sound: 1, voice: 1 }); store.clear(); });
|
||||
});
|
||||
Reference in new issue
Block a user