GENERATION
How a dimension gets made
Every part of a dimension pulls from its own stream of randomness, and every stream is just the seed hashed with a label. Nothing shares one counter, so if I add a generator later an old seed's palette doesn't move. The order below is the order the code runs in and it's protocol: the registry replays the exact same thing to get the tier.
Streams
Blake3 of the whole 32 byte seed plus a label, that seeds a xoshiro256**. The labels are "rarity", "mode", "shape", "mutation", "skin", "palette", "lights", "motion", "satellites", "planes", "debris", "particles", "rings", "background", "mood", "runtime".
Xoshiro256 subStream(const Seed32& seed, std::string_view label) {
blake3_hasher h;
blake3_hasher_init(&h);
blake3_hasher_update(&h, seed.data(), SEED_VISUAL_BYTES);
blake3_hasher_update(&h, label.data(), label.size());
uint8_t out[32];
blake3_hasher_finalize(&h, out, 32);
return Xoshiro256::fromBytes(out);
}The tier
One number from the "rarity" stream against fixed odds. These are NOT config. Change them and different seeds become mythic, so they sit in code on both sides.
Tier rollTier(Xoshiro256& r) {
double u = r.uniform();
if (u < 0.60) return Tier::Common;
if (u < 0.85) return Tier::Uncommon;
if (u < 0.95) return Tier::Rare;
if (u < 0.99) return Tier::Epic;
if (u < 0.999) return Tier::Legendary;
return Tier::Mythic;
}The tier unlocks stuff you can actually see. Common gets the base kit, every step up adds to it.
Unlocks unlocksFor(Tier t) {
Unlocks u{ 0, 0.55f, false, false, false, 0.f, false, false, 1, false };
int k = int(t);
if (k >= 1) { u.extraGens += 1; u.satBerryChance = 0.75f; }
if (k >= 2) { u.secondBerry = true; u.thirdStop = true; }
if (k >= 3) { u.forceCompound = true; u.extraGens += 2; u.afterDampBonus = 0.03f; }
if (k >= 4) { u.forceBleed = true; u.acidOnly = true; u.ringMult = 2; }
if (k >= 5) { u.gold = true; } // + the mythic-only family, not written yet
return u;
}What the registry recomputes
Tier, native mode, shape and family come from 3 streams and nothing else. This is the part the registry redoes on claim. Whatever the client wrote in the file gets ignored.
SeedProps deriveProps(const Seed32& seed, Xoshiro256* shapeRngOut) {
SeedProps p;
auto rarityRng = subStream(seed, "rarity");
auto modeRng = subStream(seed, "mode");
auto shapeRng = subStream(seed, "shape");
p.tier = rollTier(rarityRng);
p.unlocks = unlocksFor(p.tier);
p.nativeMode = p.unlocks.forceBleed ? Mode::Bleed : rollMode(modeRng);
p.geo = rollGeoParams(shapeRng);
p.shapeType = p.unlocks.forceCompound ? 40 : p.geo.type;
p.family = familyOf(p.shapeType);
std::memcpy(&p.mutationSeed, seed.data(), 4);
if (shapeRngOut) *shapeRngOut = shapeRng;
return p;
}The roll order
Then the rest, each from its own stream: mood, mode, palette, motion, lights, backdrop, the shape and its mutation lineage. Skin, satellites, planes, debris, particles and rings come after in the same file.
Dimension rollDimension(const Seed32& seed, int forcedShape, float forcedMood) {
Dimension d;
d.seed = seed;
Xoshiro256 shapeRng = Xoshiro256::fromU64(0);
SeedProps P = deriveProps(seed, &shapeRng);
auto mutRng = subStream(seed, "mutation");
auto skinRng = subStream(seed, "skin");
auto palRng = subStream(seed, "palette");
auto lightRng = subStream(seed, "lights");
auto motionRng = subStream(seed, "motion");
auto satRng = subStream(seed, "satellites");
auto foldRng = subStream(seed, "planes");
auto debRng = subStream(seed, "debris");
auto partRng = subStream(seed, "particles");
auto ringRng = subStream(seed, "rings");
auto bgRng = subStream(seed, "background");
auto moodRng = subStream(seed, "mood");
// ---- rarity first: the tier feeds everything after it ----
d.tier = P.tier;
d.unlocks = P.unlocks;
const Unlocks& U = d.unlocks;
d.runtimeRng = subStream(seed, "runtime");
// ---- mood ----
d.moodV = std::pow(float(moodRng.uniform()), 1.6f);
if (forcedMood >= 0) { d.moodV = std::clamp(forcedMood, 0.f, 1.f); d.moodLocked = true; }
d.moodSeeded = d.moodV;
d.moodTarget = d.moodV;
// ---- mode ----
d.nativeMode = P.nativeMode;
d.displayMode = d.nativeMode;
// ---- palette ----
PalettePair first = makePair(palRng, U.acidOnly ? 4 : -1);
d.palC = makeThirdStop(palRng, first);
d.pal.a = d.pal.fromA = d.pal.toA = first.a;
d.pal.b = d.pal.fromB = d.pal.toB = first.b;
d.pal.next = 2.5f + 6 * float(palRng.uniform());
d.paletteTempo = palRng.range(0.6f, 1.6f);
d.duoExposure = palRng.range(0.45f, 0.85f);
d.duoContrast = palRng.range(1.0f, 1.5f);
d.duoSplit = palRng.range(0.4f, 0.6f);
// ---- motion ----
Motion& m = d.motion;
m.rotX = motionRng.range(-1.2f, 1.2f); m.rotY = motionRng.range(-1.2f, 1.2f); m.rotZ = motionRng.range(-1.2f, 1.2f);
m.orbitSpeed = motionRng.range(0.1f, 2.5f); m.bobSpeed = motionRng.range(0.1f, 2.5f); m.bobAmp = motionRng.range(0.1f, 1.2f);
m.waveSpeed = motionRng.range(0.1f, 2.0f); m.waveAmp = motionRng.range(0.05f, 1.0f);
m.particleSpeed = motionRng.range(0.05f, 1.5f); m.particleSpread = motionRng.range(2.5f, 14);
m.pulseSpeed = motionRng.range(0.2f, 2.5f); m.pulseAmp = motionRng.range(0.02f, 0.6f);
m.chaosFactor = motionRng.range(0.1f, 3.0f); m.invertChance = float(motionRng.uniform()); m.brightnessOffset = motionRng.range(-0.3f, 0.5f);
// ---- lights ----
glm::vec3 lightPos(std::sin(lightRng.range(0, 2 * PI)) * lightRng.range(7, 13), lightRng.range(8, 16), std::cos(lightRng.range(0, 2 * PI)) * lightRng.range(7, 13));
d.lightInt[0] = lightRng.range(0.9f, 1.5f);
glm::vec3 fillPos(-lightPos.x * lightRng.range(0.7f, 1.3f), lightRng.range(-3, 5), -lightPos.z * lightRng.range(0.7f, 1.3f));
d.lightInt[1] = lightRng.range(0.2f, 0.45f);
glm::vec3 rimPos(lightRng.range(-4, 4), lightRng.range(2, 6), -lightRng.range(8, 14));
d.lightInt[2] = lightRng.range(0.4f, 0.8f);
d.lightDir[0] = glm::normalize(lightPos); d.lightDir[1] = glm::normalize(fillPos); d.lightDir[2] = glm::normalize(rimPos);
// ---- backdrop ----
d.bgDensity = bgRng.range(1.5f, 4.0f);
d.bgSeed = float(bgRng.next() & 0xffffff);
// ---- shape + lineage ----
d.geo = P.geo;
d.shapeType = forcedShape >= 0 ? forcedShape % SHAPE_COUNT : P.shapeType;
d.family = familyOf(d.shapeType);
Mesh g = buildShape(d.shapeType, d.geo, shapeRng, 0);
g = weld(g, 1e-4f); // merged shapes come back with seams, weld before displacing
d.lineage = mutateLineage(g, mutRng, U.extraGens);
g.center();
g.computeVertexNormals();
d.mainRadius = g.boundingRadius();
d.mainMesh = std::move(g);
d.wireAlpha = skinRng.range(0.04f, 0.16f);Modes and mood
A seed has a native mode (zen, calm, chaos, bleed), that one counts for value. The display mode is what your install shows, N or 1 to 4 changes it, never the seed. Mood is a 0..1 thing the seed sets and you can move, it drives tempo, mutation rate, grain.
Why it's the same everywhere
No clocks, no hardware, no platform floats in the roll. Integer rng, labeled streams, same mesh code on every machine. The repo has cross-language test vectors that prove the c++ and the registry's TypeScript agree on tier, family and mode for a few thousand seeds.