193 lines
8.8 KiB
C++
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
|