fix(persist): use project-object identity to stop forked-bank cross-contamination

M4's GUID-only classifier read a tab-switch between two Save-As forks (shared
copied GUID, different paths) as a Save-As and clobbered a bank. Thread
sameProjectObject into classifyProjectTransition: a different object always
Loads, never relocates; a forked sibling gets re-GUID'd to diverge.
This commit is contained in:
2026-07-22 21:28:54 -04:00
parent aacf8b49e8
commit 36270e2064
5 changed files with 212 additions and 116 deletions
+16 -10
View File
@@ -48,15 +48,18 @@ inline constexpr const char* kProjExtGuidKey = "project_guid";
// Owns the session's BankIndex and drives persistence against the active REAPER
// project. One instance lives for the extension's lifetime (main.cpp). It tracks
// the project identity it last saw so the timer tick can detect a project load
// (identity changed) and a Save-As (same project, path changed) and react:
// (a different project became active) and a Save-As (SAME project, path changed):
//
// * project load -> load the index from ext state, resolve bank paths
// * Save-As (new dir) -> relocate the bank folder under the new .rpp
//
// Identity is CONTENT-BASED, not pointer-based: persist keys Load/Save-As off a
// GUID it mints and stores in each project's ext state, not the ReaProject*
// pointer (REAPER recycles pointer addresses across close/open, which let a
// project switch masquerade as a Save-As and clobber a bank — the M4 defect).
// Identity rests on the live ReaProject* pointer FIRST (a genuine Save-As is one
// object saved to a new path — same pointer; a tab-switch/open is a different
// object), with a minted GUID as a SECONDARY signal to survive pointer *reuse*
// (REAPER recycles a closed project's address). The pointer is what distinguishes
// a Save-As from a switch between two FORKED siblings that share a copied GUID on
// disk (the W10 data-integrity defect: dropping the pointer let a fork tab-switch
// masquerade as a Save-As and clobber a bank).
//
// The bank itself is exposed for the capture/action layer to mutate; persist
// only reads it on save and replaces it on load.
@@ -95,12 +98,15 @@ private:
ViewModeModel view_;
// The project identity last observed by poll(), used to detect load/Save-As.
// Identity is the GUID we mint per project (kProjExtGuidKey), NOT the raw
// ReaProject* pointer — see the class comment for why. The .rpp path is
// tracked alongside so a same-GUID path change (Save-As) is distinguishable
// from a same-GUID same-path idle tick (Save in place / no change).
// The pointer is the PRIMARY signal (same object across ticks = a candidate
// Save-As; different object = a switch/open, never a relocate). The GUID and
// path travel alongside: the GUID distinguishes pointer *reuse* and drives
// forked-sibling re-divergence; the path tells a Save-As from an idle tick.
// Held as void* so the header stays REAPER-free; it is a compared-only opaque
// handle (never dereferenced), so a stale/recycled address is harmless.
void* lastProject_ = nullptr; // last active ReaProject* (opaque; compare only)
std::string lastGuid_; // "" until the first saved project is seen
std::string lastRppPath_; // .rpp path last seen for lastGuid_
std::string lastRppPath_; // .rpp path last seen for lastProject_
bool primed_ = false; // false until the first poll() observes state
// Load the index from the given project's ext state and resolve bank paths