Port authored Age features and Star Fissures to 26.3 beta

Advance to 1.0.1.54-beta.1 after player validation. Port material, vegetation, lake, terrain-volume, crystal, Dense Ores and fissure features with saved plans. Update beta documentation and release notes.

Refs #98, #104
This commit is contained in:
dnorberg committed 2026-11-02 01:51:20 -05:00
1 parent d75154fa9b
commit d2ac48f416
37 files changed
+2559 -193

No files matched your search

+10 -1
View File
@@ -7,9 +7,18 @@ of [Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to
Minecraft 1.21.1 on NeoForge. Mystcraft veterans will recognize the writing
flow, D'ni-inspired glyphs, libraries, crystals, and instability. Rivencraft
also lets you author modern terrain, biomes, structures, seasons, and celestial
bodies. It is an active pre-public-release project, not a byte-for-byte copy of
bodies. It is an independently developed project, not a byte-for-byte copy of
the 1.12.2 mod.
The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
supports authored terrain/biomes and a growing set of generation features,
but does not yet provide full 1.21.1 gameplay or optional-integration parity.
See the [26.3 beta guide](ports/mc263/README.md) and
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.54-beta.1.md).
Keep the builds and their saves separate, and back up worlds before updating.
## Getting started
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
+162
View File
@@ -522,6 +522,165 @@ terrain, biome, and structure generation. Cave and Void are intentionally
excluded until their feature placement rules are ported. This is not yet the
full 1.21.1 feature grammar or instability system.
The local `1.0.1.42-beta.1` follow-up resolves unmodified Rocks to a
seed-shuffled, full-block material and accepts up to 32 ordered Rocks pages.
Each page adds an independent, deterministic surface placement attempt.
Explicit Block modifiers still bind only to the immediately following Rocks
page. The material sequence persists in the Age index; the codec also reads
the single-material format from the previous beta. The remaining feature
families and instability scoring are not yet ported.
The local `1.0.1.43-beta.1` slice uses 26.3's `minecraft:forest_rock`
placed-feature rules and `BlockBlobFeature` for authored Rocks, supplying the
Age's selected material and removing only the vanilla biome filter. This
replaces the port's temporary 3×3 pile algorithm and follows the 1.21.1
generator's authored-placement approach. It affects new chunks only.
The local `1.0.1.44-beta.1` slice extends this Age-scoped feature wrapper to
Flowers, Trees, and Huge Trees. Ordered authored pages retain independent
attempts and use vanilla 26.3 placed-feature rules after biome decoration.
The vegetation plan is persisted alongside terrain, biome, and Rocks plans;
the dedicated-server smoke compares generated flowers and logs against a
same-seed Flat control and checks persistence after restart.
The local `1.0.1.45-beta.1` slice adds authored Dungeons separately from
surface vegetation. Its 26.3 placed-feature JSON mirrors the 1.21.1 eight
monster-room attempts over the world height; ordered duplicate pages add
attempts. The count persists in the Age index and generator codec. The server
smoke creates and samples a dungeon Age, counts monster spawners across a
4×4-chunk area, and verifies the count after restart. Room appearance and
normal book authoring remain in-game checks.
The local `1.0.1.46-beta.1` slice ports Surface Lakes, Deep Lakes, and Small
Lakes, including ordered Fluid modifiers. The 26.3 LakeFeature retains its
vanilla shape and replacement predicates while using the Age's frozen fluid
provider. Surface placement keeps Rivencraft 1.21.1's one-in-twelve rarity;
underground lakes use vanilla placement. Repeated unmodified pages resolve
their fluids independently from a seeded, shuffled source-fluid pool. Lake
plans participate in restore, unload/reload, reseed, and retirement rollback.
The server smoke compares water generation against a same-seed control and
checks explicit/random plans and fluid blocks across restart.
The local `1.0.1.47-beta.1` slice adapts the 1.21.1 FloatingIslandFeature
algorithm to 26.3 chunk access and the biomes already assigned to each chunk.
It retains source range 3, one-in-128 candidate rarity, Y=168..215 center
selection, radii 24..41, coordinate-noise contours, air-only filling, and the
biome-dependent soil cap. It supports Flat/Void in addition to noise terrains;
Block modifiers and independent seeded random materials are frozen in an
ordered terrain-feature plan. Rocks and islands share material resolution.
26.3's final NoiseBasedChunkGenerator merges noise, surface, and carving and
does not expose a pre-surface hook. This port appends islands to the completed
terrain future before decoration; existing terrain is preserved, but base
carvers do not cut the added islands. The smoke verifies actual high-altitude
material placement on noise, Flat, and Void terrain, distinct random material
choices, authored biome preservation, and block fingerprints across restart.
The local `1.0.1.48-beta.1` slice ports TendrilFeature's Legacy-derived
branching solid volumes using the 26.3 chunk mutation and height APIs. Source
range 8, rate 15, size 18, nested node/height selection, branch splitting, and
bedrock preservation match the 1.21.1 implementation. Chunk-local writes and
neighbor-source seeds preserve continuous paths across chunk borders.
The existing terrain-feature plan now supports Tendrils on all terrain types,
including Cave and Void. Compilation retains mixed feature order and applies
the 32-occurrence limit per feature. Block modifiers and seeded random choices
use the shared solid-material resolver; existing save/lifecycle handling carries
both feature types. World validation is manual at the player's request; no
development server is started for this slice.
The local `1.0.1.49-beta.1` slice ports the Legacy-derived Skylands filter
from Rivencraft 1.21.1 with 26.3 chunk height/mutation APIs. The cutoff uses
the same salt, seven octaves, initial cell width 128, amplitude 12, and base
Y=76. Solid/liquid removal and world-height coverage match the reference.
It runs once after base terrain construction and before authored solid
volumes, retaining the feature ordering used by the 1.21.1 implementation.
Skylands is a material-free feature, accepted alongside every terrain type.
Repeated pages retain the same idempotent effect. The existing terrain-feature
plan stores an optional Skylands flag, defaulting to false for earlier beta
Ages; no separate backend/lifecycle path is introduced. A Skylands-only plan
can contain no material choices. Build and static checks are automated; world
generation and restart validation are left to the player.
The local `1.0.1.50-beta.1` slice adapts the 1.21.1 SpheresFeature and
SpikesFeature to 26.3. Spheres retain their source range 8, five-percent
source chance, nested Y selection starting at 32, node geometry, and bedrock
protection. They run after the base terrain/Skylands filter and work on every
terrain type. Spikes retain their one-in-18 chunk chance, 6..37 height roll,
width 1..4, solid-footprint checks, and chunk-local writes during decoration.
They remain unavailable on Cave and Void terrain, matching 1.21.1.
Both page types use ordered saved solid-feature choices, preceding Block
modifiers, and independent seeded materials/occurrences. No backend lifecycle
branch or generated artwork is added. Compilation and static review are the
automated checks; the player handles generation and restart validation.
The local `1.0.1.51-beta.1` slice ports ObelisksFeature using the 26.3 chunk
coordinates and world-height APIs. The site salt, seed mixing, one-in-128
chance, chunk-local site selection, 4×4 plinth, and 2×12×2 shaft match 1.21.1.
Every footing column must be sturdy, dry, and free of block entities; the
base surface can vary by at most two blocks and the full shaft must be open.
No partial structure is written when a placement check fails.
The page joins the existing material-feature plan and shared material resolver.
It runs during surface decoration, with its own occurrence counter, on all
surface terrain types including Flat; Cave and Void remain unsupported.
Spikes and Obelisks use shared stage/restriction classification. Build/static
checks are automated; generation and restart checks remain with the player.
The local `1.0.1.52-beta.1` slice ports CrystalFormationsFeature directly
from 1.21.1 with only registry, ChunkPos, and height API substitutions. It
retains the site salt, one-in-fifteen rarity, 3×3 ground/clearance checks,
three-block center height, randomized outer heights, budding hearts, and
initial small buds. Existing blue crystal growth, sprites, drops, and sounds
are reused without adding assets.
Crystal Formations is an ordered surface feature with fixed crystal material,
not a consumer of Block/Fluid modifiers. It shares the existing saved feature
plan and surface-terrain restrictions (including Flat, excluding Cave/Void),
with independent occurrence seeds and the current 32-page cap. No backend
lifecycle changes are needed. The Obelisks slice passed the player's in-game
check; Crystal Formations build/static checks pass, with generation, growth,
and restart validation handed to the player. No test server is started.
The local `1.0.1.53-beta.1` slice adapts 1.21.1 AgeDenseOres to modern
`AbstractOreFeature` targets and `FeaturePlacer.placeWithBiomeCheck`. It keeps
the common ore tag/name fallback, explicit `rivencraft:dense_ores` placed-
feature opt-in tag, nearby biome discovery, stable registry-ID ordering,
and independent per-occurrence seeds. Registered modded ore features remain
eligible without depending on an ore-providing mod. Biomes and ore placements
are collected once per chunk rather than once per repeated page.
The optional Dense Ores count is saved in the existing terrain-feature plan;
older plans default to zero. Grammar accepts 1–32 occurrences on noise terrain
(including Cave), rejects Flat/Void and material modifiers, and checks explicit
biome pools for at least one eligible ore feature. No extra lifecycle path or
asset is introduced. Random feature selection and instability remain later
port work. Crystal Formations passed the player's in-game check; this slice
passes build/static checks, with ore generation and restart validation manual.
The local `1.0.1.54-beta.1` slice ports Star Fissures. Its footprint matches
1.21.1 exactly (including Legacy attribution); carving uses the dimension's
minimum Y, clears fluids, and protects Cave terrain's roof. The authored flag
is optional in the existing feature-plan codec. Duplicate pages retain the
1.21.1 single-fissure result; all terrains are accepted, with no material
modifier. Random feature selection is not introduced in this slice.
First arrival generates the fissure and stores its spawn/completion in per-Age
SavedData. Re-entry/restart does not repeat carving; an obstructed saved arrival
is rechecked for safe footing. The return block ports modern entity-inside and
TeleportTransition APIs, has a registered Rivencraft block-entity type, and
uses Minecraft's starfield renderer with 1.21.1 face offsets. Shared Overworld
spawn safety search and fallback match 1.21.1, rather than respawning at a bed.
The existing fissure OGG/model are reused. Client renderer registration stays
client-only; the block has no item/drop and is inert in the Overworld.
Dense Ores passed the player's in-game check. Build, footprint comparison,
resource packaging, and static checks pass for this slice. Generation, visual
rendering, actual return travel, and restart checks are left to the player;
no development server is started.
## Verification of this first slice
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
@@ -530,3 +689,6 @@ extraction. The GameTest server ran 93 tests; 92 passed, while
desks instead of one. That desk failure is outside the dimension-backend edit
and must be resolved or rechecked before a release. The 1.21.1 `build` task
passed again after the 26.3 registry slice.
On 2026-11-01, the default 1.21.1 GameTest server was rerun and all 93
required tests passed, including the desk test. Issue #97 was then closed;
remaining 26.3 parity work stays under #98–#104.
+140
View File
@@ -211,6 +211,146 @@ first surface-feature path; unsupported combinations stay in the Binder.
Random feature selection, unpaired materials, duplicate feature pages, and
instability scoring remain future port steps.
The local `1.0.1.42-beta.1` follow-up lets a Rocks page choose a deterministic
full-block material when no Block page precedes it. Repeated Rocks pages now
add independent placement attempts (up to 32) instead of being rejected. The
ordered material choices are frozen in the Age index and restored after a
restart; old single-material beta Ages still load. An explicit Block page
must still directly precede the Rocks page it modifies. Most other feature
pages and material-feature pairings remain unported.
The local `1.0.1.43-beta.1` slice replaces the temporary 3×3 Rocks piles
with Minecraft 26.3's forest-rock block-blob feature and its placement rules,
matching the 1.21.1 Age feature path more closely. The selected material and
repeat count still come from the frozen Age plan. Only newly generated chunks
use the new rock shape; previously generated chunks remain unchanged.
The local `1.0.1.44-beta.1` slice ports ordered Flowers, Trees, and Huge
Trees pages to surface Ages. Their 26.3 vanilla placed features run after
biome decoration, with the Age's own seed; repeated pages make independent
placement attempts. Cave/Void terrain and cave/Nether/End biome pools are
not treated as surface vegetation. The feature plan survives server restart;
already generated chunks do not gain new decoration.
The local `1.0.1.45-beta.1` slice ports the Dungeons page for Standard,
Amplified, and Island noise terrain. It uses the 1.21.1 placement definition
(eight monster-room attempts per page per chunk across the build height),
stores repeat count with the Age, and rejects Dungeons pages on Flat, Void,
or Cave terrain. New chunks only; existing chunks are not regenerated.
The local `1.0.1.46-beta.1` slice adds Surface Lakes, Deep Lakes, and Small
Lakes on surface noise terrain. A Fluid page immediately before a lake page
selects its fluid. Unmodified pages choose a seeded fluid from the same source-
fluid pool exposed in Creative, with repeated choices and their order saved
with the Age. Surface/Small Lakes retain the 1.21.1 one-in-twelve attempt rule;
Deep Lakes retain vanilla underground placement. These pages currently require
Standard, Amplified, or Island terrain. Existing chunks do not redecorate.
The local `1.0.1.47-beta.1` slice adds authored Floating Islands alongside
Standard, Amplified, Island, Flat, and Void terrain. A preceding Block page
sets the island core material; otherwise each occurrence resolves a seeded
material from the full-block pool. The 1.21.1 shape, high altitude, rarity,
and biome-dependent grass/dirt cap are retained. Repeated pages use independent
island seeds. Cave terrain remains unsupported for this feature.
26.3 combines base noise, surface, and cave carving in one terrain operation.
Floating Islands run after that operation and before decoration, so the added
islands are not cut by the base terrain's carvers. The saved feature plan is
used again after restart and for new chunks; existing chunks keep their blocks.
The local `1.0.1.48-beta.1` slice adds authored Tendrils on every terrain
type, including Cave and Void. A preceding Block page selects the material;
unmodified occurrences resolve seeded materials from the full-block pool.
Repeated pages use independent paths, with up to 32 occurrences per feature.
Mixed Floating Islands/Tendrils retain their authored order in the saved plan.
Tendrils retain the 1.21.1 branching path algorithm, source rates, and vertical
distribution concentrated below Y=128. They can replace existing terrain while
preserving bedrock. As with Floating Islands, they run after 26.3's combined
terrain operation and before decoration. Build checks are automated; in-game
generation, appearance, and restart validation are handed to the player.
The local `1.0.1.49-beta.1` slice adds the Skylands feature page. Its seeded
seven-octave filter removes base terrain at/below a varying cutoff near Y=76
and removes liquid above it, preserving the upper solid terrain. It accepts
all terrain types and no Block modifier. On low Flat terrain this removes
the base layers; on Void it leaves the empty base unchanged.
The filter runs once before authored Floating Islands and Tendrils, matching
1.21.1; duplicate Skylands pages do not compound the filter. Its flag is saved
with the terrain-feature plan. Existing chunks are unchanged. Try Amplified
terrain for a clear visual check; generation and restart validation are manual.
The local `1.0.1.50-beta.1` slice ports Spheres and Spikes using the 1.21.1
generation algorithms. Spheres add solid nodes on all terrain types, including
Cave and Void, while preserving bedrock. Spikes require solid surface terrain:
Standard, Amplified, Island, or Flat. Their circular footprint stays in one
chunk and must have solid ground beneath every column.
Each page accepts a preceding Block material or resolves a seeded random
material when unmodified. Repeated pages have independent placement seeds,
up to 32 per feature. Spheres run during terrain construction, after Skylands;
Spikes run after biome decoration. Both use the existing saved solid-feature
plan. New chunks only; in-game appearance and restart checks remain manual.
The local `1.0.1.51-beta.1` slice ports Obelisks. It preserves the 1.21.1
one-in-128 candidate chance per chunk/occurrence, 4×4 plinth, and 2×2 shaft
extending 12 blocks above it. Placement requires dry, sturdy ground across
the base, at most two blocks of surface variation, and clear/replaceable
space for the complete structure. The footprint stays within its chunk.
Obelisks support Standard, Amplified, Island, and Flat terrain. A preceding
Block page sets their material; otherwise a seeded full-block material is
selected. Repeated pages make independent attempts, up to 32 occurrences.
Spikes and Obelisks share surface-stage classification and preserve their
authored order within that stage. World and restart validation are manual.
The local `1.0.1.52-beta.1` slice ports Crystal Formations using the 1.21.1
surface-mound algorithm and the existing blue crystal blocks. Each occurrence
has a one-in-fifteen candidate chance per chunk; suitable dry, nearly level
ground produces a 3×3 mound with budding crystal and occasional small buds.
The existing budding blocks continue growing crystals through random ticks.
Crystal Formations accepts Standard, Amplified, Island, and Flat terrain,
but not Cave/Void terrain or Block/Fluid modifiers. Up to 32 repeated pages
make independent attempts. The ordered feature plan survives restart, and
only newly generated chunks receive formations. For a quick manual test,
bind a Flat/Plains Age with several Crystal Formations pages, inspect the
blue mounds and bud growth, then revisit after restarting the world.
The local `1.0.1.53-beta.1` slice ports Dense Ores. Each page replays eligible
ore placements from the Age's actual biomes, preserving their height ranges,
replacement rules, and biome filters. Vanilla and modded ore features are
recognized by their ore targets; custom placements can opt in through the
`rivencraft:dense_ores` placed-feature tag, just as in 1.21.1.
Up to 32 pages add independent passes on Standard, Amplified, Island, or Cave
terrain. Flat/Void and Block/Fluid modifiers are unsupported; an explicitly
authored biome pool must contain at least one ore-producing biome. The count
survives restart. Only new chunks gain extra ore, and this slice does not yet
port the later instability system or random-Age feature selection.
For manual validation, create a Standard/Plains Age with several Dense Ores
pages and inspect underground ore abundance, then restart and explore more
chunks. A Cave Age with a Nether biome can check Nether ore placements too.
The local `1.0.1.54-beta.1` slice ports the Star Fissures page. On first book/
operator arrival, it carves the 1.21.1 Legacy-shaped rift near the arrival chunk
and places a starfield portal at the dimension's minimum Y. Cave terrain keeps
its bedrock roof. All terrain types are supported; repeated pages still create
one fissure, and Block/Fluid modifiers are not accepted.
The first arrival and completed generation are saved per Age, so revisits and
restarts do not recarve the rift. Touching its bottom returns entities to a safe
location near the shared Overworld spawn, not a bed, with the original fissure
sound. The portal is unobtainable and inert in the Overworld. Random-Age feature
selection remains later work; author a Star Fissures page to test this slice.
For manual validation, create a Flat/Plains Age with Star Fissures. Look near
the arrival point (generally east/south), descend through the rift, and touch
the starfield to return. Re-enter after restarting; check that the same fissure
and its portal still work. A Cave Age checks that carving leaves the roof intact.
From the repository root:
```sh
@@ -0,0 +1,51 @@
# Rivencraft 26.3 beta 1.0.1.54-beta.1
This beta collects the player-validated Age feature port slices since
1.0.1.41-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
## Age generation
- Rocks now use the established 1.21.1 forest-rock placement. Unmodified
material-bearing features choose seeded materials independently; a preceding
compatible Block or Fluid page selects the material explicitly.
- Added Flowers, Trees, Huge Trees, Dungeons, Surface Lakes, Deep Lakes, and
Small Lakes using the 1.21.1 placement rules adapted to 26.3.
- Added Floating Islands, Tendrils, Skylands, Spheres, Spikes, and Obelisks
using the existing Rivencraft generation algorithms.
- Added blue Crystal Formations with budding crystal and initially seeded buds.
- Added Dense Ores: repeated pages replay eligible biome ore placements,
including compatible modded ores and explicitly tagged custom placements.
- Added Star Fissures: one Legacy-shaped rift near the first arrival point,
a starfield at the dimension's minimum Y, and return travel to the shared
Overworld spawn with the original fissure sound. Cave roofs are preserved.
- Feature plans, materials, repeated occurrences, and fissure completion
persist across restart. Existing generated chunks are not redecorated.
## Current limits
- Authored features retain terrain restrictions. Most repeated features allow
up to 32 occurrences; exceeding the current authoring cap blocks assembly.
Skylands and Star Fissures retain their single-effect behavior when repeated.
- Put a material page immediately before a compatible feature. Dangling
material assignment, full Legacy grammar recovery, instability, and random
feature selection are not yet ported. Pageless books randomize terrain and
biome distribution, not the newly authored feature list.
- 26.3 combines base terrain, surface, and carving. Added solid volumes run
after that combined operation and before decoration, so base carvers do not
cut those added volumes.
- The full 1.21.1 writing/discovery loop, structures, environmental systems,
crystal portals, and optional integration parity remain in progress. The
presence of a Creative page is not a promise that its effect is ported.
Build and static checks passed. The player reported in-game passes for these
slices. Earlier server smoke coverage was expanded, but the later generation
slices were manually validated; this release does not claim a new full
dedicated-server regression pass.
Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build,
and **Java 25**. Dynamic Dimensions and Bad Packets are not required by this
26.3 build. Do not use Minecraft 1.21.1 dependencies or saves with it. Back up
the entire world before updating; beta-save compatibility is not guaranteed.
Report issues and help with documentation at
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
+3
View File
@@ -124,6 +124,9 @@ sourceSets.main.resources.srcDir generatePortLanguage
tasks.named('processResources', ProcessResources).configure {
// Reuse the existing sprite and base model without copying binary assets.
from('../../src/main/resources') {
include 'assets/rivencraft/models/block/star_fissure.json'
include 'assets/rivencraft/blockstates/star_fissure.json'
include 'assets/rivencraft/sounds/linking/link-fissure.ogg'
include 'assets/rivencraft/textures/item/crystal_shard.png'
include 'assets/rivencraft/textures/item/page.png'
include 'assets/rivencraft/textures/item/page_link_panel.png'
+1 -1
View File
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
mod_id=rivencraft
mod_name=Rivencraft
mod_license=LGPL-3.0-or-later
mod_version=1.0.1.41-beta.1
mod_version=1.0.1.54-beta.1
@@ -8,6 +8,8 @@ import net.dnorberg.rivencraft.age.AgeCatalogSync;
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
import net.dnorberg.rivencraft.block.InkMixerBlock;
import net.dnorberg.rivencraft.block.StarFissureBlock;
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
@@ -26,6 +28,7 @@ import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
@@ -54,6 +57,11 @@ public final class Rivencraft {
public static final Logger LOGGER = LogUtils.getLogger();
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID);
private static final DeferredRegister<SoundEvent> SOUNDS =
DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID);
public static final Supplier<SoundEvent> LINK_FISSURE = SOUNDS.register(
"linking.link-fissure", () -> SoundEvent.createVariableRangeEvent(
Identifier.fromNamespaceAndPath(MOD_ID, "linking.link-fissure")));
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
private static final DeferredRegister<MenuType<?>> MENUS =
@@ -94,6 +102,12 @@ public final class Rivencraft {
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK));
public static final DeferredBlock<StarFissureBlock> STAR_FISSURE =
BLOCKS.registerBlock("star_fissure", StarFissureBlock::new,
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.END_PORTAL).lightLevel(state -> 6));
public static final Supplier<BlockEntityType<StarFissureBlockEntity>>
STAR_FISSURE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("star_fissure", () -> new BlockEntityType<>(
StarFissureBlockEntity::new, STAR_FISSURE.get()));
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
"book_binder", PortBookBinderBlock::new,
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
@@ -146,6 +160,7 @@ public final class Rivencraft {
public Rivencraft(IEventBus modEventBus) {
BLOCKS.register(modEventBus);
SOUNDS.register(modEventBus);
ITEMS.register(modEventBus);
BLOCK_ENTITY_TYPES.register(modEventBus);
MENUS.register(modEventBus);
@@ -26,7 +26,15 @@ final class AgeLevelIndex extends SavedData {
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
.forGetter(index -> index.rocks)
.forGetter(index -> index.rocks),
Codec.unboundedMap(Identifier.CODEC, PortVegetationPlan.CODEC)
.optionalFieldOf("vegetation", Map.of()).forGetter(index -> index.vegetation),
Codec.unboundedMap(Identifier.CODEC, Codec.intRange(1, 32))
.optionalFieldOf("dungeons", Map.of()).forGetter(index -> index.dungeons),
Codec.unboundedMap(Identifier.CODEC, PortLakePlan.CODEC)
.optionalFieldOf("lakes", Map.of()).forGetter(index -> index.lakes),
Codec.unboundedMap(Identifier.CODEC, PortTerrainFeaturePlan.CODEC)
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures)
).apply(instance, AgeLevelIndex::new));
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
@@ -41,6 +49,10 @@ final class AgeLevelIndex extends SavedData {
private final Map<Identifier, PortBiomePlan> biomePlans;
private final Map<Identifier, PortAgeTerrain> terrain;
private final Map<Identifier, PortRockPlan> rocks;
private final Map<Identifier, PortVegetationPlan> vegetation;
private final Map<Identifier, Integer> dungeons;
private final Map<Identifier, PortLakePlan> lakes;
private final Map<Identifier, PortTerrainFeaturePlan> terrainFeatures;
private AgeLevelIndex() {
this.ages = new LinkedHashMap<>();
@@ -49,19 +61,31 @@ final class AgeLevelIndex extends SavedData {
this.biomePlans = new LinkedHashMap<>();
this.terrain = new LinkedHashMap<>();
this.rocks = new LinkedHashMap<>();
this.vegetation = new LinkedHashMap<>();
this.dungeons = new LinkedHashMap<>();
this.lakes = new LinkedHashMap<>();
this.terrainFeatures = new LinkedHashMap<>();
}
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
Map<Identifier, Identifier> biomeOverrides,
Map<Identifier, PortBiomePlan> biomePlans,
Map<Identifier, PortAgeTerrain> terrain,
Map<Identifier, PortRockPlan> rocks) {
Map<Identifier, PortRockPlan> rocks,
Map<Identifier, PortVegetationPlan> vegetation,
Map<Identifier, Integer> dungeons,
Map<Identifier, PortLakePlan> lakes,
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures) {
this.ages = new LinkedHashMap<>(ages);
this.retired = new LinkedHashSet<>(retired);
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
this.biomePlans = new LinkedHashMap<>(biomePlans);
this.terrain = new LinkedHashMap<>(terrain);
this.rocks = new LinkedHashMap<>(rocks);
this.vegetation = new LinkedHashMap<>(vegetation);
this.dungeons = new LinkedHashMap<>(dungeons);
this.lakes = new LinkedHashMap<>(lakes);
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
}
Map<Identifier, Long> entries() {
@@ -88,9 +112,14 @@ final class AgeLevelIndex extends SavedData {
}
PortRockPlan rocks(Identifier id) { return rocks.get(id); }
PortVegetationPlan vegetation(Identifier id) { return vegetation.get(id); }
int dungeons(Identifier id) { return dungeons.getOrDefault(id, 0); }
PortLakePlan lakes(Identifier id) { return lakes.get(id); }
PortTerrainFeaturePlan terrainFeatures(Identifier id) { return terrainFeatures.get(id); }
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
PortRockPlan rockPlan) {
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
if (contains(id)) {
throw new IllegalArgumentException("Age already exists: " + id);
}
@@ -98,6 +127,10 @@ final class AgeLevelIndex extends SavedData {
if (biomePlan != null) biomePlans.put(id, biomePlan);
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
if (rockPlan != null) rocks.put(id, rockPlan);
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
if (lakePlan != null) lakes.put(id, lakePlan);
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
setDirty();
}
@@ -107,17 +140,27 @@ final class AgeLevelIndex extends SavedData {
biomePlans.remove(id);
terrain.remove(id);
rocks.remove(id);
vegetation.remove(id);
dungeons.remove(id);
lakes.remove(id);
terrainFeatures.remove(id);
retired.add(id);
setDirty();
}
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
PortAgeTerrain ageTerrain, PortRockPlan rockPlan) {
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
ages.put(id, seed);
if (biomePlan != null) biomePlans.put(id, biomePlan);
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
if (rockPlan != null) rocks.put(id, rockPlan);
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
if (lakePlan != null) lakes.put(id, lakePlan);
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
setDirty();
}
@@ -18,6 +18,8 @@ import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Prediction;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.block.Blocks;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import net.neoforged.fml.ModList;
import java.util.Map;
@@ -130,6 +132,51 @@ public final class PortAgeCommands {
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
null, new PortRockPlan(Identifier.withDefaultNamespace("gold_block")))))))
.then(Commands.literal("create_rocks_multi")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
fourGoldRocks(LongArgumentType.getLong(context, "seed")))))))
.then(Commands.literal("create_rocks_random")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(
java.util.List.of(Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/rocks")),
LongArgumentType.getLong(context, "seed")))))))
.then(Commands.literal("create_vegetation")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
null, testVegetation())))))
.then(Commands.literal("create_dungeons")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
null, null, null, testDungeons())))))
.then(Commands.literal("create_lakes")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
null, null, null, 0, testLakes(
LongArgumentType.getLong(context, "seed"), false))))))
.then(Commands.literal("create_lakes_random")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
null, null, null, 0, testLakes(
LongArgumentType.getLong(context, "seed"), true))))))
.then(Commands.literal("create_islands")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(context -> createIslandAge(context, false))))))
.then(Commands.literal("create_islands_random")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(context -> createIslandAge(context, true))))))
.then(Commands.literal("list")
.executes(context -> {
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
@@ -236,6 +283,18 @@ public final class PortAgeCommands {
.then(Commands.literal("rocks_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeRocks)))
.then(Commands.literal("vegetation_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeVegetation)))
.then(Commands.literal("dungeons_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeDungeons)))
.then(Commands.literal("lakes_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeLakes)))
.then(Commands.literal("islands_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeIslands)))
.then(Commands.literal("biome")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("x", IntegerArgumentType.integer())
@@ -255,30 +314,229 @@ public final class PortAgeCommands {
private static int probeRocks(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortRockChunkGenerator rocks)) {
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|| features.rocks() == null) {
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
return 0;
}
var material = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getValue(rocks.rocks().material());
var materials = features.rocks().materials().stream()
.map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
int count = 0;
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
if (level.getBlockState(new net.minecraft.core.BlockPos(x, y, z)).is(material)) count++;
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
if (materials.stream().anyMatch(state::is)) count++;
}
int placed = count;
context.getSource().sendSuccess(() -> Component.literal(
"Rocks probe " + id + " material=" + rocks.rocks().material() + " count=" + placed), false);
"Rocks probe " + id + " materials=" + features.rocks().materials() + " count=" + placed), false);
return placed;
}
private static int probeVegetation(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
int flowers = 0;
int logs = 0;
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
for (int y = 4; y < 32; y++) {
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
if (state.is(BlockTags.FLOWERS)) flowers++;
if (state.is(BlockTags.LOGS)) logs++;
}
}
int flowerCount = flowers;
int logCount = logs;
context.getSource().sendSuccess(() -> Component.literal(
"Vegetation probe " + id + " flowers=" + flowerCount + " logs=" + logCount), false);
return flowerCount + logCount;
}
private static PortVegetationPlan testVegetation() {
java.util.List<Identifier> pages = new java.util.ArrayList<>();
for (int i = 0; i < 10; i++) {
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/flowers"));
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/trees"));
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/huge_trees"));
}
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.vegetationPlan(pages);
}
private static int testDungeons() {
Identifier page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dungeons");
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(
java.util.Collections.nCopies(8, page));
}
private static int probeDungeons(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
context.getSource().sendFailure(Component.literal("Age has no loaded feature generator: " + id));
return 0;
}
int count = features.dungeons();
int spawners = 0;
for (int chunkX = 0; chunkX < 4; chunkX++) for (int chunkZ = 0; chunkZ < 4; chunkZ++) {
var chunk = level.getChunk(chunkX, chunkZ);
for (var pos : chunk.getBlockEntitiesPos()) {
if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++;
}
}
int placed = spawners;
context.getSource().sendSuccess(() -> Component.literal(
"Dungeons probe " + id + " pages=" + count + " spawners=" + placed), false);
return placed;
}
private static PortLakePlan testLakes(long seed, boolean randomFluids) {
java.util.List<Identifier> pages = new java.util.ArrayList<>();
for (int i = 0; i < 8; i++) {
for (String kind : java.util.List.of("surface_lakes", "deep_lakes", "lava_lakes")) {
if (!randomFluids) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
Identifier.withDefaultNamespace("water")));
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + kind));
}
}
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed);
}
private static int probeLakes(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
PortLakePlan lakes = level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
? features.lakes() : null;
// Finish neighboring features before counting, since lake bowls can
// cross a chunk boundary. Include underground lakes in the probe.
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
int[] fluids = new int[2];
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
level.getChunk(x, z).findBlocks(state -> state.is(Blocks.WATER) || state.is(Blocks.LAVA),
(pos, state) -> fluids[state.is(Blocks.WATER) ? 0 : 1]++);
}
int waterCount = fluids[0];
int lavaCount = fluids[1];
context.getSource().sendSuccess(() -> Component.literal(
"Lakes probe " + id + " pages=" + (lakes == null ? 0 : lakes.choices().size())
+ " fluids=" + (lakes == null ? java.util.List.of() : lakes.choices().stream()
.map(PortLakePlan.Choice::fluid).distinct().toList())
+ " plan=" + (lakes == null ? 0 : lakes.choices().hashCode())
+ " water=" + waterCount + " lava=" + lavaCount), false);
return waterCount + lavaCount;
}
private static int createIslandAge(CommandContext<CommandSourceStack> context, boolean randomMaterials) {
PortAgeTerrain terrain;
try {
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException exception) {
context.getSource().sendFailure(Component.literal("Unknown island test terrain"));
return 0;
}
java.util.List<Identifier> pages = new java.util.ArrayList<>();
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/forest"));
for (int i = 0; i < 4; i++) {
if (!randomMaterials) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
Identifier.withDefaultNamespace("gold_block")));
pages.add(PortTerrainFeaturePlan.FLOATING_ISLANDS);
}
try {
long seed = LongArgumentType.getLong(context, "seed");
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
pages, context.getSource().getServer().registryAccess());
var features = net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed);
return createAge(context, terrain, biomes, null, null, 0, null, features);
} catch (IllegalArgumentException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeIslands(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|| features.terrainFeatures() == null) {
context.getSource().sendFailure(Component.literal("Age has no loaded island generator: " + id));
return 0;
}
var choices = features.terrainFeatures().choices();
var materials = choices.stream().map(PortTerrainFeaturePlan.Choice::material).distinct().toList();
var blocks = materials.stream().map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
int[] count = {0};
long[] fingerprint = {0};
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
level.getChunk(x, z).findBlocks(state -> !state.isAir(), (pos, state) -> {
if (pos.getY() < 128 || pos.getY() >= 240) return;
if (blocks.stream().anyMatch(state::is)) count[0]++;
fingerprint[0] += pos.asLong() * 0x9E3779B97F4A7C15L ^ state.toString().hashCode();
});
}
context.getSource().sendSuccess(() -> Component.literal(
"Islands probe " + id + " pages=" + choices.size() + " materials=" + materials
+ " count=" + count[0] + " fingerprint=" + fingerprint[0]), false);
return count[0];
}
/** Exercise the actual ordered writing parser, not only the generator codec. */
private static PortRockPlan fourGoldRocks(long seed) {
Identifier block = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
Identifier.withDefaultNamespace("gold_block"));
Identifier feature = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/rocks");
java.util.List<Identifier> pages = new java.util.ArrayList<>();
for (int i = 0; i < 4; i++) {
pages.add(block);
pages.add(feature);
}
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan, PortRockPlan rockPlan) {
return createAge(context, terrain, biomePlan, rockPlan, null);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, 0);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, null);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, null);
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) {
String name = StringArgumentType.getString(context, "name");
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
long seed = LongArgumentType.getLong(context, "seed");
ServerLevel level;
try {
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan, terrain, rockPlan);
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
@@ -25,8 +25,13 @@ public final class PortAgeTravel {
if (!AgeCatalogSync.ready(player, id)) {
return "Age catalog not acknowledged by client; remaining in source dimension";
}
BlockPos landing = findLanding(target);
boolean fissure = target.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
&& features.terrainFeatures() != null && features.terrainFeatures().starFissures();
BlockPos landing = fissure ? PortStarFissureFeature.savedSpawn(target) : null;
if (landing != null) landing = findFootedDestination(target, landing).orElse(null);
if (landing == null) landing = findLanding(target);
if (landing == null) return "No safe test landing available; remaining in source dimension";
if (fissure) PortStarFissureFeature.generateOnce(target, landing, isCaveTerrain(target));
boolean moved = player.teleportTo(target, landing.getX() + 0.5, landing.getY(), landing.getZ() + 0.5,
Set.of(), player.getYRot(), player.getXRot(), true);
return moved ? null : "Teleportation failed; remaining in source dimension";
@@ -34,12 +39,8 @@ public final class PortAgeTravel {
private static BlockPos findLanding(ServerLevel target) {
net.minecraft.world.level.chunk.ChunkGenerator generator = target.getChunkSource().getGenerator();
if (generator instanceof PortRockChunkGenerator rocks) generator = rocks.base();
if (generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
.lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
.value().noiseSettings())) {
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
if (isCaveTerrain(target)) {
// The cave roof is not a valid arrival surface. Search down from
// Legacy's Y=64 start, then construct the normal safety platform
// at that height only when no footed opening can be found.
@@ -88,6 +89,16 @@ public final class PortAgeTravel {
return makePlatform(target, new BlockPos(0, y, 0));
}
private static boolean isCaveTerrain(ServerLevel target) {
var generator = target.getChunkSource().getGenerator();
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
return generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
.lookupOrThrow(Registries.NOISE_SETTINGS)
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
.value().noiseSettings());
}
private static BlockPos makePlatform(ServerLevel target, BlockPos foot) {
if (foot.getY() <= target.getMinY() || foot.getY() + 2 >= target.getMaxY()) return null;
// A carved pocket inside a flooded cave immediately refills. Refuse
@@ -112,7 +123,36 @@ public final class PortAgeTravel {
return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
&& target.getFluidState(foot).isEmpty()
&& target.getFluidState(foot.above()).isEmpty()
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty();
&& passable(target, foot) && passable(target, foot.above());
}
private static boolean passable(ServerLevel level, BlockPos pos) {
var state = level.getBlockState(pos);
return state.getCollisionShape(level, pos).isEmpty() || state.is(net.minecraft.tags.BlockTags.LEAVES);
}
/** Same local-then-surface safe-return search used by 1.21.1 fissures. */
public static java.util.Optional<BlockPos> findFootedDestination(ServerLevel level, BlockPos origin) {
var local = BlockPos.betweenClosedStream(origin.offset(-4, -2, -4), origin.offset(4, 2, 4))
.map(BlockPos::immutable).sorted(java.util.Comparator.comparingDouble(pos -> pos.distSqr(origin)))
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
.filter(pos -> hasFooting(level, pos)).findFirst();
if (local.isPresent()) return local;
for (int radius = 1; radius <= 64; radius++) {
final int ring = radius;
var found = BlockPos.betweenClosedStream(origin.offset(-ring, 0, -ring), origin.offset(ring, 0, ring))
.filter(pos -> Math.max(Math.abs(pos.getX() - origin.getX()), Math.abs(pos.getZ() - origin.getZ())) == ring)
.map(BlockPos::immutable)
.flatMap(pos -> {
BlockPos surface = new BlockPos(pos.getX(),
level.getHeight(Heightmap.Types.MOTION_BLOCKING, pos.getX(), pos.getZ()), pos.getZ());
return java.util.stream.Stream.of(surface, surface.above(), surface.below(),
surface.above(2), surface.below(2));
})
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
.filter(pos -> hasFooting(level, pos)).findFirst();
if (found.isPresent()) return found;
}
return java.util.Optional.empty();
}
}
@@ -0,0 +1,78 @@
package net.dnorberg.rivencraft.age;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.AmethystClusterBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
/** 26.3 port of the 1.21.1 surface crystal mound with budding hearts and seeded buds. */
final class PortCrystalFormationsFeature {
private static final long SALT = 0x4352595354414C53L;
private static final int CHANCE = 15;
private PortCrystalFormationsFeature() {}
record Site(int x, int z, long seed) {}
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
long seed = mix(ageSeed ^ SALT ^ (long) chunk.x() * 341873128712L
^ (long) chunk.z() * 132897987541L ^ (long) occurrence * 0x9E3779B97F4A7C15L);
RandomSource random = RandomSource.create(seed);
if (random.nextInt(CHANCE) != 0) return null;
return new Site(chunk.getMinBlockX() + 5 + random.nextInt(6),
chunk.getMinBlockZ() + 5 + random.nextInt(6), seed);
}
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, int occurrence) {
Site site = site(ageSeed, chunk.getPos(), occurrence);
if (site == null) return;
RandomSource random = RandomSource.create(site.seed() ^ 0x42554444494E474CL);
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, site.x(), site.z());
if (y <= level.getMinY() + 1 || y + 4 >= level.getMaxY()) return;
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
int x = site.x() + dx;
int z = site.z() + dz;
if (Math.abs(level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - y) > 1) return;
BlockState floor = level.getBlockState(pos.set(x, y - 1, z));
if (!floor.isFaceSturdy(level, pos, Direction.UP) || !floor.getFluidState().isEmpty()
|| floor.hasBlockEntity()) return;
for (int h = 0; h < 4; h++) {
BlockState existing = level.getBlockState(pos.set(x, y + h, z));
if (!existing.isAir() && !existing.canBeReplaced()) return;
}
}
}
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
boolean center = dx == 0 && dz == 0;
int height = center ? 3 : random.nextInt(3) == 0 ? 2 : 1;
for (int h = 0; h < height; h++) {
boolean budding = h == height - 1 && (center || random.nextBoolean());
level.setBlock(pos.set(site.x() + dx, y + h, site.z() + dz),
budding ? Rivencraft.BUDDING_CRYSTAL.get().defaultBlockState()
: Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 2);
}
if (level.getBlockState(pos.set(site.x() + dx, y + height - 1, site.z() + dz))
.is(Rivencraft.BUDDING_CRYSTAL) && random.nextBoolean()) {
level.setBlock(pos.set(site.x() + dx, y + height, site.z() + dz),
Rivencraft.SMALL_CRYSTAL_BUD.get().defaultBlockState()
.setValue(AmethystClusterBlock.FACING, Direction.UP), 2);
}
}
}
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,90 @@
package net.dnorberg.rivencraft.age;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.tags.TagKey;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.feature.AbstractOreFeature;
import net.minecraft.world.level.levelgen.placement.FeaturePlacer;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.neoforged.neoforge.common.Tags;
/** Replays each eligible biome ore placement once per page, matching Rivencraft 1.21.1. */
public final class PortDenseOres {
public static final TagKey<PlacedFeature> EXTRA_ORES = TagKey.create(Registries.PLACED_FEATURE,
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dense_ores"));
private PortDenseOres() {}
public static boolean hasOres(Holder<Biome> biome) {
return oreFeatures(biome).anyMatch(PortDenseOres::isOre);
}
/** Modern ore features carry their targets directly rather than an OreConfiguration. */
public static boolean isOre(Holder<PlacedFeature> holder) {
if (holder.is(EXTRA_ORES)) return true;
if (!(holder.value().feature().value() instanceof AbstractOreFeature ore)) return false;
return ore.targetStates().stream().anyMatch(target -> isOreBlock(target.state()));
}
private static boolean isOreBlock(BlockState state) {
if (state.is(Tags.BlockItems.ORES.block())) return true;
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
return path.endsWith("_ore") || path.contains("_ore_");
}
private static java.util.stream.Stream<Holder<PlacedFeature>> oreFeatures(Holder<Biome> biome) {
return biome.value().getGenerationSettings().features().stream().flatMap(HolderSet::stream);
}
public static void place(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator generator,
long ageSeed, int count) {
if (count == 0) return;
Set<Holder<Biome>> biomes = new HashSet<>();
ChunkPos.rangeClosed(chunk.getPos(), 1).forEach(pos -> {
for (var section : level.getChunk(pos.x(), pos.z()).getSections())
section.getBiomes().getAll(biomes::add);
});
biomes.retainAll(generator.getBiomeSource().possibleBiomes());
// Collect once per chunk, not once per repeated page. Stable registry-ID
// order and per-occurrence seeds retain the 1.21.1 placement sequence.
Map<Identifier, PlacedFeature> ores = new TreeMap<>();
for (Holder<Biome> biome : biomes) {
oreFeatures(biome).filter(PortDenseOres::isOre).forEach(feature ->
feature.unwrapKey().ifPresent(key -> ores.putIfAbsent(key.identifier(), feature.value())));
}
BlockPos origin = new BlockPos(chunk.getPos().getMinBlockX(), level.getMinY(),
chunk.getPos().getMinBlockZ());
FeaturePlacer placer = new FeaturePlacer(level, generator);
for (int occurrence = 0; occurrence < count; occurrence++) {
for (var entry : ores.entrySet()) {
long seed = mix(ageSeed ^ (long) origin.getX() * 341873128712L
^ (long) origin.getZ() * 132897987541L
^ (long) entry.getKey().hashCode() * 0x9E3779B97F4A7C15L
^ (long) occurrence * 0xD1B54A32D192ED03L);
placer.placeWithBiomeCheck(entry.getValue(), RandomSource.create(seed), origin);
}
}
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,269 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.writing.PortRegistryPages;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.tags.TagKey;
import net.minecraft.tags.BiomeTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate;
import net.minecraft.world.level.levelgen.feature.BlockBlobFeature;
import net.minecraft.world.level.levelgen.feature.LakeFeature;
import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider;
import net.minecraft.world.level.levelgen.placement.BiomeFilter;
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
import net.minecraft.world.level.levelgen.placement.RarityFilter;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.HolderSet;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.Block;
/** Delegates terrain generation and applies frozen Age-scoped features. */
public final class PortFeatureChunkGenerator extends ChunkGenerator {
private static final ResourceKey<PlacedFeature> FOREST_ROCK = ResourceKey.create(
Registries.PLACED_FEATURE, Identifier.withDefaultNamespace("forest_rock"));
private static final ResourceKey<PlacedFeature> DUNGEONS = ResourceKey.create(
Registries.PLACED_FEATURE, Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dungeons"));
private static final TagKey<Block> ROCK_GROUND = TagKey.create(
Registries.BLOCK, Identifier.withDefaultNamespace("forest_rock_can_place_on"));
public static final MapCodec<PortFeatureChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
PortRockPlan.CODEC.optionalFieldOf("rocks").forGetter(generator -> Optional.ofNullable(generator.rocks)),
PortVegetationPlan.CODEC.optionalFieldOf("vegetation")
.forGetter(generator -> Optional.ofNullable(generator.vegetation)),
com.mojang.serialization.Codec.intRange(0, 32).optionalFieldOf("dungeons", 0)
.forGetter(generator -> generator.dungeons),
PortLakePlan.CODEC.optionalFieldOf("lakes")
.forGetter(generator -> Optional.ofNullable(generator.lakes)),
PortTerrainFeaturePlan.CODEC.optionalFieldOf("terrain_features")
.forGetter(generator -> Optional.ofNullable(generator.terrainFeatures)),
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
).apply(instance, (base, rocks, vegetation, dungeons, lakes, terrainFeatures, seed) ->
new PortFeatureChunkGenerator(base, rocks.orElse(null), vegetation.orElse(null),
dungeons, lakes.orElse(null), terrainFeatures.orElse(null), seed)));
private final ChunkGenerator base;
private final PortRockPlan rocks;
private final PortVegetationPlan vegetation;
private final int dungeons;
private final PortLakePlan lakes;
private final PortTerrainFeaturePlan terrainFeatures;
private final long ageSeed;
public PortFeatureChunkGenerator(ChunkGenerator base, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, long ageSeed) {
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
this.base = base;
this.rocks = rocks;
this.vegetation = vegetation;
this.dungeons = dungeons;
this.lakes = lakes;
this.terrainFeatures = terrainFeatures;
this.ageSeed = ageSeed;
}
public ChunkGenerator base() { return base; }
public PortRockPlan rocks() { return rocks; }
public PortVegetationPlan vegetation() { return vegetation; }
public int dungeons() { return dungeons; }
public PortLakePlan lakes() { return lakes; }
public PortTerrainFeaturePlan terrainFeatures() { return terrainFeatures; }
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
@Override public void validate() { base.validate(); }
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
StructureManager structures, ChunkAccess chunk) {
return base.createBiomes(random, blender, structures, chunk);
}
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
RandomState random, long seed) {
return base.createState(sets, random, seed);
}
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
StructureManager structures, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> dimension) {
base.createStructures(registries, state, structures, chunk, templates, dimension);
}
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
base.createReferences(level, structures, chunk);
}
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
}
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
@Override public int getGenDepth() { return base.getGenDepth(); }
@Override public int getSeaLevel() { return base.getSeaLevel(); }
@Override public int getMinY() { return base.getMinY(); }
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
RandomState random, StructureManager structures, BiomeManager biomes,
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
CompletableFuture<ChunkAccess> built = base.buildTerrain(
chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
if (terrainFeatures == null || !terrainFeatures.skylands() && terrainFeatures.choices().isEmpty()) return built;
return built.thenApply(result -> {
// Match 1.21.1: filter the base terrain once, then add the authored
// solid volumes, irrespective of the Skylands page's written position.
if (terrainFeatures.skylands()) PortSkylandsFeature.filter(result, ageSeed);
java.util.Map<Identifier, Integer> occurrences = new java.util.HashMap<>();
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
if (PortTerrainFeaturePlan.isSurface(choice.page())) continue;
int occurrence = occurrences.merge(choice.page(), 1, Integer::sum) - 1;
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
if (choice.page().equals(PortTerrainFeaturePlan.FLOATING_ISLANDS))
PortFloatingIslandFeature.generate(result, featureSeed, material);
else if (choice.page().equals(PortTerrainFeaturePlan.TENDRILS))
PortTendrilFeature.generate(result, featureSeed, material);
else if (choice.page().equals(PortTerrainFeaturePlan.SPHERES))
PortSpheresFeature.generate(result, featureSeed, material);
else throw new IllegalStateException("Unsupported terrain feature: " + choice.page());
}
return result;
});
}
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
LevelHeightAccessor height, RandomState random) {
return base.getBaseHeight(x, z, type, height, random);
}
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
return base.getBaseColumn(x, z, height, random);
}
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
BlockPos pos, SamplerContext context) {
base.addDebugScreenInfo(info, random, pos, context);
}
@Override public void refreshFeaturesPerStep() {
base.refreshFeaturesPerStep();
super.refreshFeaturesPerStep();
}
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
StructureManager structures) {
base.applyBiomeDecoration(level, chunk, structures);
if (terrainFeatures != null) {
PortDenseOres.place(level, chunk, this, ageSeed, terrainFeatures.denseOres());
int spikeOccurrence = 0;
int obeliskOccurrence = 0;
int crystalOccurrence = 0;
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
if (!PortTerrainFeaturePlan.isSurface(choice.page())) continue;
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
if (choice.page().equals(PortTerrainFeaturePlan.SPIKES))
PortSpikesFeature.generate(level, chunk, ageSeed, material, spikeOccurrence++);
else if (choice.page().equals(PortTerrainFeaturePlan.OBELISKS))
PortObelisksFeature.generate(level, chunk, ageSeed, material, obeliskOccurrence++);
else if (choice.page().equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS))
PortCrystalFormationsFeature.generate(level, chunk, ageSeed, crystalOccurrence++);
else throw new IllegalStateException("Unsupported surface feature: " + choice.page());
}
}
ChunkPos chunkPos = chunk.getPos();
long chunkSeed = ageSeed ^ (long) chunkPos.x() * 341873128712L
^ (long) chunkPos.z() * 132897987541L;
BlockPos origin = new BlockPos(chunkPos.getMinBlockX(), level.getMinY(), chunkPos.getMinBlockZ());
if (rocks != null) {
PlacedFeature vanilla = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
.getOrThrow(FOREST_ROCK).value();
var placement = vanilla.placement().stream()
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList();
for (int occurrence = 0; occurrence < rocks.materials().size(); occurrence++) {
RandomSource random = RandomSource.create(chunkSeed
^ (long) occurrence * 0xD1B54A32D192ED03L);
BlockState material = BuiltInRegistries.BLOCK.getValue(
rocks.materials().get(occurrence)).defaultBlockState();
PlacedFeature authored = new PlacedFeature(Holder.direct(new BlockBlobFeature(
material, BlockPredicate.matchesTag(ROCK_GROUND))), placement);
authored.place(level, this, random, origin);
}
}
if (dungeons > 0) {
PlacedFeature authored = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
.getOrThrow(DUNGEONS).value();
for (int occurrence = 0; occurrence < dungeons; occurrence++) {
RandomSource random = RandomSource.create(chunkSeed
^ 0x44554E47454F4E53L ^ (long) occurrence * 0xD1B54A32D192ED03L);
authored.place(level, this, random, origin);
}
}
if (lakes != null) {
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
java.util.Map<PortLakePlan.Choice, Integer> occurrences = new java.util.HashMap<>();
for (PortLakePlan.Choice choice : lakes.choices()) {
PlacedFeature vanilla = registry.getOrThrow(ResourceKey.create(
Registries.PLACED_FEATURE, PortLakePlan.placedFeature(choice.page()))).value();
if (!(vanilla.feature().value() instanceof LakeFeature lake))
throw new IllegalStateException("Lake placement no longer references a LakeFeature: " + choice.page());
BlockState fluid = BuiltInRegistries.BLOCK.getValue(choice.fluid()).defaultBlockState();
LakeFeature feature = new LakeFeature(BlockStateProvider.holderOf(fluid), lake.barrier(),
lake.canPlaceFeature(), lake.canReplaceWithAirOrFluid(), lake.canReplaceWithBarrier());
PlacedFeature authored = new PlacedFeature(Holder.direct(feature), vanilla.placement().stream()
.filter(modifier -> !(modifier instanceof BiomeFilter))
.map(modifier -> !PortLakePlan.isDeep(choice.page()) && modifier instanceof RarityFilter
? RarityFilter.onAverageOnceEvery(12) : modifier)
.toList());
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
RandomSource random = RandomSource.create(chunkSeed
^ (long) choice.hashCode() * 0x9E3779B97F4A7C15L
^ (long) occurrence * 0xD1B54A32D192ED03L);
authored.place(level, this, random, origin);
}
}
if (vegetation != null) {
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
Holder<Biome> biome = level.getBiome(new BlockPos(
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
if (biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)) return;
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
Identifier page = vegetation.pages().get(occurrence);
Identifier placedId = PortVegetationPlan.placedFeature(page);
PlacedFeature vanilla = registry.getOrThrow(
ResourceKey.create(Registries.PLACED_FEATURE, placedId)).value();
PlacedFeature authored = new PlacedFeature(vanilla.feature(), vanilla.placement().stream()
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList());
RandomSource random = RandomSource.create(chunkSeed
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
^ (long) occurrence * 0xD1B54A32D192ED03L);
authored.place(level, this, random, origin);
}
}
}
}
@@ -0,0 +1,83 @@
package net.dnorberg.rivencraft.age;
import net.minecraft.core.BlockPos;
import net.minecraft.core.QuartPos;
import net.minecraft.tags.BiomeTags;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
import net.dnorberg.rivencraft.writing.PortRegistryPages;
/** 1.21.1 floating-island shape adapted to the 26.3 chunk terrain stage. */
final class PortFloatingIslandFeature {
private static final long SALT = 0x464C4F4154494E47L;
private static final int SOURCE_RANGE = 3;
private static final int CHANCE = 128;
private PortFloatingIslandFeature() {}
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
var target = chunk.getPos();
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
^ (long) sourceX * 341873128712L ^ (long) sourceZ * 132897987541L));
if (random.nextInt(CHANCE) != 0) continue;
int centerX = sourceX * 16 + random.nextInt(16);
int centerZ = sourceZ * 16 + random.nextInt(16);
int centerY = Math.min(chunk.getMaxY() - 30, 168 + random.nextInt(48));
if (centerY < chunk.getMinY() + 24) continue;
int radiusX = 24 + random.nextInt(18);
int radiusZ = 24 + random.nextInt(18);
fill(chunk, ageSeed, material, centerX, centerY, centerZ, radiusX, radiusZ);
}
}
}
private static void fill(ChunkAccess chunk, long ageSeed, BlockState material,
int centerX, int centerY, int centerZ, int radiusX, int radiusZ) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int localX = 0; localX < 16; localX++) {
int worldX = originX + localX;
double dx = (worldX - centerX) / (double) radiusX;
for (int localZ = 0; localZ < 16; localZ++) {
int worldZ = originZ + localZ;
double dz = (worldZ - centerZ) / (double) radiusZ;
double distance = dx * dx + dz * dz;
if (distance >= 1.0) continue;
long detail = mix(ageSeed ^ (long) centerX * 341873128712L
^ (long) centerZ * 132897987541L ^ (long) worldX * 42317861L
^ (long) worldZ * 374761393L);
int top = centerY + (int) Math.round(3 * (1 - distance)) + Math.floorMod(detail, 3) - 1;
int depth = Math.max(1, (int) Math.round(3 + 17 * Math.pow(1 - distance, 1.5)));
// Biomes are already populated by createBiomes in 26.3, so
// sampling this chunk preserves its authored biome distribution.
var biome = chunk.getNoiseBiome(QuartPos.fromBlock(worldX), QuartPos.fromBlock(top),
QuartPos.fromBlock(worldZ));
BlockState soil = biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)
? material : Blocks.DIRT.defaultBlockState();
BlockState cap = soil == material ? material : Blocks.GRASS_BLOCK.defaultBlockState();
for (int y = Math.max(top - depth, chunk.getMinY()); y <= top; y++) {
pos.set(worldX, y, worldZ);
if (!chunk.getBlockState(pos).isAir()) continue;
BlockState state = y == top ? cap : y >= top - 3 ? soil : material;
chunk.setBlockState(pos, state);
surface.update(localX, y, localZ, state);
floor.update(localX, y, localZ, state);
}
}
}
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,46 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Ordered lake pages with fluid choices frozen when the Age is bound. */
public record PortLakePlan(List<Choice> choices) {
public static final Codec<PortLakePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortLakePlan::choices)
).apply(instance, PortLakePlan::new));
public record Choice(Identifier page, Identifier fluid) {
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
Identifier.CODEC.fieldOf("fluid").forGetter(Choice::fluid)
).apply(instance, Choice::new));
public Choice {
if (!isLake(page) || fluid == null) throw new IllegalArgumentException("Invalid lake choice");
}
}
public PortLakePlan {
choices = List.copyOf(choices);
if (choices.isEmpty() || choices.size() > 32)
throw new IllegalArgumentException("Lake plan must contain 1–32 choices");
}
public static boolean isLake(Identifier page) {
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
return switch (page.getPath()) {
case "feature/surface_lakes", "feature/deep_lakes", "feature/lava_lakes" -> true;
default -> false;
};
}
public static boolean isDeep(Identifier page) { return page.getPath().equals("feature/deep_lakes"); }
public static Identifier placedFeature(Identifier page) {
if (!isLake(page)) throw new IllegalArgumentException("Unsupported lake page " + page);
return Identifier.withDefaultNamespace(isDeep(page) ? "lake_lava_underground" : "lake_lava_surface");
}
}
@@ -0,0 +1,91 @@
package net.dnorberg.rivencraft.age;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
/** 26.3 port of Rivencraft 1.21.1's Legacy-style 4×4 plinth and 2×12×2 shaft. */
final class PortObelisksFeature {
private static final long SALT = 0x4F42454C49534B53L;
private static final int CHANCE = 128;
private static final int BASE = 4;
private static final int HEIGHT = 12;
private PortObelisksFeature() {}
record Site(int x, int z) {}
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
^ (long) chunk.x() * 341873128712L ^ (long) chunk.z() * 132897987541L
^ (long) occurrence * 0x9E3779B97F4A7C15L));
if (random.nextInt(CHANCE) != 0) return null;
return new Site(chunk.getMinBlockX() + 4 + random.nextInt(8),
chunk.getMinBlockZ() + 4 + random.nextInt(8));
}
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
Site site = site(ageSeed, chunk.getPos(), occurrence);
if (site == null) return;
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
int minSurface = Integer.MAX_VALUE;
int maxSurface = Integer.MIN_VALUE;
int[][] surface = new int[BASE][BASE];
for (int dx = 0; dx < BASE; dx++) {
for (int dz = 0; dz < BASE; dz++) {
int x = site.x() + dx;
int z = site.z() + dz;
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
if (y <= level.getMinY() + 1) return;
BlockState ground = level.getBlockState(pos.set(x, y - 1, z));
if (!ground.isFaceSturdy(level, pos, Direction.UP)
|| !ground.getFluidState().isEmpty() || ground.hasBlockEntity()) return;
surface[dx][dz] = y;
minSurface = Math.min(minSurface, y);
maxSurface = Math.max(maxSurface, y);
}
}
if (maxSurface - minSurface > 2 || maxSurface + HEIGHT >= level.getMaxY()) return;
for (int dx = 0; dx < BASE; dx++) {
for (int dz = 0; dz < BASE; dz++) {
for (int y = surface[dx][dz]; y <= maxSurface; y++) {
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
}
}
}
for (int dx = 1; dx <= 2; dx++) {
for (int dz = 1; dz <= 2; dz++) {
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++) {
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
}
}
}
for (int dx = 0; dx < BASE; dx++) {
for (int dz = 0; dz < BASE; dz++) {
for (int y = surface[dx][dz]; y <= maxSurface; y++)
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
}
}
for (int dx = 1; dx <= 2; dx++) {
for (int dz = 1; dz <= 2; dz++) {
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++)
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
}
}
}
private static boolean open(BlockState state) {
return state.getFluidState().isEmpty() && (state.isAir() || state.canBeReplaced());
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -1,134 +0,0 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.NoiseColumn;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.blending.Blender;
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.Structure;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.HolderSet;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
/** Delegates 26.3 terrain generation and adds one Age-scoped Rocks decoration. */
public final class PortRockChunkGenerator extends ChunkGenerator {
public static final MapCodec<PortRockChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
PortRockPlan.CODEC.fieldOf("rocks").forGetter(generator -> generator.rocks),
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
).apply(instance, PortRockChunkGenerator::new));
private final ChunkGenerator base;
private final PortRockPlan rocks;
private final long ageSeed;
public PortRockChunkGenerator(ChunkGenerator base, PortRockPlan rocks, long ageSeed) {
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
this.base = base;
this.rocks = rocks;
this.ageSeed = ageSeed;
}
public ChunkGenerator base() { return base; }
public PortRockPlan rocks() { return rocks; }
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
@Override public void validate() { base.validate(); }
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
StructureManager structures, ChunkAccess chunk) {
return base.createBiomes(random, blender, structures, chunk);
}
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
RandomState random, long seed) {
return base.createState(sets, random, seed);
}
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
StructureManager structures, ChunkAccess chunk,
StructureTemplateManager templates, ResourceKey<Level> dimension) {
base.createStructures(registries, state, structures, chunk, templates, dimension);
}
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
base.createReferences(level, structures, chunk);
}
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
}
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
@Override public int getGenDepth() { return base.getGenDepth(); }
@Override public int getSeaLevel() { return base.getSeaLevel(); }
@Override public int getMinY() { return base.getMinY(); }
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
RandomState random, StructureManager structures, BiomeManager biomes,
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
return base.buildTerrain(chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
}
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
LevelHeightAccessor height, RandomState random) {
return base.getBaseHeight(x, z, type, height, random);
}
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
return base.getBaseColumn(x, z, height, random);
}
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
BlockPos pos, SamplerContext context) {
base.addDebugScreenInfo(info, random, pos, context);
}
@Override public void refreshFeaturesPerStep() {
base.refreshFeaturesPerStep();
super.refreshFeaturesPerStep();
}
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
StructureManager structures) {
base.applyBiomeDecoration(level, chunk, structures);
ChunkPos chunkPos = chunk.getPos();
long seed = ageSeed ^ (long) chunkPos.x() * 341873128712L ^ (long) chunkPos.z() * 132897987541L;
RandomSource random = RandomSource.create(seed);
if (random.nextInt(5) != 0) return;
BlockState material = BuiltInRegistries.BLOCK.getValue(rocks.material()).defaultBlockState();
int centerX = chunkPos.getMinBlockX() + 2 + random.nextInt(12);
int centerZ = chunkPos.getMinBlockZ() + 2 + random.nextInt(12);
for (int dx = -1; dx <= 1; dx++) for (int dz = -1; dz <= 1; dz++) {
if (random.nextInt(4) == 0) continue;
int x = centerX + dx;
int z = centerZ + dz;
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
BlockPos place = new BlockPos(x, y, z);
if (level.getBlockState(place).isAir()
&& level.getFluidState(place).isEmpty()
&& level.getBlockState(place.below()).isFaceSturdy(level, place.below(), net.minecraft.core.Direction.UP))
level.setBlock(place, material, 2);
}
}
}
@@ -2,11 +2,23 @@ package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.Optional;
import net.minecraft.resources.Identifier;
/** First frozen authored feature: one Rocks page with an explicit block material. */
public record PortRockPlan(Identifier material) {
/** Frozen, ordered Rocks materials. Old single-material beta Ages remain readable. */
public record PortRockPlan(List<Identifier> materials) {
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.fieldOf("material").forGetter(PortRockPlan::material)
).apply(instance, PortRockPlan::new));
Identifier.CODEC.optionalFieldOf("material").forGetter(plan -> Optional.empty()),
Identifier.CODEC.listOf().optionalFieldOf("materials", List.of()).forGetter(PortRockPlan::materials)
).apply(instance, (old, current) -> new PortRockPlan(current.isEmpty()
? old.map(List::of).orElse(List.of()) : current)));
public PortRockPlan {
materials = List.copyOf(materials);
if (materials.isEmpty() || materials.size() > 32)
throw new IllegalArgumentException("Rocks plan must contain 1–32 materials");
}
public PortRockPlan(Identifier material) { this(List.of(material)); }
}
@@ -0,0 +1,87 @@
package net.dnorberg.rivencraft.age;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
/**
* 26.3 port of Rivencraft's subtractive Skylands filter, adapted from
* Mystcraft Legacy's SymbolSkylands (LGPL-3.0-or-later). Keeps upper solid
* terrain above a seven-octave cutoff near Y=76 and removes liquids.
*/
final class PortSkylandsFeature {
private static final long SALT = 0x534B594C414E4453L;
private static final int BASE_CUTOFF = 76;
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
private PortSkylandsFeature() {}
static void filter(ChunkAccess chunk, long ageSeed) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
int minY = chunk.getMinY();
int maxY = chunk.getMaxY();
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int cutoff = cutoff(ageSeed, originX + localX, originZ + localZ);
for (int y = minY; y < maxY; y++) {
pos.set(originX + localX, y, originZ + localZ);
BlockState state = chunk.getBlockState(pos);
if (!removes(state, y, cutoff)) continue;
chunk.setBlockState(pos, AIR);
surface.update(localX, y, localZ, AIR);
floor.update(localX, y, localZ, AIR);
}
}
}
}
static boolean removes(BlockState state, int y, int cutoff) {
return !state.isAir() && (y <= cutoff || state.liquid());
}
static int cutoff(long ageSeed, int x, int z) {
double variation = 0.0D;
int cellSize = 128;
double amplitude = 12.0D;
for (int octave = 0; octave < 7; octave++) {
variation += valueNoise(ageSeed ^ SALT ^ (long) octave * 0x9E3779B97F4A7C15L,
x, z, cellSize) * amplitude;
cellSize /= 2;
amplitude /= 2.0D;
}
return BASE_CUTOFF + (int) Math.round(variation);
}
private static double valueNoise(long seed, int x, int z, int cellSize) {
int cellX = Math.floorDiv(x, cellSize);
int cellZ = Math.floorDiv(z, cellSize);
double u = smooth((x - cellX * cellSize) / (double) cellSize);
double v = smooth((z - cellZ * cellSize) / (double) cellSize);
double north = lerp(value(seed, cellX, cellZ), value(seed, cellX + 1, cellZ), u);
double south = lerp(value(seed, cellX, cellZ + 1), value(seed, cellX + 1, cellZ + 1), u);
return lerp(north, south, v);
}
private static double value(long seed, int x, int z) {
long hash = mix(seed ^ (long) x * 0xD1B54A32D192ED03L ^ (long) z * 0xABC98388FB8FAC03L);
return (hash >>> 11) * 0x1.0p-52 - 1.0D;
}
private static double smooth(double value) { return value * value * (3.0D - 2.0D * value); }
private static double lerp(double first, double second, double amount) {
return first + (second - first) * amount;
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
}
@@ -0,0 +1,122 @@
package net.dnorberg.rivencraft.age;
import java.util.Random;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
/**
* 26.3 adaptation of Rivencraft 1.21.1's solid sphere nodes, derived from
* Mystcraft Legacy's MapGenSpheresMyst (Mystcraft contributors,
* LGPL-3.0-or-later, commit 9bc8ddc). Writes stay within the target chunk.
*/
final class PortSpheresFeature {
private static final int SOURCE_RANGE = 8;
private static final long SALT = 0x53504845524553L;
private PortSpheresFeature() {}
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
var target = chunk.getPos();
Random master = new Random(ageSeed ^ SALT);
long xSeed = master.nextLong();
long zSeed = master.nextLong();
for (int sx = target.x() - SOURCE_RANGE; sx <= target.x() + SOURCE_RANGE; sx++) {
for (int sz = target.z() - SOURCE_RANGE; sz <= target.z() + SOURCE_RANGE; sz++) {
Random source = new Random((long) sx * xSeed ^ (long) sz * zSeed ^ ageSeed ^ SALT);
if (source.nextFloat() > 0.05F) continue;
double x = sx * 16 + source.nextInt(16);
double y = source.nextInt(source.nextInt(192) + 1) + 32;
double z = sz * 16 + source.nextInt(16);
trace(chunk, material, source.nextLong(), x, y, z,
1.0F + source.nextFloat() * 4.0F, 0, 0, -1, -1);
}
}
}
private static void trace(ChunkAccess chunk, BlockState material, long seed,
double x, double y, double z, float radius, float yaw, float pitch,
int step, int length) {
Random random = new Random(seed);
if (length <= 0) {
int maximum = SOURCE_RANGE * 16 - 16;
length = maximum - random.nextInt(maximum / 4);
}
boolean singleNode = step == -1;
if (singleNode) step = length / 2;
int splitAt = random.nextInt(length / 2) + length / 4;
boolean slowPitch = random.nextInt(6) == 0;
float yawDrift = 0;
float pitchDrift = 0;
double middleX = chunk.getPos().getMinBlockX() + 8;
double middleZ = chunk.getPos().getMinBlockZ() + 8;
for (; step < length; step++) {
double nodeRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
float horizontal = Mth.cos(pitch);
x += Mth.cos(yaw) * horizontal;
y += Mth.sin(pitch);
z += Mth.sin(yaw) * horizontal;
pitch *= slowPitch ? 0.92F : 0.7F;
pitch += pitchDrift * 0.1F;
yaw += yawDrift * 0.1F;
pitchDrift = pitchDrift * 0.9F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
yawDrift = yawDrift * 0.75F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
if (!singleNode && step == splitAt && radius > 1.0F) {
trace(chunk, material, random.nextLong(), x, y, z,
random.nextFloat() * 0.5F + 0.5F, yaw - Mth.HALF_PI, pitch / 3, step, length);
trace(chunk, material, random.nextLong(), x, y, z,
random.nextFloat() * 0.5F + 0.5F, yaw + Mth.HALF_PI, pitch / 3, step, length);
return;
}
if (!singleNode && random.nextInt(4) == 0) continue;
double dx = x - middleX;
double dz = z - middleZ;
double remaining = length - step;
double reach = radius + 18;
if (dx * dx + dz * dz - remaining * remaining > reach * reach) return;
if (Math.abs(dx) <= 16 + nodeRadius * 2 && Math.abs(dz) <= 16 + nodeRadius * 2)
fillNode(chunk, material, x, y, z, nodeRadius);
if (singleNode) break;
}
}
private static void fillNode(ChunkAccess chunk, BlockState material,
double centerX, double centerY, double centerZ, double radius) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
int minX = Math.max(0, Mth.floor(centerX - radius) - originX - 1);
int maxX = Math.min(16, Mth.floor(centerX + radius) - originX + 2);
int minZ = Math.max(0, Mth.floor(centerZ - radius) - originZ - 1);
int maxZ = Math.min(16, Mth.floor(centerZ + radius) - originZ + 2);
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - radius) - 1);
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + radius) + 2);
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int worldY = minY; worldY < maxY; worldY++) {
double dy = (worldY + 0.5D - centerY) / radius;
for (int localZ = minZ; localZ < maxZ; localZ++) {
double dz = (originZ + localZ + 0.5D - centerZ) / radius;
for (int localX = minX; localX < maxX; localX++) {
double dx = (originX + localX + 0.5D - centerX) / radius;
if (!insideSphere(dx, dy, dz)) continue;
pos.set(originX + localX, worldY, originZ + localZ);
BlockState previous = chunk.getBlockState(pos);
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
chunk.setBlockState(pos, material);
surface.update(localX, worldY, localZ, material);
floor.update(localX, worldY, localZ, material);
}
}
}
}
static boolean insideSphere(double dx, double dy, double dz) {
return dx * dx + dy * dy + dz * dz < 1.0D;
}
}
@@ -0,0 +1,56 @@
package net.dnorberg.rivencraft.age;
import net.minecraft.core.BlockPos;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.WorldGenLevel;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
/**
* 26.3 port of Rivencraft 1.21.1's rare surface spikes, adapted from
* Mystcraft Legacy's WorldGenSpikesAdv (Mystcraft contributors,
* LGPL-3.0-or-later, commit 9bc8ddc). The footprint stays within one chunk.
*/
final class PortSpikesFeature {
private static final long SALT = 0x5350494B4553L;
private PortSpikesFeature() {}
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
RandomSource random = RandomSource.create(ageSeed ^ SALT
^ (long) originX * 341873128712L ^ (long) originZ * 132897987541L
^ (long) occurrence * 0x9E3779B97F4A7C15L);
if (random.nextInt(18) != 0) return;
int x = originX + 4 + random.nextInt(8);
int z = originZ + 4 + random.nextInt(8);
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
int height = 6 + random.nextInt(32);
int width = 1 + random.nextInt(4);
if (y < level.getMinY() + 1 || y + height >= level.getMaxY()) return;
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
if (!level.isEmptyBlock(pos.set(x, y, z))) return;
for (int px = x - width; px <= x + width; px++) {
for (int pz = z - width; pz <= z + width; pz++) {
if (!insideFootprint(px - x, pz - z, width)) continue;
if (!level.getBlockState(pos.set(px, y - 1, pz)).isSolid()) return;
}
}
for (int px = x - width; px <= x + width; px++) {
for (int pz = z - width; pz <= z + width; pz++) {
if (!insideFootprint(px - x, pz - z, width)) continue;
int top = y + 1 + random.nextInt(random.nextInt(height) + 1);
for (int py = y; py < top; py++) {
pos.set(px, py, pz);
if (level.isEmptyBlock(pos)) level.setBlock(pos, material, 2);
}
}
}
}
static boolean insideFootprint(int dx, int dz, int width) {
return dx * dx + dz * dz <= width * width + 1;
}
}
@@ -0,0 +1,107 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.saveddata.SavedDataType;
/**
* Ports Rivencraft 1.21.1's Legacy fissure footprint/carving.
* Derived from Mystcraft's WorldGenMystStarFissure, LGPL-3.0,
* copyright XCompWiz and Mystcraft contributors.
*/
public final class PortStarFissureFeature {
private static final long SALT = 0x5354415246495353L;
private PortStarFissureFeature() {}
/** Per-Age data prevents revisits/restarts from carving the same fissure again. */
private static final class Arrival extends SavedData {
private static final Codec<Arrival> CODEC = BlockPos.CODEC.optionalFieldOf("spawn").codec()
.xmap(Arrival::new, data -> Optional.ofNullable(data.spawn));
private static final SavedDataType<Arrival> TYPE = new SavedDataType<>(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "star_fissure_arrival"),
() -> new Arrival(Optional.empty()), CODEC);
private BlockPos spawn;
private Arrival(Optional<BlockPos> spawn) { this.spawn = spawn.orElse(null); }
}
public static BlockPos savedSpawn(ServerLevel level) {
return level.getDataStorage().computeIfAbsent(Arrival.TYPE).spawn;
}
public static void generateOnce(ServerLevel level, BlockPos spawn, boolean caveTerrain) {
Arrival data = level.getDataStorage().computeIfAbsent(Arrival.TYPE);
if (data.spawn != null) return;
generate(level, level.getSeed(), spawn, caveTerrain);
data.spawn = spawn.immutable();
data.setDirty();
}
/** Legacy chooses 8–23 blocks after the spawn chunk, then grows along +Z. */
public static List<BlockPos> footprint(long ageSeed, BlockPos spawn) {
long chunkX = spawn.getX() >> 4;
long chunkZ = spawn.getZ() >> 4;
Random random = new Random(ageSeed ^ SALT ^ chunkX ^ (chunkZ << 32));
int originX = ((int) chunkX << 4) + random.nextInt(16) + 8;
int originZ = ((int) chunkZ << 4) + random.nextInt(16) + 8;
int length = random.nextInt(8) + 10;
int[][] intervals = new int[length][2];
List<BlockPos> columns = new ArrayList<>();
addRow(columns, originX, originZ, 0, 0, 0);
for (int row = 1; row < length; row++) {
int range = length + Math.min(row, length - row) + 1;
int right = (random.nextInt(range) - (range >> 1) + intervals[row - 1][1]) / 2;
int left = (random.nextInt(range) - (range >> 1) + intervals[row - 1][0]) / 2;
if (left > right) {
int swap = left;
left = right;
right = swap;
}
left = Math.min(left, intervals[row - 1][1]);
right = Math.max(right, intervals[row - 1][0]);
intervals[row][0] = left;
intervals[row][1] = right;
addRow(columns, originX, originZ, row, left, right);
}
return List.copyOf(columns);
}
private static void addRow(List<BlockPos> columns, int originX, int originZ, int row, int left, int right) {
for (int x = left; x <= right; x++) {
// Only X/Z are used; the real floor is the dimension's minimum Y.
columns.add(new BlockPos(originX + x, 0, originZ + row * 2));
columns.add(new BlockPos(originX + x, 0, originZ + row * 2 + 1));
}
}
private static void generate(ServerLevel level, long ageSeed, BlockPos spawn, boolean caveTerrain) {
int floorY = level.getMinY();
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
for (BlockPos column : footprint(ageSeed, spawn)) {
int top = level.getHeight(Heightmap.Types.WORLD_SURFACE, column.getX(), column.getZ());
if (caveTerrain) {
for (int y = Math.max(spawn.getY(), floorY + 1); y < top; y++) {
if (level.getBlockState(mutable.set(column.getX(), y, column.getZ())).is(Blocks.BEDROCK)) {
top = y;
break;
}
}
}
for (int y = floorY + 1; y < top; y++)
level.setBlock(mutable.set(column.getX(), y, column.getZ()), Blocks.AIR.defaultBlockState(), 2);
level.setBlock(mutable.set(column.getX(), floorY, column.getZ()),
Rivencraft.STAR_FISSURE.get().defaultBlockState(), 2);
}
}
}
@@ -0,0 +1,142 @@
package net.dnorberg.rivencraft.age;
import java.util.Random;
import net.minecraft.core.BlockPos;
import net.minecraft.util.Mth;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.Heightmap;
/**
* 26.3 adaptation of Rivencraft 1.21.1's winding solid volumes, originally
* adapted from Mystcraft Legacy's SymbolTendrils and MapGenCavesMyst (LGPL-3.0-or-later).
* Each chunk reconstructs the neighboring source paths to preserve continuous borders.
*/
final class PortTendrilFeature {
private static final long SALT = 0x54454E4452494C53L;
private static final int SOURCE_RANGE = 8;
private static final int SOURCE_RATE = 15;
private static final int SOURCE_SIZE = 18;
private PortTendrilFeature() {}
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
var target = chunk.getPos();
Random master = new Random(ageSeed ^ SALT);
long xSeed = master.nextLong();
long zSeed = master.nextLong();
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
Random source = new Random((long) sourceX * xSeed ^ (long) sourceZ * zSeed ^ ageSeed ^ SALT);
int nodes = source.nextInt(source.nextInt(source.nextInt(SOURCE_SIZE) + 1) + 1);
if (source.nextInt(SOURCE_RATE) != 0) continue;
for (int node = 0; node < nodes; node++) {
double x = sourceX * 16 + source.nextInt(16);
double y = source.nextInt(source.nextInt(120) + 8);
double z = sourceZ * 16 + source.nextInt(16);
int branches = 1;
if (source.nextInt(4) == 0) {
trace(chunk, material, source.nextLong(), x, y, z,
1.0F + source.nextFloat() * 6.0F, 0.0F, 0.0F, -1, -1, 0.5D);
branches += source.nextInt(4);
}
for (int branch = 0; branch < branches; branch++) {
float yaw = source.nextFloat() * Mth.TWO_PI;
float pitch = (source.nextFloat() - 0.5F) / 4.0F;
float radius = source.nextFloat() * 2.0F + source.nextFloat();
if (source.nextInt(10) == 0)
radius *= source.nextFloat() * source.nextFloat() * 3.0F + 1.0F;
trace(chunk, material, source.nextLong(), x, y, z,
radius, yaw, pitch, 0, 0, 1.0D);
}
}
}
}
}
private static void trace(ChunkAccess chunk, BlockState material, long seed,
double x, double y, double z, float radius, float yaw, float pitch,
int step, int length, double verticalScale) {
Random random = new Random(seed);
if (length <= 0) {
int maximum = SOURCE_RANGE * 16 - 16;
length = maximum - random.nextInt(maximum / 4);
}
boolean singleNode = step == -1;
if (singleNode) step = length / 2;
int splitAt = random.nextInt(length / 2) + length / 4;
boolean slowPitch = random.nextInt(6) == 0;
float yawDrift = 0.0F;
float pitchDrift = 0.0F;
double middleX = chunk.getPos().getMinBlockX() + 8;
double middleZ = chunk.getPos().getMinBlockZ() + 8;
for (; step < length; step++) {
double horizontalRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
double verticalRadius = horizontalRadius * verticalScale;
float horizontalMotion = Mth.cos(pitch);
x += Mth.cos(yaw) * horizontalMotion;
y += Mth.sin(pitch);
z += Mth.sin(yaw) * horizontalMotion;
pitch *= slowPitch ? 0.92F : 0.7F;
pitch += pitchDrift * 0.1F;
yaw += yawDrift * 0.1F;
pitchDrift = pitchDrift * 0.9F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
yawDrift = yawDrift * 0.75F
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
if (!singleNode && step == splitAt && radius > 1.0F) {
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
yaw - Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
yaw + Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
return;
}
if (!singleNode && random.nextInt(4) == 0) continue;
double distanceX = x - middleX;
double distanceZ = z - middleZ;
double remaining = length - step;
double reach = radius + 18.0D;
if (distanceX * distanceX + distanceZ * distanceZ - remaining * remaining > reach * reach) return;
if (x >= middleX - 16.0D - horizontalRadius * 2.0D
&& x <= middleX + 16.0D + horizontalRadius * 2.0D
&& z >= middleZ - 16.0D - horizontalRadius * 2.0D
&& z <= middleZ + 16.0D + horizontalRadius * 2.0D) {
fillNode(chunk, material, x, y, z, horizontalRadius, verticalRadius);
}
if (singleNode) break;
}
}
private static void fillNode(ChunkAccess chunk, BlockState material, double centerX, double centerY,
double centerZ, double horizontalRadius, double verticalRadius) {
int originX = chunk.getPos().getMinBlockX();
int originZ = chunk.getPos().getMinBlockZ();
int minX = Math.max(0, Mth.floor(centerX - horizontalRadius) - originX - 1);
int maxX = Math.min(16, Mth.floor(centerX + horizontalRadius) - originX + 2);
int minZ = Math.max(0, Mth.floor(centerZ - horizontalRadius) - originZ - 1);
int maxZ = Math.min(16, Mth.floor(centerZ + horizontalRadius) - originZ + 2);
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - verticalRadius) - 1);
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + verticalRadius) + 2);
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
for (int worldY = minY; worldY < maxY; worldY++) {
double dy = (worldY + 0.5D - centerY) / verticalRadius;
if (dy <= -0.7D || dy * dy >= 1.0D) continue;
for (int localZ = minZ; localZ < maxZ; localZ++) {
double dz = (originZ + localZ + 0.5D - centerZ) / horizontalRadius;
for (int localX = minX; localX < maxX; localX++) {
double dx = (originX + localX + 0.5D - centerX) / horizontalRadius;
if (dx * dx + dy * dy + dz * dz >= 1.0D) continue;
pos.set(originX + localX, worldY, originZ + localZ);
BlockState previous = chunk.getBlockState(pos);
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
chunk.setBlockState(pos, material);
surface.update(localX, worldY, localZ, material);
floor.update(localX, worldY, localZ, material);
}
}
}
}
}
@@ -0,0 +1,79 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import java.util.HashMap;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Frozen terrain filters and ordered solid/surface features for an Age. */
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/floating_islands");
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
public static final Identifier SKYLANDS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/skylands");
public static final Identifier SPHERES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spheres");
public static final Identifier SPIKES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spikes");
public static final Identifier OBELISKS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/obelisks");
public static final Identifier CRYSTAL_FORMATIONS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/crystal_formations");
public static final Identifier CRYSTAL_MATERIAL = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "crystal_block");
public static final Identifier DENSE_ORES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dense_ores");
public static final Identifier STAR_FISSURES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/star_fissures");
public static final int MAX_PER_FEATURE = 32;
public static final Codec<PortTerrainFeaturePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortTerrainFeaturePlan::choices),
Codec.BOOL.optionalFieldOf("skylands", false).forGetter(PortTerrainFeaturePlan::skylands),
Codec.intRange(0, MAX_PER_FEATURE).optionalFieldOf("dense_ores", 0)
.forGetter(PortTerrainFeaturePlan::denseOres),
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures)
).apply(instance, PortTerrainFeaturePlan::new));
public record Choice(Identifier page, Identifier material) {
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
Identifier.CODEC.fieldOf("material").forGetter(Choice::material)
).apply(instance, Choice::new));
public Choice {
if (!isSupported(page) || material == null)
throw new IllegalArgumentException("Invalid terrain feature choice");
if (CRYSTAL_FORMATIONS.equals(page) && !CRYSTAL_MATERIAL.equals(material))
throw new IllegalArgumentException("Crystal formations use Rivencraft crystal, not a Block modifier");
}
}
public PortTerrainFeaturePlan {
choices = List.copyOf(choices);
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures)
throw new IllegalArgumentException("Terrain feature plan must not be empty");
var counts = new HashMap<Identifier, Integer>();
for (Choice choice : choices) {
if (counts.merge(choice.page(), 1, Integer::sum) > MAX_PER_FEATURE)
throw new IllegalArgumentException("Terrain feature plan permits at most 32 occurrences per feature");
}
}
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres) {
this(choices, skylands, denseOres, false);
}
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands) { this(choices, skylands, 0, false); }
public PortTerrainFeaturePlan(List<Choice> choices) { this(choices, false, 0, false); }
public static boolean isSupported(Identifier page) {
return FLOATING_ISLANDS.equals(page) || TENDRILS.equals(page) || SPHERES.equals(page) || isSurface(page);
}
public static boolean acceptsMaterial(Identifier page) {
return isSupported(page) && !CRYSTAL_FORMATIONS.equals(page);
}
public static boolean isSurface(Identifier page) {
return SPIKES.equals(page) || OBELISKS.equals(page) || CRYSTAL_FORMATIONS.equals(page);
}
}
@@ -0,0 +1,38 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Ordered, frozen authored vegetation pages for surface Ages. */
public record PortVegetationPlan(List<Identifier> pages) {
public static final Identifier FLOWERS = page("flowers");
public static final Identifier TREES = page("trees");
public static final Identifier HUGE_TREES = page("huge_trees");
public static final Codec<PortVegetationPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages)
).apply(instance, PortVegetationPlan::new));
public PortVegetationPlan {
pages = List.copyOf(pages);
if (pages.isEmpty() || pages.size() > 32 || pages.stream().anyMatch(page -> !isVegetation(page)))
throw new IllegalArgumentException("Vegetation plan requires 1–32 supported pages");
}
public static boolean isVegetation(Identifier page) {
return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES);
}
public static Identifier placedFeature(Identifier page) {
if (page.equals(FLOWERS)) return Identifier.withDefaultNamespace("flower_default");
if (page.equals(TREES)) return Identifier.withDefaultNamespace("trees_plains");
if (page.equals(HUGE_TREES)) return Identifier.withDefaultNamespace("trees_old_growth_spruce_taiga");
throw new IllegalArgumentException("Unsupported vegetation page " + page);
}
private static Identifier page(String name) {
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + name);
}
}
@@ -57,7 +57,8 @@ public final class RuntimeAgeBackend {
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
try {
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
index.terrain(age.getKey()), index.rocks(age.getKey()));
index.terrain(age.getKey()), index.rocks(age.getKey()), index.vegetation(age.getKey()),
index.dungeons(age.getKey()), index.lakes(age.getKey()), index.terrainFeatures(age.getKey()));
} catch (RuntimeException exception) {
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
}
@@ -80,12 +81,37 @@ public final class RuntimeAgeBackend {
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
return create(server, id, seed, biomePlan, terrain, rocks, null);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, 0);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, null);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, null);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures) {
checkServerThread(server);
ageName(id);
AgeLevelIndex index = index(server);
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks);
index.add(id, seed, biomePlan, terrain, rocks);
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
// A newly bound book may be used immediately. Persist its Age identity
// before publishing the catalog so a later crash cannot leave that
// book pointing at a world omitted from the on-disk index.
@@ -106,7 +132,8 @@ public final class RuntimeAgeBackend {
checkServerThread(server);
long seed = requireSeed(server, id);
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id),
index(server).rocks(id));
index(server).rocks(id), index(server).vegetation(id), index(server).dungeons(id), index(server).lakes(id),
index(server).terrainFeatures(id));
AgeCatalogSync.onCatalogChanged(server);
return level;
}
@@ -119,18 +146,22 @@ public final class RuntimeAgeBackend {
PortBiomePlan biomePlan = index.biomePlan(id);
PortAgeTerrain terrain = index.terrain(id);
PortRockPlan rocks = index.rocks(id);
PortVegetationPlan vegetation = index.vegetation(id);
int dungeons = index.dungeons(id);
PortLakePlan lakes = index.lakes(id);
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
if (previousSeed == seed) return;
boolean loaded = server.getLevel(dimensionKey(id)) != null;
if (loaded) unloadLoaded(server, id);
try {
if (loaded) load(server, id, seed, biomePlan, terrain, rocks);
if (loaded) load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
index.reseed(id, seed);
server.overworld().getDataStorage().saveAndJoin();
if (loaded) AgeCatalogSync.onCatalogChanged(server);
} catch (RuntimeException exception) {
if (loaded) {
try {
load(server, id, previousSeed, biomePlan, terrain, rocks);
load(server, id, previousSeed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
AgeCatalogSync.onCatalogChanged(server);
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
@@ -149,6 +180,10 @@ public final class RuntimeAgeBackend {
PortBiomePlan biomePlan = index.biomePlan(id);
PortAgeTerrain terrain = index.terrain(id);
PortRockPlan rocks = index.rocks(id);
PortVegetationPlan vegetation = index.vegetation(id);
int dungeons = index.dungeons(id);
PortLakePlan lakes = index.lakes(id);
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
boolean loaded = server.getLevel(dimensionKey(id)) != null;
ArchivePaths paths = archivePaths(server, name);
if (loaded) unloadLoaded(server, id);
@@ -167,14 +202,15 @@ public final class RuntimeAgeBackend {
}
} catch (IOException | RuntimeException exception) {
try {
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks);
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures);
server.overworld().getDataStorage().saveAndJoin();
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
}
if (loaded) {
try {
load(server, id, seed, biomePlan, terrain, rocks);
load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
}
@@ -290,7 +326,9 @@ public final class RuntimeAgeBackend {
}
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures) {
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
@@ -361,7 +399,8 @@ public final class RuntimeAgeBackend {
};
generator = new NoiseBasedChunkGenerator(source, settings);
}
if (rocks != null) generator = new PortRockChunkGenerator(generator, rocks, seed);
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
@@ -0,0 +1,61 @@
package net.dnorberg.rivencraft.block;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.InsideBlockEffectApplier;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EndPortalBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.portal.TeleportTransition;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.CollisionContext;
import net.minecraft.world.phys.shapes.VoxelShape;
/** Non-obtainable starfield portal returning entities to the shared Overworld spawn. */
public final class StarFissureBlock extends EndPortalBlock {
private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 1.6, 16);
public StarFissureBlock(BlockBehaviour.Properties properties) { super(properties); }
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new StarFissureBlockEntity(pos, state);
}
@Override
public void animateTick(BlockState state, Level level, BlockPos pos, net.minecraft.util.RandomSource random) {
// Legacy fissures have a starfield, not vanilla End portal smoke.
}
@Override
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return SHAPE;
}
@Override
protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity,
InsideBlockEffectApplier effects, boolean precise) {
// Do not inherit End credits or End-dimension travel from the vanilla block.
if (entity.canUsePortal(false)) entity.setAsInsidePortal(this, pos);
}
@Override
public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
if (level.dimension() == Level.OVERWORLD) return null;
ServerLevel overworld = level.getServer().overworld();
if (overworld == null) return null;
BlockPos spawn = overworld.getRespawnData().pos();
BlockPos landing = net.dnorberg.rivencraft.age.PortAgeTravel.findFootedDestination(overworld, spawn).orElse(spawn);
level.playSound(null, pos, Rivencraft.LINK_FISSURE.get(), SoundSource.BLOCKS, 1.0F, 1.0F);
return new TeleportTransition(overworld, Vec3.atBottomCenterOf(landing), entity.getDeltaMovement(),
entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET);
}
}
@@ -0,0 +1,12 @@
package net.dnorberg.rivencraft.block.entity;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
public final class StarFissureBlockEntity extends TheEndPortalBlockEntity {
public StarFissureBlockEntity(BlockPos pos, BlockState state) {
super(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), pos, state);
}
}
@@ -14,6 +14,12 @@ import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
public final class PortBookBinderClientEvents {
private PortBookBinderClientEvents() {}
@SubscribeEvent
public static void registerBlockEntityRenderers(
net.neoforged.neoforge.client.event.EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), context -> new StarFissureRenderer());
}
@SubscribeEvent
public static void registerScreens(RegisterMenuScreensEvent event) {
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
@@ -0,0 +1,34 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.vertex.PoseStack;
import java.util.List;
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.blockentity.AbstractEndPortalRenderer;
import net.minecraft.client.renderer.blockentity.state.EndPortalRenderState;
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.core.Direction;
/** Retains 1.21.1's UP-at-zero/DOWN-at-one planes with the modern starfield renderer. */
public final class StarFissureRenderer extends AbstractEndPortalRenderer<StarFissureBlockEntity, EndPortalRenderState> {
@Override
public EndPortalRenderState createRenderState() { return new EndPortalRenderState(); }
@Override
public void submit(EndPortalRenderState state, PoseStack pose, SubmitNodeCollector collector,
CameraRenderState camera) {
if (state.facesToShow.contains(Direction.UP)) {
pose.pushPose();
pose.translate(0, -1, 0);
submitCube(List.of(Direction.UP), RenderTypes.endPortal(), pose, collector);
pose.popPose();
}
if (state.facesToShow.contains(Direction.DOWN)) {
pose.pushPose();
pose.translate(0, 1, 0);
submitCube(List.of(Direction.DOWN), RenderTypes.endPortal(), pose, collector);
pose.popPose();
}
}
}
@@ -135,7 +135,11 @@ public final class PortBookNetworking {
biomePlan = random.biomes();
}
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
PortAuthoredWriting.rockPlan(pages));
PortAuthoredWriting.rockPlan(pages, seed),
PortAuthoredWriting.vegetationPlan(pages),
PortAuthoredWriting.dungeonCount(pages),
PortAuthoredWriting.lakePlan(pages, seed),
PortAuthoredWriting.terrainFeaturePlan(pages, seed));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return null;
@@ -3,7 +3,7 @@ package net.dnorberg.rivencraft.registry;
import com.mojang.serialization.MapCodec;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
import net.dnorberg.rivencraft.age.PortRockChunkGenerator;
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.neoforged.bus.api.IEventBus;
@@ -16,7 +16,8 @@ public final class PortChunkGenerators {
static {
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
GENERATORS.register("age_rocks", () -> PortRockChunkGenerator.CODEC);
// Keep the beta codec ID so previously saved Rocks Ages remain readable.
GENERATORS.register("age_rocks", () -> PortFeatureChunkGenerator.CODEC);
}
private PortChunkGenerators() {}
@@ -2,18 +2,30 @@ package net.dnorberg.rivencraft.writing;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;
import java.util.HashMap;
import java.util.Map;
import net.dnorberg.rivencraft.age.PortBiomePlan;
import net.dnorberg.rivencraft.age.PortAgeTerrain;
import net.dnorberg.rivencraft.age.PortRockPlan;
import net.dnorberg.rivencraft.age.PortVegetationPlan;
import net.dnorberg.rivencraft.age.PortLakePlan;
import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.Identifier;
import net.minecraft.resources.ResourceKey;
import net.minecraft.tags.BiomeTags;
/** Authored noise terrain and biome distribution supported by the 26.3 backend. */
public final class PortAuthoredWriting {
private static final int MAX_AUTHORED_BIOMES = 252;
private static final int MAX_ROCKS = 32;
private static final int MAX_VEGETATION = 32;
private static final int MAX_DUNGEONS = 32;
private static final int MAX_LAKES = 32;
private static final Identifier STANDARD_TERRAIN = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "terrain/standard");
private static final Identifier AMPLIFIED_TERRAIN = Identifier.fromNamespaceAndPath(
@@ -28,6 +40,8 @@ public final class PortAuthoredWriting {
Rivencraft.MOD_ID, "terrain/cave");
private static final Identifier ROCKS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/rocks");
private static final Identifier DUNGEONS = Identifier.fromNamespaceAndPath(
Rivencraft.MOD_ID, "feature/dungeons");
private PortAuthoredWriting() {}
@@ -36,19 +50,51 @@ public final class PortAuthoredWriting {
boolean terrain = false;
int biomes = 0;
int controllers = 0;
boolean material = false;
boolean rocks = false;
String pendingMaterial = null;
int rocks = 0;
int vegetation = 0;
int dungeons = 0;
int lakes = 0;
int skylands = 0;
int denseOres = 0;
int starFissures = 0;
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
for (Identifier page : pages) {
if (pendingMaterial != null && !(pendingMaterial.equals("rivencraft:block")
&& (page.equals(ROCKS) || PortTerrainFeaturePlan.acceptsMaterial(page))
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
if (isTerrain(page) && !terrain) {
terrain = true;
} else if (controller(page) != null && controllers++ == 0) {
// An explicit controller needs an authored pool.
} else if (isBlockPage(page) && !material && !rocks
} else if (isBlockPage(page) && pendingMaterial == null
&& PortSymbolPages.find(page) != null
&& PortSymbolPages.find(page).category().equals("rivencraft:block")) {
material = true;
} else if (page.equals(ROCKS) && material && !rocks) {
rocks = true;
&& (PortSymbolPages.find(page).category().equals("rivencraft:block")
|| PortSymbolPages.find(page).category().equals("rivencraft:fluid"))) {
pendingMaterial = PortSymbolPages.find(page).category();
} else if (page.equals(ROCKS) && rocks < MAX_ROCKS) {
pendingMaterial = null;
rocks++;
} else if (PortLakePlan.isLake(page) && lakes < MAX_LAKES) {
pendingMaterial = null;
lakes++;
} else if (PortTerrainFeaturePlan.isSupported(page)
&& terrainFeatures.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
pendingMaterial = null;
terrainFeatures.merge(page, 1, Integer::sum);
} else if (page.equals(PortTerrainFeaturePlan.SKYLANDS)
&& skylands < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
skylands++;
} else if (page.equals(PortTerrainFeaturePlan.DENSE_ORES)
&& denseOres < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
denseOres++;
} else if (page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
&& starFissures < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
starFissures++;
} else if (PortVegetationPlan.isVegetation(page) && vegetation < MAX_VEGETATION) {
vegetation++;
} else if (page.equals(DUNGEONS) && dungeons < MAX_DUNGEONS) {
dungeons++;
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
&& PortSymbolPages.find(page) != null
@@ -57,7 +103,14 @@ public final class PortAuthoredWriting {
biomes++;
} else return false;
}
if (material != rocks || rocks && (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
if (pendingMaterial != null || (rocks > 0 || vegetation > 0)
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
if ((dungeons > 0 || lakes > 0) && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|| pages.contains(CAVE_TERRAIN))) return false;
if (denseOres > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
if (pages.contains(PortTerrainFeaturePlan.FLOATING_ISLANDS) && pages.contains(CAVE_TERRAIN)) return false;
if (pages.stream().anyMatch(PortTerrainFeaturePlan::isSurface)
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
if (controllers != 0) {
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
.filter(value -> value != null).findFirst().orElseThrow();
@@ -107,11 +160,94 @@ public final class PortAuthoredWriting {
return PortAgeTerrain.STANDARD;
}
public static PortRockPlan rockPlan(List<Identifier> pages) {
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
return pages.contains(ROCKS) ? new PortRockPlan(pages.stream()
.filter(PortAuthoredWriting::isBlockPage).map(PortRegistryPages::blockId)
.findFirst().orElseThrow()) : null;
List<Identifier> materials = solidMaterials(pages, seed, ROCKS);
return materials.isEmpty() ? null : new PortRockPlan(materials);
}
public static PortTerrainFeaturePlan terrainFeaturePlan(List<Identifier> pages, long seed) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
Map<Identifier, List<Identifier>> materials = new HashMap<>();
Map<Identifier, Integer> occurrences = new HashMap<>();
List<PortTerrainFeaturePlan.Choice> choices = new ArrayList<>();
for (Identifier page : pages) {
if (!PortTerrainFeaturePlan.isSupported(page)) continue;
if (page.equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS)) {
choices.add(new PortTerrainFeaturePlan.Choice(page, PortTerrainFeaturePlan.CRYSTAL_MATERIAL));
continue;
}
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(pages, seed, feature));
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
choices.add(new PortTerrainFeaturePlan.Choice(page, selected.get(occurrence)));
}
boolean skylands = pages.contains(PortTerrainFeaturePlan.SKYLANDS);
int denseOres = (int) pages.stream().filter(PortTerrainFeaturePlan.DENSE_ORES::equals).count();
boolean starFissures = pages.contains(PortTerrainFeaturePlan.STAR_FISSURES);
return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures ? null
: new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures);
}
private static List<Identifier> solidMaterials(List<Identifier> pages, long seed, Identifier feature) {
if (!pages.contains(feature)) return List.of();
List<Identifier> randomPool = new ArrayList<>(PortRegistryPages.solidMaterialIds());
if (randomPool.isEmpty()) throw new IllegalStateException("No full-block materials are registered");
Collections.shuffle(randomPool, new Random(mix(seed ^ (long) feature.hashCode() * 0x9E3779B97F4A7C15L)));
List<Identifier> materials = new ArrayList<>();
Identifier pending = null;
int randomIndex = 0;
for (Identifier page : pages) {
if (isBlockPage(page)) pending = PortRegistryPages.blockId(page);
else if (page.equals(feature)) {
materials.add(pending != null ? pending : randomPool.get(randomIndex++ % randomPool.size()));
pending = null;
} else pending = null;
}
return List.copyOf(materials);
}
public static PortLakePlan lakePlan(List<Identifier> pages, long seed) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
if (pages.stream().noneMatch(PortLakePlan::isLake)) return null;
List<Identifier> fluidPool = PortRegistryPages.fluidMaterialIds();
if (fluidPool.isEmpty()) throw new IllegalStateException("No source-fluid materials are registered");
Map<Identifier, List<Identifier>> orders = new HashMap<>();
Map<Identifier, Integer> occurrences = new HashMap<>();
List<PortLakePlan.Choice> choices = new ArrayList<>();
Identifier pending = null;
for (Identifier page : pages) {
if (isBlockPage(page)) pending = PortRegistryPages.blockId(page);
else if (PortLakePlan.isLake(page)) {
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
List<Identifier> fluids = orders.computeIfAbsent(page, id -> {
List<Identifier> shuffled = new ArrayList<>(fluidPool);
Collections.shuffle(shuffled, new Random(mix(seed ^ (long) id.hashCode()
* 0x9E3779B97F4A7C15L)));
return shuffled;
});
choices.add(new PortLakePlan.Choice(page,
pending != null ? pending : fluids.get(occurrence % fluids.size())));
pending = null;
} else pending = null;
}
return new PortLakePlan(choices);
}
public static PortVegetationPlan vegetationPlan(List<Identifier> pages) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
List<Identifier> vegetation = pages.stream().filter(PortVegetationPlan::isVegetation).toList();
return vegetation.isEmpty() ? null : new PortVegetationPlan(vegetation);
}
public static int dungeonCount(List<Identifier> pages) {
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
return (int) pages.stream().filter(DUNGEONS::equals).count();
}
private static long mix(long value) {
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
return value ^ value >>> 31;
}
/** Resolves all authored biomes from the active registry before creating a level. */
@@ -121,8 +257,15 @@ public final class PortAuthoredWriting {
List<Identifier> biomes = new ArrayList<>();
PortBiomePlan.Distribution distribution = null;
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
boolean hasSurfaceBiome = false;
boolean hasOreBiome = false;
for (Identifier page : pages) {
if (isTerrain(page) || page.equals(ROCKS) || isBlockPage(page)) continue;
if (isTerrain(page) || page.equals(ROCKS) || page.equals(DUNGEONS)
|| page.equals(PortTerrainFeaturePlan.SKYLANDS)
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|| page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|| PortLakePlan.isLake(page) || PortTerrainFeaturePlan.isSupported(page) || isBlockPage(page)
|| PortVegetationPlan.isVegetation(page)) continue;
PortBiomePlan.Distribution selected = controller(page);
if (selected != null) { distribution = selected; continue; }
Identifier biome = PortRegistryPages.biomeId(page);
@@ -131,8 +274,16 @@ public final class PortAuthoredWriting {
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biome));
if (caveTerrain && !PortRegistryPages.isCaveBiome(holder))
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biome);
if (!PortRegistryPages.isCaveBiome(holder) && !holder.is(BiomeTags.IS_END))
hasSurfaceBiome = true;
if (net.dnorberg.rivencraft.age.PortDenseOres.hasOres(holder)) hasOreBiome = true;
biomes.add(biome);
}
if (pages.stream().anyMatch(PortVegetationPlan::isVegetation)
&& !biomes.isEmpty() && !hasSurfaceBiome)
throw new IllegalArgumentException("Vegetation pages need at least one surface Overworld biome");
if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome)
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
if (distribution == PortBiomePlan.Distribution.NATIVE)
return new PortBiomePlan(distribution, List.of());
if (biomes.isEmpty()) return null;
@@ -76,13 +76,27 @@ public final class PortRegistryPages {
public static List<ItemStack> materialPages(boolean fluids) {
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
return materialIds(category).stream()
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
}
public static boolean isCaveBiome(net.minecraft.core.Holder.Reference<net.minecraft.world.level.biome.Biome> holder) {
String path = holder.key().identifier().getPath();
/** The same full-block pool used by Creative pages, in stable registry-ID order. */
public static List<Identifier> solidMaterialIds() {
return materialIds("rivencraft:block");
}
public static List<Identifier> fluidMaterialIds() {
return materialIds("rivencraft:fluid");
}
private static List<Identifier> materialIds(String category) {
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
.toList();
}
public static boolean isCaveBiome(net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> holder) {
String path = holder.unwrapKey().map(key -> key.identifier().getPath()).orElse("");
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|| path.equals("deep_dark") || path.endsWith("_caves");
}
@@ -0,0 +1,5 @@
{
"linking.link-fissure": {
"sounds": [{"name": "rivencraft:linking/link-fissure"}]
}
}
@@ -0,0 +1,15 @@
{
"feature": "minecraft:monster_room",
"placement": [
{ "type": "minecraft:count", "count": 8 },
{ "type": "minecraft:in_square" },
{
"type": "minecraft:height_range",
"height": {
"type": "minecraft:uniform",
"min_inclusive": { "above_bottom": 1 },
"max_inclusive": { "below_top": 1 }
}
}
]
}
+142 -5
View File
@@ -77,6 +77,18 @@ send_and_wait 'rivencraft_port age create_multi smoke_void_multi 13580 void' 'Cr
send_and_wait 'rivencraft_port age create_multi smoke_cave_multi 112234 cave' 'Created rivencraft:age/smoke_cave_multi with seed 112234 terrain=CAVE'
send_and_wait 'rivencraft_port age create_random smoke_random 889900' 'Random Age plan: terrain='
send_and_wait 'rivencraft_port age create_rocks smoke_rocks 445566' 'Created rivencraft:age/smoke_rocks with seed 445566 terrain=FLAT'
send_and_wait 'rivencraft_port age create_rocks_multi smoke_rocks_multi 445566' 'Created rivencraft:age/smoke_rocks_multi with seed 445566 terrain=FLAT'
send_and_wait 'rivencraft_port age create_rocks_random smoke_rocks_random 445566' 'Created rivencraft:age/smoke_rocks_random with seed 445566 terrain=FLAT'
send_and_wait 'rivencraft_port age create_vegetation smoke_vegetation 24680' 'Created rivencraft:age/smoke_vegetation with seed 24680 terrain=FLAT'
send_and_wait 'rivencraft_port age create_dungeons smoke_dungeons 135791' 'Created rivencraft:age/smoke_dungeons with seed 135791 terrain=STANDARD'
send_and_wait 'rivencraft_port age create smoke_lake_control 667788' 'Created rivencraft:age/smoke_lake_control with seed 667788 terrain=STANDARD'
send_and_wait 'rivencraft_port age create_lakes smoke_lakes 667788' 'Created rivencraft:age/smoke_lakes with seed 667788 terrain=STANDARD'
send_and_wait 'rivencraft_port age create_lakes_random smoke_lakes_random 667788' 'Created rivencraft:age/smoke_lakes_random with seed 667788 terrain=STANDARD'
# This fixture includes a rare island candidate near the origin at normal rarity.
send_and_wait 'rivencraft_port age create_islands smoke_islands 887767 void' 'Created rivencraft:age/smoke_islands with seed 887767 terrain=VOID'
send_and_wait 'rivencraft_port age create_islands smoke_islands_flat 887767 flat' 'Created rivencraft:age/smoke_islands_flat with seed 887767 terrain=FLAT'
send_and_wait 'rivencraft_port age create_islands smoke_islands_noise 887767 standard' 'Created rivencraft:age/smoke_islands_noise with seed 887767 terrain=STANDARD'
send_and_wait 'rivencraft_port age create_islands_random smoke_islands_random 887767 void' 'Created rivencraft:age/smoke_islands_random with seed 887767 terrain=VOID'
# The index must reach disk while the server is still running. A graceful
# shutdown would hide the interrupted-save regression this checks.
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
@@ -94,6 +106,15 @@ gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void_multi'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave_multi'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_random'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks_multi'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks_random'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_vegetation'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_dungeons'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_lakes'
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_lakes_random'
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
gzip -dc "$index_file" | strings | grep -Fq "rivencraft:age/$island_age"
done
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
@@ -104,6 +125,52 @@ send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smok
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_rocks_multi' 'rivencraft:age/smoke_rocks_multi seed=445566 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_rocks_random' 'rivencraft:age/smoke_rocks_random seed=445566 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_vegetation' 'rivencraft:age/smoke_vegetation seed=24680 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_dungeons' 'rivencraft:age/smoke_dungeons seed=135791 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age dungeons_probe smoke_dungeons' 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners='
dungeons_before=$(grep -F 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners=' "$log_file" | tail -1 | sed 's/.*spawners=//')
[[ "$dungeons_before" =~ ^[1-9][0-9]*$ ]] || {
printf 'Authored Dungeons placed no monster spawners: %s\n' "$dungeons_before" >&2
exit 1
}
send_and_wait 'rivencraft_port age lakes_probe smoke_lake_control' 'Lakes probe rivencraft:age/smoke_lake_control pages=0'
lake_control=$(grep -F 'Lakes probe rivencraft:age/smoke_lake_control pages=0' "$log_file" | tail -1 | sed 's/.* water=//; s/ lava=.*//')
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes' 'Lakes probe rivencraft:age/smoke_lakes pages=24 fluids=[minecraft:water]'
lakes_before=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
lake_water=$(printf '%s' "$lakes_before" | sed 's/.* water=//; s/ lava=.*//')
[[ "$lake_water" -gt "$lake_control" ]] || {
printf 'Water lake pages did not add water: %s -> %s\n%s\n' "$lake_control" "$lake_water" "$lakes_before" >&2
exit 1
}
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes_random' 'Lakes probe rivencraft:age/smoke_lakes_random pages=24'
lakes_random_before=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes_random pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
[[ "$lakes_random_before" == *minecraft:water* && "$lakes_random_before" == *minecraft:lava* ]] || {
printf 'Unmodified lake pages did not resolve both vanilla fluids: %s\n' "$lakes_random_before" >&2
exit 1
}
declare -A islands_before
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
send_and_wait "rivencraft_port age islands_probe $island_age" "Islands probe rivencraft:age/$island_age pages=4"
island_result=$(grep -F "Islands probe rivencraft:age/$island_age pages=4" "$log_file" | tail -1 | sed 's/.*Islands probe /Islands probe /')
island_blocks=$(printf '%s' "$island_result" | sed 's/.* count=//; s/ fingerprint=.*//')
[[ "$island_blocks" =~ ^[1-9][0-9]*$ ]] || {
printf 'Floating Islands placed no selected material: %s\n' "$island_result" >&2
exit 1
}
islands_before[$island_age]=$island_result
done
island_materials=${islands_before[smoke_islands_random]#*materials=[}
island_materials=${island_materials%%]*}
IFS=',' read -r -a island_material_list <<< "$island_materials"
[[ ${#island_material_list[@]} -eq 4 ]] || {
printf 'Repeated unmodified islands did not choose independent materials: %s\n' "$island_materials" >&2
exit 1
}
send_and_wait 'execute in rivencraft:age/smoke_islands if block 0 0 0 minecraft:air run say Island Void remains empty below' 'Island Void remains empty below'
send_and_wait 'execute in rivencraft:age/smoke_islands_flat if block 0 0 0 minecraft:bedrock run say Island Flat retains its floor' 'Island Flat retains its floor'
send_and_wait 'rivencraft_port age biome smoke_islands 128 128' 'rivencraft:age/smoke_islands biome at 128,128=minecraft:forest'
send_and_wait 'execute in rivencraft:age/smoke_flat run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_void run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_flat_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat_multi to be force loaded'
@@ -111,12 +178,40 @@ send_and_wait 'execute in rivencraft:age/smoke_void_multi run forceload add 0 0'
send_and_wait 'execute in rivencraft:age/smoke_cave run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_cave_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave_multi to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_rocks run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks to be force loaded'
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count='
rocks_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count=' "$log_file" | tail -1 | sed 's/.*count=//')
send_and_wait 'execute in rivencraft:age/smoke_rocks_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks_multi to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_rocks_random run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks_random to be force loaded'
send_and_wait 'execute in rivencraft:age/smoke_vegetation run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_vegetation to be force loaded'
send_and_wait 'rivencraft_port age vegetation_probe smoke_flat' 'Vegetation probe rivencraft:age/smoke_flat flowers='
flat_vegetation=$(grep -F 'Vegetation probe rivencraft:age/smoke_flat flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
send_and_wait 'rivencraft_port age vegetation_probe smoke_vegetation' 'Vegetation probe rivencraft:age/smoke_vegetation flowers='
authored_vegetation=$(grep -F 'Vegetation probe rivencraft:age/smoke_vegetation flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
flat_flowers=${flat_vegetation%% logs=*}
flat_logs=${flat_vegetation##* logs=}
authored_flowers=${authored_vegetation%% logs=*}
authored_logs=${authored_vegetation##* logs=}
[[ "$authored_flowers" -gt "$flat_flowers" && "$authored_logs" -gt "$flat_logs" ]] || {
printf 'Authored vegetation did not increase flowers and logs: %s -> %s\n' \
"$flat_vegetation" "$authored_vegetation" >&2
exit 1
}
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count='
rocks_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count=' "$log_file" | tail -1 | sed 's/.*count=//')
[[ "$rocks_before" =~ ^[1-9][0-9]*$ ]] || {
printf 'Authored Rocks placed no gold blocks: %s\n' "$rocks_before" >&2
exit 1
}
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_multi' 'Rocks probe rivencraft:age/smoke_rocks_multi materials=[minecraft:gold_block, minecraft:gold_block, minecraft:gold_block, minecraft:gold_block] count='
rocks_multi_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_multi materials=' "$log_file" | tail -1 | sed 's/.*count=//')
[[ "$rocks_multi_before" =~ ^[1-9][0-9]*$ && "$rocks_multi_before" -gt "$rocks_before" ]] || {
printf 'Repeated Rocks pages did not increase placement: %s -> %s\n' "$rocks_before" "$rocks_multi_before" >&2
exit 1
}
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_random' 'Rocks probe rivencraft:age/smoke_rocks_random materials=['
rocks_random_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_random materials=' "$log_file" | tail -1 | sed 's/.*Rocks probe /Rocks probe /')
[[ "$rocks_random_before" =~ count=[1-9][0-9]*$ ]] || {
printf 'Unmodified Rocks page placed no material: %s\n' "$rocks_random_before" >&2
exit 1
}
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 0 0 minecraft:bedrock run say Flat bedrock starts at Y0' 'Flat bedrock starts at Y0'
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 3 0 minecraft:grass_block run say Flat surface is Y3' 'Flat surface is Y3'
send_and_wait 'execute in rivencraft:age/smoke_void if block 0 0 0 minecraft:air run say Void remains empty' 'Void remains empty'
@@ -155,8 +250,32 @@ send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smok
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count='
rocks_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks material=minecraft:gold_block count=' "$log_file" | tail -1 | sed 's/.*count=//')
send_and_wait 'rivencraft_port age sample smoke_rocks_multi' 'rivencraft:age/smoke_rocks_multi seed=445566 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_rocks_random' 'rivencraft:age/smoke_rocks_random seed=445566 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_vegetation' 'rivencraft:age/smoke_vegetation seed=24680 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age sample smoke_dungeons' 'rivencraft:age/smoke_dungeons seed=135791 chunkOffset=0 terrain='
send_and_wait 'rivencraft_port age dungeons_probe smoke_dungeons' 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners='
dungeons_after=$(grep -F 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners=' "$log_file" | tail -1 | sed 's/.*spawners=//')
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes' 'Lakes probe rivencraft:age/smoke_lakes pages=24 fluids=[minecraft:water]'
lakes_after=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes_random' 'Lakes probe rivencraft:age/smoke_lakes_random pages=24'
lakes_random_after=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes_random pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
send_and_wait "rivencraft_port age islands_probe $island_age" "Islands probe rivencraft:age/$island_age pages=4"
island_result=$(grep -F "Islands probe rivencraft:age/$island_age pages=4" "$log_file" | tail -1 | sed 's/.*Islands probe /Islands probe /')
[[ "${islands_before[$island_age]}" == "$island_result" ]] || {
printf 'Floating Islands changed across restart: %s\n%s\n' "${islands_before[$island_age]}" "$island_result" >&2
exit 1
}
done
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count='
rocks_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count=' "$log_file" | tail -1 | sed 's/.*count=//')
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_multi' 'Rocks probe rivencraft:age/smoke_rocks_multi materials=['
rocks_multi_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_multi materials=' "$log_file" | tail -1 | sed 's/.*count=//')
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_random' 'Rocks probe rivencraft:age/smoke_rocks_random materials=['
rocks_random_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_random materials=' "$log_file" | tail -1 | sed 's/.*Rocks probe /Rocks probe /')
send_and_wait 'rivencraft_port age vegetation_probe smoke_vegetation' 'Vegetation probe rivencraft:age/smoke_vegetation flowers='
authored_vegetation_after=$(grep -F 'Vegetation probe rivencraft:age/smoke_vegetation flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
send_and_wait 'rivencraft_port age biome smoke_flat_multi 384 128' 'rivencraft:age/smoke_flat_multi biome at 384,128=minecraft:forest'
send_and_wait 'rivencraft_port age biome smoke_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
@@ -185,5 +304,23 @@ random_after=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 te
"$rocks_before" "$rocks_after" >&2
exit 1
}
[[ "$rocks_multi_before" == "$rocks_multi_after" && "$rocks_random_before" == "$rocks_random_after" ]] || {
printf 'Repeated or unmodified Rocks changed across restart\n' >&2
exit 1
}
[[ "$authored_vegetation" == "$authored_vegetation_after" ]] || {
printf 'Authored vegetation changed across restart: %s -> %s\n' \
"$authored_vegetation" "$authored_vegetation_after" >&2
exit 1
}
[[ "$dungeons_before" == "$dungeons_after" ]] || {
printf 'Authored Dungeons changed across restart: %s -> %s\n' \
"$dungeons_before" "$dungeons_after" >&2
exit 1
}
[[ "$lakes_before" == "$lakes_after" && "$lakes_random_before" == "$lakes_random_after" ]] || {
printf 'Explicit or random lake plans/blocks changed across restart\n' >&2
exit 1
}
stop_server
printf '26.3 dedicated-server smoke passed: Standard, Flat, Void, Cave, random and authored Rocks Ages persisted across restart.\n'
printf '26.3 dedicated-server smoke passed: terrain, biomes, Rocks, vegetation, Dungeons, lakes, and Floating Islands persisted across restart.\n'