Public Access
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48fbae9be9 | ||
|
|
c223f3fdca | ||
|
|
d75154fa9b | ||
|
|
9ccec18c18 | ||
|
|
d9464fad76 | ||
|
|
d6e9a58a24 |
No files matched your search
@@ -7,9 +7,19 @@ 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, structure and vegetation pages, independent
|
||||
Age weather, 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.65-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.
|
||||
|
||||
@@ -495,6 +495,411 @@ accepts the existing authored distribution pages for those terrains. New Flat
|
||||
chunks can vary vegetation by biome; Void keeps biome climate without adding
|
||||
a floor or decorations. These are beta-port slices, not full 1.21.1 parity.
|
||||
|
||||
The local `1.0.1.37-beta.1` slice ports Cave terrain authoring using the
|
||||
26.3 caves noise preset. Cave and Nether Biome pages can be distributed
|
||||
through an Age, and travel searches for footing from Y=64 downward rather
|
||||
than mistaking the cave roof for the surface. Nether-style cave material
|
||||
replacement from 1.21.1 remains a separate port task. The local
|
||||
`1.0.1.38-beta.1` follow-up
|
||||
keeps Cave Ages dry by default: the vanilla 26.3 Cave preset otherwise fills
|
||||
low openings with water. This changes newly generated chunks only; a future
|
||||
Flooded page will deliberately set an authored water level in both maintained
|
||||
versions.
|
||||
The local `1.0.1.39-beta.1` follow-up makes the 26.3 landing search reject
|
||||
fluid at feet and head height and prevents a submerged fallback platform.
|
||||
|
||||
The local `1.0.1.40-beta.1` slice gives pageless Descriptive Books a
|
||||
seed-selected terrain and biome plan. It follows the 1.21.1 rank weights for
|
||||
terrain and biome distribution, selects registered Cave/Nether biomes for Cave
|
||||
Ages and overworld-source biomes for other non-Void terrains, and persists the
|
||||
resolved choices. Other random Age traits remain a later port task.
|
||||
|
||||
The local `1.0.1.41-beta.1` slice adds the first Age-scoped feature wrapper to
|
||||
the 26.3 generator. A Block page followed by Rocks now places clusters of the
|
||||
chosen block in new Standard, Amplified, Island, or Flat terrain chunks. The
|
||||
Age index persists that selection, and the wrapper delegates all vanilla
|
||||
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.
|
||||
|
||||
The local `1.0.1.55-beta.1` slice begins the authored structure-set port with
|
||||
Villages, Mineshafts, and Strongholds. Saved page occurrences use the existing
|
||||
feature plan's new optional structure list. Terrain gates, random-spread
|
||||
frequency/spacing scaling, and bounded concentric-ring count/spread follow
|
||||
1.21.1. Modern `AbstractSpreadingStructurePlacement` accessors replace the old
|
||||
placement-class accessors. Scaled holder copies never mutate global registries.
|
||||
|
||||
Structure-state planning includes authored eligibility biomes while continuing
|
||||
to sample the actual terrain source. A scoped Structure mixin carries over
|
||||
1.21.1's authored biome override. The wrapper handles structure starts itself
|
||||
when a structure plan exists, allowing the hook to recognize the authored Age;
|
||||
unrelated/unplanned generators remain delegated. Only these three controlled
|
||||
families are gated in this slice; remaining structure families, random picks,
|
||||
and biome-mismatch instability remain unported. Existing plans without the
|
||||
optional list retain their previous structure path. Mod metadata now correctly
|
||||
calls this target Beta rather than Alpha.
|
||||
|
||||
The player passed Star Fissures. Build/static checks pass for this new slice.
|
||||
A disposable dedicated-server startup reached `Done` without mixin-loading
|
||||
errors. Its console `/stop` command hit a null-level CommandSourceStack error;
|
||||
the exact test JVM was then stopped through SIGTERM and saved/shut down. This
|
||||
was a startup check, not authored generation or density validation. The
|
||||
disposable world is removed; client authoring, actual structures, density,
|
||||
biome overrides, and restart checks are handed to the player.
|
||||
|
||||
The local `1.0.1.56-beta.1` follow-up corrects the structure biome override.
|
||||
Client logs confirmed the build-55 mixin was applied, but 26.3 JigsawPlacement
|
||||
checks `GenerationContext.couldStructureExistInColumn` inside generation,
|
||||
before the final check bypassed by the 1.21.1 hook. The replacement ModifyArg
|
||||
changes only the biome predicate passed to the GenerationContext constructor
|
||||
in Structure.generate for explicitly authored Age structures. Both early
|
||||
and final checks now see the override; all other generation constraints and
|
||||
unrelated generators retain their existing behavior.
|
||||
|
||||
Build/static checks pass. An isolated `--initSettings` bootstrap initialized
|
||||
settings without a mixin application error, then FML exited nonzero because
|
||||
that settings-only mode creates no server thread. This is not a full server
|
||||
or generation pass. No world was created, and the temporary settings directory
|
||||
is removed. In-game validation requires a new Age or ungenerated chunks;
|
||||
previously rejected chunks are not regenerated. No commit/release is made.
|
||||
|
||||
The local `1.0.1.57-beta.1` slice completes the existing 1.21.1 structure-page
|
||||
mapping (Dungeons remains its separate placed feature) and enables 26.3's
|
||||
Abandoned Camps page. The shared planner now recognizes 15 structure sets.
|
||||
Page/terrain restrictions match AgeStructureSelection for the 1.21.1 families;
|
||||
Abandoned Camps follows surface noise terrain. No material modifiers are added.
|
||||
|
||||
The separate Fortress/Bastion set resources are reused directly from 1.21.1.
|
||||
The controlled-set gate includes the vanilla combined Nether set, preventing
|
||||
unrequested cross-family placement. All structure pages share the previously
|
||||
validated saved list, density logic, and corrected context-level biome override.
|
||||
Unknown non-spreading placements are left unchanged rather than cast unsafely.
|
||||
Unmodified/no-authored-structure Ages retain their prior native path; previously
|
||||
saved authored plans now gate all supported families in new chunks. Instability
|
||||
and random feature selection remain pending.
|
||||
|
||||
The player passed the build-56 biome fix. Build, diff checks, all 15 registry
|
||||
and Creative-page mappings, and target structure resource existence checks pass.
|
||||
Packaged Fortress/Bastion definitions match 1.21.1 byte-for-byte. No server is
|
||||
started; actual structure placement, terrain-specific limits, repeated density,
|
||||
and restart validation are handed to the player. Changes remain local.
|
||||
|
||||
The local `1.0.1.58-beta.1` slice ports Caves/Ravines feature pages separately
|
||||
from Cave terrain. A new optional saved carver-page list participates in the
|
||||
existing feature-plan lifecycle. Grammar accepts noise terrain including Cave,
|
||||
rejects Flat/Void and material modifiers, and checks the modern CARVER registry.
|
||||
|
||||
26.3 combines carving with terrain construction and exposes carvers that emit
|
||||
geometry rather than mutate blocks. A scoped NoiseBasedChunkGenerator mixin
|
||||
adds authored geometry to its native CarvingMask immediately before that mask
|
||||
is tested/applied. It only acts for the matching runtime Age wrapper, using
|
||||
its frozen seed and actual biome resolver. Vanilla applies the combined mask
|
||||
with its own aquifer/surface rules; no closed NoiseChunk is accessed afterward.
|
||||
The source range, salts, Nether cave selection, and Nether ravine skip match
|
||||
1.21.1. Duplicate Caves/Ravines repeat identical seeds in the baseline, so the
|
||||
port avoids redundant identical mask passes while retaining authored pages.
|
||||
Added terrain volumes still follow the combined terrain operation, as noted
|
||||
for the earlier Floating Islands port.
|
||||
|
||||
The player passed structure slice 57. Build/static checks and packaging checks
|
||||
for the hook, Creative entries, and three required vanilla carvers pass. A
|
||||
settings-only bootstrap completed initialization without a mixin application
|
||||
error; FML then returned nonzero because no server thread exists in that mode.
|
||||
This is not world-generation validation. No world was created; the temporary
|
||||
settings directory is removed. Actual carving and restart checks are manual,
|
||||
and no commit/push/publication is performed.
|
||||
|
||||
The local `1.0.1.59-beta.1` slice replaces stale per-page port information
|
||||
and adds the requested red hue for unsupported pages. A shared
|
||||
PortAuthoredWriting.isPageImplemented classifier uses the same terrain,
|
||||
controller, material/biome, and feature recognizers as the grammar, without
|
||||
misclassifying context-dependent modifiers/controllers by testing them alone.
|
||||
Page tooltips, the extracted item-render color, and the Binder background use
|
||||
this single availability check. Tinting does not generate extra textures or
|
||||
change stored page data. Corrupted-page names/descriptions remain obfuscated.
|
||||
|
||||
Port language overrides remove the old Standard/one-Biome claim, distinguish
|
||||
implemented/unimplemented pages and invalid combinations, and correct all
|
||||
15 structure-family descriptions, material adjacency, and selected terrain/
|
||||
feature restrictions. The 1.21.1 language assets are unchanged. This is a UI
|
||||
and information slice, not new generation functionality.
|
||||
|
||||
The player passed Caves/Ravines. Build, diff checks, unique language-key checks,
|
||||
and packaged-language override checks pass. No server/world is started.
|
||||
Inventory tint, Binder tint/layering, and tooltip readability await the
|
||||
player's visual check. Changes remain local and unpublished.
|
||||
|
||||
The local `1.0.1.60-beta.1` slice implements the four requested 26.3-only
|
||||
vegetation pages: Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf
|
||||
Mushrooms. They extend PortVegetationPlan recognition, so grammar, saved plans,
|
||||
Creative status, item tint, and tooltip availability agree without another
|
||||
page list. The existing ordered vegetation cap and surface-terrain/biome
|
||||
restrictions are retained. The 1.21.1 code/assets are unchanged.
|
||||
|
||||
Poplar Trees selects only the three vanilla poplar variants, retaining their
|
||||
relative native weights and six placement attempts. Fallen poplars reuse the
|
||||
vanilla feature but add authored in-square/surface placement before its sapling
|
||||
check; the vanilla placed feature alone has no surface-height placement. Red
|
||||
Shrubs directly reuses its registered patch. No tree/shrub algorithm or artwork
|
||||
is duplicated.
|
||||
|
||||
Shelf Mushrooms is a tree decorator in 26.3, not a placed feature. The authored
|
||||
page therefore applies ShelfMushroomDecorator to existing connected log hosts
|
||||
at its position in the vegetation sequence. Section palettes skip log-free
|
||||
sections, repeated pages reuse the host scan, and tree placements invalidate
|
||||
that cache. Host ordering and page seeds are deterministic; callback writes
|
||||
are chunk-local, fluid-free, supported, and height-bounded. The decorator keeps
|
||||
its native standing/fallen logic and side/age/adjacency checks. This is an
|
||||
authored extension using the native decorator, not a new vanilla feature ID.
|
||||
|
||||
Build/static and resource-reference checks pass. The four pages' descriptions
|
||||
explain their actual effects and requirements. No server/world is started;
|
||||
the player validates vegetation appearance, support/tint, ordering, and restart.
|
||||
Changes remain local and unpublished.
|
||||
|
||||
The local `1.0.1.61-beta.1` follow-up separates log-hosted Shelf Mushrooms
|
||||
from surface plants. A shared requiresSurface classification now governs
|
||||
terrain/explicit-biome validation; decoration handles Shelf Mushrooms before
|
||||
the surface-biome skip. Trees, shrubs, and flowers retain their restrictions.
|
||||
Void/Cave/End-biome log hosts are no longer excluded by the vegetation path.
|
||||
|
||||
The host scan is a stable log-position list rather than connected tree groups.
|
||||
Native tree decorators assume a meaningful base and only decorate a limited
|
||||
height band, which is unsuitable for log tendrils. The authored extension
|
||||
therefore checks exposed log faces at all heights, reusing the native decorator's
|
||||
replaceability/water predicate and block facing/age/survival rules, with spacing
|
||||
and chunk bounds. Cached scans still avoid repeated full-volume work. No tree
|
||||
page or log-producing page is required, but actual log hosts must exist during
|
||||
chunk generation. This supersedes slice 60's whole-tree height-band behavior.
|
||||
|
||||
Build/static checks pass. Manual validation: new Void + Oak Log + Tendrils +
|
||||
Shelf Mushrooms Age, without any tree page; inspect exposed log faces and
|
||||
restart/new-chunk generation. No server is started and nothing is published.
|
||||
|
||||
The player passed the local `1.0.1.62-beta.1` sound slice: the complete 1.21.1
|
||||
sound manifest/OGGs are reused, book travel emits modifier-selected audio and
|
||||
cloud bursts, and Star Fissures play audio after arrival as well as departure.
|
||||
Portal/meteor events are registered but their gameplay ports remain separate.
|
||||
|
||||
The local `1.0.1.63-beta.1` slice adds authored Normal/Fast/Slow/Off/Always/Storm
|
||||
weather policies. One policy is accepted per book, with availability/tint and
|
||||
tooltips following the shared classifier. Cave terrain always suppresses it.
|
||||
An omitted policy defaults to Normal for now; random-trait selection is pending.
|
||||
|
||||
Unlike 1.21.1, 26.3 ServerLevel.getWeatherData delegates to the server's shared
|
||||
weather object. A scoped Runtime Age ServerLevel subclass instead loads native
|
||||
WeatherData from that Age's data storage, alongside its saved policy. Native
|
||||
weather ticking and client packets remain in charge; only the data owner and
|
||||
Legacy-style duration policy change. The constructor replaces inherited global
|
||||
rain/thunder visual levels before publishing the level. Retirement, reseeding,
|
||||
reload and restart retain the policy through existing per-Age storage lifecycle.
|
||||
Vanilla /weather remains a global server command; the Age policy is not a
|
||||
global command override. Native biome precipitation rules still apply.
|
||||
|
||||
Build/diff checks pass. A disposable dedicated server created all six weather
|
||||
policies: every Age reported independent native data and the expected flags/
|
||||
duration ranges. Setting global thunder left the Off Age clear. Unload/reload
|
||||
retained Storm, and a full stop/restart restored all six policies and their
|
||||
live timers. Both servers shut down cleanly; the temporary test world is
|
||||
deleted after completion. Client appearance, cross-Age travel, authoring/tint,
|
||||
and Cave suppression remain manual checks.
|
||||
No commit/push/publication is authorized by the player's testing pass alone.
|
||||
|
||||
The local `1.0.1.64-beta.1` follow-up fixes weather-only authoring. Build 63's
|
||||
server weather checks bypassed the Binder's biome compiler, whose exclusion
|
||||
list did not yet include weather. As a result weather/storm was incorrectly
|
||||
resolved as a biome ID despite grammar acceptance. The biome compiler now
|
||||
selects actual biome-category entries after full-book grammar validation;
|
||||
controllers retain their separate handling. This avoids repeating the same
|
||||
missed-exclusion bug as more non-biome page families are ported. The operator
|
||||
weather fixture now calls that compiler before backend creation. All six
|
||||
weather-only symbol lists compiled and created their expected weather states
|
||||
on a fresh dedicated server. Build/diff checks pass. The server shut down
|
||||
cleanly and its disposable world was removed; player GUI validation remains.
|
||||
|
||||
The local `1.0.1.65-beta.1` spawn correction traces non-Cave arrival columns
|
||||
from maximum build height, inspecting actual fully generated chunk blocks.
|
||||
The configured Flat-layer shortcut could select ground below later authored
|
||||
terrain, and the old heightmap-only path did not explicitly ensure an exposed
|
||||
surface. The new path stops at the first fluid/leaves/collision, requires dry
|
||||
footing and headroom, and otherwise tries the next column rather than searching
|
||||
below an obstruction. Chunk lookup is cached per column and a mutable position
|
||||
avoids per-air-block allocations. A fallback platform goes above the actual
|
||||
highest obstruction; empty Void retains floor Y=63. Surface footing accepts
|
||||
leaves, rejects fluid-bearing floors, and checks build/border bounds.
|
||||
|
||||
Stored non-Cave Star Fissure arrivals also use the surface search rather than
|
||||
accepting old underground pockets. Cave terrain keeps its Y=64-down search.
|
||||
Exact bound Linking Books and the general fissure Overworld-return anchor
|
||||
search are not redirected to the sky. A read-only surface probe command helps
|
||||
validate the chosen column/nearby surface (chunk generation is possible).
|
||||
Build/diff checks pass. A disposable dedicated-server probe selected Y=4 on
|
||||
plain Flat ground, Y=141 above leaves placed at Y=140, and a neighboring dry
|
||||
column instead of descending below water added above those leaves. A larger
|
||||
roof-fixture fill was rejected by vanilla because some target chunks were not
|
||||
loaded; that fixture is not claimed as tested. The server shut down cleanly,
|
||||
and its temporary world was deleted. Actual player arrival in the affected
|
||||
Age and the unchanged Cave/Linking Book paths remain manual checks.
|
||||
|
||||
## Verification of this first slice
|
||||
|
||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||
@@ -503,3 +908,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.
|
||||
+21
-2
@@ -1,4 +1,4 @@
|
||||
# Publishing Rivencraft with the Modrinth and CurseForge APIs
|
||||
# Publishing Rivencraft with the Gitea, Modrinth, and CurseForge APIs
|
||||
|
||||
The local helper is [`scripts/publish_mod_sites.py`](../scripts/publish_mod_sites.py).
|
||||
It previews a release unless `--publish` is explicitly supplied. The current
|
||||
@@ -8,6 +8,12 @@ is enabled for it.
|
||||
|
||||
## One-time token setup
|
||||
|
||||
For Gitea, an existing personal access token is sufficient if it has
|
||||
`write:repository` scope. That scope covers release creation and attachment
|
||||
upload; do not grant `all` if a narrower token is available. Create or inspect
|
||||
tokens under [Gitea user settings → Applications](https://git.paddedcell.org/user/settings/applications).
|
||||
The helper checks token identity and scope without displaying the secret.
|
||||
|
||||
Create a [Modrinth personal access token](https://docs.modrinth.com/api/#authentication)
|
||||
in account settings with `VERSION_CREATE` (upload) and `USER_READ` (read-only
|
||||
credential check) scopes. Create a
|
||||
@@ -21,6 +27,7 @@ corresponding token into the hidden prompt:
|
||||
```sh
|
||||
python3 scripts/publish_mod_sites.py store-token modrinth
|
||||
python3 scripts/publish_mod_sites.py store-token curseforge
|
||||
python3 scripts/publish_mod_sites.py store-token gitea
|
||||
```
|
||||
|
||||
The tokens are stored in the desktop Secret Service keyring under
|
||||
@@ -34,6 +41,7 @@ Verify the credentials without uploading anything:
|
||||
```sh
|
||||
python3 scripts/publish_mod_sites.py check-token modrinth
|
||||
python3 scripts/publish_mod_sites.py check-token curseforge
|
||||
python3 scripts/publish_mod_sites.py check-token gitea
|
||||
```
|
||||
|
||||
## Release workflow
|
||||
@@ -54,7 +62,18 @@ For a *new* version only, append `--publish` to upload to both sites. Use
|
||||
existing version number; CurseForge may not, so check its file list before any
|
||||
retry after a partial failure. CurseForge approval may take additional time.
|
||||
|
||||
The helper uses Modrinth's documented multipart
|
||||
The default destination is now all three sites. The Gitea release step requires
|
||||
the annotated version tag to have been pushed first. Gitea's `Vx.y.z.b-mc26.3-beta.1`
|
||||
tag convention is derived from the version and channel; an existing release
|
||||
with different title or notes is never overwritten. If release creation
|
||||
succeeds but its JAR upload fails, rerun with `--site gitea --publish` to resume
|
||||
only the missing attachment. `--site both` retains the earlier Modrinth and
|
||||
CurseForge-only behavior.
|
||||
|
||||
The helper uses Gitea's documented
|
||||
[`POST /repos/{owner}/{repo}/releases`](https://docs.gitea.com/api/operations/repo-create-release/)
|
||||
and [release attachment endpoint](https://docs.gitea.com/api/operations/repo-create-release-attachment/),
|
||||
plus Modrinth's documented multipart
|
||||
[`POST /version`](https://docs.modrinth.com/api/operations/createversion/) and
|
||||
CurseForge's documented
|
||||
[`POST /api/projects/{projectId}/upload-file`](https://support.curseforge.com/support/solutions/articles/9000197321)
|
||||
|
||||
+385
-4
@@ -3,10 +3,10 @@
|
||||
This is an **experimental beta port** for issues #97–#104, not gameplay parity
|
||||
with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and
|
||||
their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can
|
||||
author Standard, Amplified, Island, Flat, or Void terrain with supported
|
||||
biome-distribution pages. Other terrain families, feature/material effects,
|
||||
portals, and much of
|
||||
the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
|
||||
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
|
||||
biome-distribution pages, selected material/vegetation/structure features, and
|
||||
independent Age weather. Celestial/lighting systems, instability, crystal
|
||||
portals, and much of the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
|
||||
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
|
||||
|
||||
The entries below record how the port was built, including earlier alpha-only
|
||||
@@ -177,6 +177,387 @@ surface decoration in Flat. Void continues to generate no floor or biome
|
||||
decoration. The operator-only `age create_multi` probe creates two-biome test
|
||||
Ages for the dedicated-server smoke test.
|
||||
|
||||
The local `1.0.1.37-beta.1` slice adds the Cave terrain page to Binder
|
||||
authoring. It uses Minecraft's caves noise settings, accepts Cave and Nether
|
||||
biome pages (including multi-biome distribution), and searches below Y=64 for
|
||||
a safe arrival instead of landing on the roof. The 26.3 vanilla Cave preset
|
||||
defaults to water below Y=32, so this port derives a dry Cave setting with
|
||||
air as its default fluid and the sea level below the world floor. This applies
|
||||
to newly generated Cave chunks; it does not drain existing chunks. Cave-specific Nether surface
|
||||
materials from 1.21.1 are not ported yet. The dedicated-server smoke now
|
||||
creates and reloads Cave Ages alongside the other terrain families.
|
||||
|
||||
The dry-Cave follow-up is packaged as `1.0.1.38-beta.1`; no 26.3 release is
|
||||
published until the in-game check passes and publication is explicitly requested.
|
||||
|
||||
The local `1.0.1.39-beta.1` follow-up rejects fluid at both feet and head
|
||||
height during safe-landing searches. It also refuses to carve the fallback
|
||||
platform into submerged space if no dry footing can be found.
|
||||
|
||||
The `1.0.1.40-beta.1` port slice seeds terrain and biome selection for
|
||||
Descriptive Books with no authored symbol pages. It uses the same terrain and
|
||||
biome-controller rank weights as the 1.21.1 line, freezes the resulting plan
|
||||
in the Age index, and accepts the result after restart. The operator-only
|
||||
`age create_random <name> <seed>` probe exercises this path. Random feature,
|
||||
sky, weather, and instability choices are not yet ported; this slice covers
|
||||
terrain and biomes only.
|
||||
|
||||
The `1.0.1.41-beta.1` slice establishes an Age-scoped decoration wrapper
|
||||
for 26.3 and accepts one Block material page immediately before a Rocks feature
|
||||
page. Newly generated surface chunks place small rock clusters made from that
|
||||
block without modifying the shared biome registry. The selected block and
|
||||
generator survive a restart. Cave and Void terrain remain unsupported for this
|
||||
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.
|
||||
|
||||
The local `1.0.1.55-beta.1` slice adds authored Villages, Mineshafts, and
|
||||
Strongholds. Repeated pages (up to 32 per type) scale the registered placement
|
||||
using the 1.21.1 frequency/spacing and ring-count rules. Copies are Age-local:
|
||||
the Overworld's placements and biome source are not modified. Authored
|
||||
structures can bypass their usual biome eligibility without changing the
|
||||
terrain biome; their instability cost remains pending with the instability port.
|
||||
|
||||
Villages and Strongholds allow surface noise terrain and Flat, but not Cave
|
||||
or Void. Mineshafts allow noise terrain including Cave, but not Flat or Void.
|
||||
These pages do not accept Block/Fluid modifiers. Their ordered counts persist
|
||||
in the feature plan. In books using these structure pages, unselected sets
|
||||
from these three families are disabled; other structure families and random
|
||||
feature selection remain later port work. Earlier books without this new
|
||||
structure plan retain their existing native structure behavior.
|
||||
|
||||
For manual checks, bind a new Standard/Plains Age with Villages, Mineshafts,
|
||||
and Strongholds, then use `/locate structure minecraft:village_plains`,
|
||||
`/locate structure minecraft:mineshaft`, and
|
||||
`/locate structure minecraft:stronghold` to inspect generated structures.
|
||||
Compare one page with several copies in a different Age, and restart/revisit.
|
||||
Flat/Plains with Villages and an otherwise incompatible surface biome with
|
||||
Villages provide separate terrain and biome-override checks.
|
||||
|
||||
The local `1.0.1.56-beta.1` follow-up fixes authored structures outside their
|
||||
normal biomes. Build 55 only bypassed the final biome check; 26.3's jigsaw
|
||||
placement can reject a whole biome column earlier. The override now supplies
|
||||
the generation context's biome predicate before either check, only for the
|
||||
explicitly authored structures in that Age. Terrain/height/geometry rules and
|
||||
unrelated structures keep their normal checks.
|
||||
|
||||
Test with a new Standard or Flat Age using a normally unsupported village
|
||||
biome, such as The End, plus a Villages page. Multiple copies increase density.
|
||||
Use `/locate structure #minecraft:village` rather than selecting only the
|
||||
Plains variant: an override Age can choose other village styles. Old chunks
|
||||
that already rejected their structure attempt are not regenerated; existing
|
||||
build-55 Ages can also test the fix in previously unexplored chunks.
|
||||
|
||||
The local `1.0.1.57-beta.1` slice adds the remaining 1.21.1 authored structure
|
||||
families: Nether Fortresses, Bastions, Trial Chambers, Buried Treasure,
|
||||
Shipwrecks, Ocean Ruins, Pillager Outposts, Ruined Portals, Nether Fossils,
|
||||
Ancient Cities, and Trail Ruins. It also enables the existing 26.3-only
|
||||
Abandoned Camps page using Minecraft's registered camp set and variants.
|
||||
|
||||
All 15 supported structure-set pages share saved counts, biome overrides,
|
||||
and repeated-page density scaling (up to 32 per type). Fortress and Bastion
|
||||
sets reuse the separate 1.21.1 placement definitions; the combined vanilla
|
||||
Nether set is disabled for authored structure plans so the pages stay separate.
|
||||
Unselected supported families are gated in these plans. Already generated
|
||||
chunks and Ages without an authored structure plan keep their existing path.
|
||||
|
||||
Only Villages/Strongholds support Flat; none support Void. Fortress, Bastion,
|
||||
and Nether Fossils require Cave terrain. Villages/Strongholds, Buried Treasure,
|
||||
Shipwrecks, Ocean Ruins, Outposts, Trail Ruins, and Abandoned Camps require
|
||||
surface terrain. Mineshafts, Trial Chambers, Ancient Cities, and Ruined Portals
|
||||
allow surface noise or Cave terrain. Physical terrain/height checks still apply
|
||||
even when the authored page overrides biome eligibility. Instability penalties
|
||||
and random selection of these features are not part of this slice.
|
||||
|
||||
For a quick in-game check, author a new Standard Age with Trial Chambers,
|
||||
Trail Ruins, and Abandoned Camps. Use `/locate structure minecraft:trial_chambers`,
|
||||
`/locate structure minecraft:trail_ruins`, and
|
||||
`/locate structure minecraft:abandoned_camp_forest` to inspect actual generation.
|
||||
Separately, test a Cave/Nether Wastes Age with Fortress and Bastion pages
|
||||
(`/locate structure minecraft:fortress` and
|
||||
`/locate structure minecraft:bastion_remnant`). Repeated pages and restart/
|
||||
new-chunk generation remain useful checks. Structures can still fail when
|
||||
their geometry or required terrain is unavailable.
|
||||
|
||||
The local `1.0.1.58-beta.1` slice enables Caves and Ravines feature pages.
|
||||
They add carving to the chosen noise terrain (Standard, Amplified, Island,
|
||||
or Cave); they are not aliases for the Cave terrain page. Flat/Void and
|
||||
Block/Fluid modifiers remain unsupported, matching the 1.21.1 feature rules.
|
||||
|
||||
Authored paths retain 1.21.1's source range and seeded salts, using 26.3's
|
||||
registered cave/Nether-cave/canyon carvers. Nether-biome sources select the
|
||||
Nether cave carver and skip Ravines. The paths merge into the native carving
|
||||
mask before Minecraft applies aquifer, replacement, and surface rules; no
|
||||
second approximation carves raw air after terrain generation. Biomes are not
|
||||
replaced. The page list is saved, and old plans default to no extra carving.
|
||||
|
||||
Duplicate pages are accepted up to 32 per type, but repeat the same seeded
|
||||
pattern in 1.21.1; this port skips those redundant passes rather than multiplying
|
||||
carving density. Random feature selection and instability remain pending.
|
||||
New chunks only. For manual testing, author a Standard/Plains Age with one
|
||||
Caves page and one Ravines page and inspect underground in Spectator mode.
|
||||
Restart and explore more chunks. A separate Cave/Nether Wastes Age with Caves
|
||||
checks the Nether variant without altering its biome distribution.
|
||||
|
||||
The local `1.0.1.59-beta.1` slice audits page presentation for the current port.
|
||||
Each Symbol Page now reports whether its effect is implemented. Unported pages
|
||||
have red-tinted item artwork and a red Binder page background, plus a red status
|
||||
line. Supported pages retain their normal artwork. This indicates port
|
||||
availability only: supported pages in an invalid order, terrain combination,
|
||||
or excessive quantity are not individually tinted red. Link Panels are unchanged.
|
||||
|
||||
The stale Standard/one-Biome message is removed. Structure descriptions now
|
||||
describe authored biome overrides rather than falsely requiring vanilla biome
|
||||
eligibility. Material pages explain the immediate-before-feature requirement;
|
||||
Flat/Caves/Ravines/Floating Islands descriptions reflect the current rules.
|
||||
These overrides are specific to the 26.3 language file and do not change 1.21.1.
|
||||
No new generation effect is introduced in this UI slice.
|
||||
|
||||
For manual validation, compare a working Villages or Caves page with an
|
||||
unported celestial or lighting page in Creative inventory
|
||||
and the Binder. Only the latter should be red; supported Block/Fluid modifiers
|
||||
and biome controllers should stay normal even though they need other pages.
|
||||
|
||||
The local `1.0.1.60-beta.1` slice implements Poplar Trees, Fallen Poplar Trees,
|
||||
Red Shrubs, and Shelf Mushrooms. They use the existing saved vegetation plan,
|
||||
ordered attempts, surface-biome rules, and combined 32-vegetation-page limit.
|
||||
They accept Standard, Amplified, Island, and Flat terrain, but not Cave/Void
|
||||
or Block/Fluid modifiers. The four pages now show as supported, without a red tint.
|
||||
|
||||
Poplar Trees uses the vanilla red/orange/yellow leaf-litter tree variants with
|
||||
their relative Dappled Forest weights and six attempts per page per chunk;
|
||||
it does not also select spruce or fallen trees. Fallen Poplar Trees makes one
|
||||
surface attempt per page, using the vanilla fallen-tree feature, decorators,
|
||||
and sapling survival rules. Red Shrubs uses the vanilla patch placement.
|
||||
All still require suitable ground and space; authoring does not bypass those
|
||||
requirements, and existing chunks are not redecorated.
|
||||
|
||||
Shelf Mushrooms applies Minecraft's standing/fallen-log decorator to existing
|
||||
log groups, not a new tree feature. It can decorate natural trees or logs from
|
||||
earlier authored tree pages; no suitable logs means no mushrooms. The vanilla
|
||||
side/age/spacing and water-avoidance logic is retained. Writes stay within the
|
||||
current chunk and require valid support. Consecutive mushroom pages reuse the
|
||||
host scan; adding trees invalidates the cached host list for subsequent pages.
|
||||
|
||||
For manual testing, bind a new Flat/Plains Age with Poplar Trees, Fallen Poplar
|
||||
Trees, Red Shrubs, then Shelf Mushrooms. Check leaf colours, fallen trunks,
|
||||
shrubs, and mushrooms on logs. The four pages should no longer be red. Restart,
|
||||
revisit existing terrain, and explore new chunks to check persistence.
|
||||
|
||||
The local `1.0.1.61-beta.1` follow-up removes the inappropriate surface-terrain
|
||||
and surface-biome gates from Shelf Mushrooms only. No tree page is required:
|
||||
natural logs or generated log Tendrils/Spheres/other features are hosts on
|
||||
any terrain, including Void and Cave, and in any biome. Other plants keep their
|
||||
existing terrain/biome restrictions. For example, Void + Oak Log + Tendrils +
|
||||
Shelf Mushrooms is valid authoring; there must still be exposed log faces.
|
||||
|
||||
The mushroom scan now considers log faces throughout the chunk height instead
|
||||
of treating a connected tendril as a standing tree with a small base-height
|
||||
decoration band. Placement retains native replaceability/water checks, valid
|
||||
support, random age, a per-side chance, and spacing checks. It does not grow
|
||||
hosts or continuously decorate player-built logs after chunk generation.
|
||||
Test a new Void Age with the example pages and inspect tendril surfaces.
|
||||
|
||||
The local `1.0.1.62-beta.1` sound slice reuses the complete 1.21.1 sound manifest
|
||||
and all six OGG files directly in the build. Successful Linking/Descriptive Book
|
||||
travel now plays the normal, Intra-Linking, or Disarm sound at departure and
|
||||
arrival, with the same modifier precedence and cloud bursts as 1.21.1. Failed
|
||||
travel stays silent. Star Fissures also play their return sound after arrival,
|
||||
instead of relying only on audio sent immediately before a dimension switch.
|
||||
Portal and meteor sound events are bundled for their later gameplay ports;
|
||||
this does not implement those systems. Binding still uses vanilla page audio.
|
||||
Manual checks: normal book travel, Intra-Linking, Disarm, rejected travel, and
|
||||
a Star Fissure return. Existing books/Ages do not need to be recreated.
|
||||
|
||||
The local `1.0.1.63-beta.1` slice ports the six active weather pages: Normal,
|
||||
Fast, Slow, Off, Always, and Storm. One weather page is accepted per book and
|
||||
its policy is saved in the Age's own data directory. Missing weather pages
|
||||
currently use Normal; random weather selection is still part of the pending
|
||||
random-trait port. Cave terrain forces Off, matching 1.21.1. Precipitation
|
||||
still follows biome temperature and precipitation rules; a desert does not
|
||||
start showing rain merely because Always or Storm is selected.
|
||||
|
||||
26.3 normally shares server WeatherData. Runtime Ages now use an isolated
|
||||
native WeatherData instance with its own persisted rain/thunder timers. The
|
||||
weather controller runs before the native cycle, allowing vanilla rendering,
|
||||
lightning, snow and client dimension-specific packets to handle the result.
|
||||
Fixed weather is refreshed before its timers expire; Fast and Slow use the
|
||||
1.21.1 duration ranges and preserve saved timers on reload. Overworld weather
|
||||
and other Ages are not changed. The vanilla `/weather` command still targets
|
||||
the server's global weather, not the independent Age policy.
|
||||
|
||||
Operator diagnostics: `/rivencraft_port age create_weather <name> <seed> <mode>`
|
||||
creates a Flat test Age; `/rivencraft_port age weather_probe <name>` reports
|
||||
its policy, rain/thunder state and timers, data independence, and global state.
|
||||
For manual testing, bind separate Storm/Always/Off books with Plains, compare
|
||||
the Overworld, revisit and restart, and check that Cave + Storm remains dry.
|
||||
The six weather pages should no longer be tinted red. Existing Ages get a
|
||||
Normal independent cycle; author a new book to select another policy.
|
||||
|
||||
The local `1.0.1.64-beta.1` fix corrects the Binder's biome compiler: it now
|
||||
positively selects biome-category pages instead of treating every page not
|
||||
in an exclusion list as a biome. Weather pages no longer trigger an unknown
|
||||
biome lookup. The weather fixture command now exercises the same page compiler
|
||||
before creating an Age, covering the weather-only symbol list used by a Link
|
||||
Panel + Storm book. Existing Binder contents can be reused after updating.
|
||||
|
||||
The local `1.0.1.65-beta.1` spawn correction removes the Flat-layer shortcut
|
||||
and heightmap-only choice for non-Cave Descriptive Book arrivals. The search
|
||||
starts at the actual maximum build height and traces each nearby column down
|
||||
to its first colliding block, leaves, or fluid. A column without safe dry
|
||||
footing/headroom is skipped instead of descending through its roof or water
|
||||
into an underground pocket. Leaves are valid footing; fluids are not.
|
||||
|
||||
This also revalidates a stored Star Fissure arrival from above on non-Cave
|
||||
terrain. Cave terrain retains its Y=64-downward search, and bound Linking
|
||||
Books retain their exact coordinates. If no natural surface is safe, the
|
||||
safety platform is placed above the actual highest obstruction (or floor
|
||||
Y=63 in an empty Void), never buried below it. Plain Flat terrain still lands
|
||||
on its ground, not an artificially elevated platform. No new Age is required.
|
||||
Operator-only `/rivencraft_port age surface_probe <name> <x> <z>` reports the
|
||||
actual highest obstruction and a safe nearby surface without creating a
|
||||
platform or teleporting. It can generate chunks while inspecting the region.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.39-beta.1
|
||||
|
||||
This beta adds Cave terrain to authored Ages on Minecraft 26.3.
|
||||
|
||||
- Bind Descriptive Books with the Cave terrain page and Cave or Nether biome pages, including supported multi-biome distributions.
|
||||
- Cave Ages use the cave noise shape but remain dry by default. Newly generated chunks no longer flood below the vanilla cave sea level.
|
||||
- Cave travel searches from Y=64 downward for dry, properly footed space instead of landing on the cave roof or underwater.
|
||||
- Expanded dedicated-server checks for Cave creation, biome distribution, and persistence after restart.
|
||||
|
||||
The player verified the Cave and safe-landing changes in game. Existing generated chunks are not drained or regenerated. A future Flooded page will provide an authored water level; see [issue #107](https://git.paddedcell.org/dnorberg/rivencraft/issues/107).
|
||||
|
||||
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
|
||||
|
||||
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.41-beta.1
|
||||
|
||||
This beta expands Age generation on Minecraft 26.3.
|
||||
|
||||
- Descriptive Books with no authored symbol pages now choose a seeded random terrain and biome distribution. The chosen plan is saved with the Age and restored after restart.
|
||||
- A Block material page followed by a Rocks feature page now generates small clusters of that block in newly generated surface chunks. This first feature path supports Standard, Amplified, Island, and Flat terrain; Cave and Void combinations remain unsupported.
|
||||
- Expanded dedicated-server checks cover random Age plans and authored Rocks generation across restart.
|
||||
|
||||
The player verified the Rocks feature in game. Random feature selection, duplicate feature pages, unpaired materials, and instability scoring are not yet ported. Existing chunks are not regenerated.
|
||||
|
||||
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
|
||||
|
||||
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
|
||||
@@ -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).
|
||||
@@ -0,0 +1,72 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.65-beta.1
|
||||
|
||||
This beta collects the player-tested port slices since 1.0.1.54-beta.1.
|
||||
It is not yet feature-complete with the Minecraft 1.21.1 release.
|
||||
|
||||
## Age generation
|
||||
|
||||
- Added authored structure pages: Villages, Mineshafts, Strongholds, Trial
|
||||
Chambers, Buried Treasure, Shipwrecks, Ocean Ruins, Pillager Outposts,
|
||||
Ruined Portals, Nether Fossils, Ancient Cities, Trail Ruins, Nether
|
||||
Fortresses, Bastions, and 26.3's Abandoned Camps. Dungeons remain their
|
||||
separate feature.
|
||||
- Fixed the early structure-biome check so authored structures can generate
|
||||
outside their native biome restrictions, within the supported terrain rules.
|
||||
Repeated structure pages increase density up to the current authoring cap.
|
||||
- Added Caves and Ravines feature pages through 26.3's native carving stage.
|
||||
These are distinct from Cave terrain; Flat/Void support remains pending.
|
||||
- Added Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf Mushrooms.
|
||||
Shelf Mushrooms find exposed logs at any height, including log Tendrils in
|
||||
Void terrain. No Trees page is required.
|
||||
- Added Normal, Fast, Slow, Off, Always, and Storm weather pages. Each Age
|
||||
has independent saved weather data and timers; its policy does not change
|
||||
the Overworld or other Ages. Cave terrain keeps weather disabled.
|
||||
|
||||
## Fixes and feedback
|
||||
|
||||
- Non-Cave Descriptive Book arrivals trace actual blocks down from maximum
|
||||
build height instead of using a Flat-layer shortcut or only a heightmap.
|
||||
The search stops at roofs/fluids rather than selecting underground pockets.
|
||||
Leaves count as footing; submerged footing is rejected. Fallback platforms
|
||||
sit above obstructions, with floor Y=63 in an empty Void.
|
||||
- Stored surface-Age Star Fissure arrivals are rechecked from above. Cave
|
||||
terrain still searches down from Y=64, and Linking Books retain exact
|
||||
bound destinations.
|
||||
- Restored Legacy linking sounds and successful-link cloud bursts. Disarm
|
||||
and Intra-Linking use their respective sounds. Star Fissure return audio
|
||||
also plays after arrival so a dimension switch does not discard it.
|
||||
- Fixed weather-only authoring, including Link Panel + Storm books: non-biome
|
||||
pages no longer get interpreted as biome IDs.
|
||||
- Unsupported pages now have a red tint and availability tooltip. Supported
|
||||
pages and their descriptions reflect the implemented 26.3 behavior.
|
||||
- Added operator weather and surface diagnostics for testing.
|
||||
|
||||
## Current limits
|
||||
|
||||
- Only the supported page/terrain combinations can be bound. Most repeated
|
||||
features are capped at 32; vegetation uses a combined 32-page limit. The
|
||||
full 1.21.1 grammar recovery and instability systems remain unported.
|
||||
- Pageless books currently randomize terrain and biome distribution, not the
|
||||
complete feature/environment list. Omitted weather currently means Normal.
|
||||
- Base carvers do not carve authored solid volumes added after 26.3's combined
|
||||
terrain operation. Previously generated chunks are not regenerated.
|
||||
- The full writing/discovery loop, celestial/lighting systems, crystal portals,
|
||||
instability, and optional-integration parity remain in progress. Creative
|
||||
availability alone does not mean a page is supported; check its tooltip.
|
||||
- Vanilla `/weather` targets global server weather, not the independent Age
|
||||
policy. Biomes retain their normal precipitation rules.
|
||||
|
||||
The player reported in-game passes for these slices. Build/static checks passed.
|
||||
Targeted disposable dedicated-server checks covered all six weather policies,
|
||||
Overworld isolation, unload/reload and restart persistence, weather-only page
|
||||
compilation, and dry surface/leaf spawn probes. This is not a claim of full
|
||||
dedicated-server or optional-mod regression coverage. Test servers were stopped
|
||||
and their temporary worlds removed.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build,
|
||||
and **Java 25**. This build does not require Dynamic Dimensions or Bad Packets.
|
||||
Keep it separate from the 1.21.1 build and saves. Back up entire worlds before
|
||||
updating; beta-save compatibility is not guaranteed.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -124,6 +124,12 @@ 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 'data/rivencraft/worldgen/structure_set/fortresses.json'
|
||||
include 'data/rivencraft/worldgen/structure_set/bastions.json'
|
||||
include 'assets/rivencraft/models/block/star_fissure.json'
|
||||
include 'assets/rivencraft/blockstates/star_fissure.json'
|
||||
include 'assets/rivencraft/sounds.json'
|
||||
include 'assets/rivencraft/sounds/**/*.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'
|
||||
|
||||
@@ -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.36-beta.1
|
||||
mod_version=1.0.1.65-beta.1
|
||||
@@ -6,19 +6,26 @@
|
||||
"item.rivencraft.page.blank": "Blank Page",
|
||||
"item.rivencraft.page.link_panel": "Link Panel Page",
|
||||
"item.rivencraft.page.unresolved": "Unknown Symbol Page",
|
||||
"item.rivencraft.page.symbol.port_status": "Binder currently supports Standard terrain and one Biome page; other effects are still being ported.",
|
||||
"item.rivencraft.page.symbol.implemented": "Implemented in 26.3; book and terrain requirements still apply.",
|
||||
"symbol.rivencraft.weather.normal.description": "Use an independent normal weather cycle. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.fast.description": "Use the faster Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.slow.description": "Use the slower Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.off.description": "Keep this Age clear without changing Overworld weather.",
|
||||
"symbol.rivencraft.weather.always.description": "Keep this Age rainy, or snowy in cold biomes, without thunder. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.storm.description": "Keep this Age rainy and thundering. Precipitation still depends on the biome; Cave terrain keeps weather off.",
|
||||
"item.rivencraft.page.symbol.unimplemented": "Not yet implemented in 26.3; cannot be bound into an Age.",
|
||||
"item.rivencraft.link_property.disarm": "Disarm",
|
||||
"item.rivencraft.link_property.maintain_momentum": "Maintain Momentum",
|
||||
"item.rivencraft.link_property.intra_linking": "Intra-Linking",
|
||||
"item.rivencraft.ink_vial": "Ink Vial",
|
||||
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
|
||||
"item.rivencraft.ink_vial.usage": "Use with paper in the other hand, or place in an Ink Mixer.",
|
||||
"item.rivencraft.page.blank.port_status": "Base for future symbol writing; not yet usable in this port.",
|
||||
"item.rivencraft.page.blank.port_status": "Internal symbol-page base; not an authoring ingredient.",
|
||||
"item.rivencraft.page.link_panel.port_status": "Use with leather in a Book Binder to make a Descriptive Book.",
|
||||
"block.rivencraft.book_binder": "Book Binder",
|
||||
"container.rivencraft.book_binder": "Book Binder",
|
||||
"screen.rivencraft.book_binder.missing_panel": "Link Panel required",
|
||||
"screen.rivencraft.book_binder.authored_pending": "Writing not yet ported",
|
||||
"screen.rivencraft.book_binder.authored_pending": "Unsupported page or page combination",
|
||||
"block.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"container.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"screen.rivencraft.ink_mixer.ink": "Ink: %s/%s",
|
||||
@@ -33,13 +40,33 @@
|
||||
"itemGroup.rivencraft": "Rivencraft",
|
||||
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
||||
"symbol.rivencraft.feature.poplar_trees": "Poplar Trees",
|
||||
"symbol.rivencraft.feature.poplar_trees.description": "Poplar tree generation symbol.",
|
||||
"symbol.rivencraft.feature.poplar_trees.description": "Add red, orange, and yellow poplar trees to surface terrain with suitable soil.",
|
||||
"symbol.rivencraft.feature.fallen_poplar_trees": "Fallen Poplar Trees",
|
||||
"symbol.rivencraft.feature.fallen_poplar_trees.description": "Fallen poplar tree generation symbol.",
|
||||
"symbol.rivencraft.feature.fallen_poplar_trees.description": "Add fallen poplar logs and stumps to surface terrain with suitable soil.",
|
||||
"symbol.rivencraft.feature.shelf_mushrooms": "Shelf Mushrooms",
|
||||
"symbol.rivencraft.feature.shelf_mushrooms.description": "Shelf mushroom generation symbol.",
|
||||
"symbol.rivencraft.feature.shelf_mushrooms.description": "Add shelf mushrooms to exposed log faces on any terrain, including Void. No tree page is required.",
|
||||
"symbol.rivencraft.feature.red_shrubs": "Red Shrubs",
|
||||
"symbol.rivencraft.feature.red_shrubs.description": "Red shrub generation symbol.",
|
||||
"symbol.rivencraft.feature.red_shrubs.description": "Add red shrub patches to surface terrain with suitable ground.",
|
||||
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
|
||||
"symbol.rivencraft.feature.abandoned_camps.description": "Abandoned camp structure generation symbol."
|
||||
"symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.block.generated.description": "Place immediately before a compatible feature; unpaired materials are not yet supported.",
|
||||
"symbol.rivencraft.biome.native.description": "Use the terrain's native biome source; do not include Biome pages.",
|
||||
"symbol.rivencraft.terrain.flat.description": "A level, biome-decorated landscape with its foundation at Y=0.",
|
||||
"symbol.rivencraft.feature.floating_islands.description": "Add high floating islands alongside noise, Flat, or Void terrain; not Cave terrain.",
|
||||
"symbol.rivencraft.feature.caves.description": "Add extra cave carving to noise terrain, including Cave; not Flat or Void.",
|
||||
"symbol.rivencraft.feature.ravines.description": "Add canyon carving to noise terrain; not Flat or Void. Skips Nether biomes.",
|
||||
"symbol.rivencraft.feature.mineshafts.description": "Allow mineshafts in noise terrain, including Cave, regardless of normal biome eligibility.",
|
||||
"symbol.rivencraft.feature.nether_fortresses.description": "Allow Nether fortresses in Cave terrain; the authored page overrides biome eligibility.",
|
||||
"symbol.rivencraft.feature.strongholds.description": "Allow strongholds in surface noise or Flat terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.feature.villages.description": "Allow villages in surface noise or Flat terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.feature.trial_chambers.description": "Allow underground Trial Chambers in noise terrain, including Cave, regardless of biome eligibility.",
|
||||
"symbol.rivencraft.feature.bastions.description": "Allow Bastion Remnants in Cave terrain; the authored page overrides biome eligibility.",
|
||||
"symbol.rivencraft.feature.buried_treasure.description": "Allow buried treasure in surface noise terrain; biome eligibility is overridden, but suitable terrain is required.",
|
||||
"symbol.rivencraft.feature.shipwrecks.description": "Allow shipwrecks in surface noise terrain, including outside their usual ocean and beach biomes.",
|
||||
"symbol.rivencraft.feature.ocean_ruins.description": "Allow ocean ruins in surface noise terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.feature.pillager_outposts.description": "Allow pillager outposts in surface noise terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.feature.ruined_portals.description": "Allow ruined portals in noise terrain, including Cave, regardless of normal biome eligibility.",
|
||||
"symbol.rivencraft.feature.nether_fossils.description": "Allow Nether fossils in Cave terrain; the authored page overrides biome eligibility.",
|
||||
"symbol.rivencraft.feature.ancient_cities.description": "Allow underground Ancient Cities in noise terrain, including Cave, regardless of biome eligibility.",
|
||||
"symbol.rivencraft.feature.trail_ruins.description": "Allow Trail Ruins in surface noise terrain, including outside their usual biomes."
|
||||
}
|
||||
@@ -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,19 @@ 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 = sound("linking.link");
|
||||
public static final Supplier<SoundEvent> LINK_DISARM = sound("linking.link-disarm");
|
||||
public static final Supplier<SoundEvent> LINK_INTRA = sound("linking.link-intra");
|
||||
public static final Supplier<SoundEvent> LINK_FISSURE = sound("linking.link-fissure");
|
||||
public static final Supplier<SoundEvent> LINK_PORTAL = sound("linking.link-portal");
|
||||
public static final Supplier<SoundEvent> METEOR_ROAR = sound("entity.meteor.roar");
|
||||
|
||||
private static Supplier<SoundEvent> sound(String name) {
|
||||
return SOUNDS.register(name, () -> SoundEvent.createVariableRangeEvent(
|
||||
Identifier.fromNamespaceAndPath(MOD_ID, name)));
|
||||
}
|
||||
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
|
||||
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
|
||||
private static final DeferredRegister<MenuType<?>> MENUS =
|
||||
@@ -94,6 +110,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 +168,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);
|
||||
@@ -155,6 +178,7 @@ public final class Rivencraft {
|
||||
PortBiomeSources.register(modEventBus);
|
||||
PortChunkGenerators.register(modEventBus);
|
||||
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
|
||||
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAgeWeather::onLevelTickPre);
|
||||
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
|
||||
|
||||
@@ -24,7 +24,17 @@ final class AgeLevelIndex extends SavedData {
|
||||
.forGetter(index -> index.biomePlans),
|
||||
Codec.unboundedMap(Identifier.CODEC,
|
||||
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
||||
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain)
|
||||
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
|
||||
.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<>(
|
||||
@@ -38,6 +48,11 @@ final class AgeLevelIndex extends SavedData {
|
||||
private final Map<Identifier, Identifier> biomeOverrides;
|
||||
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<>();
|
||||
@@ -45,17 +60,32 @@ final class AgeLevelIndex extends SavedData {
|
||||
this.biomeOverrides = new LinkedHashMap<>();
|
||||
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, PortAgeTerrain> terrain,
|
||||
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() {
|
||||
@@ -81,13 +111,26 @@ final class AgeLevelIndex extends SavedData {
|
||||
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
|
||||
}
|
||||
|
||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain) {
|
||||
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, PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||
if (contains(id)) {
|
||||
throw new IllegalArgumentException("Age already exists: " + 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();
|
||||
}
|
||||
|
||||
@@ -96,16 +139,28 @@ final class AgeLevelIndex extends SavedData {
|
||||
biomeOverrides.remove(id);
|
||||
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) {
|
||||
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;
|
||||
@@ -66,6 +68,22 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("age")
|
||||
.then(Commands.literal("surface_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||
.then(Commands.argument("z", IntegerArgumentType.integer())
|
||||
.executes(PortAgeCommands::probeSurface)))))
|
||||
.then(Commands.literal("create_weather")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("mode", StringArgumentType.word())
|
||||
.suggests((context, builder) -> net.minecraft.commands.SharedSuggestionProvider.suggest(
|
||||
java.util.Arrays.stream(PortWeatherMode.values())
|
||||
.map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder))
|
||||
.executes(PortAgeCommands::createWeatherAge)))))
|
||||
.then(Commands.literal("weather_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeWeather)))
|
||||
.then(Commands.literal("create")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
@@ -78,7 +96,7 @@ public final class PortAgeCommands {
|
||||
context, "terrain").toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal(
|
||||
"Unknown terrain; use standard, amplified, island, flat, or void"));
|
||||
"Unknown terrain; use standard, amplified, island, cave, flat, or void"));
|
||||
return 0;
|
||||
}
|
||||
return createAge(context, terrain);
|
||||
@@ -94,19 +112,87 @@ public final class PortAgeCommands {
|
||||
context, "terrain").toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal(
|
||||
"Unknown terrain; use flat or void"));
|
||||
"Unknown terrain; use cave, flat, or void"));
|
||||
return 0;
|
||||
}
|
||||
if (terrain != PortAgeTerrain.FLAT && terrain != PortAgeTerrain.VOID) {
|
||||
if (terrain != PortAgeTerrain.CAVE
|
||||
&& terrain != PortAgeTerrain.FLAT
|
||||
&& terrain != PortAgeTerrain.VOID) {
|
||||
context.getSource().sendFailure(Component.literal(
|
||||
"Multi-biome probe supports flat or void only"));
|
||||
"Multi-biome probe supports cave, flat, or void only"));
|
||||
return 0;
|
||||
}
|
||||
return createAge(context, terrain, new PortBiomePlan(
|
||||
PortBiomePlan.Distribution.TILED, java.util.List.of(
|
||||
Identifier.withDefaultNamespace("plains"),
|
||||
Identifier.withDefaultNamespace("forest"))));
|
||||
PortBiomePlan.Distribution.TILED,
|
||||
terrain == PortAgeTerrain.CAVE
|
||||
? java.util.List.of(Identifier.withDefaultNamespace("lush_caves"),
|
||||
Identifier.withDefaultNamespace("dripstone_caves"))
|
||||
: java.util.List.of(Identifier.withDefaultNamespace("plains"),
|
||||
Identifier.withDefaultNamespace("forest"))));
|
||||
})))))
|
||||
.then(Commands.literal("create_random")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
|
||||
context.getSource().getServer(), seed);
|
||||
int created = createAge(context, selected.terrain(), selected.biomes());
|
||||
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Random Age plan: terrain=" + selected.terrain()
|
||||
+ " biomes=" + selected.biomes()), false);
|
||||
return created;
|
||||
}))))
|
||||
.then(Commands.literal("create_rocks")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.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());
|
||||
@@ -210,6 +296,21 @@ public final class PortAgeCommands {
|
||||
.then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0))
|
||||
.executes(context -> sampleAge(context,
|
||||
IntegerArgumentType.getInteger(context, "chunkOffset"))))))
|
||||
.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())
|
||||
@@ -217,18 +318,297 @@ public final class PortAgeCommands {
|
||||
.executes(PortAgeCommands::sampleBiome)))))));
|
||||
}
|
||||
|
||||
private static int probeSurface(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 x = IntegerArgumentType.getInteger(context, "x");
|
||||
int z = IntegerArgumentType.getInteger(context, "z");
|
||||
var obstruction = PortAgeTravel.highestObstruction(level, x, z);
|
||||
var landing = PortAgeTravel.findSurfaceLanding(level, x, z);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Surface probe " + id
|
||||
+ " highest=" + obstruction + " landing=" + landing), false);
|
||||
return landing == null ? 0 : 1;
|
||||
}
|
||||
|
||||
private static int createWeatherAge(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
PortWeatherMode mode = PortWeatherMode.valueOf(
|
||||
StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT));
|
||||
Identifier id = ageId(context);
|
||||
var server = context.getSource().getServer();
|
||||
// Exercise the Binder's page compiler too, not just direct backend creation.
|
||||
var pages = java.util.List.of(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"weather/" + mode.name().toLowerCase(Locale.ROOT)));
|
||||
var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
pages, server.registryAccess());
|
||||
RuntimeAgeBackend.create(server, id,
|
||||
LongArgumentType.getLong(context, "seed"), biomePlan, PortAgeTerrain.FLAT,
|
||||
null, null, 0, null, null, mode);
|
||||
return probeWeather(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int probeWeather(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
var server = context.getSource().getServer();
|
||||
ServerLevel loaded = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (!(loaded instanceof PortAgeLevel level)) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
var weather = level.getWeatherData();
|
||||
context.getSource().sendSuccess(() -> Component.literal("Weather probe " + id
|
||||
+ " mode=" + level.weatherMode() + " raining=" + weather.isRaining()
|
||||
+ " thundering=" + weather.isThundering() + " rainTicks=" + weather.getRainTime()
|
||||
+ " thunderTicks=" + weather.getThunderTime()
|
||||
+ " independent=" + (weather != server.getWeatherData())
|
||||
+ " overworldRaining=" + server.getWeatherData().isRaining()
|
||||
+ " overworldThundering=" + server.getWeatherData().isThundering()), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context, PortAgeTerrain terrain) {
|
||||
return createAge(context, terrain, null);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan) {
|
||||
return createAge(context, terrain, biomePlan, null);
|
||||
}
|
||||
|
||||
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 PortFeatureChunkGenerator features)
|
||||
|| features.rocks() == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
|
||||
return 0;
|
||||
}
|
||||
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;
|
||||
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 + " 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);
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.saveddata.WeatherData;
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
|
||||
/** Runtime Age with independent native weather, rather than 26.3's shared server cycle. */
|
||||
final class PortAgeLevel extends ServerLevel {
|
||||
private final WeatherData ageWeather;
|
||||
private final PortWeatherSettings weatherSettings;
|
||||
private final boolean cave;
|
||||
|
||||
PortAgeLevel(MinecraftServer server, ResourceKey<Level> key, LevelStem stem,
|
||||
long biomeZoomSeed, PortAgeTerrain terrain) {
|
||||
super(server, server.executor, server.storageSource,
|
||||
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
|
||||
key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false);
|
||||
ageWeather = getDataStorage().computeIfAbsent(WeatherData.TYPE);
|
||||
weatherSettings = getDataStorage().computeIfAbsent(PortWeatherSettings.TYPE);
|
||||
cave = terrain == PortAgeTerrain.CAVE;
|
||||
PortAgeWeather.apply(this);
|
||||
// The base constructor initializes visual levels from server weather.
|
||||
// Replace them before publishing this Age or sending its join packets.
|
||||
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeatherData getWeatherData() {
|
||||
// The base constructor may query weather before our fields are initialized.
|
||||
return ageWeather == null ? super.getWeatherData() : ageWeather;
|
||||
}
|
||||
|
||||
PortWeatherMode weatherMode() { return cave ? PortWeatherMode.OFF : weatherSettings.mode(); }
|
||||
|
||||
void configureWeather(PortWeatherMode mode) {
|
||||
weatherSettings.setMode(mode);
|
||||
PortAgeWeather.apply(this);
|
||||
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||
getDataStorage().saveAndJoin();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ public enum PortAgeTerrain {
|
||||
STANDARD,
|
||||
AMPLIFIED,
|
||||
ISLAND,
|
||||
CAVE,
|
||||
FLAT,
|
||||
VOID
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
|
||||
/** Shared, server-authoritative landing for the 26.3 book and operator probe. */
|
||||
public final class PortAgeTravel {
|
||||
@@ -25,47 +23,96 @@ 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 = isCaveTerrain(target)
|
||||
? findFootedDestination(target, landing).orElse(null)
|
||||
: findSurfaceLanding(target, landing.getX(), landing.getZ());
|
||||
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";
|
||||
}
|
||||
|
||||
private static BlockPos findLanding(ServerLevel target) {
|
||||
// A Flat Age has a known surface. Use it directly before a heightmap
|
||||
// fallback can place a platform in the air above a low superflat world.
|
||||
FlatLevelGeneratorSettings flatSettings = switch (target.getChunkSource().getGenerator()) {
|
||||
case FlatLevelSource flat -> flat.settings();
|
||||
case PortFlatChunkGenerator flat -> flat.settings();
|
||||
default -> null;
|
||||
};
|
||||
if (flatSettings != null && !flatSettings.getLayersInfo().isEmpty()) {
|
||||
int surfaceY = target.getMinY() + flatSettings.getLayersInfo().stream()
|
||||
.mapToInt(layer -> layer.getHeight()).sum();
|
||||
BlockPos surface = new BlockPos(0, surfaceY, 0);
|
||||
if (hasFooting(target, surface)) return surface;
|
||||
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.
|
||||
for (int radius = 0; radius <= 16; radius++) {
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
for (int y = Math.min(64, target.getMaxY() - 2); y > target.getMinY(); y--) {
|
||||
BlockPos foot = new BlockPos(x, y, z);
|
||||
if (hasFooting(target, foot)) return foot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return makePlatform(target, new BlockPos(0, 64, 0));
|
||||
}
|
||||
BlockPos surface = findSurfaceLanding(target, 0, 0);
|
||||
if (surface != null) return surface;
|
||||
// Only build above the actual highest obstruction, never in a pocket
|
||||
// beneath it. Void Ages retain the usual platform floor at Y=63.
|
||||
BlockPos obstruction = highestObstruction(target, 0, 0);
|
||||
int y = obstruction == null ? 64 : Math.max(64, obstruction.getY() + 3);
|
||||
return makePlatform(target, new BlockPos(0, y, 0));
|
||||
}
|
||||
|
||||
static BlockPos findSurfaceLanding(ServerLevel target, int originX, int originZ) {
|
||||
for (int radius = 0; radius <= 16; radius++) {
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
int y = target.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z);
|
||||
BlockPos foot = new BlockPos(x, y, z);
|
||||
if (hasFooting(target, foot)) {
|
||||
return foot;
|
||||
}
|
||||
BlockPos floor = highestObstruction(target, originX + x, originZ + z);
|
||||
if (floor != null && hasFooting(target, floor.above())) return floor.above();
|
||||
}
|
||||
}
|
||||
}
|
||||
// The operator/book probe needs a deterministic landing even when
|
||||
// origin is ocean or foliage. Final book landing rules are ported later.
|
||||
int y = Math.max(64, target.getHeight(Heightmap.Types.MOTION_BLOCKING, 0, 0) + 2);
|
||||
if (y + 2 >= target.getMaxY()) return null;
|
||||
BlockPos foot = new BlockPos(0, y, 0);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** First obstructing surface seen from the sky; never continue through a roof or liquid. */
|
||||
static BlockPos highestObstruction(ServerLevel level, int x, int z) {
|
||||
var chunk = level.getChunk(x >> 4, z >> 4);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, level.getMaxY() - 1, z);
|
||||
for (int y = level.getMaxY() - 1; y >= level.getMinY(); y--) {
|
||||
pos.setY(y);
|
||||
var state = chunk.getBlockState(pos);
|
||||
if (!state.isAir() && (!state.getFluidState().isEmpty()
|
||||
|| state.is(net.minecraft.tags.BlockTags.LEAVES)
|
||||
|| !state.getCollisionShape(level, pos).isEmpty())) return pos.immutable();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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
|
||||
// that landing instead of placing the player in submerged spawn space.
|
||||
if (!target.getFluidState(foot).isEmpty() || !target.getFluidState(foot.above()).isEmpty()) return null;
|
||||
for (Direction direction : Direction.Plane.HORIZONTAL) {
|
||||
if (!target.getFluidState(foot.relative(direction)).isEmpty()
|
||||
|| !target.getFluidState(foot.above().relative(direction)).isEmpty()) return null;
|
||||
}
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
target.setBlock(new BlockPos(x, y - 1, z), Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
||||
target.setBlock(foot.offset(x, -1, z), Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
||||
}
|
||||
}
|
||||
target.setBlock(foot, Blocks.AIR.defaultBlockState(), 3);
|
||||
@@ -74,9 +121,45 @@ public final class PortAgeTravel {
|
||||
}
|
||||
|
||||
private static boolean hasFooting(ServerLevel target, BlockPos foot) {
|
||||
if (foot.getY() <= target.getMinY() || foot.getY() + 1 >= target.getMaxY()
|
||||
|| !target.getWorldBorder().isWithinBounds(foot)) return false;
|
||||
BlockPos floor = foot.below();
|
||||
return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
|
||||
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
|
||||
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty();
|
||||
var floorState = target.getBlockState(floor);
|
||||
return (floorState.isFaceSturdy(target, floor, Direction.UP)
|
||||
|| floorState.is(net.minecraft.tags.BlockTags.LEAVES))
|
||||
&& floorState.getFluidState().isEmpty()
|
||||
&& target.getFluidState(foot).isEmpty()
|
||||
&& target.getFluidState(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,52 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.world.level.saveddata.WeatherData;
|
||||
import net.neoforged.neoforge.event.tick.LevelTickEvent;
|
||||
|
||||
/** 1.21.1 weather policies, applied before the independent native Age cycle advances. */
|
||||
public final class PortAgeWeather {
|
||||
private static final Map<PortAgeLevel, Integer> STATES = new WeakHashMap<>();
|
||||
|
||||
private PortAgeWeather() {}
|
||||
|
||||
public static void onLevelTickPre(LevelTickEvent.Pre event) {
|
||||
if (event.getLevel() instanceof PortAgeLevel level) apply(level);
|
||||
}
|
||||
|
||||
static void apply(PortAgeLevel level) {
|
||||
PortWeatherMode mode = level.weatherMode();
|
||||
if (mode == PortWeatherMode.NORMAL) {
|
||||
STATES.remove(level);
|
||||
return;
|
||||
}
|
||||
WeatherData weather = level.getWeatherData();
|
||||
if (mode == PortWeatherMode.FAST || mode == PortWeatherMode.SLOW) {
|
||||
int state = (weather.isRaining() ? 1 : 0) | (weather.isThundering() ? 2 : 0);
|
||||
Integer previous = STATES.put(level, state);
|
||||
if (previous != null && previous == state) return;
|
||||
// Keep persisted timers on reload; rescale only when a cycle changes.
|
||||
if (previous == null && weather.getRainTime() > 0) return;
|
||||
int scale = mode == PortWeatherMode.FAST ? 1 : 4;
|
||||
int duration = 6000 * scale + level.getRandom().nextInt(
|
||||
(weather.isRaining() ? 6000 : 84000) * scale);
|
||||
set(weather, duration, weather.isRaining(), weather.isThundering());
|
||||
} else {
|
||||
STATES.remove(level);
|
||||
boolean rain = mode != PortWeatherMode.OFF;
|
||||
boolean thunder = mode == PortWeatherMode.STORM;
|
||||
if (weather.isRaining() != rain || weather.isThundering() != thunder
|
||||
|| weather.getRainTime() < 200 || weather.getThunderTime() < 200
|
||||
|| weather.getClearWeatherTime() != 0) set(weather, 6000, rain, thunder);
|
||||
}
|
||||
}
|
||||
|
||||
private static void set(WeatherData data, int duration, boolean rain, boolean thunder) {
|
||||
data.setClearWeatherTime(0);
|
||||
data.setRainTime(duration);
|
||||
data.setThunderTime(duration);
|
||||
data.setRaining(rain);
|
||||
data.setThundering(thunder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.CarvingMask;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.WorldGenerationContext;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.carver.WorldCarver;
|
||||
|
||||
/** Authored Caves/Ravines, independent of the terrain page, with 1.21.1 source seeds. */
|
||||
public final class PortCarverPages {
|
||||
public static final Identifier CAVES = Identifier.fromNamespaceAndPath("rivencraft", "feature/caves");
|
||||
public static final Identifier RAVINES = Identifier.fromNamespaceAndPath("rivencraft", "feature/ravines");
|
||||
private static final long CAVE_SALT = 0x4341564553464541L;
|
||||
private static final long RAVINE_SALT = 0x524156494E45534CL;
|
||||
|
||||
private PortCarverPages() {}
|
||||
|
||||
public static boolean supports(Identifier page) { return CAVES.equals(page) || RAVINES.equals(page); }
|
||||
|
||||
public static void validateCounts(List<Identifier> pages) {
|
||||
Map<Identifier, Integer> counts = new HashMap<>();
|
||||
for (Identifier page : pages) if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32)
|
||||
throw new IllegalArgumentException("Carver plan requires Caves/Ravines pages, at most 32 per type");
|
||||
}
|
||||
|
||||
public static void validateRegistry(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
if (pages.stream().noneMatch(PortCarverPages::supports)) return;
|
||||
var carvers = registries.lookupOrThrow(Registries.CARVER);
|
||||
for (String name : pages.contains(CAVES) ? List.of("cave", "nether_cave", "canyon") : List.of("canyon")) {
|
||||
if (name.equals("canyon") && !pages.contains(RAVINES)) continue;
|
||||
carvers.get(ResourceKey.create(Registries.CARVER, Identifier.withDefaultNamespace(name)))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing authored carver minecraft:" + name));
|
||||
}
|
||||
}
|
||||
|
||||
public static void carve(ChunkGenerator generator, ChunkAccess chunk, RandomState randomState,
|
||||
HolderLookup.Provider registries, List<Identifier> pages, long seed, CarvingMask mask) {
|
||||
if (pages.isEmpty()) return;
|
||||
var registry = registries.lookupOrThrow(Registries.CARVER);
|
||||
var biomes = generator.getBiomeSource().createUncachedResolver(randomState);
|
||||
var context = new WorldGenerationContext(generator, chunk.getHeightAccessorForGeneration());
|
||||
var random = new WorldgenRandom(new LegacyRandomSource(0L));
|
||||
ChunkPos target = chunk.getPos();
|
||||
// 1.21.1 repeats identical seeds for duplicate Caves/Ravines pages.
|
||||
// Their union is identical; avoid replaying the same mask up to 32 times.
|
||||
for (Identifier page : pages.stream().distinct().toList()) {
|
||||
boolean ravine = page.equals(RAVINES);
|
||||
WorldCarver overworld = registry.getOrThrow(ResourceKey.create(Registries.CARVER,
|
||||
Identifier.withDefaultNamespace(ravine ? "canyon" : "cave"))).value();
|
||||
WorldCarver nether = ravine ? null : registry.getOrThrow(ResourceKey.create(Registries.CARVER,
|
||||
Identifier.withDefaultNamespace("nether_cave"))).value();
|
||||
for (int dx = -8; dx <= 8; dx++) {
|
||||
for (int dz = -8; dz <= 8; dz++) {
|
||||
ChunkPos source = new ChunkPos(target.x() + dx, target.z() + dz);
|
||||
boolean isNether = biomes.getNoiseBiome(QuartPos.fromBlock(source.getMinBlockX()), 0,
|
||||
QuartPos.fromBlock(source.getMinBlockZ())).is(BiomeTags.IS_NETHER);
|
||||
if (ravine && isNether) continue;
|
||||
WorldCarver carver = isNether ? nether : overworld;
|
||||
random.setLargeFeatureSeed(seed ^ (ravine ? RAVINE_SALT : CAVE_SALT), source.x(), source.z());
|
||||
if (carver.isStartChunk(random)) carver.carve(context, random, target, source, mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -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,290 @@
|
||||
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) {
|
||||
if (hasStructurePages()) {
|
||||
var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource());
|
||||
var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures());
|
||||
if (base instanceof PortFlatChunkGenerator || base instanceof net.minecraft.world.level.levelgen.FlatLevelSource)
|
||||
return ChunkGeneratorStructureState.createForFlat(random, seed, getOrigin(random), planning,
|
||||
lookup.listElements().map(holder -> (Holder<StructureSet>) holder));
|
||||
return ChunkGeneratorStructureState.createForNormal(random, seed, getOrigin(random), planning, lookup);
|
||||
}
|
||||
return base.createState(sets, random, seed);
|
||||
}
|
||||
|
||||
private boolean hasStructurePages() {
|
||||
return terrainFeatures != null && !terrainFeatures.structures().isEmpty();
|
||||
}
|
||||
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||
StructureManager structures, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
if (hasStructurePages()) super.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
else 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));
|
||||
boolean surfaceBiome = !biome.is(BiomeTags.IS_END) && !PortRegistryPages.isCaveBiome(biome);
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||
List<BlockPos> mushroomHosts = null;
|
||||
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
|
||||
Identifier page = vegetation.pages().get(occurrence);
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
if (page.equals(PortVegetationPlan.SHELF_MUSHROOMS)) {
|
||||
if (mushroomHosts == null) mushroomHosts = PortShelfMushrooms.findHosts(chunk);
|
||||
PortShelfMushrooms.decorate(level, chunk, mushroomHosts, random);
|
||||
continue;
|
||||
}
|
||||
if (!surfaceBiome) continue;
|
||||
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());
|
||||
authored.place(level, this, random, origin);
|
||||
if (PortVegetationPlan.addsLogs(page)) mushroomHosts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Seed-frozen terrain and biome choices for a book with no authored symbols. */
|
||||
public final class PortRandomAgeSelection {
|
||||
private PortRandomAgeSelection() {}
|
||||
|
||||
public record Selection(PortAgeTerrain terrain, PortBiomePlan biomes) {}
|
||||
|
||||
public static Selection select(MinecraftServer server, long seed) {
|
||||
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
|
||||
// the other terrain families are rarer. The sorted order is deliberate.
|
||||
PortAgeTerrain terrain = weighted(new Random(seed ^ 0x5445525241494E4CL),
|
||||
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
||||
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
|
||||
new int[]{1, 1, 5, 1, 6, 1});
|
||||
if (terrain == PortAgeTerrain.VOID)
|
||||
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
||||
List.of(Identifier.withDefaultNamespace("the_void"))));
|
||||
|
||||
PortBiomePlan.Distribution distribution = weighted(new Random(seed ^ 0x524956454E435241L),
|
||||
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
|
||||
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
|
||||
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
|
||||
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
|
||||
PortBiomePlan.Distribution.HUGE),
|
||||
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
|
||||
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
||||
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
// 26.3 has no native multi-cave biome source; choose a seeded Cave
|
||||
// biome rather than silently using Lush Caves for every random Age.
|
||||
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
|
||||
|
||||
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
if (count > 1) {
|
||||
Random countRandom = new Random(seed ^ 0x42494F4D45434F55L);
|
||||
while (count < eligible.size() && countRandom.nextInt(3) != 0) count++;
|
||||
}
|
||||
List<Identifier> shuffled = new ArrayList<>(eligible);
|
||||
Collections.shuffle(shuffled, new Random(seed ^ 0x42494F4D45504F4FL));
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
||||
}
|
||||
|
||||
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||
if (terrain == PortAgeTerrain.CAVE) {
|
||||
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(PortRegistryPages::isCaveBiome)
|
||||
.map(holder -> holder.key().identifier())
|
||||
.sorted().toList();
|
||||
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
||||
}
|
||||
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
||||
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
||||
.map(key -> key.identifier()).distinct().sorted(Comparator.naturalOrder()).toList();
|
||||
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
|
||||
}
|
||||
|
||||
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
||||
int total = 0;
|
||||
for (int weight : weights) total += weight;
|
||||
int ticket = random.nextInt(total);
|
||||
for (int index = 0; index < choices.size(); index++) {
|
||||
ticket -= weights[index];
|
||||
if (ticket < 0) return choices.get(index);
|
||||
}
|
||||
throw new IllegalStateException("Random Age ticket was not consumed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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;
|
||||
|
||||
/** 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.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,70 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.ShelfMushroomBlock;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.feature.treedecorators.ShelfMushroomDecorator;
|
||||
import net.minecraft.world.level.levelgen.feature.treedecorators.TreeDecorator;
|
||||
|
||||
/** Shelf mushrooms on exposed log faces, regardless of how the logs were generated. */
|
||||
final class PortShelfMushrooms {
|
||||
private PortShelfMushrooms() {}
|
||||
|
||||
static List<BlockPos> findHosts(ChunkAccess chunk) {
|
||||
List<BlockPos> logs = new ArrayList<>();
|
||||
var sections = chunk.getSections();
|
||||
for (int index = 0; index < sections.length; index++) {
|
||||
var section = sections[index];
|
||||
if (!section.maybeHas(state -> state.is(BlockTags.LOGS))) continue;
|
||||
int baseY = chunk.getSectionYFromSectionIndex(index) << 4;
|
||||
for (int y = 0; y < 16; y++) for (int z = 0; z < 16; z++) for (int x = 0; x < 16; x++) {
|
||||
if (section.getBlockState(x, y, z).is(BlockTags.LOGS))
|
||||
logs.add(new BlockPos(chunk.getPos().getMinBlockX() + x, baseY + y,
|
||||
chunk.getPos().getMinBlockZ() + z));
|
||||
}
|
||||
}
|
||||
return logs;
|
||||
}
|
||||
|
||||
static void decorate(WorldGenLevel level, ChunkAccess chunk, List<BlockPos> logs, RandomSource random) {
|
||||
// Reuse vanilla replaceability/water checks, but not its tree-base height
|
||||
// band: a log tendril or sphere has no meaningful tree base.
|
||||
TreeDecorator.Context context = new TreeDecorator.Context(level,
|
||||
(pos, state) -> level.setBlock(pos, state, 2), random, Set.of(), Set.of(), Set.of());
|
||||
for (BlockPos log : logs) {
|
||||
if (!level.getBlockState(log).is(BlockTags.LOGS)) continue;
|
||||
Direction first = Direction.Plane.HORIZONTAL.getRandomDirection(random);
|
||||
for (Direction facing : List.of(first, first.getClockWise())) {
|
||||
if (random.nextFloat() > 0.25F) continue;
|
||||
BlockPos pos = log.relative(facing);
|
||||
if ((pos.getX() >> 4) != chunk.getPos().x() || (pos.getZ() >> 4) != chunk.getPos().z()) continue;
|
||||
if (pos.getY() < level.getMinY() || pos.getY() >= level.getMaxY()) continue;
|
||||
if (!level.getFluidState(pos).isEmpty()
|
||||
|| !ShelfMushroomDecorator.isBlockReplaceableWithShelfMushroom(context, pos)
|
||||
|| level.getBlockState(pos.below()).is(Blocks.SHELF_MUSHROOM)) continue;
|
||||
boolean adjacent = false;
|
||||
for (Direction side : Direction.Plane.HORIZONTAL) {
|
||||
if (level.getBlockState(pos.relative(side)).is(Blocks.SHELF_MUSHROOM)) {
|
||||
adjacent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (adjacent) continue;
|
||||
var mushroom = Blocks.SHELF_MUSHROOM.defaultBlockState()
|
||||
.setValue(ShelfMushroomBlock.FACING, facing)
|
||||
.setValue(ShelfMushroomBlock.AGE, random.nextInt(2));
|
||||
if (!mushroom.canSurvive(level, pos)) continue;
|
||||
context.setBlock(pos, mushroom);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,180 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import net.dnorberg.rivencraft.mixin.StructurePlacementAccessor;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderOwner;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||
|
||||
/** 1.21.1 structure planning/density rules adapted to modern placement classes. */
|
||||
public final class PortStructurePages {
|
||||
private static final Map<Identifier, Identifier> SETS = Map.ofEntries(
|
||||
vanillaSet("villages", "villages"),
|
||||
vanillaSet("mineshafts", "mineshafts"),
|
||||
vanillaSet("strongholds", "strongholds"),
|
||||
vanillaSet("trial_chambers", "trial_chambers"),
|
||||
vanillaSet("buried_treasure", "buried_treasures"),
|
||||
vanillaSet("shipwrecks", "shipwrecks"),
|
||||
vanillaSet("ocean_ruins", "ocean_ruins"),
|
||||
vanillaSet("pillager_outposts", "pillager_outposts"),
|
||||
vanillaSet("ruined_portals", "ruined_portals"),
|
||||
vanillaSet("nether_fossils", "nether_fossils"),
|
||||
vanillaSet("ancient_cities", "ancient_cities"),
|
||||
vanillaSet("trail_ruins", "trail_ruins"),
|
||||
vanillaSet("abandoned_camps", "abandoned_camp"),
|
||||
Map.entry(page("nether_fortresses"), Identifier.fromNamespaceAndPath("rivencraft", "fortresses")),
|
||||
Map.entry(page("bastions"), Identifier.fromNamespaceAndPath("rivencraft", "bastions")));
|
||||
// Suppress vanilla's combined Nether set so an authored Fortress page does
|
||||
// not also introduce Bastions (or vice versa).
|
||||
private static final Set<Identifier> CONTROLLED_SETS = Stream.concat(SETS.values().stream(),
|
||||
Stream.of(Identifier.withDefaultNamespace("nether_complexes")))
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
private static final Set<Identifier> CAVE_ONLY = Set.of(
|
||||
page("nether_fortresses"), page("bastions"), page("nether_fossils"));
|
||||
private static final Set<Identifier> SURFACE_ONLY = Set.of(
|
||||
page("villages"), page("strongholds"), page("buried_treasure"), page("shipwrecks"),
|
||||
page("ocean_ruins"), page("pillager_outposts"), page("trail_ruins"), page("abandoned_camps"));
|
||||
private static final Set<Identifier> FLAT_ALLOWED = Set.of(page("villages"), page("strongholds"));
|
||||
|
||||
private PortStructurePages() {}
|
||||
|
||||
private static Identifier page(String name) {
|
||||
return Identifier.fromNamespaceAndPath("rivencraft", "feature/" + name);
|
||||
}
|
||||
|
||||
private static Map.Entry<Identifier, Identifier> vanillaSet(String page, String set) {
|
||||
return Map.entry(page(page), Identifier.withDefaultNamespace(set));
|
||||
}
|
||||
|
||||
public static boolean supports(Identifier page) { return SETS.containsKey(page); }
|
||||
|
||||
public static void validateCounts(List<Identifier> pages) {
|
||||
Map<Identifier, Integer> counts = new HashMap<>();
|
||||
for (Identifier page : pages) {
|
||||
if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32)
|
||||
throw new IllegalArgumentException("Structure plan requires supported pages, at most 32 per feature");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean supportsTerrain(Identifier page, PortAgeTerrain terrain) {
|
||||
if (!supports(page) || terrain == PortAgeTerrain.VOID) return false;
|
||||
if (terrain == PortAgeTerrain.FLAT && !FLAT_ALLOWED.contains(page)) return false;
|
||||
if (CAVE_ONLY.contains(page)) return terrain == PortAgeTerrain.CAVE;
|
||||
return terrain != PortAgeTerrain.CAVE || !SURFACE_ONLY.contains(page);
|
||||
}
|
||||
|
||||
public static void validateRegistry(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
|
||||
pages.stream().filter(PortStructurePages::supports).distinct().forEach(page ->
|
||||
sets.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing structure set for " + page)));
|
||||
}
|
||||
|
||||
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
|
||||
Map<Identifier, Integer> result = new HashMap<>();
|
||||
for (Identifier page : pages) result.merge(SETS.get(page), 1, Integer::sum);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean authored(Structure structure, HolderLookup.Provider registries, List<Identifier> pages) {
|
||||
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
|
||||
return enabled(pages).keySet().stream()
|
||||
.map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value())
|
||||
.flatMap(set -> set.structures().stream()).anyMatch(entry -> entry.structure().value() == structure);
|
||||
}
|
||||
|
||||
/** Extra possible biomes affect planning only; terrain still uses its original source. */
|
||||
public static BiomeSource planningBiomes(BiomeSource actual, HolderLookup<StructureSet> sets, List<Identifier> pages) {
|
||||
List<Holder<Biome>> extra = enabled(pages).keySet().stream()
|
||||
.map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value())
|
||||
.flatMap(set -> set.structures().stream()).flatMap(entry -> entry.structure().value().biomes().stream())
|
||||
.distinct().toList();
|
||||
return new BiomeSource() {
|
||||
@Override protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Planning biome sources are never serialized");
|
||||
}
|
||||
@Override protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return Stream.concat(actual.possibleBiomes().stream(), extra.stream());
|
||||
}
|
||||
@Override public net.minecraft.world.level.biome.BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
return actual.createResolver(sampler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static HolderLookup<StructureSet> filtered(HolderLookup<StructureSet> source, List<Identifier> pages,
|
||||
BiomeSource actual) {
|
||||
Map<Identifier, Integer> counts = enabled(pages);
|
||||
Map<ResourceKey<StructureSet>, Holder.Reference<StructureSet>> copies = new HashMap<>();
|
||||
return new HolderLookup<>() {
|
||||
@Override public Optional<Holder.Reference<StructureSet>> get(ResourceKey<StructureSet> key) {
|
||||
if (CONTROLLED_SETS.contains(key.identifier()) && !counts.containsKey(key.identifier())) return Optional.empty();
|
||||
return source.get(key).filter(holder -> counts.containsKey(key.identifier()) || hasBiome(holder.value(), actual))
|
||||
.map(holder -> copies.computeIfAbsent(key, ignored -> {
|
||||
int count = counts.getOrDefault(key.identifier(), 1);
|
||||
return count == 1 ? holder : new LocalReference(key, scaled(holder.value(), count));
|
||||
}));
|
||||
}
|
||||
@Override public Optional<HolderSet.Named<StructureSet>> get(TagKey<StructureSet> key) { return source.get(key); }
|
||||
@Override public Stream<Holder.Reference<StructureSet>> listElements() {
|
||||
return source.listElements().flatMap(holder -> get(holder.key()).stream());
|
||||
}
|
||||
@Override public Stream<HolderSet.Named<StructureSet>> listTags() { return source.listTags(); }
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hasBiome(StructureSet set, BiomeSource source) {
|
||||
Set<Holder<Biome>> possible = source.possibleBiomes();
|
||||
return set.structures().stream().flatMap(entry -> entry.structure().value().biomes().stream()).anyMatch(possible::contains);
|
||||
}
|
||||
|
||||
private static StructureSet scaled(StructureSet set, int count) {
|
||||
var placement = set.placement();
|
||||
if (!(placement instanceof RandomSpreadStructurePlacement)
|
||||
&& !(placement instanceof ConcentricRingsStructurePlacement)) return set;
|
||||
StructurePlacementAccessor settings = (StructurePlacementAccessor) placement;
|
||||
if (placement instanceof RandomSpreadStructurePlacement random) {
|
||||
float original = settings.rivencraft$frequency();
|
||||
float frequency = Math.min(1.0F, original * count);
|
||||
double frequencyFactor = original > 0 ? frequency / original : 1;
|
||||
int spacing = Math.max(1, (int) Math.round(random.spacing() / Math.sqrt(count / frequencyFactor)));
|
||||
int separation = spacing == 1 ? 0 : Math.min(spacing - 1,
|
||||
(int) Math.round((double) random.separation() * spacing / random.spacing()));
|
||||
return new StructureSet(set.structures(), new RandomSpreadStructurePlacement(placement.locateOffset(),
|
||||
settings.rivencraft$frequencyReductionMethod(), frequency, settings.rivencraft$salt(),
|
||||
settings.rivencraft$exclusionZone(), spacing, separation, random.spreadType()));
|
||||
}
|
||||
if (placement instanceof ConcentricRingsStructurePlacement rings) {
|
||||
int total = (int) Math.min(4095, (long) rings.count() * count);
|
||||
int spread = Math.min(total, (int) Math.min(1023, (long) rings.spread() * count));
|
||||
return new StructureSet(set.structures(), new ConcentricRingsStructurePlacement(placement.locateOffset(),
|
||||
settings.rivencraft$frequencyReductionMethod(), settings.rivencraft$frequency(),
|
||||
settings.rivencraft$salt(), settings.rivencraft$exclusionZone(), rings.distance(), spread,
|
||||
total, rings.preferredBiomes()));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private static final class LocalReference extends Holder.Reference<StructureSet> {
|
||||
private static final HolderOwner<StructureSet> OWNER = new HolderOwner<>() {};
|
||||
private LocalReference(ResourceKey<StructureSet> key, StructureSet value) { super(Type.STAND_ALONE, OWNER, key, value); }
|
||||
}
|
||||
}
|
||||
@@ -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,95 @@
|
||||
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, List<Identifier> structures, List<Identifier> carvers) {
|
||||
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),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers)
|
||||
).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);
|
||||
structures = List.copyOf(structures);
|
||||
carvers = List.copyOf(carvers);
|
||||
PortStructurePages.validateCounts(structures);
|
||||
PortCarverPages.validateCounts(carvers);
|
||||
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
|
||||
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
|
||||
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty())
|
||||
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,
|
||||
boolean starFissures, List<Identifier> structures) {
|
||||
this(choices, skylands, denseOres, starFissures, structures, List.of());
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
|
||||
this(choices, skylands, denseOres, starFissures, List.of());
|
||||
}
|
||||
|
||||
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,56 @@
|
||||
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 plants and log decorations; not all require surface terrain. */
|
||||
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 Identifier POPLAR_TREES = page("poplar_trees");
|
||||
public static final Identifier FALLEN_POPLAR_TREES = page("fallen_poplar_trees");
|
||||
public static final Identifier RED_SHRUBS = page("red_shrubs");
|
||||
public static final Identifier SHELF_MUSHROOMS = page("shelf_mushrooms");
|
||||
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)
|
||||
|| page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES)
|
||||
|| page.equals(RED_SHRUBS) || page.equals(SHELF_MUSHROOMS);
|
||||
}
|
||||
|
||||
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");
|
||||
if (page.equals(POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "poplar_trees");
|
||||
if (page.equals(FALLEN_POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "fallen_poplar_trees");
|
||||
if (page.equals(RED_SHRUBS)) return Identifier.withDefaultNamespace("patch_red_shrub");
|
||||
throw new IllegalArgumentException("Unsupported vegetation page " + page);
|
||||
}
|
||||
|
||||
public static boolean addsLogs(Identifier page) {
|
||||
return page.equals(TREES) || page.equals(HUGE_TREES)
|
||||
|| page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES);
|
||||
}
|
||||
|
||||
public static boolean requiresSurface(Identifier page) {
|
||||
return isVegetation(page) && !page.equals(SHELF_MUSHROOMS);
|
||||
}
|
||||
|
||||
private static Identifier page(String name) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** The six active weather pages in the 1.21.1 writing system. */
|
||||
public enum PortWeatherMode {
|
||||
NORMAL, FAST, SLOW, OFF, ALWAYS, STORM;
|
||||
|
||||
public static final Codec<PortWeatherMode> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(java.util.Locale.ROOT)),
|
||||
mode -> mode.name().toLowerCase(java.util.Locale.ROOT));
|
||||
|
||||
public static PortWeatherMode forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "weather/normal" -> NORMAL;
|
||||
case "weather/fast" -> FAST;
|
||||
case "weather/slow" -> SLOW;
|
||||
case "weather/off" -> OFF;
|
||||
case "weather/always" -> ALWAYS;
|
||||
case "weather/storm" -> STORM;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static PortWeatherMode fromPages(List<Identifier> pages) {
|
||||
return pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElse(NORMAL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Age-local policy; native WeatherData separately persists its live cycle. */
|
||||
final class PortWeatherSettings extends SavedData {
|
||||
private static final Codec<PortWeatherSettings> CODEC = PortWeatherMode.CODEC
|
||||
.fieldOf("mode").codec().xmap(PortWeatherSettings::new, settings -> settings.mode);
|
||||
static final SavedDataType<PortWeatherSettings> TYPE = new SavedDataType<>(
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "weather_policy"),
|
||||
() -> new PortWeatherSettings(PortWeatherMode.NORMAL), CODEC);
|
||||
|
||||
private PortWeatherMode mode;
|
||||
|
||||
private PortWeatherSettings(PortWeatherMode mode) { this.mode = mode; }
|
||||
PortWeatherMode mode() { return mode; }
|
||||
void setMode(PortWeatherMode mode) {
|
||||
this.mode = mode;
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
@@ -33,7 +34,6 @@ import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import net.neoforged.neoforge.common.world.chunk.ForcedChunkManager;
|
||||
@@ -56,7 +56,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.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);
|
||||
}
|
||||
@@ -74,12 +75,51 @@ public final class RuntimeAgeBackend {
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
||||
return create(server, id, seed, biomePlan, terrain, null);
|
||||
}
|
||||
|
||||
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) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, PortWeatherMode.NORMAL);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather) {
|
||||
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);
|
||||
index.add(id, seed, biomePlan, terrain);
|
||||
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
((PortAgeLevel) level).configureWeather(weather);
|
||||
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.
|
||||
@@ -99,7 +139,9 @@ public final class RuntimeAgeBackend {
|
||||
public static ServerLevel reload(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
long seed = requireSeed(server, id);
|
||||
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id));
|
||||
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(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;
|
||||
}
|
||||
@@ -111,18 +153,23 @@ public final class RuntimeAgeBackend {
|
||||
long previousSeed = requireSeed(server, id);
|
||||
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);
|
||||
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);
|
||||
load(server, id, previousSeed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
@@ -140,6 +187,11 @@ public final class RuntimeAgeBackend {
|
||||
long seed = requireSeed(server, id);
|
||||
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);
|
||||
@@ -158,14 +210,15 @@ public final class RuntimeAgeBackend {
|
||||
}
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
try {
|
||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain);
|
||||
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);
|
||||
load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
@@ -281,7 +334,9 @@ public final class RuntimeAgeBackend {
|
||||
}
|
||||
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
||||
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);
|
||||
|
||||
@@ -325,7 +380,19 @@ public final class RuntimeAgeBackend {
|
||||
|| terrain != PortAgeTerrain.STANDARD) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise))
|
||||
throw new IllegalStateException("Overworld generator cannot accept authored noise settings");
|
||||
var source = generator.getBiomeSource();
|
||||
var source = terrain == PortAgeTerrain.CAVE
|
||||
? new FixedBiomeSource(server.registryAccess().lookupOrThrow(Registries.BIOME)
|
||||
.getOrThrow(Biomes.LUSH_CAVES))
|
||||
: generator.getBiomeSource();
|
||||
if (terrain == PortAgeTerrain.CAVE && biomePlan != null) {
|
||||
var biomes = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
for (Identifier biomeId : biomePlan.biomes()) {
|
||||
var holder = biomes.get(ResourceKey.create(Registries.BIOME, biomeId))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biomeId));
|
||||
if (!PortRegistryPages.isCaveBiome(holder))
|
||||
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biomeId);
|
||||
}
|
||||
}
|
||||
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
|
||||
source = authoredBiomeSource(server, biomePlan, seed);
|
||||
}
|
||||
@@ -335,15 +402,16 @@ public final class RuntimeAgeBackend {
|
||||
.getOrThrow(NoiseGeneratorSettings.AMPLIFIED);
|
||||
case ISLAND -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(NoiseGeneratorSettings.FLOATING_ISLANDS);
|
||||
case CAVE -> dryCaveSettings(server);
|
||||
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource");
|
||||
};
|
||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||
}
|
||||
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,
|
||||
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
|
||||
key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false);
|
||||
ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain);
|
||||
|
||||
// Publish only after the constructor succeeds. If the load event fails,
|
||||
// roll back the map and close the newly opened chunk/entity stores.
|
||||
@@ -369,4 +437,14 @@ public final class RuntimeAgeBackend {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private static Holder<NoiseGeneratorSettings> dryCaveSettings(MinecraftServer server) {
|
||||
NoiseGeneratorSettings caves = server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(NoiseGeneratorSettings.CAVES).value();
|
||||
return Holder.direct(new NoiseGeneratorSettings(
|
||||
caves.noiseSettings(), caves.defaultBlock(), Blocks.AIR.defaultBlockState(),
|
||||
caves.noiseRouter(), caves.materialRule(), caves.spawnTarget(),
|
||||
caves.noiseSettings().minY() - 1, caves.disableMobGeneration(), Optional.empty(),
|
||||
caves.useLegacyRandomSource(), caves.debugFunctions()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
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.then(arrived -> {
|
||||
// Departure audio can be discarded when the client switches levels.
|
||||
// Play after arrival too so the traveler hears the successful return.
|
||||
arrived.level().playSound(null, arrived.blockPosition(), Rivencraft.LINK_FISSURE.get(),
|
||||
SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||
}));
|
||||
}
|
||||
}
|
||||
+12
@@ -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);
|
||||
}
|
||||
}
|
||||
+6
@@ -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);
|
||||
|
||||
@@ -94,6 +94,8 @@ public final class PortBookBinderScreen extends AbstractContainerScreen<PortBook
|
||||
} else if (PageItem.isLinkPanel(page)) {
|
||||
graphics.fill(x + 5, y + 6, x + 22, y + 17, 0xFF000000);
|
||||
} else {
|
||||
if (!PortAuthoredWriting.isPageImplemented(page.get(Rivencraft.SYMBOL_PAGE_ID.get())))
|
||||
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0x88FF3333);
|
||||
// Render the full-size transparent glyph through the working
|
||||
// item pipeline, without its small parchment-backed icon.
|
||||
ItemStack glyphOnly = page.copy();
|
||||
|
||||
+10
-8
@@ -5,6 +5,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
@@ -18,7 +19,8 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
|
||||
public PageArt extractArgument(ItemStack stack) {
|
||||
Identifier symbol = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
return symbol == null ? null : new PageArt(symbol,
|
||||
stack.getOrDefault(Rivencraft.PAGE_GLYPH_ONLY.get(), false));
|
||||
stack.getOrDefault(Rivencraft.PAGE_GLYPH_ONLY.get(), false),
|
||||
PortAuthoredWriting.isPageImplemented(symbol) ? 0xFFFFFFFF : 0xFFFF6666);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -30,10 +32,10 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
|
||||
: PortSymbolPageTextures.forItem(art.symbol());
|
||||
collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture),
|
||||
(matrix, vertices) -> {
|
||||
vertex(vertices, matrix, 0, 0, 0, 1, light, overlay);
|
||||
vertex(vertices, matrix, 1, 0, 1, 1, light, overlay);
|
||||
vertex(vertices, matrix, 1, 1, 1, 0, light, overlay);
|
||||
vertex(vertices, matrix, 0, 1, 0, 0, light, overlay);
|
||||
vertex(vertices, matrix, 0, 0, 0, 1, light, overlay, art.color());
|
||||
vertex(vertices, matrix, 1, 0, 1, 1, light, overlay, art.color());
|
||||
vertex(vertices, matrix, 1, 1, 1, 0, light, overlay, art.color());
|
||||
vertex(vertices, matrix, 0, 1, 0, 0, light, overlay, art.color());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,12 +46,12 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
|
||||
}
|
||||
|
||||
private static void vertex(VertexConsumer vertices, PoseStack.Pose pose,
|
||||
float x, float y, float u, float v, int light, int overlay) {
|
||||
vertices.addVertex(pose, x, y, 0.5F).setColor(255, 255, 255, 255)
|
||||
float x, float y, float u, float v, int light, int overlay, int color) {
|
||||
vertices.addVertex(pose, x, y, 0.5F).setColor(color)
|
||||
.setUv(u, v).setOverlay(overlay).setLight(light).setNormal(pose, 0, 0, 1);
|
||||
}
|
||||
|
||||
public record PageArt(Identifier symbol, boolean glyphOnly) {}
|
||||
public record PageArt(Identifier symbol, boolean glyphOnly, int color) {}
|
||||
|
||||
public record Unbaked() implements SpecialModelRenderer.Unbaked<PageArt> {
|
||||
public static final Unbaked INSTANCE = new Unbaked();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Same successful-link feedback and sound precedence as the 1.21.1 target. */
|
||||
public final class LinkVisualEffects {
|
||||
private LinkVisualEffects() {}
|
||||
|
||||
public static SoundEvent sound(LinkProperties properties, boolean portal) {
|
||||
if (properties.has(LinkProperties.DISARM)) return Rivencraft.LINK_DISARM.get();
|
||||
if (portal) return Rivencraft.LINK_PORTAL.get();
|
||||
if (properties.has(LinkProperties.INTRA_LINKING)) return Rivencraft.LINK_INTRA.get();
|
||||
return Rivencraft.LINK.get();
|
||||
}
|
||||
|
||||
public static void play(ServerLevel source, Vec3 departure, ServerPlayer player,
|
||||
LinkProperties properties, boolean portal) {
|
||||
SoundEvent event = sound(properties, portal);
|
||||
burst(source, departure, event);
|
||||
burst(player.level(), player.position(), event);
|
||||
}
|
||||
|
||||
private static void burst(ServerLevel level, Vec3 position, SoundEvent event) {
|
||||
level.playSound(null, position.x, position.y, position.z, event,
|
||||
SoundSource.PLAYERS, 0.8F, level.getRandom().nextFloat() * 0.2F + 0.9F);
|
||||
level.sendParticles(ParticleTypes.CLOUD, position.x, position.y, position.z,
|
||||
50, 0.5, 1.0, 0.5, 0.005);
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ public final class LinkingBookItem extends Item {
|
||||
/** Link Panel effects are shared by Linking and Descriptive Books. */
|
||||
public static void applySuccessfulLink(ServerPlayer player, LinkProperties properties,
|
||||
ServerLevel source, Vec3 departure, Vec3 incoming, float destinationYaw) {
|
||||
LinkVisualEffects.play(source, departure, player, properties, false);
|
||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) {
|
||||
double speed = Math.hypot(incoming.x, incoming.z);
|
||||
double yaw = Math.toRadians(destinationYaw);
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.item;
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -12,7 +13,7 @@ import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
|
||||
/** One Page item with a persistent Link Panel variant; symbol writing is not ported yet. */
|
||||
/** Shared Page item with persistent Link Panel and authored symbol variants. */
|
||||
public final class PageItem extends Item {
|
||||
public PageItem(Properties properties) {
|
||||
super(properties);
|
||||
@@ -72,8 +73,10 @@ public final class PageItem extends Item {
|
||||
? description.copy().withStyle(ChatFormatting.GRAY, ChatFormatting.OBFUSCATED)
|
||||
: description.copy().withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
tooltip.accept(Component.translatable("item.rivencraft.page.symbol.port_status")
|
||||
.withStyle(ChatFormatting.DARK_GRAY));
|
||||
boolean implemented = PortAuthoredWriting.isPageImplemented(symbolId);
|
||||
tooltip.accept(Component.translatable(implemented
|
||||
? "item.rivencraft.page.symbol.implemented" : "item.rivencraft.page.symbol.unimplemented")
|
||||
.withStyle(implemented ? ChatFormatting.DARK_GRAY : ChatFormatting.RED));
|
||||
return;
|
||||
}
|
||||
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.dnorberg.rivencraft.age.PortCarverPages;
|
||||
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.chunk.CarvingMask;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseChunk;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.material.rule.MaterialRule;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Merge authored geometry before vanilla consumes its mask with aquifer/surface rules. */
|
||||
@Mixin(NoiseBasedChunkGenerator.class)
|
||||
public abstract class AgeCarversMixin {
|
||||
@Inject(method = "generateCarvers", at = @At(value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/level/chunk/CarvingMask;isEmpty()Z"))
|
||||
private void rivencraft$authoredCarvers(ChunkAccess chunk, Blender blender, NoiseChunk noise,
|
||||
RandomState random, BiomeManager biomes, WorldGenRegion region, MaterialRule material,
|
||||
CallbackInfo ci, @Local CarvingMask mask) {
|
||||
if (region == null) return;
|
||||
var level = region.getLevel();
|
||||
if (level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator generator
|
||||
&& generator.base() == (Object) this && generator.terrainFeatures() != null) {
|
||||
PortCarverPages.carve((NoiseBasedChunkGenerator) (Object) this, chunk, random,
|
||||
region.registryAccess(), generator.terrainFeatures().carvers(), level.getSeed(), mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||
import net.dnorberg.rivencraft.age.PortStructurePages;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||
|
||||
/** Matches 1.21.1's explicit-page biome override, scoped only to authored Age structures. */
|
||||
@Mixin(Structure.class)
|
||||
public abstract class AgeStructureBiomeMixin {
|
||||
// 26.3 jigsaw placement checks biome columns inside findGenerationPoint,
|
||||
// before the final isValidBiome test. Set the predicate when constructing
|
||||
// the context so both checks see the explicit-page override.
|
||||
@ModifyArg(method = "generate", at = @At(value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/level/levelgen/structure/Structure$GenerationContext;<init>(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/world/level/biome/BiomeSource;Lnet/minecraft/world/level/biome/Climate$Sampler;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager;JLnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/LevelHeightAccessor;Ljava/util/function/Predicate;)V"),
|
||||
index = 9)
|
||||
private Predicate<Holder<Biome>> rivencraft$authoredBiome(
|
||||
RegistryAccess registries, ChunkGenerator chunkGenerator, BiomeSource biomes,
|
||||
Climate.Sampler sampler, RandomState random, StructureTemplateManager templates,
|
||||
long seed, ChunkPos chunk, LevelHeightAccessor height, Predicate<Holder<Biome>> original) {
|
||||
if (chunkGenerator instanceof PortFeatureChunkGenerator generator
|
||||
&& generator.terrainFeatures() != null
|
||||
&& PortStructurePages.authored((Structure) (Object) this, registries,
|
||||
generator.terrainFeatures().structures())) {
|
||||
return biome -> true;
|
||||
}
|
||||
return original;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
/** Read-only placement settings for Age-local copies, never edits global placements. */
|
||||
@Mixin(AbstractSpreadingStructurePlacement.class)
|
||||
public interface StructurePlacementAccessor {
|
||||
@Accessor("frequencyReductionMethod")
|
||||
AbstractSpreadingStructurePlacement.FrequencyReductionMethod rivencraft$frequencyReductionMethod();
|
||||
@Accessor("frequency") float rivencraft$frequency();
|
||||
@Accessor("salt") int rivencraft$salt();
|
||||
@Accessor("exclusionZone")
|
||||
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> rivencraft$exclusionZone();
|
||||
}
|
||||
@@ -2,7 +2,9 @@ package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTravel;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
@@ -125,8 +127,20 @@ public final class PortBookNetworking {
|
||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
}
|
||||
try {
|
||||
RuntimeAgeBackend.create(server, id, player.getRandom().nextLong(), biomePlan,
|
||||
PortAuthoredWriting.terrain(pages));
|
||||
long seed = player.getRandom().nextLong();
|
||||
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
|
||||
if (pages.isEmpty()) {
|
||||
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
|
||||
terrain = random.terrain();
|
||||
biomePlan = random.biomes();
|
||||
}
|
||||
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||
PortAuthoredWriting.rockPlan(pages, seed),
|
||||
PortAuthoredWriting.vegetationPlan(pages),
|
||||
PortAuthoredWriting.dungeonCount(pages),
|
||||
PortAuthoredWriting.lakePlan(pages, seed),
|
||||
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
|
||||
net.dnorberg.rivencraft.age.PortWeatherMode.fromPages(pages));
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||
return null;
|
||||
|
||||
@@ -3,6 +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.PortFeatureChunkGenerator;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
@@ -15,6 +16,8 @@ public final class PortChunkGenerators {
|
||||
|
||||
static {
|
||||
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
|
||||
// Keep the beta codec ID so previously saved Rocks Ages remain readable.
|
||||
GENERATORS.register("age_rocks", () -> PortFeatureChunkGenerator.CODEC);
|
||||
}
|
||||
|
||||
private PortChunkGenerators() {}
|
||||
|
||||
+223
-4
@@ -2,17 +2,33 @@ 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.age.PortStructurePages;
|
||||
import net.dnorberg.rivencraft.age.PortCarverPages;
|
||||
import net.dnorberg.rivencraft.age.PortWeatherMode;
|
||||
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(
|
||||
@@ -23,19 +39,93 @@ public final class PortAuthoredWriting {
|
||||
Rivencraft.MOD_ID, "terrain/flat");
|
||||
private static final Identifier VOID_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/void");
|
||||
private static final Identifier CAVE_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
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() {}
|
||||
|
||||
/** Port availability only, not validity of a particular ordered book. */
|
||||
public static boolean isPageImplemented(Identifier page) {
|
||||
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||
var entry = PortSymbolPages.find(page);
|
||||
if (entry == null) return false;
|
||||
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null) return true;
|
||||
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
|
||||
if (isBlockPage(page) && (entry.category().equals("rivencraft:block")
|
||||
|| entry.category().equals("rivencraft:fluid"))) return true;
|
||||
return page.equals(ROCKS) || page.equals(DUNGEONS) || PortLakePlan.isLake(page)
|
||||
|| PortVegetationPlan.isVegetation(page) || PortTerrainFeaturePlan.isSupported(page)
|
||||
|| page.equals(PortTerrainFeaturePlan.SKYLANDS)
|
||||
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|
||||
|| page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|
||||
|| PortStructurePages.supports(page) || PortCarverPages.supports(page);
|
||||
}
|
||||
|
||||
/** A page never disappears into an Age unless this port can apply its effect. */
|
||||
public static boolean supports(List<Identifier> pages) {
|
||||
boolean terrain = false;
|
||||
int biomes = 0;
|
||||
int controllers = 0;
|
||||
String pendingMaterial = null;
|
||||
int rocks = 0;
|
||||
int vegetation = 0;
|
||||
int dungeons = 0;
|
||||
int lakes = 0;
|
||||
int skylands = 0;
|
||||
int denseOres = 0;
|
||||
int starFissures = 0;
|
||||
int weather = 0;
|
||||
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
|
||||
Map<Identifier, Integer> structures = new HashMap<>();
|
||||
Map<Identifier, Integer> carvers = 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 (PortWeatherMode.forPage(page) != null && weather++ == 0) {
|
||||
// A single independent weather policy for this Age.
|
||||
} else if (controller(page) != null && controllers++ == 0) {
|
||||
// An explicit controller needs an authored pool.
|
||||
} else if (isBlockPage(page) && pendingMaterial == null
|
||||
&& PortSymbolPages.find(page) != null
|
||||
&& (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 (PortCarverPages.supports(page)
|
||||
&& carvers.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||
carvers.merge(page, 1, Integer::sum);
|
||||
} else if (PortStructurePages.supports(page)
|
||||
&& structures.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||
structures.merge(page, 1, Integer::sum);
|
||||
} 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
|
||||
@@ -44,6 +134,20 @@ public final class PortAuthoredWriting {
|
||||
biomes++;
|
||||
} else return false;
|
||||
}
|
||||
if (pendingMaterial != null || (rocks > 0 || pages.stream().anyMatch(PortVegetationPlan::requiresSurface))
|
||||
&& (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 (!carvers.isEmpty() && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
PortAgeTerrain structureTerrain = pages.contains(VOID_TERRAIN) ? PortAgeTerrain.VOID
|
||||
: pages.contains(FLAT_TERRAIN) ? PortAgeTerrain.FLAT
|
||||
: pages.contains(CAVE_TERRAIN) ? PortAgeTerrain.CAVE : PortAgeTerrain.STANDARD;
|
||||
if (structures.keySet().stream().anyMatch(page -> !PortStructurePages.supportsTerrain(page, structureTerrain)))
|
||||
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();
|
||||
@@ -60,7 +164,11 @@ public final class PortAuthoredWriting {
|
||||
private static boolean isTerrain(Identifier page) {
|
||||
return page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN)
|
||||
|| page.equals(ISLAND_TERRAIN) || page.equals(FLAT_TERRAIN)
|
||||
|| page.equals(VOID_TERRAIN);
|
||||
|| page.equals(VOID_TERRAIN) || page.equals(CAVE_TERRAIN);
|
||||
}
|
||||
|
||||
private static boolean isBlockPage(Identifier page) {
|
||||
return page.getNamespace().equals(Rivencraft.MOD_ID) && page.getPath().startsWith("block/");
|
||||
}
|
||||
|
||||
private static PortBiomePlan.Distribution controller(Identifier page) {
|
||||
@@ -85,25 +193,136 @@ public final class PortAuthoredWriting {
|
||||
if (pages.contains(ISLAND_TERRAIN)) return PortAgeTerrain.ISLAND;
|
||||
if (pages.contains(FLAT_TERRAIN)) return PortAgeTerrain.FLAT;
|
||||
if (pages.contains(VOID_TERRAIN)) return PortAgeTerrain.VOID;
|
||||
if (pages.contains(CAVE_TERRAIN)) return PortAgeTerrain.CAVE;
|
||||
return PortAgeTerrain.STANDARD;
|
||||
}
|
||||
|
||||
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
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);
|
||||
List<Identifier> structures = pages.stream().filter(PortStructurePages::supports).toList();
|
||||
List<Identifier> carvers = pages.stream().filter(PortCarverPages::supports).toList();
|
||||
return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty() ? null
|
||||
: new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures, structures, carvers);
|
||||
}
|
||||
|
||||
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. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException(
|
||||
"Unsupported terrain or biome-page combination in this port slice");
|
||||
List<Identifier> biomes = new ArrayList<>();
|
||||
PortStructurePages.validateRegistry(pages, registries);
|
||||
PortCarverPages.validateRegistry(pages, registries);
|
||||
PortBiomePlan.Distribution distribution = null;
|
||||
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
|
||||
boolean hasSurfaceBiome = false;
|
||||
boolean hasOreBiome = false;
|
||||
for (Identifier page : pages) {
|
||||
if (isTerrain(page)) continue;
|
||||
PortBiomePlan.Distribution selected = controller(page);
|
||||
if (selected != null) { distribution = selected; continue; }
|
||||
// supports() has already checked the whole book. Resolve only biome
|
||||
// pages here, not every page absent from a growing exclusion list.
|
||||
var entry = PortSymbolPages.find(page);
|
||||
if (entry == null || !entry.category().equals("rivencraft:biome")) continue;
|
||||
Identifier biome = PortRegistryPages.biomeId(page);
|
||||
var registry = registries.lookupOrThrow(Registries.BIOME);
|
||||
if (registry.get(ResourceKey.create(Registries.BIOME, biome)).isEmpty())
|
||||
throw new IllegalArgumentException("Unknown authored biome: " + biome);
|
||||
var holder = registry.get(ResourceKey.create(Registries.BIOME, biome))
|
||||
.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::requiresSurface)
|
||||
&& !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;
|
||||
|
||||
@@ -67,7 +67,7 @@ public final class PortRegistryPages {
|
||||
public static List<ItemStack> biomePages(HolderLookup.Provider registries, boolean caves) {
|
||||
return registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(holder -> !holder.key().identifier().equals(DEFERRED_PLACEHOLDER))
|
||||
.filter(holder -> isCave(holder) == caves)
|
||||
.filter(holder -> isCaveBiome(holder) == caves)
|
||||
.map(holder -> PageItem.createSymbolPage(biomePageId(holder.key().identifier())))
|
||||
.sorted((left, right) -> left.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()
|
||||
.compareTo(right.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()))
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
private static boolean isCave(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,8 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"minecraft:lush_caves",
|
||||
"minecraft:dripstone_caves",
|
||||
"minecraft:deep_dark"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"type": "minecraft:weighted_random_selector",
|
||||
"features": [
|
||||
{"data": "minecraft:red_poplar_leaf_litter", "weight": 200},
|
||||
{"data": "minecraft:orange_poplar_leaf_litter", "weight": 240},
|
||||
{"data": "minecraft:yellow_poplar_leaf_litter", "weight": 90}
|
||||
]
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"feature": "minecraft:fallen_poplar_tree",
|
||||
"placement": [
|
||||
{"type": "minecraft:in_square"},
|
||||
{"type": "minecraft:surface_water_depth_filter", "max_water_depth": 0},
|
||||
{"type": "minecraft:heightmap", "heightmap": "OCEAN_FLOOR"},
|
||||
{
|
||||
"type": "minecraft:block_predicate_filter",
|
||||
"predicate": {"type": "minecraft:would_survive", "state": "minecraft:poplar_sapling"}
|
||||
}
|
||||
]
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"feature": "rivencraft:poplar_trees",
|
||||
"placement": [
|
||||
{"type": "minecraft:count", "count": 6},
|
||||
{"type": "minecraft:in_square"},
|
||||
{"type": "minecraft:surface_water_depth_filter", "max_water_depth": 0},
|
||||
{"type": "minecraft:heightmap", "heightmap": "OCEAN_FLOOR"}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "net.dnorberg.rivencraft.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin"],
|
||||
"injectors": {"defaultRequire": 1}
|
||||
}
|
||||
@@ -4,15 +4,18 @@ issueTrackerURL="https://git.paddedcell.org/dnorberg/rivencraft/issues"
|
||||
[[mods]]
|
||||
modId="${mod_id}"
|
||||
version="${mod_version}"
|
||||
displayName="${mod_name} 26.3 Alpha"
|
||||
displayName="${mod_name} 26.3 Beta"
|
||||
displayURL="https://git.paddedcell.org/dnorberg/rivencraft"
|
||||
authors="dnorberg and contributors"
|
||||
description='''
|
||||
Experimental Minecraft 26.3 alpha. Provides a runtime Age prototype and basic
|
||||
Linking Book travel. Most Rivencraft gameplay has not yet been ported. Use a
|
||||
test world and keep backups; this alpha is not compatible with 1.21.1 worlds.
|
||||
Experimental Minecraft 26.3 beta with books, writing stations and selected
|
||||
authored Age generation. Full 1.21.1 gameplay parity is still in progress.
|
||||
Use a test world and keep backups; 1.21.1 worlds are not compatible.
|
||||
'''
|
||||
|
||||
[[mixins]]
|
||||
config="rivencraft.mixins.json"
|
||||
|
||||
[[dependencies.${mod_id}]]
|
||||
modId="neoforge"
|
||||
type="required"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish a Rivencraft JAR to Modrinth and/or CurseForge.
|
||||
"""Publish a Rivencraft JAR to Gitea, Modrinth, and/or CurseForge.
|
||||
|
||||
Preview is the default. Tokens live in the Secret Service keyring, never in git.
|
||||
"""
|
||||
@@ -19,8 +19,9 @@ import urllib.request
|
||||
|
||||
MODRINTH_PROJECT = "GH2xfZJ8"
|
||||
CURSEFORGE_PROJECT = "1766326"
|
||||
GITEA_API = "https://git.paddedcell.org/api/v1/repos/dnorberg/rivencraft"
|
||||
USER_AGENT = "RivencraftReleaseTool/1.0 (https://git.paddedcell.org/dnorberg/rivencraft)"
|
||||
SITES = ("modrinth", "curseforge")
|
||||
SITES = ("gitea", "modrinth", "curseforge")
|
||||
|
||||
|
||||
def token_for(site: str) -> str:
|
||||
@@ -59,7 +60,13 @@ def store_token(site: str) -> None:
|
||||
|
||||
def check_token(site: str) -> None:
|
||||
token = token_for(site)
|
||||
if site == "modrinth":
|
||||
if site == "gitea":
|
||||
account = request_json("https://git.paddedcell.org/api/v1/token", token=f"token {token}")
|
||||
scopes = account.get("scopes", [])
|
||||
if "write:repository" not in scopes and "all" not in scopes:
|
||||
raise RuntimeError("Gitea token is valid but lacks write:repository scope")
|
||||
print(f"Gitea API access works for {account['user']['login']} (write:repository)")
|
||||
elif site == "modrinth":
|
||||
try:
|
||||
account = request_json("https://api.modrinth.com/v2/user", token=token)
|
||||
except RuntimeError as error:
|
||||
@@ -81,7 +88,7 @@ def check_token(site: str) -> None:
|
||||
|
||||
def request_json(url: str, *, token: str | None = None, token_header: str = "Authorization",
|
||||
payload: bytes | None = None, content_type: str | None = None,
|
||||
method: str = "GET") -> dict | list:
|
||||
method: str = "GET", not_found_ok: bool = False) -> dict | list | None:
|
||||
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
|
||||
if token:
|
||||
headers[token_header] = token
|
||||
@@ -92,12 +99,14 @@ def request_json(url: str, *, token: str | None = None, token_header: str = "Aut
|
||||
with urllib.request.urlopen(request, timeout=120) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code == 404 and not_found_ok:
|
||||
return None
|
||||
# Error bodies can contain server details, but never echo tokens.
|
||||
detail = error.read(600).decode("utf-8", "replace")
|
||||
raise RuntimeError(f"{url}: HTTP {error.code}: {detail}") from None
|
||||
|
||||
|
||||
def multipart(fields: dict[str, str], jar: Path) -> tuple[bytes, str]:
|
||||
def multipart(fields: dict[str, str], jar: Path, *, file_field: str = "file") -> tuple[bytes, str]:
|
||||
boundary = "rivencraft-" + secrets.token_hex(16)
|
||||
chunks = []
|
||||
for name, value in fields.items():
|
||||
@@ -108,7 +117,7 @@ def multipart(fields: dict[str, str], jar: Path) -> tuple[bytes, str]:
|
||||
filename = jar.name.replace('"', "")
|
||||
mime = mimetypes.guess_type(filename)[0] or "application/java-archive"
|
||||
chunks.extend((
|
||||
f"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\nContent-Type: {mime}\r\n\r\n".encode(),
|
||||
f"--{boundary}\r\nContent-Disposition: form-data; name=\"{file_field}\"; filename=\"{filename}\"\r\nContent-Type: {mime}\r\n\r\n".encode(),
|
||||
jar.read_bytes(), b"\r\n", f"--{boundary}--\r\n".encode(),
|
||||
))
|
||||
return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
|
||||
@@ -140,7 +149,6 @@ def release_metadata(args: argparse.Namespace) -> tuple[dict, dict]:
|
||||
"gameVersionNames": [args.minecraft, "NeoForge", "Client", "Server", f"Java {args.java}"],
|
||||
"releaseType": args.channel,
|
||||
"isMarkedForManualRelease": False,
|
||||
"relations": {"projects": []},
|
||||
}
|
||||
return modrinth, curseforge
|
||||
|
||||
@@ -172,6 +180,53 @@ def publish_curseforge(metadata: dict, jar: Path) -> None:
|
||||
print(f"CurseForge: https://www.curseforge.com/minecraft/mc-mods/rivencraft/files/{result['id']} (pending approval)")
|
||||
|
||||
|
||||
def publish_gitea(args: argparse.Namespace) -> None:
|
||||
base_version, suffix = args.version.split("-", 1)
|
||||
if suffix != f"{args.channel}.1":
|
||||
raise RuntimeError("Gitea tag convention requires a version ending in the selected channel and .1")
|
||||
tag = f"V{base_version}-mc{args.minecraft}-{suffix}"
|
||||
# The release must refer to a tag already pushed by the normal Git workflow.
|
||||
remote_tag = subprocess.run(
|
||||
["git", "ls-remote", "origin", f"refs/tags/{tag}"],
|
||||
text=True, capture_output=True, check=True,
|
||||
)
|
||||
if not remote_tag.stdout.strip():
|
||||
raise RuntimeError(f"Push the Gitea tag {tag} before publishing")
|
||||
|
||||
token = f"token {token_for('gitea')}"
|
||||
release_url = f"{GITEA_API}/releases/tags/{tag}"
|
||||
release = request_json(release_url, token=token, not_found_ok=True)
|
||||
notes = args.changelog.read_text(encoding="utf-8")
|
||||
title = f"Rivencraft {args.minecraft} {args.channel} {args.version}"
|
||||
if release is None:
|
||||
metadata = {
|
||||
"tag_name": tag,
|
||||
"target_commitish": "main",
|
||||
"name": title,
|
||||
"body": notes,
|
||||
"draft": False,
|
||||
"prerelease": args.channel != "release",
|
||||
}
|
||||
release = request_json(
|
||||
f"{GITEA_API}/releases", token=token,
|
||||
payload=json.dumps(metadata).encode(), content_type="application/json", method="POST",
|
||||
)
|
||||
elif release.get("name") != title or release.get("body", "").strip() != notes.strip():
|
||||
raise RuntimeError(f"Gitea release {tag} exists with different notes or title; inspect it before retrying")
|
||||
|
||||
assets = release.get("assets", [])
|
||||
if any(asset.get("name") == args.jar.name for asset in assets):
|
||||
raise RuntimeError(f"Gitea release {tag} already has {args.jar.name}")
|
||||
body, content_type = multipart({}, args.jar, file_field="attachment")
|
||||
attachment = request_json(
|
||||
f"{GITEA_API}/releases/{release['id']}/assets", token=token,
|
||||
payload=body, content_type=content_type, method="POST",
|
||||
)
|
||||
if attachment.get("name") != args.jar.name or attachment.get("size") != args.jar.stat().st_size:
|
||||
raise RuntimeError("Gitea returned unexpected attachment metadata; inspect the release")
|
||||
print(f"Gitea: {release['html_url']}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
@@ -187,7 +242,7 @@ def main() -> None:
|
||||
publish.add_argument("--minecraft", required=True)
|
||||
publish.add_argument("--java", required=True)
|
||||
publish.add_argument("--channel", choices=("alpha", "beta", "release"), required=True)
|
||||
publish.add_argument("--site", choices=(*SITES, "both"), default="both")
|
||||
publish.add_argument("--site", choices=(*SITES, "both", "all"), default="all")
|
||||
publish.add_argument("--publish", action="store_true", help="Actually upload; omitted by default")
|
||||
args = parser.parse_args()
|
||||
if args.command == "store-token":
|
||||
@@ -205,13 +260,15 @@ def main() -> None:
|
||||
if args.jar.name != f"rivencraft-{args.minecraft}-{args.version}.jar":
|
||||
raise RuntimeError("JAR filename does not match Minecraft and release versions")
|
||||
modrinth, curseforge = release_metadata(args)
|
||||
sites = SITES if args.site == "both" else (args.site,)
|
||||
sites = SITES if args.site == "all" else (("modrinth", "curseforge") if args.site == "both" else (args.site,))
|
||||
print(f"JAR: {args.jar.resolve()} ({args.jar.stat().st_size} bytes)")
|
||||
print(f"Release: {args.version} | {args.channel} | Minecraft {args.minecraft} | NeoForge | Java {args.java}")
|
||||
print(f"Destinations: {', '.join(sites)}")
|
||||
if not args.publish:
|
||||
print("PREVIEW ONLY: add --publish to upload")
|
||||
return
|
||||
if "gitea" in sites:
|
||||
publish_gitea(args)
|
||||
if "modrinth" in sites:
|
||||
publish_modrinth(modrinth, args.jar)
|
||||
if "curseforge" in sites:
|
||||
|
||||
+180
-1
@@ -71,8 +71,24 @@ send_and_wait 'rivencraft_port age create smoke_alpha 123456789' 'Created rivenc
|
||||
send_and_wait 'rivencraft_port age create smoke_beta 987654321' 'Created rivencraft:age/smoke_beta with seed 987654321'
|
||||
send_and_wait 'rivencraft_port age create smoke_flat 24680 flat' 'Created rivencraft:age/smoke_flat with seed 24680 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create smoke_void 13579 void' 'Created rivencraft:age/smoke_void with seed 13579 terrain=VOID'
|
||||
send_and_wait 'rivencraft_port age create smoke_cave 112233 cave' 'Created rivencraft:age/smoke_cave with seed 112233 terrain=CAVE'
|
||||
send_and_wait 'rivencraft_port age create_multi smoke_flat_multi 24681 flat' 'Created rivencraft:age/smoke_flat_multi with seed 24681 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_multi smoke_void_multi 13580 void' 'Created rivencraft:age/smoke_void_multi with seed 13580 terrain=VOID'
|
||||
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"
|
||||
@@ -84,18 +100,118 @@ gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_alpha'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_beta'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat_multi'
|
||||
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='
|
||||
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave seed=112233 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 chunkOffset=0 terrain='
|
||||
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'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_void_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void_multi to be force loaded'
|
||||
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 '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'
|
||||
@@ -105,10 +221,13 @@ send_and_wait 'execute in rivencraft:age/smoke_void_multi if block 0 0 0 minecra
|
||||
send_and_wait 'rivencraft_port age biome smoke_flat_multi 128 128' 'rivencraft:age/smoke_flat_multi biome at 128,128=minecraft:plains'
|
||||
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 128 128' 'rivencraft:age/smoke_cave_multi biome at 128,128=minecraft:lush_caves'
|
||||
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
|
||||
alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
beta_before=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
flat_before=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
void_before=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
random_before=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
if [[ "$alpha_before" == "$beta_before" ]]; then
|
||||
printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2
|
||||
exit 1
|
||||
@@ -125,14 +244,46 @@ send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alp
|
||||
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='
|
||||
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave seed=112233 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 chunkOffset=0 terrain='
|
||||
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_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'
|
||||
alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
beta_after=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
flat_after=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
void_after=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
random_after=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
[[ "$alpha_before" == "$alpha_after" && "$beta_before" == "$beta_after" ]] || {
|
||||
printf 'Age terrain changed across restart: %s/%s -> %s/%s\n' \
|
||||
"$alpha_before" "$beta_before" "$alpha_after" "$beta_after" >&2
|
||||
@@ -143,5 +294,33 @@ void_after=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain
|
||||
"$flat_before" "$void_before" "$flat_after" "$void_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$random_before" == "$random_after" ]] || {
|
||||
printf 'Random Age terrain changed across restart: %s -> %s\n' \
|
||||
"$random_before" "$random_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$rocks_before" == "$rocks_after" ]] || {
|
||||
printf 'Authored Rocks changed across restart: %s -> %s\n' \
|
||||
"$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 and Void 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'
|
||||
Reference in new issue
Block a user