Files
reasampler/src/shell/persist/session.cpp
T

193 lines
8.8 KiB
C++

// session.cpp — the ReaSamplerSession lifecycle half of the persist seam (see
// session.h for the identity-transition design and the TU map).
//
// Compiled into the reaper_reasampler module. Includes reaper_plugin_functions.h
// WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API
// pointers; here they are extern (CLAUDE.md §contract).
//
// classifyProjectTransition (pure, capture_paths) takes a `sameProjectObject`
// bool so the decision stays REAPER-free and testable; poll() executes the
// verdict. REAPER exposes no stable per-project GUID (GetSetProjectInfo_String
// has no PROJECT_GUID desc; GetProjectStateChangeCount is a session-local
// counter, not cross-open identity), so we mint one (genGuid/guidToString)
// under kProjExtGuidKey; Save-As copies the whole .rpp including our ext
// state, so the new project initially shares the old GUID, and poll()
// re-GUIDs it after relocating (or on the forked-sibling Load branch).
//
// Division of labour for undo/redo: the identity-transition poll (this file)
// owns open/tab-switch/new/forked-sibling/Save-As. The `projectconfig` hook
// (main.cpp, BeginLoadProjectState with isUndo) owns undo/redo, where identity
// is unchanged but ext state rolled back/forward on disk — the identity poll
// would see NoOp there, so the hook requests a reload that poll() drains next
// tick, once REAPER has restored the <EXTSTATE> block. The hook fires on
// undo, redo, AND normal open, but the reload flag is set only for isUndo, so
// a normal open never double-loads.
#include "shell/persist/session.h"
#include <string>
#include "shell/persist/ext_state_io.h"
#include "shell/persist/persist_internal.h"
#include "core/capture/capture_paths.h" // classifyProjectTransition / deriveRelocationPlan (pure)
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_MarkProjectDirty
#define REAPERAPI_WANT_SetProjExtState
#define REAPERAPI_WANT_ShowConsoleMsg
#include "reaper_plugin_functions.h"
namespace reasampler {
using persist_detail::ensureProjectGuid;
using persist_detail::genProjectGuidString;
using persist_detail::getProjExtStateString;
using persist_detail::projectDirOf;
using persist_detail::readActiveProject;
using persist_detail::relocateBankFolder;
void ReaSamplerSession::recordCreated(const model::Sample& sample,
tracking::OriginKind kind) {
tracking::OriginRecord rec;
rec.relativePath = sample.relativePath;
rec.kind = kind;
rec.sampleId = sample.id;
// The Sample's own provenance is where the parent was resolved; reading it here
// rather than re-deriving keeps one source for the lineage fact. Absent
// provenance is a root capture, not a gap.
if (sample.provenance) rec.parentSampleId = sample.provenance->parentSampleId;
const tracking::RecordResult result = tracking_.record(rec);
// A rejection means a file exists that nothing attributes to us — invisible
// otherwise, and exactly the gap this ledger exists to close. AlreadyPresent is
// the normal dedup outcome, not a gap.
if (result == tracking::RecordResult::RejectedEmptyPath ||
result == tracking::RecordResult::RejectedAbsolutePath) {
ShowConsoleMsg(("ReaSampler: could not record the origin of '" +
sample.relativePath +
"' -- the path is empty or absolute. The file is NOT tracked and "
"prune will treat it as a foreign file.\n").c_str());
}
}
bool ReaSamplerSession::consumeLoadSignal() {
const bool pending = loadPending_;
loadPending_ = false;
return pending;
}
void ReaSamplerSession::requestReload() {
// Set-only; poll() drains it next tick. Idempotent — multiple undo/redo
// callbacks before the next tick collapse to one reload.
reloadRequested_ = true;
}
void ReaSamplerSession::poll() {
std::string rppPath;
void* proj = readActiveProject(rppPath);
const std::string currentGuid =
proj ? getProjExtStateString(proj, projExtNamespace(), kProjExtGuidKey)
: std::string{};
if (!primed_) {
// First observation: adopt current identity and load its index, without
// treating it as a "change" (avoids a spurious relocation on startup).
primed_ = true;
loadFromProject(proj, projectDirOf(rppPath));
lastProject_ = proj;
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
lastRppPath_ = rppPath;
reloadRequested_ = false; // priming already loaded — a co-tick request is moot
return;
}
// Undo/redo reload, owned by the projectconfig hook, not the identity
// classifier below: an undo/redo keeps the same project identity, so
// classifyProjectTransition would return NoOp and never re-read ext
// state. By now REAPER has finished restoring the <EXTSTATE> block, so
// GetProjExtState returns the post-undo value. Reload and identity-adopt
// (no relocation — path unchanged). This is the ONLY undo/redo reload
// path — the timer never polls ext-state content to detect an undo.
if (reloadRequested_) {
reloadRequested_ = false;
loadFromProject(proj, projectDirOf(rppPath));
lastProject_ = proj;
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
lastRppPath_ = rppPath;
return;
}
// Pointer identity is the primary signal: a genuine Save-As keeps the SAME
// ReaProject* (one object saved elsewhere); a tab-switch/open is a different
// object. Passing the bool (not the pointer) keeps the classifier pure.
const bool sameProjectObject = (proj == lastProject_);
const capture::ProjectTransition transition = capture::classifyProjectTransition(
sameProjectObject, lastGuid_, lastRppPath_, currentGuid, rppPath);
switch (transition) {
case capture::ProjectTransition::NoOp:
return;
case capture::ProjectTransition::Load: {
// A different project of record is active. Load its index; never relocate.
//
// Forked-sibling re-GUID: gate on `!sameProjectObject` so this fires
// only for the fork case (same GUID, different object) — a Save-As
// fork that copied our GUID and never re-saved. A recycled-pointer
// Load (currentGuid != lastGuid_) must NOT re-GUID — it is already
// a distinct identity; currentGuid == lastGuid_ can only hold here
// when that case did not fire.
if (proj && !sameProjectObject && !currentGuid.empty() &&
currentGuid == lastGuid_ && !rppPath.empty()) {
const std::string fresh = genProjectGuidString();
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
kProjExtGuidKey, fresh.c_str());
MarkProjectDirty(static_cast<ReaProject*>(proj));
loadFromProject(proj, projectDirOf(rppPath));
lastProject_ = proj;
lastGuid_ = fresh;
lastRppPath_ = rppPath;
return;
}
// Normal load: establish identity the same way prime does.
loadFromProject(proj, projectDirOf(rppPath));
lastProject_ = proj;
lastGuid_ = ensureProjectGuid(proj, rppPath, currentGuid);
lastRppPath_ = rppPath;
return;
}
case capture::ProjectTransition::SaveAsRelocate: {
// Same project object, new .rpp path: a genuine Save-As. Relocate
// the bank folder so the wavs sit under the new .rpp and the
// index's relative paths still resolve. Keep the in-memory bank
// as-is (Save-As copied our ext state) — do NOT reload.
const std::string oldDir = projectDirOf(lastRppPath_);
const std::string newDir = projectDirOf(rppPath);
const capture::BankRelocation plan =
capture::deriveRelocationPlan(oldDir, newDir);
if (plan.needed) {
relocateBankFolder(plan.oldBankDir, plan.newBankDir);
}
// Save-As duplicated our ext state, so the new project shares the
// old GUID; mint a fresh one and mark dirty so it flushes on the
// next save, and A/B no longer collide on identity when reopened.
const std::string fresh = genProjectGuidString();
if (proj) {
SetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
kProjExtGuidKey, fresh.c_str());
MarkProjectDirty(static_cast<ReaProject*>(proj));
}
lastProject_ = proj; // unchanged (same object) — set for symmetry
lastGuid_ = fresh;
lastRppPath_ = rppPath;
return;
}
}
}
} // namespace reasampler