feat(persist): M4 bank persistence + Save-As relocation
Serialize BankIndex to project ext state ('reasampler'), reload on project
load, resolve paths project-relative. Save-As copies the bank to the new .rpp;
identity keyed off a minted GUID (not the recycled ReaProject*) so project
switches don't clobber banks. Pure classifyProjectTransition tested.
This commit is contained in:
@@ -53,4 +53,80 @@ BankPaths deriveBankPaths(const std::string& projectDir,
|
||||
const std::string& baseName,
|
||||
const std::string& uniqueTag);
|
||||
|
||||
// --- Persist-side path arithmetic (M4) --------------------------------------
|
||||
//
|
||||
// The index stores relative paths only; on project load the persist shell must
|
||||
// turn each entry's relativePath back into an absolute path against the CURRENT
|
||||
// project directory (so a project opened from a new location still resolves its
|
||||
// bank). This is the inverse of the relativePath the capture path produced.
|
||||
//
|
||||
// projectDir : absolute directory of the current .rpp (any slash style)
|
||||
// relativePath : a project-relative index entry (e.g. "reasampler_bank/x.wav")
|
||||
//
|
||||
// Returns "<projectDir>/<relativePath>" forward-slashed. Returns empty when
|
||||
// either input is empty (no default-location fallback — CLAUDE.md invariant) so
|
||||
// a caller that ignores an unsaved/unset project fails loudly rather than
|
||||
// resolving against CWD.
|
||||
std::string resolveBankFile(const std::string& projectDir,
|
||||
const std::string& relativePath);
|
||||
|
||||
// A relocation plan for the physical bank folder on Save-As to a new project
|
||||
// location. The index's relative paths do NOT change (they are relative to the
|
||||
// project dir, which is what moved with the .rpp), so relocation is purely a
|
||||
// folder move: copy/move the whole bank subfolder from the old project dir to
|
||||
// the new one. Both dirs are absolute, forward-slashed, trailing-slash-stripped.
|
||||
struct BankRelocation {
|
||||
std::string oldBankDir; // <oldProjectDir>/reasampler_bank
|
||||
std::string newBankDir; // <newProjectDir>/reasampler_bank
|
||||
bool needed = false; // false when old==new (Save in place, not Save-As)
|
||||
};
|
||||
|
||||
// Derives the relocation plan from the old and new project directories.
|
||||
// oldProjectDir : project dir the bank currently sits under (any slash style)
|
||||
// newProjectDir : project dir the .rpp was just saved to (any slash style)
|
||||
// `needed` is true iff the normalized dirs differ (a genuine Save-As-to-new-dir).
|
||||
// Returns a plan with empty dirs and needed=false when either input is empty.
|
||||
BankRelocation deriveRelocationPlan(const std::string& oldProjectDir,
|
||||
const std::string& newProjectDir);
|
||||
|
||||
// --- Project-identity transition (M4 defect fix) ----------------------------
|
||||
//
|
||||
// What the persist timer must do on each tick, decided purely from the LAST
|
||||
// observed identity and the CURRENT one. Identity is CONTENT-BASED: a project
|
||||
// GUID we mint and store in our ext state (REAPER exposes no stable per-project
|
||||
// GUID). The raw ReaProject* is deliberately NOT part of this decision — REAPER
|
||||
// recycles pointer addresses across project close/open, and keying Save-As off
|
||||
// the pointer let a project switch masquerade as a Save-As and clobber a bank.
|
||||
enum class ProjectTransition {
|
||||
NoOp, // same project, same location — nothing to do
|
||||
Load, // a different project is active — load ITS index from ext state
|
||||
SaveAsRelocate, // same project, new .rpp location — relocate the bank folder
|
||||
};
|
||||
|
||||
// Classifies what a poll tick observed.
|
||||
// lastGuid : the GUID of the project persist last acted on ("" if none/unsaved)
|
||||
// lastPath : that project's .rpp path when last seen ("" if unsaved)
|
||||
// currentGuid : the GUID stored in the now-active project's ext state ("" if
|
||||
// unsaved or never written)
|
||||
// currentPath : the now-active project's .rpp path ("" if unsaved)
|
||||
//
|
||||
// Rules (GUID is the identity; path only distinguishes Save vs Save-As within
|
||||
// the SAME identity):
|
||||
// * currentGuid != lastGuid -> Load (a different project)
|
||||
// * same non-empty GUID, currentPath == lastPath -> NoOp (Save in place / idle)
|
||||
// * same non-empty GUID, currentPath != lastPath -> SaveAsRelocate
|
||||
// * both GUIDs empty, same path -> NoOp (idle unsaved project)
|
||||
// * both GUIDs empty, different path -> Load (can't PROVE same
|
||||
// project without a GUID — a
|
||||
// first-save or a switch
|
||||
// between unsaved projects;
|
||||
// never a relocate)
|
||||
// The both-empty/different-path -> Load rule is what makes the recycled-pointer
|
||||
// bug impossible: absent GUID corroboration, a path change is treated as a new
|
||||
// project (safe: load), never a relocate (destructive: copy-over).
|
||||
ProjectTransition classifyProjectTransition(const std::string& lastGuid,
|
||||
const std::string& lastPath,
|
||||
const std::string& currentGuid,
|
||||
const std::string& currentPath);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
Reference in New Issue
Block a user