Skip to content

Fix HARM3 persisting when switching to Harmony on songs with fewer parts#1515

Open
XaiaX wants to merge 1 commit into
YARC-Official:devfrom
XaiaX:bug/bad-harmony-number
Open

Fix HARM3 persisting when switching to Harmony on songs with fewer parts#1515
XaiaX wants to merge 1 commit into
YARC-Official:devfrom
XaiaX:bug/bad-harmony-number

Conversation

@XaiaX

@XaiaX XaiaX commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Depends on: YARG.Core PR #395.

Summary

Fixes a bug where a player could launch a song as HARM3 even when the chart only has two harmony parts.

Steps to reproduce

  1. Select HARM3 from the difficulty select menu in any song with 3 parts.
  2. Select Vocals from the difficulty select menu in any song with 3 parts.
  3. Navigate to a song with only 2 harmony parts.
  4. In Difficulty Select, change Instrument from VOCALS → HARMONY.
  5. Bug: the Harmony row shows "3" and the game can be started as HARM3.

Root cause

YargProfile.HarmonyIndex returns 0 when CurrentInstrument != Harmony — this is correct and load-bearing: VocalsPlayer indexes into Parts[HarmonyIndex] for both VOCALS and HARMONY players, and VOCALS always has exactly one part at index 0. If the getter returned the raw stored value for VOCALS players, a stale index would cause an ArgumentOutOfRangeException at song load.
However, this same guard masks the out-of-range check in DifficultySelectMenu.ChangePlayer:

// Before fix — reads 0 when instrument is VOCALS, check never fires:
if (profile.HarmonyIndex >= _maxHarmonyIndex)
    profile.HarmonyIndex = 0;

When the player's current instrument is VOCALS, HarmonyIndex reads 0, 0 >= 2 is false, and _harmonyIndex is never corrected. The stale value (e.g. 2 for HARM3) remains. When the player then switches to HARMONY in the instrument menu, CurrentInstrument becomes Harmony, the getter now returns the real _harmonyIndex, and the Difficulty Select main screen displays "3" — and the game launches with it.

Fix

Replace the direct HarmonyIndex comparison with profile.ResolveHarmonyIndex(_maxHarmonyIndex) (new method on YargProfile, see Core PR). This recomputes the effective harmony index for the current song from the player's last explicit selection, operating on the backing fields directly so it works regardless of CurrentInstrument. The same call is also added in CreateInstrumentMenu after an instrument switch, as a defensive resolve at the point of use.

On a song with fewer parts the effective index clamps to _maxHarmonyIndex - 1 (highest available part) rather than 0, so a HARM3 player on a two-part song plays HARM2 rather than HARM1. The explicit selection itself is never overwritten by the resolve, so returning to a song with three parts restores HARM3 — the same retention behavior DifficultyFallback provides for Expert+ (it survives songs that lack it as long as the player doesn't explicitly pick something else).

Changes

File Change
Assets/Script/Menu/DifficultySelect/DifficultySelectMenu.cs Replace HarmonyIndex comparison in ChangePlayer with ResolveHarmonyIndex; add same call in CreateInstrumentMenu after instrument switch

Why not change the getter instead?

Removing the CurrentInstrument guard from the getter would break VocalsPlayer: Parts[profile.HarmonyIndex] is called for VOCALS players where Parts has only one element. A stale _harmonyIndex of 2 would immediately throw ArgumentOutOfRangeException at song load. The getter guard is correct and necessary; ResolveHarmonyIndex is the right way to access the raw value for validation purposes only.

No Core-only fix possible

The resolve logic depends on _maxHarmonyIndex, which is computed Unity-side from song.VocalsCount in DifficultySelectMenu. Core has no concept of which song is currently selected.

Testing done

  • Reproduced the original steps: played a 3-part song as HARM3, navigated to a 2-part song, switched from VOCALS → HARMONY — Difficulty Select now shows HARM2 (highest available), not HARM3.
  • Confirmed the game launches correctly on HARM2 with notes present.
  • Re-entering Difficulty Select for the same song on a subsequent visit also resolves correctly.
  • Confirmed VOCALS still launches correctly (was broken by an earlier approach that removed the getter guard).
  • Selection retention (HARM3 → view 2-part song → back to 3-part song shows HARM3 again) is covered by Core unit tests in YargProfileHarmonyIndexTests and verified manually in-game.

Beneficial Side Effect

  • Fixes an undiagnosed issue where if a player had selected HARM3 in a song and then navigated to a song with only 2 parts, it would change their selected part permanently to HARM2. (Discovered in nightly) With the fallback behavior, this bug is also resolved.

A player who launched a song as HARM3 and later played as VOCALS could
open a two-part song, switch the instrument to HARMONY, and confirm as
HARM3 — a part that doesn't exist, producing a song with no notes.

The out-of-range check in ChangePlayer compared profile.HarmonyIndex,
whose getter returns 0 whenever CurrentInstrument != Harmony (that guard
is load-bearing: VocalsPlayer indexes Parts[HarmonyIndex] for VOCALS
players too). With the player on VOCALS the check could never fire, so
the stale stored index survived until the instrument switch exposed it.

Use YargProfile.ResolveHarmonyIndex (added in the corresponding
YARG.Core change), which validates the stored value regardless of the
current instrument and recomputes the effective index per song from the
player's last explicit selection: clamped to the highest available part
on songs with fewer parts, restored in full on songs with enough — the
same retention DifficultyFallback gives Expert+. Also resolved in
CreateInstrumentMenu right after an instrument switch, covering the
path from the report.

Requires a YARG.Core version containing ResolveHarmonyIndex.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants