Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,10 @@ window.feedBackViz_my_viz = function () {
// `bundle` is a snapshot with: currentTime, songInfo, isReady,
// notes, chords, anchors (all difficulty-filter-aware),
// beats, sections, chordTemplates, stringCount, lyrics,
// toneChanges, toneBase, mastery, hasPhraseData, inverted,
// lefty, renderScale, lyricsVisible, the 2D coordinate
// helpers project and fretX, and getNoteState (see below).
// toneChanges, toneBase, mastery, difficultyOverride,
// hasPhraseData, inverted, lefty, renderScale, lyricsVisible,
// the 2D coordinate helpers project and fretX, and
// getNoteState (see below).
// The bundle OBJECT is reused across frames (mutated in
// place — no per-frame allocation): never cache it or
// compare its identity between frames; field values are
Expand Down Expand Up @@ -367,7 +368,9 @@ Plugins that add a layer on top of whichever visualization is active — HUDs, f
- `highway.getFilteredNotes()` / `highway.getFilteredChords()` — difficulty-filtered variants. Returns the master-difficulty-filtered arrays when the song has phrase-level data (slider active); falls through to the raw arrays for songs with a single difficulty level (slider disabled). Plugins that process only the notes the player is currently expected to play should use these instead of `getNotes()` / `getChords()`
- `highway.hasPhraseData()` — returns `true` only when at least one phrase actually has more than one difficulty level (i.e. the mastery slider can change what's rendered somewhere in the song, and `getFilteredNotes()` / `getFilteredChords()` return a filtered subset). Use this to gate logic that only makes sense when difficulty filtering is available. **Not** the same as "phrase data exists" — see `getPhrases()` below.
- `highway.getPhrases()` — phrase timing windows `[{ index, start_time, end_time, max_difficulty, top_difficulty }]` for the current song. `max_difficulty` is the phrase's tier scale (the slider picks tier `floor(mastery * (max_difficulty + 1))` and the phrase plays its last level whose `difficulty` is ≤ that tier); `top_difficulty` is the tier from which the phrase plays in full — equal to `max_difficulty` for a fully authored ladder, lower when duplicate tiers were collapsed or a generated phrase is complete early. Returns `null` only when the source has no phrase data at all (GP imports, single-difficulty charts). A chart whose declared difficulty levels never actually differ (e.g. an unsimplified CDLC arrangement) still returns real timing windows here — each phrase's ladder just collapses to one level server-side — so `getPhrases()` can be non-null even when `hasPhraseData()` is `false`. Don't assume the two are equivalent; pair `hasPhraseData()` with logic that needs a real ladder, and use `getPhrases()` on its own for phrase-boundary-only needs (e.g. Section Practice's phrase-sized looping). Read-only; do not mutate.
- `highway.getMastery()` — current master-difficulty slider value as a fraction `0..1`. Reflects the same value the mastery slider is set to; meaningful only when `hasPhraseData()` is true.
- `highway.getMastery()` — current master-difficulty slider value as a fraction `0..1`. Reflects the same value the mastery slider is set to; meaningful only when `hasPhraseData()` is true. **Not** affected by a practice override — see `getEffectiveMastery()` below.
- `highway.getEffectiveMastery(time)` — the difficulty fraction actually in force at a chart `time` (or at the current playback time when omitted). Equal to `getMastery()` everywhere except inside an active practice override's half-open `[start_time, end_time)` window. Use this, not `getMastery()`, when you need the difficulty the player is currently seeing.
- `highway.getDifficultyOverride()` — the active practice override as `{ startTime, endTime, fraction }`, or `null`. Set only via the `practice-difficulty` capability domain (feedBack#136); see [docs/capability-recipes.md](docs/capability-recipes.md). Read-only.
- `highway.getChordTemplates()` — chord shape lookup table; index by `chord.id` from `getChords()` to get `{ name, fingers, frets }`. `fingers` and `frets` are per-string arrays (length matches the tuning's string count); within `fingers`, `-1` = unused, `0` = open string, `n > 0` = finger number. arrangement XML sources populate real fingerings; GP imports currently emit all `-1` since pre-import sources don't carry finger data. Not filter-aware: templates are static metadata, every `chord_id` referenced by `getChords()` is guaranteed valid
- `highway.getSongInfo()` — tuning, arrangement, capo
- `highway.getStringCount()` — number of strings on the active arrangement (4 for bass, 6 for guitar, 7+ for extended-range GP imports). Derived server-side as `max(notes-max-string + 1, name-based fallback, len(tuning))` where the tuning length only contributes when it isn't the arrangement XML padded 6-string form (sloppak / GP-imported sources carry trimmed tuning lengths). The name-based fallback is 4 for arrangements containing "bass" (case-insensitive) and 6 otherwise. This combination handles partial-string-usage charts (a 6-string lead that never plays string 5), extended-range GP imports (5-string bass, 7-string guitar), and sloppaks that explicitly encode the instrument range — without requiring plugins to do their own arrangement-name matching
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Practice difficulty override capability.** New core-owned `practice-difficulty` domain (#136) lets a practice plugin play part of a chart at a different difficulty than the rest of the song — a slowed verse, an isolated tricky bar — via `activate` / `clear` against the player's current context. The override is a half-open `[start_time, end_time)` window installed on that player's highway, with `difficulty_pct` clamped to 0–100; it is complementary to the song-wide `player-difficulty.v1` slider and deliberately never rewrites it, so `getMastery()` keeps reporting the slider while the new `getEffectiveMastery(time)` / `getDifficultyOverride()` (and `bundle.difficultyOverride` for renderers) expose the value actually in force. One override is active per player context and conflicts resolve incumbent-wins, so competing practice surfaces can't stack or fight. State is released automatically on clear, explicit unregister, song replacement, and context replacement/departure; diagnostics are count-only and redact all identity.
- **3D Highway: general per-panel zoom.** A new `cameraZoom` setting (Zoom In ↔ Out, 0.5 = unchanged) scales the camera distance in every camera mode and appears first in Split Screen's Viz ⚙ popover. Previously the only zoom control was "Locked zoom", which does nothing unless "Lock camera at frets 1–12" is on and the visible notes are at or below fret 12, so a small split panel could not be brought closer in the normal auto-framed view. Default is neutral; existing layouts are unchanged. `window.h3dBgSetCameraZoom(v)` sets the global value.
- **Host compatibility matrix (`docs/compatibility.md`).** A maintained table of the host APIs current plugins depend on — `dlc_paths._resolve_dlc_path`, `sloppak.read_member_bytes`, the `chart-transform` capability, the `/ws/sync/{session_id}` relay, `renderFrameAt`/`renderFrame`/`setExternalFrameDriver`, `playerContexts`/`player-difficulty.v1`, and phrase-tier `getPhrases().top_difficulty` — each row pinned to an exact commit, the plugins and features that need it, whether the need is required or optional, and whether the commit is upstream or fork-only. Written because the version string cannot express any of it: `VERSION` has read `0.3.0-alpha.2` since 2026-08-10 across 83 commits, so a version comparison cannot separate a build that has `renderFrameAt` from one that does not. Documentation only — no plugin loading, capability, or `minHost` behaviour changes. Every row is source-audited, not runtime-certified; the document says so, and records the host and plugin revisions it was verified against (#131).
- **Keys Highway 3D: per-panel hand filter.** The plugin declares a `handFilter` setting (Both / LH / RH) in `capabilities.visualization.settings`, and each renderer instance implements `applySetting` / `getSetting`. Split Screen can now show a different hand in each panel (feedBack-plugin-splitscreen#66). An instance with no override follows the global Settings value as before. Re-saving the *same* global hand value no longer rebuilds the notes or resets the current score mid-song.
Expand Down
11 changes: 7 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,10 @@ window.feedBackViz_my_viz = function () {
// `bundle` is a snapshot with: currentTime, songInfo, isReady,
// notes, chords, anchors (all difficulty-filter-aware),
// beats, sections, chordTemplates, stringCount, lyrics,
// toneChanges, toneBase, mastery, hasPhraseData, inverted,
// lefty, renderScale, lyricsVisible, the 2D coordinate
// helpers project and fretX, and getNoteState (see below).
// toneChanges, toneBase, mastery, difficultyOverride,
// hasPhraseData, inverted, lefty, renderScale, lyricsVisible,
// the 2D coordinate helpers project and fretX, and
// getNoteState (see below).
// The bundle OBJECT is reused across frames (mutated in
// place — no per-frame allocation): never cache it or
// compare its identity between frames; field values are
Expand Down Expand Up @@ -358,7 +359,9 @@ Plugins that add a layer on top of whichever visualization is active — HUDs, f
- `highway.getFilteredNotes()` / `highway.getFilteredChords()` — difficulty-filtered variants. Returns the master-difficulty-filtered arrays when the song has phrase-level data (slider active); falls through to the raw arrays for songs with a single difficulty level (slider disabled). Plugins that process only the notes the player is currently expected to play should use these instead of `getNotes()` / `getChords()`
- `highway.hasPhraseData()` — returns `true` only when at least one phrase actually has more than one difficulty level (i.e. the mastery slider can change what's rendered somewhere in the song, and `getFilteredNotes()` / `getFilteredChords()` return a filtered subset). Use this to gate logic that only makes sense when difficulty filtering is available. **Not** the same as "phrase data exists" — see `getPhrases()` below.
- `highway.getPhrases()` — phrase timing windows `[{ index, start_time, end_time, max_difficulty, top_difficulty }]` for the current song. `max_difficulty` is the phrase's tier scale (the slider picks tier `floor(mastery * (max_difficulty + 1))` and the phrase plays its last level whose `difficulty` is ≤ that tier); `top_difficulty` is the tier from which the phrase plays in full — equal to `max_difficulty` for a fully authored ladder, lower when duplicate tiers were collapsed or a generated phrase is complete early. Returns `null` only when the source has no phrase data at all (GP imports, single-difficulty charts). A chart whose declared difficulty levels never actually differ (e.g. an unsimplified CDLC arrangement) still returns real timing windows here — each phrase's ladder just collapses to one level server-side — so `getPhrases()` can be non-null even when `hasPhraseData()` is `false`. Don't assume the two are equivalent; pair `hasPhraseData()` with logic that needs a real ladder, and use `getPhrases()` on its own for phrase-boundary-only needs (e.g. Section Practice's phrase-sized looping). Read-only; do not mutate.
- `highway.getMastery()` — current master-difficulty slider value as a fraction `0..1`. Reflects the same value the mastery slider is set to; meaningful only when `hasPhraseData()` is true.
- `highway.getMastery()` — current master-difficulty slider value as a fraction `0..1`. Reflects the same value the mastery slider is set to; meaningful only when `hasPhraseData()` is true. **Not** affected by a practice override — see `getEffectiveMastery()` below.
- `highway.getEffectiveMastery(time)` — the difficulty fraction actually in force at a chart `time` (or at the current playback time when omitted). Equal to `getMastery()` everywhere except inside an active practice override's half-open `[start_time, end_time)` window. Use this, not `getMastery()`, when you need the difficulty the player is currently seeing.
- `highway.getDifficultyOverride()` — the active practice override as `{ startTime, endTime, fraction }`, or `null`. Set only via the `practice-difficulty` capability domain (feedBack#136); see [docs/capability-recipes.md](docs/capability-recipes.md). Read-only.
- `highway.getChordTemplates()` — chord shape lookup table; index by `chord.id` from `getChords()` to get `{ name, fingers, frets }`. `fingers` and `frets` are per-string arrays (length matches the tuning's string count); within `fingers`, `-1` = unused, `0` = open string, `n > 0` = finger number. arrangement XML sources populate real fingerings; GP imports currently emit all `-1` since pre-import sources don't carry finger data. Not filter-aware: templates are static metadata, every `chord_id` referenced by `getChords()` is guaranteed valid
- `highway.getSongInfo()` — tuning, arrangement, capo
- `highway.getStringCount()` — number of strings on the active arrangement (4 for bass, 6 for guitar, 7+ for extended-range GP imports). Derived server-side as `max(notes-max-string + 1, name-based fallback, len(tuning))` where the tuning length only contributes when it isn't the arrangement XML padded 6-string form (sloppak / GP-imported sources carry trimmed tuning lengths). The name-based fallback is 4 for arrangements containing "bass" (case-insensitive) and 6 otherwise. This combination handles partial-string-usage charts (a 6-string lead that never plays string 5), extended-range GP imports (5-string bass, 7-string guitar), and sloppaks that explicitly encode the instrument range — without requiring plugins to do their own arrangement-name matching
Expand Down
Loading
Loading