Skip to content

Ordering and Composition

Zaldaryon edited this page Aug 28, 2026 · 1 revision

Ordering and composition

WorldgenLib gives each seam its own ordered delegate list. An order value is compared only with other registrations on the same seam. The host sorts by ascending order and uses registration order as the stable tie breaker.

Register during StartServerSide. At InitWorldGen, each list freezes into an immutable snapshot for the generation hot path. A late registration throws InvalidOperationException.

Order bands

Use the named constants instead of magic numbers:

Constant Value Purpose
BeforeVanillaMin -100 Lower bound for input rewrites
BeforeVanillaMax -1 Last position before the canonical pass
Vanilla 0 Reserved for the WorldgenLib vanilla-equivalent chain
AfterVanillaMin 1 First normal post-vanilla effect
AfterVanillaMax 100 Last normal post-vanilla effect
FinalOverrideMin 1000 Terminal owner for a complete replacement

The constants are ranges, not locks. WorldgenLib accepts any finite order, so consumers should document their chosen offset and keep it stable between releases.

The audited starting offsets are:

Consumer or effect Recommended offset
Rivers-Mod terrain effects AfterVanillaMin + 50
VSRiverGen terrain effects AfterVanillaMin + 50
Watersheds erosion and stream effects AfterVanillaMin + 60
Terra Prety normal terrain effects AfterVanillaMin + 50
Terra Prety post-placement carving AfterVanillaMin + 70
Terra Prety BlockLayers filter AfterVanillaMin + 60

These values are coordination defaults. They do not make two algorithms compatible when both claim the same semantic ownership.

Choosing a seam

Prefer the smallest hook that expresses the effect:

Need Preferred seam
Rewrite border taper for one chunk Step 0
Change landform weights or octave inputs Step 2
Add vertical displacement Step 4
Select fresh, salt, or ice water Step 5
Modify solid or air threshold Step 7
Carve or fill after placement Step 10
Persist data once all columns finish TerrainFinalize
Change one map layer's output The matching map step
Wrap a vanilla map generator RegisterMapGenerator
Change map padding RegisterMapPadding
Replace one complete region pass Full map-region adapter
Modify BlockLayers raise Raise modifier
Suppress sea-level rise for a column Sea-level filter
Replace an entire BlockLayers pass Full BlockLayers adapter

An atomic hook receives the current result and should make a bounded change. For example, a threshold hook returns its input unchanged outside the river channel. A terminal adapter has ownership of the full request and must return true only after it has handled that request. Terminal adapters need explicit coordination because one complete-pass owner can prevent the canonical pass from running.

Composition examples

Rivers-Mod and VSRiverGen implement similar river effects. They are alternative providers by default. Running both can create duplicate landforms and carving, even if their orders are deterministic. Select one provider or define a consumer-level policy for their shared state.

Watersheds and Terra Prety can share the pipeline when each effect is split into its map, terrain, and BlockLayers hooks. The order must reflect the intended meaning. A map smoothing wrapper that must receive the vanilla generator goes before the canonical map chain. An erosion transform that should see the finished vanilla height inputs goes after the corresponding terrain step.

Full adapters are migration bridges for algorithms that cannot yet be split. Do not register a full adapter and an equivalent set of atomic hooks for the same effect. That runs the effect twice or makes the result depend on which terminal callback claims the request first.

Failure isolation

Registration validates the owner ID, finite order, and non-null handler. During generation, a thrown exception disables all entries owned by that mod in the affected hook list and the host logs a warning. A non-finite numeric result is handled the same way for numeric hooks. Other mods continue to run.

This boundary does not make shared data safe. If a consumer stores mutable data in ChunkContext.CustomData, it must provide its own synchronization or partitioning. Per-column callbacks run in parallel and their arrival order is not a composition contract.

Determinism

Use the world seed, coordinates, and stable inputs. Avoid process-global random state, wall-clock values, unordered shared mutation, and dependence on callback arrival order. If two hooks need to exchange data, use namespaced request data, registered region maps, or a finalization hook with a clearly defined owner.

Clone this wiki locally