diff --git a/CHANGELOG.md b/CHANGELOG.md index 960cbdc2..d57c72d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Highway arrangement fallback excludes drum arrangements.** When no explicit arrangement is requested, the most-notes fallback now skips drum arrangements (detected via manifest `type: drums` or legacy name keywords) so songs open on a guitar/bass part instead of the drum part. Drum-only packs still fall back to the first arrangement. - **Keys Highway 3D: a mastery change no longer resets the run's score.** Hits, misses, streak and best streak now carry over when the mastery slider (or Difficulty Ladder's auto-adjust) rebuilds the playable notes, so the stats posted at song end cover the whole run (#94). +- **Keys Highway 3D: changing the hand filter mid-song no longer resets the run's score** (#130). Both / LH / RH rebuilds the playable notes but kept calling `_resetScoring()`, wiping hits, misses, streak and best streak — reachable mid-song from Split Screen's per-panel popover. Judged-note keys are `(t, midi)`-keyed so they stay valid across the rebuild and the miss sweep only runs over the visible notes, so only the sweep cursor is re-anchored, mirroring the mastery path (#94). - **Docker and Proxmox builds now include the starter feedpaks.** First-run seeding can find all listed sources instead of repeatedly warning that the packs are missing. - **`gp8_audio_sync` and `gp_autosync` now route GPIF parsing through the shared hardened parser (`lib/safe_xml.py`) instead of each carrying its own local `defusedxml`-or-stdlib fallback.** Both files parse `Content/score.gpif` out of an imported `.gp`/`.gpx` container — attacker-influenceable input — and previously duplicated the exact try/except-ImportError-then-stdlib-`ET.fromstring` pattern `safe_xml.py` centralized for every other untrusted-XML call site. Consolidating removes two divergent copies of security-relevant parsing logic; also corrected a stale `requirements.txt` comment that claimed both modules already delegated to `safe_xml.py`. diff --git a/plugins/keys_highway_3d/plugin.json b/plugins/keys_highway_3d/plugin.json index be45ca83..abc39dfb 100644 --- a/plugins/keys_highway_3d/plugin.json +++ b/plugins/keys_highway_3d/plugin.json @@ -1,7 +1,7 @@ { "id": "keys_highway_3d", "name": "Keys Highway 3D", - "version": "0.2.6", + "version": "0.2.7", "description": "RS+-style 3D falling-note piano highway fed by the Sloppak Notation Format, with Web MIDI input scoring.", "type": "visualization", "bundled": true, diff --git a/plugins/keys_highway_3d/screen.js b/plugins/keys_highway_3d/screen.js index cf221601..00132245 100644 --- a/plugins/keys_highway_3d/screen.js +++ b/plugins/keys_highway_3d/screen.js @@ -3384,7 +3384,7 @@ // Switch this instance's hand filter. A no-op when unchanged, so a // host re-applying the same value (splitscreen restores on every - // provider refresh) can't reset scoring mid-song. Meshes are rebuilt + // provider refresh) can't touch scoring mid-song. Meshes are rebuilt // only once the scene exists; before that, the next build reads the // refiltered `playable`. function _setHandFilter(next) { @@ -3395,9 +3395,19 @@ if (!notesGroup) return; const curTime = _latestTime; buildNoteMeshes(); - _resetScoring(); - // same as the mastery path: resume the sweep where playback is - // now — notes already elapsed must not re-sweep as misses + // Same as the mastery path, this is the SAME run: hits / misses + // / streak / best streak carry over, so changing the hand filter + // during play doesn't wipe the score (and the stats posted at + // song end cover the whole run). The judged-note sets are keyed + // by noteKey(t, midi), not by list index, so they stay valid + // across the new playable set — and keeping them stops a note + // already judged from being counted again if a later hand change + // brings it back into view. The miss sweep only runs over the + // visible `playable`, so a note hidden by the filter can never + // accrue a miss while hidden. Only the sweep cursor indexes the + // old list, so re-anchor it: playback kept advancing, and notes + // the player already passed must not be retroactively counted + // as misses. _anchorMissSweep(curTime); } @@ -3631,13 +3641,12 @@ // miss sweep at the current playback position instead of the list // head. _resetScoring() rewinds _sweepCursor to 0 — right for a fresh // chart, wrong here, where playback keeps advancing: a sweep from 0 - // against a just-emptied _hitNoteKeys/_missedNoteKeys would re-mark - // every already-elapsed visible note (including ones already hit) as - // a miss and tank the run's accuracy. The floor keeps the tolerance - // band around the change retro-free (a "run restart", matching what - // the reset already does to hits/streak), and _latestTime is restored - // so a note-on landing before the next draw still judges against real - // song time, not the reset's 0. + // would re-mark every already-elapsed visible note never judged + // (including ones the previous filter hid and this change just + // revealed) as a miss and tank the run's accuracy. The floor keeps + // the tolerance band around the change retro-free, and _latestTime + // is restored so a note-on landing before the next draw still + // judges against real song time. function _anchorMissSweep(now) { _sweepCursor.idx = sweepStartIndex(_notation && _notation.playable, now, HIT_TOLERANCE_S); if (Number.isFinite(Number(now))) { diff --git a/plugins/keys_highway_3d/tests/instance_invariants.test.js b/plugins/keys_highway_3d/tests/instance_invariants.test.js index 92e3ba7e..5f845445 100644 --- a/plugins/keys_highway_3d/tests/instance_invariants.test.js +++ b/plugins/keys_highway_3d/tests/instance_invariants.test.js @@ -75,6 +75,19 @@ test('a mastery change keeps the run totals instead of resetting scoring', () => assert.match(fn[0], /_anchorMissSweep\(now\)/); }); +test('a hand-filter change keeps the run totals instead of resetting scoring (#130)', () => { + // Same shape as the mastery path: changing Both / LH / RH mid-song + // rebuilds the playable notes, so wiping hits/misses/streak made the + // posted stats cover only the time since the change. Judged-note keys + // are (t, midi)-keyed so they survive the rebuild; the miss sweep only + // runs over the visible playable, so a hidden-hand note can never + // accrue a miss while hidden. Only the sweep cursor needs re-anchoring. + const fn = src.match(/function _setHandFilter\(next\)\s*\{[\s\S]*?\n {8}\}\n/); + assert.ok(fn, '_setHandFilter not found'); + assert.doesNotMatch(fn[0], /_resetScoring\(\)/); + assert.match(fn[0], /_anchorMissSweep\(curTime\)/); +}); + test('updateScene windows its arrays instead of walking every note (feedBack#95)', () => { // The window helpers are pure and unit-tested in data_layer.test.js, but // nothing there can catch them being exported and never wired up — the