Files
reasampler/src/app_version.cpp
T
daniel fe55a9a96e feat(vst): S18 VST3 channel isolation — beta ReaSampler 9000 pairs with beta extension only
Fork the VST3 class UID (beta UID minted+frozen), on-disk name, and display
name by the one channel bit via app_version; couple UID selection to the
channel at compile time so a refactor can't split identity from data.
2026-07-27 04:16:49 -04:00

170 lines
6.6 KiB
C++

// app_version.cpp — implementation of the pure version-identity core (Phase V, V1 + V4).
// See app_version.h for the contract. The version STRING and the channel bit both come
// from version_generated.h (produced by CMake configure_file from the one
// REASAMPLER_VERSION variable + the REASAMPLER_CHANNEL flag) — this TU re-exports them and
// owns every pure derivation: the channel-qualified identity strings (V4) and the
// parse/compare/classify logic (V1). No #ifdef forks leak beyond this file; the shells
// consume the accessors below, so channel identity is one auditable definition.
#include "app_version.h"
#include <string>
#include "version_generated.h" // REASAMPLER_VERSION_STRING + REASAMPLER_CHANNEL_IS_BETA
namespace reasampler {
namespace {
// The one channel predicate every derivation below branches on — the single point the
// configure_file'd bit enters the pure module. constexpr so the branches fold at compile
// time; the accessors still return by const ref for a stable shared instance.
constexpr bool kIsBeta = (REASAMPLER_CHANNEL_IS_BETA != 0);
} // namespace
Channel channel() { return kIsBeta ? Channel::Beta : Channel::Stable; }
bool isBeta() { return kIsBeta; }
const std::string& appVersion() {
// The user-visible render. Stable: EXACTLY the CMake string (leading zero and all).
// Beta: the same numeric string plus a plain "-beta" suffix (V2). Function-local
// static so callers share one authoritative instance.
static const std::string kVersion =
kIsBeta ? std::string(REASAMPLER_VERSION_STRING) + "-beta"
: std::string(REASAMPLER_VERSION_STRING);
return kVersion;
}
const std::string& stampVersion() {
// The ext-state stamp value — the NUMERIC TRIPLE ONLY, IDENTICAL on both channels.
// No "-beta" suffix: it must parse as Stamped on read-back (a suffixed stamp classifies
// as Unknown), and stable's stamp stays byte-identical regardless of the channel build.
// The channel is carried by extStateNamespace(), never baked into the stamp.
static const std::string kStamp = REASAMPLER_VERSION_STRING;
return kStamp;
}
const std::string& extStateNamespace() {
// Stable "reasampler" is byte-identical to the pre-V4 build; beta is isolated.
// FOREVER-STABLE per channel.
static const std::string kNs = kIsBeta ? "reasampler_beta" : "reasampler";
return kNs;
}
const std::string& commandIdPrefix() {
// Stable prefix is byte-identical to every shipped command id; beta is a distinct
// forever-family. FOREVER-STABLE per channel.
static const std::string kPrefix =
kIsBeta ? "CEREBELLUM_REASAMPLER_BETA_" : "CEREBELLUM_REASAMPLER_";
return kPrefix;
}
const std::string& actionDisplayPrefix() {
// Actions-list legibility: two channels must be distinguishable by name. Trailing
// space so callers append the action phrase directly.
static const std::string kDisp = kIsBeta ? "ReaSampler beta: " : "ReaSampler: ";
return kDisp;
}
const std::string& binaryName() {
static const std::string kName =
kIsBeta ? "reaper_reasampler_beta" : "reaper_reasampler";
return kName;
}
const std::string& dockTitle() {
static const std::string kTitle =
kIsBeta ? "ReaSampler Bank beta" : "ReaSampler Bank";
return kTitle;
}
const std::string& dockIdent() {
// Persisted dock-position ident — FOREVER-STABLE per channel (changing it strands the
// saved dock slot). Beta qualified so the two panels do not fight over one slot.
static const std::string kIdent =
kIsBeta ? "reasampler_bank_panel_beta" : "reasampler_bank_panel";
return kIdent;
}
const std::string& vstOutputName() {
// The .vst3 module OUTPUT_NAME base — FOREVER-STABLE per channel. Stable is
// byte-identical to pre-S18 ("reasampler_9000"); beta is isolated so both install
// side-by-side without a filename collision.
static const std::string kName =
kIsBeta ? "reasampler_9000_beta" : "reasampler_9000";
return kName;
}
const std::string& vstPluginName() {
// The factory display name / editor title / embed label. Stable is byte-identical to
// pre-S18 ("ReaSampler 9000"); beta appends " beta" so the two channels are distinct
// plugins in the FX browser.
static const std::string kName =
kIsBeta ? "ReaSampler 9000 beta" : "ReaSampler 9000";
return kName;
}
std::string channelCommandId(const std::string& suffix) {
return commandIdPrefix() + suffix;
}
std::string channelActionName(const std::string& phrase) {
return actionDisplayPrefix() + phrase;
}
std::optional<Version> parseVersion(const std::string& s) {
// Split on '.' into exactly three non-empty all-digit components. No sign, no
// whitespace, no trailing garbage. Leading zeros are allowed (0.9.01 parses to
// {0,9,1}) — the display fidelity of the zero is the string's job, not this parse's.
Version v;
int component = 0; // 0=major, 1=minor, 2=patch
long long acc = 0; // current component accumulator (long long guards overflow)
bool digitsInComponent = false;
int* const out[3] = {&v.major, &v.minor, &v.patch};
for (char c : s) {
if (c == '.') {
if (!digitsInComponent) return std::nullopt; // empty component (".", "1..2")
if (component >= 2) return std::nullopt; // too many dots
*out[component] = static_cast<int>(acc);
++component;
acc = 0;
digitsInComponent = false;
continue;
}
if (c < '0' || c > '9') return std::nullopt; // non-digit (sign, letter, ws)
acc = acc * 10 + (c - '0');
if (acc > 1'000'000'000LL) return std::nullopt; // absurdly large -> reject
digitsInComponent = true;
}
if (component != 2 || !digitsInComponent) return std::nullopt; // too few components
*out[2] = static_cast<int>(acc);
return v;
}
bool versionLess(const Version& a, const Version& b) {
if (a.major != b.major) return a.major < b.major;
if (a.minor != b.minor) return a.minor < b.minor;
return a.patch < b.patch;
}
WritingVersion classifyWritingVersion(const std::string& rawStamp) {
WritingVersion wv;
if (rawStamp.empty()) {
wv.kind = WritingVersion::Kind::PreVersioning; // no stamp -> pre-versioning
return wv;
}
std::optional<Version> parsed = parseVersion(rawStamp);
if (!parsed) {
wv.kind = WritingVersion::Kind::Unknown; // present but malformed -> ignored
wv.raw = rawStamp;
return wv;
}
wv.kind = WritingVersion::Kind::Stamped;
wv.raw = rawStamp;
wv.parsed = *parsed;
return wv;
}
} // namespace reasampler