#pragma once // app_version — the REAPER-free version-identity core (Phase V, V1 + V4). The single // source of truth for the version STRING lives in CMake (a `REASAMPLER_VERSION` // variable threaded in via configure_file -> version_generated.h); this module // re-exports it as the canonical constant and owns every pure operation on it: the // exact-string render, the parse/compare arithmetic a within-channel forward // migration will lean on, and the "which version wrote this project" result that // persist reads back from ext state (absent stamp = pre-versioning, never an error). // // V4 (beta-in-isolation) extends this module into the SINGLE SOURCE OF TRUTH FOR // CHANNEL IDENTITY too. A compile-time flag (`-DREASAMPLER_CHANNEL=beta`, threaded // through the same configure_file'd version_generated.h as REASAMPLER_CHANNEL_IS_BETA) // selects stable (the default, absent-flag build — byte-for-byte today's identity) or // a fully isolated beta build. Every channel-qualified identity string the shells // register with REAPER — the display suffix, the ext-state namespace, the command-id // prefix, the Actions-list name prefix, the binary/dock idents — is DERIVED HERE from // the one channel bit, so no scattered #ifdef forks live across the translation units; // the shells just consume these accessors. This keeps "what makes a beta a beta" one // auditable definition and makes the channel-derived rendering unit-testable. // // PURE MODULE: NO REAPER types, NO SWELL, NO vendor/ includes. Standard library // only. Builds and unit-tests without REAPER (mirror of bank_model / tail_control). // // Leading-zero fidelity (V1, Daniel-fixed): the displayed/stamped string is EXACTLY // "0.9.01" — two-digit zero-padded patch. That exactness is why the version STRING is // the authoritative artifact (sourced verbatim from the one CMake variable), not a // reconstruction from numeric components — CMake's `project(VERSION)` may normalize a // numeric patch field, so we never round-trip the string through integers to render it. #include #include namespace reasampler { // --- Channel identity (V4, beta-in-isolation) --------------------------------------- // // The build channel, fixed at compile time by REASAMPLER_CHANNEL_IS_BETA (0 = stable, // the default absent-flag build; 1 = beta, from -DREASAMPLER_CHANNEL=beta). Stable is // today's build with byte-identical identity in EVERY string below — any divergence on // the stable channel is a defect. Beta forks every identity so a beta binary coexists // with stable in one REAPER (both are dlopen'd at startup) without colliding on project // ext-state, keybindings, or any REAPER-global registration. enum class Channel { Stable, Beta }; // The channel this build was compiled for. Constant per binary. Channel channel(); // True on the beta build only. Convenience over channel() == Channel::Beta. bool isBeta(); // The user-visible version render. Stable: EXACTLY the CMake string ("0.9.01"). Beta: // that string plus a plain "-beta" suffix ("0.9.01-beta") — a plain suffix, NOT a // git-describe decoration (V2, Daniel-fixed). This is what the show-version action and // the bank-panel readout display. It is NOT the ext-state stamp value (see stampVersion). const std::string& appVersion(); // The ext-state STAMP value — the writing-version recorded into a saved project. This is // the NUMERIC TRIPLE ONLY ("0.9.01") on BOTH channels: it deliberately carries NO channel // suffix, so (a) parseVersion classifies it as Stamped when its own channel reads it back // (a "-beta"-suffixed stamp would classify as Unknown — the V4 stamp-classifiability // requirement), and (b) stable's stamp value is byte-identical regardless of the channel // build. The channel is carried by the ISOLATED namespace (see extStateNamespace), never // baked into the stamp. Distinct from appVersion() precisely so the display can say // "-beta" while the stamp stays classifiable and stable-identical. const std::string& stampVersion(); // The project ext-state namespace this channel reads and writes. Stable: "reasampler" // (byte-identical to the pre-V4 build). Beta: "reasampler_beta". FOREVER-STABLE per // channel once shipped — changing either orphans every already-saved project's state. // // ISOLATION SEMANTICS (V4, accepted — not a bug): a channel reads/writes ONLY its own // namespace. A project saved by stable shows empty/default ReaSampler state when opened // in beta, and vice versa. There is NO cross-namespace read, migration, or fallback in // this wave — that isolation is the safety property (a beta can never read or rewrite a // stable project's bank/view/tail state). const std::string& extStateNamespace(); // The FOREVER-STABLE command-id prefix every bindable action mints its id from. Stable: // "CEREBELLUM_REASAMPLER_" (byte-identical to the shipped ids). Beta: // "CEREBELLUM_REASAMPLER_BETA_", a DISTINCT forever-family so beta and stable actions // never collide in REAPER's one Actions list and their keybindings stay independent. // Callers concatenate their per-action suffix onto this (e.g. prefix + "CAPTURE_TRACK"). // PERMANENT once a beta ships — mark any minted id FOREVER-STABLE like stable's. const std::string& commandIdPrefix(); // The Actions-list DISPLAY-NAME prefix, so two coexisting channels are distinguishable in // REAPER's Actions list. Stable: "ReaSampler: " (unchanged). Beta: "ReaSampler beta: ". // Callers build a gaccel desc as actionDisplayPrefix() + "capture selected track", etc. const std::string& actionDisplayPrefix(); // The binary/module OUTPUT NAME base. Stable: "reaper_reasampler". Beta: // "reaper_reasampler_beta". Mirrors the CMake OUTPUT_NAME (which is the authoritative // artifact name); exposed here for any in-binary self-identification. REAPER dlopen's // any reaper_* module, so both channels load side-by-side. const std::string& binaryName(); // The docked bank-panel identity strings, channel-qualified so the two panels are // distinguishable and do not fight over one persisted dock slot (a REAPER-global // collision surface — DockWindowAddEx's identstr keys the saved dock position). // dockTitle() — the visible dock tab title. Stable: "ReaSampler Bank". // Beta: "ReaSampler Bank beta". // dockIdent() — the persisted dock-position ident. Stable: "reasampler_bank_panel". // Beta: "reasampler_bank_panel_beta". FOREVER-STABLE per channel. const std::string& dockTitle(); const std::string& dockIdent(); // --- Channel-qualified action id / name builders ------------------------------------ // // The two composition helpers every action-registering shell (main.cpp, actions.cpp) // funnels through, so command ids and Actions-list names are qualified IDENTICALLY on // every channel from ONE definition — no shell re-implements the concatenation. // // channelCommandId(suffix): commandIdPrefix() + suffix. `suffix` is the per-action tail // WITHOUT the family prefix (e.g. "CAPTURE_TRACK", "SHOW_VERSION"). Stable yields the // exact shipped id ("CEREBELLUM_REASAMPLER_CAPTURE_TRACK"); beta yields the isolated // forever-family id ("CEREBELLUM_REASAMPLER_BETA_CAPTURE_TRACK"). FOREVER-STABLE per // channel — a suffix, once shipped, is as permanent as the prefix. // channelActionName(phrase): actionDisplayPrefix() + phrase. `phrase` is the action's // human description WITHOUT the "ReaSampler: " lead (e.g. "capture selected track(s)"). // Stable yields "ReaSampler: capture selected track(s)"; beta prefixes "ReaSampler beta: " // so the two channels' actions are distinguishable in one Actions list. std::string channelCommandId(const std::string& suffix); std::string channelActionName(const std::string& phrase); // A parsed semver triple. Kept minimal — major.minor.patch as integers, for ORDERING // only. It deliberately does NOT round-trip back to the display string (the leading // zero is a rendering concern owned by the authoritative string, not reconstructable // from the integer patch). parseVersion returns nullopt on malformed input. struct Version { int major = 0; int minor = 0; int patch = 0; }; // Parse "x.y.z" (each component a non-negative integer, leading zeros allowed) into a // Version. Returns nullopt on anything malformed: wrong component count, non-digits, a // leading `-`, empty components, or trailing garbage. Used for ordering two stamps and // for validating a stored stamp before comparing. std::optional parseVersion(const std::string& s); // Numeric ordering by (major, minor, patch). a < b iff a precedes b. So // 0.9.01 < 0.9.02 < 0.10.01 (numeric compare, NOT lexicographic — 10 > 9). bool versionLess(const Version& a, const Version& b); // The writing-version a project was last saved with, recovered from its ext-state // stamp. A project saved before this feature shipped has NO stamp — that is the // explicit `preVersioning` case (kind), not an error and not a warning. A present-but- // malformed value is `unknown` (also silent — a corrupt stamp is ignored, never // throws). A well-formed value is `stamped` and carries the exact stored string plus // its parsed triple for comparison. struct WritingVersion { enum class Kind { PreVersioning, // no stamp stored — a project written before versioning shipped Unknown, // a stamp was stored but is not parseable — ignored, not an error Stamped, // a well-formed stamp }; Kind kind = Kind::PreVersioning; std::string raw; // the exact stored string (empty for PreVersioning) Version parsed; // meaningful only when kind == Stamped }; // Classify a raw stored stamp value (exactly what GetProjExtState returned for the // version key). Empty -> PreVersioning; non-empty but unparseable -> Unknown; parseable // -> Stamped. Pure so the three-way classification is test-pinned; persist calls this // with the raw ext-state read and never has to reason about the cases itself. WritingVersion classifyWritingVersion(const std::string& rawStamp); } // namespace reasampler