Repository navigation
Same Seed Same Galaxy
The seed promise: from 1.0.0, a seed, the options and the generator version (the v1 in every address) give the same output on Windows, Linux and macOS, on .NET 10 and .NET Framework 4.8, for the whole 1.x line. The README extends it to Unity (Mono and IL2CPP); this wiki's runs did not cover Unity. A change that would alter any seed's output must add a generator version and keep the old one.
The same call twice gives the same galaxy, down to every system. And any object regenerated alone from its address equals the same object generated in place:
using UniverseGeneration;
var first = Galaxy.Generate("my-seed").ToJson(children: true);
var second = Galaxy.Generate("my-seed").ToJson(children: true);
Console.WriteLine(first == second);
var galaxy = Galaxy.Generate("my-seed");
var inPlace = galaxy.System(31);
var alone = (StarSystem)Universe.At(inPlace.Address);
Console.WriteLine(inPlace.ToJson() == alone.ToJson());prints:
True
True
ToJson(children: true) writes every field of the galaxy and of all its systems, the same text on every runtime. Its hash is a fingerprint of the whole galaxy:
using System.Security.Cryptography;
using System.Text;
using UniverseGeneration;
var json = Galaxy.Generate("my-seed").ToJson(children: true);
using var sha = SHA256.Create();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(json));
Console.WriteLine($"{json.Length} characters, SHA-256 {BitConverter.ToString(hash).Replace("-", "").Substring(0, 16)}...");prints, on .NET 10 and on .NET Framework 4.8 alike:
909995 characters, SHA-256 D486B72BA56ACAEF...
Run it on your own machine: the same line means you have the same galaxy.
Every object has an Address, such as v1-my-seed/galaxy/system/31/planet/1, and Universe.At(address) regenerates that object alone. Save the address, not the object. An address does not carry options: pass the options the object was made with, or save a link from Universe.Link(address, options), which carries them after a ? (see Recipes).
Generating a level on its own gives it its own address, so StarSystem.Generate("my-seed") (v1-my-seed/system) is a different system from Galaxy.Generate("my-seed").System(0) (v1-my-seed/galaxy/system/0).
Seeds are compared as exact text. Case and spaces make a different seed, and a space is escaped as %20 in the address:
using UniverseGeneration;
foreach (var seed in new[] { "my-seed", "My-Seed", "my-seed ", "hello world" })
{
var galaxy = Galaxy.Generate(seed);
Console.WriteLine($"\"{seed}\": {galaxy.Address}, {galaxy.Name}, first system {galaxy.Map[0].Name}");
}prints:
"my-seed": v1-my-seed/galaxy, Wyvern Galaxy, first system HD 147927
"My-Seed": v1-My-Seed/galaxy, Pallas Galaxy, first system 5 Trianguli
"my-seed ": v1-my-seed%20/galaxy, Quill Galaxy, first system HD 129976
"hello world": v1-hello%20world/galaxy, Wyvern Galaxy, first system Southern Crab
Galaxy names come from a fixed list, so two seeds can share a galaxy name ("my-seed" and "hello world" both make a Wyvern Galaxy) and still be different galaxies. Trim or normalise user-typed seeds yourself if "My-Seed" and "my-seed " should mean the same world. An empty seed is accepted and is a seed like any other.
The options are part of what a seed means. DangerShift, ExtraLanes and Arms change values after every draw (the CHANGELOG for 1.0.0), so DangerShift moves danger and nothing else. Systems is a different galaxy: here its first system keeps the name but not the danger.
using UniverseGeneration;
var plain = Galaxy.Generate("my-seed");
var shifted = Galaxy.Generate("my-seed", Preset.Default with { DangerShift = 2 });
var bigger = Galaxy.Generate("my-seed", Preset.Default with { Systems = 61 });
Console.WriteLine($"{plain.Map[0].Name}, {shifted.Map[0].Name}, {bigger.Map[0].Name}");
Console.WriteLine($"danger {plain.Map[0].Danger}, {shifted.Map[0].Danger}, {bigger.Map[0].Danger}");
Console.WriteLine(plain.Map.Zip(shifted.Map, (a, b) => a.Name == b.Name && a.X == b.X).All(same => same));prints:
HD 147927, HD 147927, HD 147927
danger 9, 10, 7
True
Danger stays within 1 to 10 (the DangerShift documentation), so the first system's 9 shifted by 2 shows as 10, and the last line says every system kept its name and position.
GeneratorHooks run your code on each object after it is made. They can rename or tag, but the generator's draws happen before them, so nothing else changes:
using UniverseGeneration;
var hooks = new GeneratorHooks { OnSystem = s => s with { Name = "New " + s.Name } };
var plain = Galaxy.Generate("my-seed").System(0);
var hooked = Galaxy.Generate("my-seed", null, hooks).System(0);
Console.WriteLine($"{plain.Name} / {hooked.Name}");
Console.WriteLine(plain.Star == hooked.Star && plain.Planets.Count == hooked.Planets.Count);prints:
HD 147927 / New HD 147927
True
Pass the same hooks to Universe.At and it runs them again on what it regenerates. A hook that is not itself deterministic (one that reads the clock or a random number) breaks the promise for its own fields only.
- A different seed text, different options, or different registered tables under the same id.
- A hook that gives different results for the same object.
- Reading positions through Unity's float adapters and comparing them as if they were the generated doubles: the README says those floats are for drawing, and the doubles are what the promise covers.
- A future generator version. Addresses start with
v1; a version 2 would be asked for byv2-addresses, and 1.0.0 refuses those with "This package generates version 1; the address asks for version 2." (errors).
These rules come from the repository's determinism rules, with the evidence for each:
- Its own random numbers, specified exactly: PCG32 for draws and SplitMix64 to derive seeds. Never
System.Random(its seeded sequence is not promised across .NET versions, andnew Random(s)andnew Random(-s)give the same sequence) orUnityEngine.Random. - Hierarchical seeds: each object's seed comes from its parent's seed and its label, hashed with FNV-1a over UTF-8 (never
string.GetHashCode, which .NET randomises per process). Each purpose (layout, star, planets, names, lanes) draws from its own stream, so a new field or a tuned probability shifts nothing else. - Decisions on integer draws against integer weights; a continuous value is quantised before it decides anything.
- Maths from
+ - * /and square root only, which IEEE 754 rounds the same everywhere.Exp,Log,Pow,Sin,CosandAtan2are built from those, because the platform versions can differ in the last bit between Windows, Linux, macOS, Mono and IL2CPP. - Doubles inside, rounded to fixed decimals before a value is stored or written, with invariant formatting.
- A fixed number of draws per object and stream; an optional feature draws from its own stream.
- A seed's identity is the seed text and the generator version, and golden files per version are never regenerated.
The proof is in the repository: golden files in tests/Golden/v1/ that the tests compare on .NET 10 and .NET Framework 4.8, run by CI on Windows, Linux and macOS. 1.0.0 added new golden files for its new features and changed none of 1.0.0-beta.1's, so every 1.0.0-beta.1 seed gives the same output on 1.0.0 (Versions and upgrading).
This wiki describes UniverseGenerator 1.0.0 and was last updated on 2026-10-03. The library is MIT licensed. Report problems in the issues. On NuGet: UniverseGenerator. On OpenUPM: com.m4bwav.universe-generator.
Using it
- Getting started
- API reference
- Same seed, same galaxy
- Plausibility rules
- Errors and edge cases
- Recipes
- Feature matrix
- FAQ
The releases
Contributing
Elsewhere