feat(persist): persist BankBook under banks key, retire legacy bank_index, route capture to active bank (B2)

This commit is contained in:
2026-07-23 20:26:15 -04:00
parent 5b19525ec0
commit 85993ebc3e
6 changed files with 218 additions and 55 deletions
+60 -32
View File
@@ -5,11 +5,15 @@
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
// pointers; here they are extern (CLAUDE.md §contract).
//
// Storage: SetProjExtState / GetProjExtState, namespace "reasampler", key
// "bank_index". Ext state is stored INSIDE the .rpp, so the index travels with
// the project automatically (CONTEXT.md §Persistence & paths). The only thing
// that does NOT travel for free is the physical bank folder; on Save-As to a new
// directory we relocate it so the index's relative paths still resolve.
// Storage: SetProjExtState / GetProjExtState, namespace "reasampler". Phase B: the
// whole BankBook (pool as bank-zero + named banks) is written under key "banks"
// (authoritative); the legacy single-bank key "bank_index" is RETIRED — cleared on
// save (SetProjExtState with "" deletes it) and read only once, to migrate a pre-
// multi-bank project's index into the pool. Ext state is stored INSIDE the .rpp, so
// the banks travel with the project automatically (CONTEXT.md §Persistence & paths).
// The only thing that does NOT travel for free is the physical bank folder; on
// Save-As to a new directory we relocate it so the indices' relative paths still
// resolve.
//
// PROJECT-LOAD / SAVE-AS DETECTION (chosen mechanism):
// Driven by REAPER's "timer" register (main.cpp). Each poll() reads the active
@@ -174,12 +178,22 @@ void ReaSamplerSession::saveToActiveProject() {
if (!proj) return; // no active project — nothing to persist
if (rppPath.empty()) return; // unsaved project — no .rpp to store into
const std::string json = bank_.serialize();
// Phase B: the whole book (pool as bank-zero + named banks) is authoritative and
// rides in the `banks` key.
const std::string banksJson = book_.serialize();
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
kProjExtIndexKey, json.c_str());
kProjExtBanksKey, banksJson.c_str());
// Additive: the Design-View model rides alongside the bank in its own key.
// Independent write — does not disturb the bank_index above.
// Retire the legacy single-bank `bank_index` key: SetProjExtState with an empty
// value DELETES the key (SDK header ~6288: val NULL or "" deletes the data). This
// realizes retirement concretely — after any save, a formerly-legacy project
// carries `banks` and NO `bank_index`, and going forward the legacy key is never
// written. Cheap and idempotent when the key is already absent.
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
kProjExtIndexKey, "");
// Additive: the Design-View model rides alongside the banks in its own key.
// Independent write — does not disturb the `banks` blob above.
const std::string viewJson = view_.serialize();
SetProjExtState(static_cast<ReaProject*>(proj), kProjExtNamespace,
kProjExtViewKey, viewJson.c_str());
@@ -226,32 +240,46 @@ void ReaSamplerSession::loadFromProject(void* proj, const std::string& projectDi
view_ = loadViewModel(static_cast<ReaProject*>(proj));
if (!proj) {
bank_ = BankIndex{};
book_ = BankBook{};
return;
}
const std::string json =
getProjExtStateString(static_cast<ReaProject*>(proj), kProjExtNamespace,
kProjExtIndexKey);
if (json.empty()) {
// No stored index (new or never-captured project) — start empty.
bank_ = BankIndex{};
return;
}
std::optional<BankIndex> loaded = BankIndex::deserialize(json);
if (!loaded) {
ShowConsoleMsg("ReaSampler: stored bank index is malformed — ignoring.\n");
bank_ = BankIndex{};
return;
}
bank_ = std::move(*loaded);
// Project-relative resolution is a READ-time concern: the index stores only
// relative paths (invariant), and consumers (M5 panel, M6 insert) resolve
// each entry against the CURRENT project dir via resolveBankFile(projectDir,
// relativePath). We do NOT rewrite the stored paths to absolute here — that
// would break the relative-only invariant and the travel-with-.rpp property.
// projectDir is threaded through for those consumers; nothing to do at load
// time beyond replacing the in-memory bank.
// Read both possible sources: the authoritative `banks` blob and the retired-but-
// possibly-still-present legacy `bank_index`. The precedence + migration decision
// (`banks` wins; else the legacy index migrates into the pool; else an empty book)
// is pure logic; it is inlined here rather than via BankBook::loadFromPersisted only
// so a malformed `banks` blob can be warned on the console (single parse) — a corrupt
// blob must read as "ignored", not silent loss, mirroring the prior malformed-index
// warning. A malformed `banks` degrades to an empty book and does NOT fall back to
// the stale legacy key (which would resurrect superseded single-bank state).
const std::string banksJson =
getProjExtStateString(static_cast<ReaProject*>(proj), kProjExtNamespace,
kProjExtBanksKey);
if (!banksJson.empty()) {
std::optional<BankBook> loaded = BankBook::deserialize(banksJson);
if (!loaded) {
ShowConsoleMsg("ReaSampler: stored banks are malformed — ignoring.\n");
book_ = BankBook{};
} else {
book_ = std::move(*loaded);
}
} else {
// No `banks` yet — fall back to the legacy `bank_index`, migrated into the pool
// by BankBook's parse-time promotion. loadFromPersisted covers the legacy-or-
// empty tail; passing "" for banksJson takes exactly that branch.
const std::string legacyJson =
getProjExtStateString(static_cast<ReaProject*>(proj), kProjExtNamespace,
kProjExtIndexKey);
book_ = BankBook::loadFromPersisted(std::string{}, legacyJson);
}
// Project-relative resolution is a READ-time concern: every BankIndex in the book
// stores only relative paths (invariant, enforced per-bank at add()), and consumers
// (M5 panel, M6 insert) resolve each entry against the CURRENT project dir via
// resolveBankFile(projectDir, relativePath). We do NOT rewrite stored paths to
// absolute here — that would break the relative-only invariant and travel-with-.rpp.
// projectDir is threaded through for those consumers; nothing to do at load time
// beyond replacing the in-memory book.
(void)projectDir;
}