Skip to content

v0.6.8

Choose a tag to compare

@github-actions github-actions released this 13 May 14:53

RockSniffer v0.6.8

Summary

v0.6.8 delivers a long-awaited unlock: reliable arrangement-ID resolution in Nonstop Play, plus the downstream improvements that the unlock enabled - full playthrough tracking for Nonstop, a redesigned mode field that classifies every Rocksmith gameStage (not just in-song states), and a small set of defensive cleanups in dependent code.

Three categories of change, all shipping together:

  1. New PLAY_arrID memory chain: closes the Nonstop-Play arrangement-ID gap that has existed since the cross-reference logic was introduced.
  2. mode field redesign: derived from gameStage instead of from note-data pointer resolution, with a much richer classification covering menu states, mode-specific tuners, Session Mode, Lessons, Guitarcade, and a contextual MENU bucket.
  3. Playthrough Nonstop unblock: the wasNonstopMode gate added in v0.6.5 hotfix4 (which skipped all history/tracker writes for Nonstop plays) is removed now that PLAY_arrID makes the data trustworthy.

1. PLAY_arrID chain – reliable Nonstop arrangement ID

What it fixes

Pre-v0.6.8, the arrangement_hash memory chain didn't populate during Nonstop Play. Songs played in NSP had blank arrangement IDs in their downstream readouts, which:

  • Caused playthrough_history rows to have empty arrangement_id columns
  • Made it impossible to distinguish between regular and bonus/alternate arrangements (e.g., regular Bass vs. bonus Bass) – only Path-byte fallback was available, which encodes the type (Lead/Rhythm/Bass) but not the specific GUID
  • Triggered the v0.6.5 hotfix4 gate that skipped persistent writes entirely for Nonstop plays

How it works

A new pointer chain – internally PLAY_arrID – reads the currently-loaded arrangement GUID from a separate memory location that does populate in Nonstop. The chain walks:

Rocksmith2014.exe + 0x00F5C5AC (Beta-base)
  [+ 0x3080 for Remastered, + 0x4080 for LaP]
  → offsets [0xB0, 0x18, 0x4, 0x84, 0x20]
  → 16 raw bytes → Guid → 32-char uppercase hex string

This matches the format of PSARC arrangementID fields directly, so cross-reference against songDetails.arrangements[] is a straight string comparison.

Dispatch

The arrangement-ID read now dispatches by gameStage:

gameStage Chain used
las_game, las_pause, nonstopplaygame, nsp_pause PLAY_arrID (new)
sa_game, sa_pause Legacy arrangement_hash (unchanged)
All other states (menus, song-select, song-review) Legacy arrangement_hash (unchanged – preserves pre-v0.6.8 behavior)

gameStage is now read above the arrangement-ID block in DoReadout so the dispatch has the signal it needs.

Files changed

  • RockSnifferLib/RSHelpers/MemoryOffsets.cs – added GetPlayArrIDPointer(RSEdition) (Beta/Remastered/LaP shifts)
  • RockSnifferLib/RSHelpers/RSMemoryReader.cs – added private ReadPlayArrIDFromMemory(IntPtr) helper, reordered gameStage read, added dispatch

2. mode field redesign – gameStage-derived classification

What it fixes

Pre-v0.6.8, the readout.mode field was assigned inside ReadNoteData / ReadScoreAttackNoteData, meaning:

  1. NSP gameplay was misclassified as LEARNASONG because Nonstop reuses the LaS note-data subsystem internally (same magic-number-111000 struct shape). The enum had no NONSTOPPLAY value to distinguish them, and even if it had, this code path couldn't have set it – both modes produced an identical LearnASongNoteData read.
  2. All menu states were classified UNKNOWN because note-data pointers only populate during active gameplay. Mainmenu, song-select, song-review, options screens, the Score Attack hub, and every transition screen all came through as UNKNOWN.
  3. The classification was implicitly coupled to note-data pointer resolution, conflating two unrelated concerns ("what note data do I read?" vs. "what mode is the user in?").

How it works

mode is now derived from gameStage, which is the canonical signal for what the user is doing in the game (read from a static memory address since v0.6.6 – reliable across all states including menus, transitions, and Nonstop). A single classifier function DeriveModeFromGameStage does exact-match-then-prefix-match into the expanded enum.

Enum expansion

Five new values appended to RSMode. Integer values 0–3 preserved for any external integer-based serialization:

public enum RSMode
{
    UNKNOWN,        // 0
    LEARNASONG,     // 1
    SCOREATTACK,    // 2
    MULTIPLAYER,    // 3
    NONSTOPPLAY,    // 4  (new)
    GUITARCADE,     // 5  (new)
    SESSION,        // 6  (new)
    LESSONS,        // 7  (new)
    MENU            // 8  (new)
}

Full mapping

Mode Exact gameStages Prefix matches
LEARNASONG learnasong, las_songs, las_options, las_tuner, las_game, las_pause, las_songreview –
SCOREATTACK scoreattack, panel_bib, scoreattack_presongtuner, sa_game, sa_pause, sa_songreview –
GUITARCADE gcpre, gcade, gcade_game, guitarcade_tuner gc_
NONSTOPPLAY nonstopplay, nsp_main, nonstopplayhub, nsp_tuner, nonstopplaygame, nsp_pause –
SESSION – sm_
LESSONS getuner, pregametuner ge_
MULTIPLAYER split_game mp_, duet_, h2h_
MENU titlescreen, profileselect, main, mainmenu, statsmenu, shop, contentpanelchord, sidelist tonedesigner
UNKNOWN (anything else) –

Case-insensitive throughout – input is lowercased once at the top of the classifier.

Special case: bare tuner

The bare tuner gameStage fires when the universal tuner is accessed from any parent context (pause menu of any mode, main menu, Session Mode, etc.). It's distinct from the mode-specific tuners (las_tuner, nsp_tuner, scoreattack_presongtuner, guitarcade_tuner, getuner, pregametuner) which are classified under their parent mode.

Because the bare tuner is stateless from the gameStage alone, the prior mode is preserved: DoReadout skips the reclassification when it sees bare tuner, leaving readout.mode as whatever it was on the previous poll. Edge case: if the first observed gameStage after RockSniffer attaches is the bare tuner with no prior state, mode falls through to UNKNOWN and resyncs the moment the user navigates away.

Caller-side impacts

  • Sniffer.cs:1057 (end-of-song log enrichment): Compatible. At LogSongEnd time, gameStage is still the in-song state (transitions to menus happen after the snapshot), so the SA-stat appendage fires correctly when expected.
  • PlaythroughHistory.cs:250 / :327 (SA column population): Compatible. Already pattern-matched against noteData is ScoreAttackNoteData, which remains the correct disambiguator under the decoupled mode/note-data contract.
  • PlaythroughHistory.cs:289 / :350 (game_mode column): Behavior change documented below.
  • DiscordRPCHandler.cs:127 (SA score appendage on Discord RPC): Defensive guard added (see Section 3).

game_mode column behavior change

Both the SQLite playthrough_history table's game_mode column and its CSV equivalent are populated by readout.mode.ToString(). Going forward:

  • Nonstop plays correctly write "NONSTOPPLAY" (rather than the pre-v0.6.8 incidental "LEARNASONG" that resulted from NSP reusing the LaS note-data subsystem).
  • Any other newly-classified stage writes its appropriate label ("GUITARCADE", "SESSION", "LESSONS", "MENU").
  • Existing rows in the database are unchanged – only new rows reflect the corrected classification.

If you query for Nonstop plays, the predicate is now WHERE game_mode = 'NONSTOPPLAY' going forward, while historical rows still match WHERE game_mode = 'LEARNASONG' (mixed with legitimate LaS plays – there's no way to retroactively disambiguate without re-running the readout against the original memory state).

Files changed

  • RockSnifferLib/RSHelpers/RSMode.cs – enum expansion + doc comments
  • RockSnifferLib/RSHelpers/RSMemoryReader.cs – added DeriveModeFromGameStage static helper, wired into DoReadout with the bare-tuner persistence wrapper, removed the three legacy readout.mode = … writes (LaS branch, SA branch, UNKNOWN fallback)

3. Playthrough Nonstop unblock

What it fixes

The wasNonstopMode gate added in v0.6.5 hotfix4 skipped all playthrough_history (CSV + SQLite) and playthrough_tracker (per-attempt) writes for songs played in Nonstop Play. The gate was added because the missing arrangement_hash chain and the bonus/alternate ambiguity made the data untrustworthy – historic records should be trustworthy or absent.

PLAY_arrID resolves both stated reasons for the gate:

  • Arrangement-ID memory pointer now populates in Nonstop
  • Per-arrangement GUID directly distinguishes regular from bonus and alternate arrangements

So the gate is lifted. Nonstop plays now save on equal footing with LaS and SA plays.

What this changes for users

  • Nonstop plays now appear in playthrough_history (SQLite + CSV)
  • Per-attempt stats in playthrough_tracker (the per-section/per-phrase storage backing addons) now persist across Nonstop sessions
  • Per-song "previous best" lookups work correctly for songs whose only attempts have been in Nonstop

What this preserves

  • wasNonstopMode is still set on the OnActualSongStart / OnActualSongEnd event args by Sniffer.cs – it just no longer gates writes. Any future or external consumer that wants the contextual flag still has it.
  • All defensive null/empty checks remain in place (no-currentAttempt guards, no-arrangement guards, etc.).

Files changed

  • RockSniffer/History/PlaythroughHistory.cs – early-return gate at OnActualSongEnd removed
  • RockSniffer/addons/_deps/playthrough-tracker.js – wasNonstopMode field, capture, and gate all removed; stale onSongChanged comment cleaned up

Multiplayer gate (added)

While the Nonstop gate is lifted, a new gate is added for Multiplayer modes (split_game, mp_, duet_, h2h_*). Full multiplayer support requires a separate larger effort – multiple user note-data streams and per-user arrangements need first-class tracking before MP rows can be written with the same data quality as LaS / SA / NSP rows. Until that lands, MP plays are excluded from playthrough_history (SQLite + CSV) and playthrough_tracker storage.

The gate is captured at song start using the v0.6.8 gameStage-derived mode field (currentMemoryReadout.mode == RSMode.MULTIPLAYER), propagated through OnActualSongStartArgs / OnActualSongEndArgs as a new wasMultiplayerMode field, and consumed symmetrically on both the C# history side and the JS playthrough-tracker side.

Files added/changed for MP gate:

  • RockSnifferLib/Events/OnActualSongStartArgs.cs – added wasMultiplayerMode field
  • RockSnifferLib/Events/OnActualSongEndArgs.cs – added wasMultiplayerMode field
  • RockSnifferLib/Sniffing/Sniffer.cs – added currentSongRunWasMultiplayerMode capture at LogSongStart, propagation through both event invocations, reset at LogSongEnd and elsewhere
  • RockSniffer/History/PlaythroughHistory.cs – added if (e.wasMultiplayerMode) return; gate
  • RockSniffer/addons/_deps/playthrough-tracker.js – added wasMultiplayerMode capture in onSongStarted, gate in onSongEnded

4. Other cleanups

Defensive cast guard in DiscordRPCHandler.cs

Pre-v0.6.8, the SA-score-appendage block in DiscordRPCHandler.OnFrame was guarded only by mode == RSMode.SCOREATTACK, then performed an unguarded cast (ScoreAttackNoteData)readout.noteData. That was safe under the old contract because mode == SCOREATTACK strictly implied a successful SA note-data read in the same poll.

Under the new gameStage-derived mode, the two are decoupled – e.g., in panel_bib (SA options menu) mode is SCOREATTACK but noteData may be stale LearnASongNoteData from a prior session. The cast would have thrown InvalidCastException.

A pattern-match guard (matching the style already used in PlaythroughHistory.cs) makes the access safe:

if (readout.mode == RSMode.SCOREATTACK && readout.noteData is ScoreAttackNoteData sand)

Removed redundant readout.mode = RSMode.UNKNOWN fallback

The note-data dispatch in DoReadout previously fell through to readout.mode = RSMode.UNKNOWN when neither chain resolved. Under gameStage-derived mode, this fallback would clobber a correctly-classified menu mode (e.g., classifying mainmenu as MENU, then immediately overwriting with UNKNOWN because note-data didn't resolve). Removed.

Stale comment update – Nonstop arrangement-ID resolution

Sniffer.cs's warning-suppression logic for Nonstop arrangement resolution previously documented the gap as "known unfixable until a Nonstop-compatible arrangementID memory pointer is found." That comment is now updated to reflect the v0.6.8 unlock: PLAY_arrID is that pointer. The warning suppression itself stays in place – brief Nonstop song-to-song transients can still produce momentary Path-based fallback that would be noisy if logged – but the rationale is now accurate.

Discord RPC nonstop classifier – bug fix

DiscordRPCHandler.cs's gameStage prefix classifier (which sets the RPC state label like "Learn A Song", "Score Attack", etc.) previously used gameStage.StartsWith("nsp") to detect Nonstop, which only caught the nsp_* family (nsp_main, nsp_pause, nsp_tuner). The full-word gameStages nonstopplay, nonstopplaygame, and nonstopplayhub fell through to no state at all on the Discord RPC display. Pre-existing pre-v0.6.8 bug. Now patched: the prefix check also accepts nonstopplay as a prefix.

Compatibility notes

  • Pre-v0.6.8 RSMode integer values (0–3) preserved: UNKNOWN=0, LEARNASONG=1, SCOREATTACK=2, MULTIPLAYER=3. Any external consumer using integer-based serialization continues to read existing values correctly. New values (4–8) are appended.
  • JSON output is name-based (StringEnumConverter on the field) – any consumer reading the JSON sees "mode": "NONSTOPPLAY" etc. for the new values. Consumers that explicitly check for mode == "LEARNASONG" will no longer see that for Nonstop plays; update accordingly.
  • No JS addons read memoryReadout.mode as of this audit – every app.mode == 1 / this.mode = 0 reference in shipped addons is local addon visibility/animation state, not the memoryReadout field.
  • Existing playthrough_history rows unchanged. Only new rows reflect the corrected game_mode classification.

Validation

All changes type-check clean in the syntax-only Roslyn pass, and the full RockSnifferLib compile is clean for all touched files. The mode-assignment audit confirms a single source of truth: readout.mode is now written in exactly one place (the DeriveModeFromGameStage call inside DoReadout), with all legacy write sites converted to documentation comments.

Acknowledgments

  • PoizenJam – reverse-engineering of the PLAY_arrID chain via Cheat Engine, all design and integration work.