Repository navigation
v0.6.8
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:
- New PLAY_arrID memory chain: closes the Nonstop-Play arrangement-ID gap that has existed since the cross-reference logic was introduced.
modefield redesign: derived fromgameStageinstead 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.- Playthrough Nonstop unblock: the
wasNonstopModegate 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– addedGetPlayArrIDPointer(RSEdition)(Beta/Remastered/LaP shifts)RockSnifferLib/RSHelpers/RSMemoryReader.cs– added privateReadPlayArrIDFromMemory(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:
- 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
NONSTOPPLAYvalue to distinguish them, and even if it had, this code path couldn't have set it – both modes produced an identicalLearnASongNoteDataread. - 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.
- 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 againstnoteData 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 commentsRockSnifferLib/RSHelpers/RSMemoryReader.cs– addedDeriveModeFromGameStagestatic helper, wired intoDoReadoutwith the bare-tuner persistence wrapper, removed the three legacyreadout.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
wasNonstopModeis still set on theOnActualSongStart/OnActualSongEndevent args bySniffer.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 atOnActualSongEndremovedRockSniffer/addons/_deps/playthrough-tracker.js–wasNonstopModefield, capture, and gate all removed; staleonSongChangedcomment 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– addedwasMultiplayerModefieldRockSnifferLib/Events/OnActualSongEndArgs.cs– addedwasMultiplayerModefieldRockSnifferLib/Sniffing/Sniffer.cs– addedcurrentSongRunWasMultiplayerModecapture atLogSongStart, propagation through both event invocations, reset atLogSongEndand elsewhereRockSniffer/History/PlaythroughHistory.cs– addedif (e.wasMultiplayerMode) return;gateRockSniffer/addons/_deps/playthrough-tracker.js– addedwasMultiplayerModecapture inonSongStarted, gate inonSongEnded
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 (
StringEnumConverteron the field) – any consumer reading the JSON sees"mode": "NONSTOPPLAY"etc. for the new values. Consumers that explicitly check formode == "LEARNASONG"will no longer see that for Nonstop plays; update accordingly. - No JS addons read
memoryReadout.modeas of this audit – everyapp.mode == 1/this.mode = 0reference 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.