Skip to content

Registries and Sampling

Zaldaryon edited this page Aug 28, 2026 · 1 revision

Registries and sampling

WorldgenLib exposes two registries for shared world data and a sampler for queries that need the canonical terrain math. Register everything during StartServerSide. The registries freeze at InitWorldGen.

LandformRegistry

LandformRegistry keeps vanilla and consumer landforms in one canonical index space. It imports the vanilla variants, appends custom variants, initializes their indexes for the active world height, and rebinds the list if vanilla landforms are reloaded before the freeze.

Member Use
Register(code, variant) Add a custom LandformVariant and receive its canonical index
GetByCode(code) Find a variant by code, or null
GetIndex(code) Find a canonical index, or -1
GetThresholds(index) Read a cloned interpolated Y-threshold array
SetThresholds(index, thresholds) Replace thresholds before the freeze
GetTerrainOctaves(index) Read a cloned octave-amplitude array
GetTerrainOctaveThresholds(index) Read a cloned octave-threshold array
All Read all entries in canonical index order
Landforms Access the active vanilla property after initialization
Reload() Reload vanilla landforms and re-append registered custom variants

Example lookup:

int riverIndex = LandformRegistry.GetIndex("game:riverlandform");
if (riverIndex < 0)
{
    api.Logger.Warning("The configured river landform is not available.");
    return;
}

float[] thresholds = LandformRegistry.GetThresholds(riverIndex);
for (int y = 0; y < thresholds.Length; y++)
    thresholds[y] += GetRiverAdjustment(y);

LandformRegistry.SetThresholds(riverIndex, thresholds);

To register a custom variant, build a fully populated LandformVariant using the Vintage Story landform fields, then register it with a namespaced code:

LandformVariant variant = BuildMyLandformVariant();
int index = LandformRegistry.Register("my-mod:river-valley", variant);

The variant must provide nonempty TerrainOctaves, TerrainOctaveThresholds, TerrainYKeyPositions, and TerrainYKeyThresholds. The two Y-key arrays must have the same length. The registry assigns the canonical Code and initializes the variant with the active world manager.

GetThresholds, GetTerrainOctaves, and GetTerrainOctaveThresholds return clones. Changing a returned array does not change the active variant until SetThresholds is called. SetThresholds requires finite values and, once the world manager is known, exactly one value for every Y level in the world.

Call Reload() only before InitWorldGen freezes the registry. It is useful when another system reloads the vanilla landform property during startup. A reload after the freeze throws and requires a new worldgen session.

RegionMapRegistry

RegionMapRegistry allocates named IntDataMap2D slots attached to an IMapRegion. Each slot declares its dimensions and format version up front. The aggregate declaration budget is 4 MiB per region.

private static readonly RegionMapSlot StreamMap =
    RegionMapRegistry.Register(
        "my-mod",
        "my-mod:stream-strength",
        innerSize: 32,
        padding: 2,
        formatVersion: 1);

private static void OnMapsFinalized(RegionContext context)
{
    IntDataMap2D map = context.GetMap(StreamMap);
    map.Data[map.TopLeftPadding] = ComputeStreamValue(context);
}

TotalSize is InnerSize + 2 * Padding. The real region API is the persistent path:

IntDataMap2D map = StreamMap.GetMap(
    context.MapRegion, context.RegionX, context.RegionZ);

int[] replacement = CreateReplacement(map.Data.Length);
StreamMap.SetMap(
    context.MapRegion, context.RegionX, context.RegionZ, replacement);

GetMap(IMapRegion, ...) loads the namespaced moddata when the region is first accessed and retains the map for that region. SetMap(IMapRegion, ...) clones the supplied array, updates ModMaps, and marks the region dirty. The host flushes loaded slots after the map region pass. Data is encoded with a magic value, format version, total size, and element count. A mismatched or unknown payload is rejected and a new map is created.

The coordinate-only overloads, GetMap(int regionX, int regionZ) and SetMap(int regionX, int regionZ, ...), are compatibility helpers for in-memory callers and tests. They do not persist data to a real IMapRegion. Use the overloads that take IMapRegion for saved world data.

Use a unique, stable map code. The persistence key is namespaced as worldgenlib:region-map:<map-code>, so changing a code creates a new map. A changed layout requires a deliberate format-version migration. Do not use a region map for one record per block.

TerrainSampler

TerrainSampler evaluates the canonical terrain pipeline at arbitrary world coordinates without placing blocks. It uses request-local scratch state and is safe to call from concurrent consumers after worldgen initialization.

int baseHeight = TerrainSampler.SampleBaseTerrainHeight(worldX, worldZ);
int height = TerrainSampler.SampleHeight(worldX, worldZ);
double threshold = TerrainSampler.SampleThreshold(worldX, worldZ, posY);

var points = TerrainSampler.SampleTerrainHeightsBatch(
    new[] { (worldX, worldZ), (worldX + 1, worldZ) });

Available methods:

Method Meaning
SampleHeight Canonical terrain height at one position
SampleTerrainHeight Compatibility name for SampleHeight
SampleTerrainHeightsBatch Batch terrain heights keyed by input coordinate
SampleBaseTerrainHeight Explicit unmodified canonical sample
SampleBaseTerrainHeightsBatch Batch form of the base sample
SampleThreshold Threshold value at a world X, Z, and Y
InvalidateRegion Invalidate cached landform interpolation for a region

SamplingModifiers can apply consumer-owned effects to a sample:

var modifiers = new SamplingModifiers
{
    DistYDelta = erosionLift,
    LandformWeightTransform = weights => ApplyRiverWeights(weights),
    ThresholdTransform = (y, threshold) =>
        y < seaLevel ? threshold - channelDepth : threshold
};

int estimatedHeight = TerrainSampler.SampleHeight(worldX, worldZ, modifiers);

DistYDelta shifts vertical distortion. LandformWeightTransform receives a sample-local array. ThresholdTransform receives the Y coordinate and the current threshold. Keep transforms deterministic for a given seed and coordinate.

The ignoreRivers parameter on SampleTerrainHeightsBatch remains for compatibility with existing sampler call sites. WorldgenLib currently owns no built-in river field, so the canonical base result is already river-neutral and the flag has no separate effect.

The sampler throws InvalidOperationException before the WorldgenLib worldgen host is initialized. It does not create blocks, update heightmaps, or persist the result. Consumers that mutate maps must call InvalidateRegion before sampling affected regions.

Clone this wiki locally