-
-
Notifications
You must be signed in to change notification settings - Fork 0
Terrain Hooks
GenTerraHost exposes the parts of a terrain column that consumer mods need to change. The host keeps the vanilla stage order and gives each hook the state produced by the earlier stage and earlier registrations.
| Registration | Timing | Delegate | Main mutable state |
|---|---|---|---|
RegisterStep0 |
Once per chunk, before the column loop | BorderTaperHook(ChunkContext) |
ChunkContext.TaperMap |
RegisterStep2 |
Once per column inside the parallel loop | BuildOctavesHook(ChunkContext, ref ColumnContext) |
Landform weights and octave spans |
RegisterStep4 |
Once per column inside the parallel loop | VerticalDistortionHook(ChunkContext, ref ColumnContext) |
ColumnContext.DistY |
RegisterStep5 |
Once per column inside the parallel loop | WaterSelectHook(ChunkContext, ref ColumnContext) |
ColumnContext.WaterBlockId |
RegisterStep7 |
Once per column and Y level | ThresholdHook(ChunkContext, ref ColumnContext, int, double) |
Returned threshold |
RegisterStep10 |
Once per column after block placement | PostPlacementHook(ChunkContext, ref ColumnCarvingContext) |
Blocks, fluids, heightmaps, moddata |
RegisterTerrainFinalize |
Once after every column in a request finishes | TerrainFinalizeHook(ChunkContext) |
Request-wide persistence and summaries |
All registrations use the same (modId, order, hook) shape. Register them during StartServerSide, before InitWorldGen freezes the lists.
Step 0 prepares border smoothing from neighboring heightmaps. It runs once per generated chunk column before the main terrain loop. A hook can inspect the chunk context and edit TaperMap when it needs to change the border transition.
Use this step for border policy. Do not use it for per-column noise or block edits.
Step 2 runs for each X/Z column. ColumnContext exposes:
-
LandformWeightsas a mutableSpan<float> -
OctaveAmplitudesas a mutableSpan<double> -
OctaveThresholdsas a mutableSpan<double> -
WorldX,WorldZ,LocalX, andLocalZ
A landform blending hook normally edits LandformWeights and then calls column.RecalculateOctaves(). That method rebuilds the octave spans from the active landform definitions. If the hook edits the octave spans directly, it owns the consistency of that transformation.
private static void BlendRiverLandform(ChunkContext chunk, ref ColumnContext column)
{
int riverIndex = LandformRegistry.GetIndex("my-mod:riverlandform");
if (riverIndex < 0)
return;
column.LandformWeights[riverIndex] += 0.25f;
column.RecalculateOctaves();
}The span is valid only during the callback. Do not store it or pass it to another thread.
Step 4 runs once per column after the host has calculated the column's upheaval and oceanicity. The hook can add or subtract from column.DistY.
UpheavalStrength and Oceanicity are available as read-only derived values. A consumer should add its own effect to the existing DistY rather than replacing an unrelated consumer's value.
private static void AddTectonicLift(ChunkContext chunk, ref ColumnContext column)
{
column.DistY += column.UpheavalStrength * 0.1f;
}Step 5 runs once per column before the noise loop. column.WaterBlockId starts with the host's resolved water block and can be changed to another valid block ID. chunk.FreshWaterBlockId, SaltWaterBlockId, and LakeIceBlockId come from the active world configuration.
This is the appropriate step for a river or watershed adapter that changes salt water to fresh water. It is not a block-carving step.
Step 7 runs inside the Y loop. The hook receives the current threshold and must return the value to use for that voxel:
private static double LowerThreshold(
ChunkContext chunk,
ref ColumnContext column,
int posY,
double threshold)
{
if (column.WorldX < -3 || column.WorldX > 3)
return threshold;
return posY < chunk.SeaLevel ? threshold - 0.2 : threshold;
}This is the hottest extension point. At a 32 by 32 chunk and a 256 block world height it can run roughly 262,144 times for one chunk request. Avoid allocations, I/O, locks, and expensive noise construction here. A threshold modification is subject to the host's normal early-exit behavior. Use Step 10 when the effect must explicitly replace blocks after the terrain pass.
Step 10 runs once per column after the host has resolved solidity and placed blocks. ColumnCarvingContext provides:
- global and local coordinates
- all vertical
IServerChunkinstances -
BlockDatafor the bottom vertical chunk -
GetBlockDataAtY(int y)for a global Y coordinate ChunkIndex3d(int x, int localY, int z)SetFluid(int localX, int globalY, int localZ, int blockId)- mutable terrain and rain heightmaps
- chunk and map-chunk moddata methods
Use local X/Z when calling SetFluid. The helper chooses the correct vertical chunk and handles negative world coordinates through the context's floor-mod calculation.
RegisterTerrainFinalize is the request-wide persistence boundary. It runs after all columns and Step 10 hooks have completed. Use it for arrays such as flow vectors or distances that must be written once after parallel generation has finished.
Do not use a Step 10 hook to assume that another column has already completed. Column execution is parallel.
WorldgenLibAPI.RegisterFullTerrainGeneration(
"my-mod",
OrderBands.FinalOverrideMin,
request =>
{
GenerateCompleteColumn(request);
return true;
});Return true only after the consumer owns the complete request. Return false when the next terminal adapter or the normal WorldgenLib pass should continue. A full adapter is a migration bridge, not a substitute for decomposable hooks.