Public Access
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7a73d3c17 | ||
|
|
48fbae9be9 | ||
|
|
c223f3fdca | ||
|
|
d75154fa9b |
No files matched your search
@@ -7,9 +7,20 @@ 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/lighting, Overworld/Nether/End and animated Static skies,
|
||||
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.71-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.
|
||||
|
||||
@@ -508,6 +508,563 @@ 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.
|
||||
|
||||
The local `1.0.1.66-beta.1` slice ports the three 1.21.1 lighting controls.
|
||||
Normal surface types keep the host Overworld holder; Dark disables skylight,
|
||||
Bright sets ambient light to 0.25. Cave Bright is explicitly supported with
|
||||
0.25 ambient and no skylight; Cave Normal/Dark have 0.0 ambient and no skylight.
|
||||
The native vanilla registry-sync path supplies types to clients before travel.
|
||||
Generated data derives three variants from the exact 26.3 vanilla Overworld
|
||||
resource, preserving every key except has_skylight and ambient_light. The
|
||||
generator has declared inputs/outputs and depends on Minecraft artifacts.
|
||||
|
||||
The Age index has an optional lighting map, defaulting old entries to Normal.
|
||||
Creation supplies the choice before constructing the level and saving the
|
||||
index. Restore/reload/reseed read it from the index; retirement removes it and
|
||||
failed retirement restores it before reloading. Pages share grammar/tint and
|
||||
the positive biome-page filter, avoiding the weather-only compiler regression.
|
||||
Missing lighting stays Normal until the random-trait port; Bright instability
|
||||
is pending with the instability system. No sky/celestial renderer is added.
|
||||
The static variants derive vanilla data, not arbitrary data-pack overrides
|
||||
to the host's Overworld type; broader custom type inheritance is not claimed.
|
||||
|
||||
Build/diff checks and generated-data equality checks pass (only the two
|
||||
lighting fields differ from vanilla). A disposable server with a normal
|
||||
Overworld compiled all six surface/Cave lighting-page combinations and
|
||||
reported the expected type, skylight and ambient values. Cave Bright survived
|
||||
unload/reload and reseeding, and all six choices survived a full restart.
|
||||
Overworld skylight/ambient stayed unchanged. An earlier Superflat-host fixture
|
||||
could test surface variants but could not create Cave terrain because the
|
||||
existing backend requires a noise-based host generator; it was discarded and
|
||||
the Cave checks ran on a fresh normal-host fixture. Test servers stopped
|
||||
cleanly and both temporary worlds were deleted. Client-side appearance,
|
||||
authoring, travel, placed lights and multiplayer registry behavior are manual
|
||||
checks, not inferred from the server probes. No commit/push/publication.
|
||||
|
||||
The local `1.0.1.67-beta.1` corrects the visual failure found in slice 66:
|
||||
LightmapRenderStateExtractor and lightmap.fsh use AMBIENT_LIGHT_COLOR, while
|
||||
dimension ambient_light only affects other brightness queries. Merely porting
|
||||
1.21.1's 0.25 field was insufficient. Bright types now provide white native
|
||||
ambient color and ambient_light 1.0; Cave Bright retains no skylight. A scoped
|
||||
NeoForge spawn-placement handler denies natural/chunk-generation MONSTER
|
||||
spawns in Bright runtime Ages, leaving passive/existing mobs, spawners,
|
||||
commands/eggs and event spawns untouched. It does not manufacture propagated
|
||||
block light or alter crop light checks. This supersedes slice 66's Bright
|
||||
appearance and natural-spawn limitation; existing Bright types update on boot.
|
||||
|
||||
Requested follow-up for the color-page port: add ambient-light color as a
|
||||
separate authorable target using minecraft:visual/ambient_light_color. Keep
|
||||
the existing number/color mixing semantics and white Bright default; do not
|
||||
confuse ambient tint with sky/celestial color or its server spawn policy.
|
||||
Color pages themselves remain unported in this slice.
|
||||
Build/diff and generated attribute checks pass. A disposable dedicated server
|
||||
verified the registered event handler denies a successful-default natural
|
||||
zombie placement in surface/Cave Bright, leaves spawner zombies and natural
|
||||
cows unchanged, and leaves Normal unaffected. Those are isolated event-policy
|
||||
probes, not a long-running natural-spawn census. The server shut down and its
|
||||
temporary world was deleted. Actual client appearance awaits player validation.
|
||||
|
||||
The local `1.0.1.68-beta.1` removes the requested-against MobSpawnEvent veto.
|
||||
An Age-scoped LevelLightEngine.getRawBrightness return modifier contributes
|
||||
Bright's full ambient minimum of 15 to combined brightness on both sides.
|
||||
Vanilla Monster.isDarkEnoughToSpawn now sees that light, rather than being
|
||||
overridden or filtered by mob category/spawn reason. Light-ignoring mobs and
|
||||
special spawns retain vanilla rules; no spawner exemptions are introduced.
|
||||
Block/sky storage and propagation remain physical, while combined-light
|
||||
consumers (including plant checks) see ambient light. Cave Bright retains
|
||||
no skylight, and the native white render attribute remains. This supersedes
|
||||
slice 67's separate spawn policy and unchanged-crop-light behavior.
|
||||
|
||||
The lighting probe now calls vanilla's darkness predicate and combined
|
||||
brightness instead of constructing synthetic spawn events. Ambient-color
|
||||
authoring remains a planned part of the color-page port.
|
||||
Build/diff checks pass. A disposable dedicated server loaded the required
|
||||
mixin and reported effective brightness 15 / vanilla darkness false for
|
||||
surface/Cave Bright, versus 0 / true for Dark. These are direct brightness/
|
||||
vanilla-predicate probes, not a natural-spawn census or a fully generated
|
||||
underground-room test; an unloaded Normal column returned native sky light 15.
|
||||
The server stopped cleanly and its temporary world was deleted. Client visual
|
||||
validation remains with the player; no publication is performed.
|
||||
|
||||
The local `1.0.1.69-beta.1` slice completes missing weather/lighting choices
|
||||
at book binding. PortEnvironmentSelection resolves explicit supported pages
|
||||
first and uses independent seed-salted Legacy-rank rolls for omissions.
|
||||
Normal is rank 1, remaining supported modes rank 2: 3:1:1 lighting and
|
||||
6:1:1:1:1:1 weather. These weights derive from the active enum pool and reuse
|
||||
the existing weighted ticket routine, rather than duplicating random-choice
|
||||
logic or enabling unported Cloudy/Rain/Snow modes. It is rank-policy parity,
|
||||
not exact seed-stream parity with 1.21.1's broader trait pool.
|
||||
|
||||
The selected weather/lighting are passed through the existing save-before-
|
||||
catalog creation path. Restoring Ages never calls the resolver, so old Ages
|
||||
retain their saved values. Cave weather is always Off; Cave lighting remains
|
||||
independent and supports Bright. Explicit developer fixtures remain controlled,
|
||||
but create_random now uses the same resolver as pageless books. A read-only
|
||||
environment probe checks seed repeatability and explicit overrides. Superseded
|
||||
Normal-only fromPages helpers are removed. Random feature, sky/celestial,
|
||||
blocklist policy and instability work remain pending.
|
||||
Build/diff checks pass. Disposable dedicated-server probes repeated seeds
|
||||
0/4/7, verified explicit overrides and independent one-field completion, and
|
||||
confirmed Cave weather Off with Bright lighting. Three random Ages saved
|
||||
Always/Normal, Normal/Dark and Fast/Bright respectively; weather/lighting
|
||||
choices and live weather timers persisted through a full server restart.
|
||||
Overworld weather/type stayed unchanged. The server stopped cleanly and its
|
||||
temporary world was deleted. Player book-authoring/visual checks remain
|
||||
manual. No commit, push or publication is performed.
|
||||
|
||||
The local `1.0.1.70-beta.1` slice ports native Overworld/Nether/End sky styles.
|
||||
The shared grammar accepts one style and the biome compiler ignores it by
|
||||
category. Sky is saved in an optional Age-index map with Overworld fallback;
|
||||
restore/reseed/retirement rollback use the saved choice. It does not alter
|
||||
the missing-weather/lighting seed-completion rules, add random sky selection,
|
||||
or enable Static Sky/color/individual-celestial pages prematurely.
|
||||
|
||||
The dimension-data generator now produces 11 sky/lighting variants from the
|
||||
vanilla Overworld baseline. It changes visual skybox/fog/cloud settings only
|
||||
in addition to lighting. Geometry, scale, ceiling, monster settings, default
|
||||
clock and gameplay attributes are retained; no Nether gameplay or End dragon
|
||||
fight is inherited. A derived native day timeline omits only sky/fog/cloud
|
||||
color tracks for non-Overworld styles, preserving schedules and illumination.
|
||||
Native End/no-sky rendering needs no optional renderer or new artwork. The
|
||||
Legacy animated Static Sky renderer cannot be replaced by native flat gray
|
||||
without losing its behavior, so its page remains red/unimplemented.
|
||||
|
||||
Bright detection includes all registered sky variants, preserving both white
|
||||
ambient rendering and the effective brightness floor for surface/Cave Bright.
|
||||
Data equality checks confirm all 11 variants preserve geometry/gameplay and
|
||||
the day timeline keeps native tracks except the three excluded visual colors.
|
||||
Its global time markers remain defined only by minecraft:day, not duplicated
|
||||
in the derived timeline. The grouped timeline list is registered as a tag.
|
||||
Client appearance and multiplayer travel validation remain manual.
|
||||
Registry validation initially caught an invalid inline tag reference and
|
||||
duplicate global clock markers in the derived day timeline; both were fixed
|
||||
before handoff. The sky diagnostic also now supplies a position when sampling
|
||||
the spatial fog attribute. A disposable dedicated server then restored all
|
||||
sampled types after restart, including Nether normal/Bright and End normal/
|
||||
Cave Bright, with the expected reddish/purple fog at the sample position,
|
||||
unchanged geometry/gameplay flags, and effective light 15 on Bright variants.
|
||||
Cave Bright End retained its type after unload/reload; a reseeded Nether Age
|
||||
retained its style after restart. Servers stopped cleanly and temporary test
|
||||
directories/worlds were deleted. These are server/data checks, not a rendered
|
||||
client pass. No commit/push/publication is performed.
|
||||
|
||||
## Static Sky renderer slice
|
||||
|
||||
`1.0.1.71-beta.1` ports the 1.21.1 Legacy-style animated Static Sky, superseding
|
||||
the previous unsupported/red status. The six independent 48×48 noise faces
|
||||
use the existing default gray tint (146 per channel), noise range 65–224,
|
||||
and a two-world-tick refresh. A cached 24-vertex cube and a single 288×48
|
||||
dynamic texture use the native textured sky pipeline instead of rebuilding
|
||||
thousands of colored cells every frame. No new asset or dependency is needed.
|
||||
|
||||
Client-only mixins carry the animation frame in extracted sky state, reset
|
||||
it with the state, and own/close GPU resources with SkyRenderer. Static types
|
||||
have native skybox NONE; the extracted state alone requests a sky pass and
|
||||
the custom renderer replaces it. End flashes and End gameplay are not enabled.
|
||||
Normal/Dark/Bright/Cave Bright variants preserve the existing lighting policy,
|
||||
geometry and gameplay. Existing persistence handles STATIC as a sky enum value.
|
||||
Random sky selection, color pages, and individual celestial bodies remain
|
||||
separate unfinished work; this slice does not claim complete sky parity.
|
||||
|
||||
Verification: port build passed (no automated unit tests configured). A
|
||||
disposable dedicated server created Static Standard Normal and Static Cave
|
||||
Bright Ages, confirmed their gray fog, NONE native skybox and correct skylight/
|
||||
ambient values, then restored both choices after restart. It stopped cleanly;
|
||||
the temporary server/world was deleted. Client animation, travel between sky
|
||||
styles, and restart appearance still require manual validation. No user world
|
||||
was modified, and nothing was committed, pushed or published.
|
||||
|
||||
## Verification of this first slice
|
||||
|
||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||
@@ -516,3 +1073,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.
|
||||
+507
-3
@@ -4,9 +4,9 @@ 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, Void, or Cave 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
|
||||
biome-distribution pages, selected material/vegetation/structure features, and
|
||||
independent Age weather and lighting. Celestial 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
|
||||
@@ -194,6 +194,510 @@ 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.
|
||||
|
||||
The local `1.0.1.66-beta.1` slice ports Normal, Dark, and Bright Lighting.
|
||||
One lighting page is accepted per book and its choice is saved in the Age
|
||||
index before catalog publication. Missing lighting currently defaults to
|
||||
Normal; random lighting selection and Bright's instability cost await those
|
||||
systems' later ports. Lighting pages should no longer be tinted red.
|
||||
|
||||
Normal surface Ages retain the Overworld type. Dark removes skylight with
|
||||
ambient light 0.0; Bright uses ambient light 0.25 and retains surface skylight,
|
||||
matching 1.21.1. **Cave + Bright is supported:** it has ambient light 0.25
|
||||
without skylight. Cave Normal/Dark use no skylight and ambient light 0.0.
|
||||
Placed light sources continue working; ambient light is not a placed light
|
||||
level or a guarantee that monsters cannot spawn.
|
||||
|
||||
The build derives three registered dimension-type variants from the current
|
||||
vanilla Overworld JSON, changing only has_skylight and ambient_light. This
|
||||
preserves native attributes/timelines and synchronizes types through the
|
||||
normal login registry exchange; no dynamic client registry mutation is used.
|
||||
These variants currently use the vanilla type, not custom runtime changes to
|
||||
the host Overworld dimension type made by another data pack.
|
||||
Saved lighting survives reload, reseed, restart and retirement rollback.
|
||||
Existing non-Cave Ages without a choice remain Normal; old Cave Ages now use
|
||||
the no-skylight baseline. Use new Ages for clear visual comparisons and avoid
|
||||
confusing previously lit chunks with the current dimension settings.
|
||||
|
||||
Manual check: create Normal/Dark/Bright surface books and Cave + Bright;
|
||||
compare an unlit enclosed room, check placed torches, revisit/restart and
|
||||
confirm the Overworld stays unchanged. Operator diagnostics:
|
||||
`/rivencraft_port age create_lighting <name> <seed> <mode> <terrain>` and
|
||||
`/rivencraft_port age lighting_probe <name>` report the registered type,
|
||||
skylight and ambient light. Test JAR: `1.0.1.66-beta.1`.
|
||||
|
||||
The local `1.0.1.67-beta.1` correction supersedes slice 66's Bright rendering:
|
||||
26.3's lightmap uses `minecraft:visual/ambient_light_color`, not the old
|
||||
dimension `ambient_light` field. Bright surface/Cave types now set that native
|
||||
attribute to white and ambient_light to 1.0. Cave still has no skylight.
|
||||
Bright Ages reject natural/chunk-generation hostile spawns independently of
|
||||
stored light levels. Existing mobs, passive mobs, spawners, eggs, commands
|
||||
and special-event spawns are not deleted or globally disabled. Actual block
|
||||
light propagation and plant light requirements are unchanged. Existing Bright
|
||||
Ages receive the corrected registered types after restart; no new Age needed.
|
||||
|
||||
Planned color-page target: **ambient light color**, alongside sky/celestial
|
||||
colors. Reuse the native 26.3 attribute for authored tints; Bright defaults to
|
||||
neutral white until the color-page port. This does not enable color pages yet.
|
||||
|
||||
The local `1.0.1.68-beta.1` replaces slice 67's special spawn veto with actual
|
||||
effective ambient brightness. The spawn event handler is removed. Bright's
|
||||
combined light-engine reading now has a minimum of 15 on server and client,
|
||||
including after sky darkening. Its white rendering attribute is retained.
|
||||
Vanilla darkness checks therefore reject light-sensitive monsters naturally;
|
||||
light-ignoring mobs, spawners and other spawn sources use their native rules,
|
||||
not Rivencraft exemptions or bans. This does not remove existing mobs.
|
||||
|
||||
Ambient light is not stored as emitted block light or fabricated skylight.
|
||||
Consumers of combined brightness, including plant light checks, now see the
|
||||
ambient contribution. Normal/Dark and non-Rivencraft dimensions are unchanged.
|
||||
Cave Bright remains without skylight. Existing Bright Ages update on restart.
|
||||
This supersedes slice 67's statement that crop light checks are unchanged.
|
||||
|
||||
The local `1.0.1.69-beta.1` slice seed-completes missing weather and lighting
|
||||
pages for newly bound books, including Link-Panel-only random Ages. Explicit
|
||||
pages take precedence. Cave weather remains Off, while Cave lighting can
|
||||
still choose Bright. Old Ages retain their saved choices; they are not rerolled.
|
||||
|
||||
The supported pool uses the 1.21.1 Legacy-rank rule: Normal is rank 1, other
|
||||
choices rank 2. Lighting weights are 3:1:1 for Normal/Dark/Bright; weather
|
||||
weights are 6:1:1:1:1:1 for Normal/Fast/Slow/Off/Always/Storm. The common weight
|
||||
is derived from the supported pool size. Unported Cloudy/Rain/Snow modes are
|
||||
not selected. Independent seed salts keep a missing lighting roll unchanged
|
||||
when weather is explicitly written, and vice versa. This matches the rank
|
||||
policy, not the exact 1.21.1 seed stream/pool. No random feature list, sky or
|
||||
celestial choices are added by this slice.
|
||||
|
||||
`/rivencraft_port age create_random <name> <seed>` uses the same environment
|
||||
resolver and logs its frozen plan. `/rivencraft_port age environment_probe
|
||||
<seed> <terrain>` inspects deterministic defaults and explicit Storm/Bright
|
||||
precedence without creating an Age. Other explicit operator fixtures keep
|
||||
their Normal defaults to isolate the feature under test.
|
||||
|
||||
For manual checking, create several Link-Panel-only books and verify weather/
|
||||
lighting varies across Ages and persists on revisit/restart. Then author an
|
||||
explicit Normal Weather + Normal Lighting book, and a Cave + Bright book.
|
||||
Include explicit Normal pages when comparing terrain without random lighting
|
||||
or weather changes. Random blocklist policy and instability remain pending.
|
||||
|
||||
The local `1.0.1.70-beta.1` slice ports the Overworld, Nether, and End Sky
|
||||
pages. One sky style is accepted per book and persisted independently of
|
||||
terrain, weather and lighting. Missing sky pages still use Overworld; random
|
||||
sky selection is not part of this slice. Static Sky remains unsupported/red
|
||||
until its Legacy animated renderer is ported, rather than being replaced by
|
||||
a flat gray backdrop. Color and individual celestial pages are also pending.
|
||||
|
||||
Nether Sky uses the native no-sky backdrop and reddish fog; End Sky uses the
|
||||
native End backdrop. Neither changes dimension height, coordinate scale,
|
||||
terrain, Nether water/bed rules, or enables an End dragon fight. All lighting
|
||||
choices work with these styles, including Cave + Bright. Bright's render
|
||||
attribute and effective ambient level 15 are retained on each variant.
|
||||
|
||||
The registered variants derive the vanilla Overworld type and preserve its
|
||||
gameplay attributes. Non-Overworld skies use a derived day timeline without
|
||||
the three sky/fog/cloud tint tracks, retaining native gameplay schedules and
|
||||
illumination while avoiding Overworld day tint on those backdrops. Their
|
||||
cloud color is transparent. Biome/weather fog modifiers still apply. Types
|
||||
sync through normal registry login; no new rendering dependency is required.
|
||||
Sky choices survive reload/reseed/restart and retirement rollback. Old Ages
|
||||
without a sky choice retain Overworld style.
|
||||
|
||||
For visual checks, author Plains + Normal Lighting + No Weather with each
|
||||
sky page; compare the sky/fog and verify the same terrain and normal bed/water
|
||||
behavior. Then try Cave + Bright + End Sky and revisit after restarting.
|
||||
Static Sky should remain red. Operator fixtures:
|
||||
`/rivencraft_port age create_sky <name> <seed> <sky> <lighting> <terrain>` and
|
||||
`/rivencraft_port age sky_probe <name>`. Use new books to author a sky choice;
|
||||
this does not rewrite the sky choice of already-bound books.
|
||||
|
||||
The local `1.0.1.71-beta.1` slice adds the previously pending Static Sky.
|
||||
Its client renderer adapts the 1.21.1 Legacy-style background: six independent
|
||||
48×48 noise faces, default gray tint `#929292`, and noise intensity 65–224.
|
||||
The noise updates every two world ticks. A single small dynamic texture and
|
||||
cached cube replace thousands of per-cell draw vertices; resources are closed
|
||||
with the sky renderer. No artwork or rendering dependency is added.
|
||||
|
||||
Static Sky is now supported rather than red, and persists through the same
|
||||
Age index as other sky styles. Four registered lighting variants retain the
|
||||
existing terrain/gameplay rules. Their native skybox is NONE (no End flashes);
|
||||
only the extracted client state schedules the custom background sky pass.
|
||||
Animation state resets between frames and does not consult a different live
|
||||
world while rendering. Color pages, celestial pages, and random sky selection
|
||||
remain pending.
|
||||
|
||||
Build and disposable dedicated-server creation/restart checks passed for
|
||||
Static + Normal + Standard and Static + Bright + Cave. These do not validate
|
||||
client rendering. For manual testing, bind a new Static Sky + Normal Lighting
|
||||
+ No Weather book, look around/above/below for animated gray static, then
|
||||
visit an Overworld/End Sky Age and return to check there is no sky-state leak.
|
||||
Revisit after restarting. Also try Static Sky + Cave + Bright (look outside
|
||||
terrain or in spectator mode). The fixture accepts `static` as its sky value.
|
||||
The dedicated test server was stopped and its temporary world deleted.
|
||||
Changes remain local; no commit/push/publication was requested.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
|
||||
@@ -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).
|
||||
@@ -0,0 +1,59 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.71-beta.1
|
||||
|
||||
This beta collects the player-tested lighting and sky port slices since
|
||||
1.0.1.65-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Lighting and seeded environments
|
||||
|
||||
- Added Normal, Dark, and Bright lighting pages with saved per-Age settings.
|
||||
- Bright works in Cave terrain too, without adding skylight or weather.
|
||||
- Bright supplies effective ambient light level 15 to native combined-light
|
||||
checks and the white ambient rendering attribute. Vanilla darkness-based
|
||||
monster checks respond to that lighting; there is no separate mob-spawn
|
||||
veto. Mobs that ignore light retain their normal rules.
|
||||
- Missing weather and lighting controllers now resolve deterministically from
|
||||
the Age seed, using independent choices and Legacy-style Normal weighting.
|
||||
Explicit pages win; Cave terrain still forces weather Off. Existing Ages
|
||||
keep their saved choices rather than rerolling on load.
|
||||
|
||||
## Sky styles
|
||||
|
||||
- Added Overworld, Nether, End, and animated Static Sky pages, independently
|
||||
of terrain, weather, and lighting. Choices persist on reload and restart.
|
||||
- Nether Sky uses a reddish fog backdrop; End Sky uses the native End sky.
|
||||
These are visual styles, not Nether/End terrain or gameplay conversions:
|
||||
dimension geometry, coordinate scale, bed/water rules, and dragon behavior
|
||||
are not changed.
|
||||
- Static Sky adapts the existing Legacy-style renderer: six independent
|
||||
48×48 animated gray noise faces. A cached cube and small dynamic texture
|
||||
avoid rebuilding thousands of per-cell vertices each frame. Client-only
|
||||
rendering resources are released with the sky renderer.
|
||||
- Non-Overworld backgrounds have transparent clouds and a derived day
|
||||
timeline that avoids Overworld sky/fog/cloud color tracks while retaining
|
||||
gameplay and illumination schedules.
|
||||
- Updated supported-page feedback and added operator sky/environment probes.
|
||||
|
||||
## Current limits and testing
|
||||
|
||||
- Missing Sky pages still default to Overworld; random sky selection, color
|
||||
pages and individual celestial bodies remain pending. Number-controlled
|
||||
Static Sky resolution is tracked separately in issue #109 for both versions.
|
||||
- The writing/discovery loop, instability, portals, and optional-integration
|
||||
parity are still incomplete. Creative availability is not a support claim;
|
||||
check page tooltips and the beta guide for supported combinations.
|
||||
- Previously generated chunks are not regenerated. Save compatibility during
|
||||
the beta is not guaranteed; keep backups and keep both version tracks apart.
|
||||
|
||||
The player reported in-game passes for the included slices, including Static
|
||||
Sky. Build checks passed for both version tracks. Targeted disposable-server
|
||||
checks covered environment defaults, explicit choices, registered sky/lighting
|
||||
variants, persistence and restart, including Cave + Bright and Static Sky.
|
||||
This is not a claim of complete dedicated-server or optional-mod coverage.
|
||||
Test servers were stopped and temporary worlds removed.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99** and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required by this build. The 1.21.1 release
|
||||
and its dependencies are unchanged.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -121,9 +121,80 @@ def generatePortLanguage = tasks.register('generatePortLanguage', GeneratePortLa
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortLanguage
|
||||
|
||||
// Derive registered presentation variants without duplicating vanilla gameplay settings.
|
||||
abstract class GeneratePortDimensionTypes extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getMinecraftJar()
|
||||
@OutputDirectory abstract DirectoryProperty getOutputDir()
|
||||
|
||||
@TaskAction void generate() {
|
||||
new java.util.zip.ZipFile(minecraftJar.get().asFile).withCloseable { archive ->
|
||||
def entry = archive.getEntry('data/minecraft/dimension_type/overworld.json')
|
||||
if (entry == null) throw new GradleException('Missing vanilla Overworld dimension type')
|
||||
String template = archive.getInputStream(entry).withCloseable { it.getText('UTF-8') }
|
||||
def dayEntry = archive.getEntry('data/minecraft/timeline/day.json')
|
||||
if (dayEntry == null) throw new GradleException('Missing vanilla day timeline')
|
||||
def day = archive.getInputStream(dayEntry).withCloseable {
|
||||
new groovy.json.JsonSlurper().parseText(it.getText('UTF-8'))
|
||||
}
|
||||
// Keep gameplay schedules and illumination, but do not tint these
|
||||
// native non-Overworld sky backgrounds blue/black with the day cycle.
|
||||
['fog_color', 'sky_color', 'cloud_color'].each { day.tracks.remove("minecraft:visual/${it}".toString()) }
|
||||
// Markers are global per clock and already owned by minecraft:day.
|
||||
day.remove('time_markers')
|
||||
File dayFile = new File(outputDir.get().asFile, 'data/rivencraft/timeline/age_sky_day.json')
|
||||
dayFile.parentFile.mkdirs()
|
||||
dayFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(day)) + '\n'
|
||||
File timelineTag = new File(outputDir.get().asFile, 'data/rivencraft/tags/timeline/age_sky.json')
|
||||
timelineTag.parentFile.mkdirs()
|
||||
timelineTag.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([
|
||||
values: ['#minecraft:universal', 'rivencraft:age_sky_day',
|
||||
'minecraft:moon', 'minecraft:early_game']])) + '\n'
|
||||
['overworld', 'nether', 'end', 'static'].each { sky ->
|
||||
['age_normal', 'age_dark', 'age_bright', 'age_cave_bright'].each { lighting ->
|
||||
if (sky == 'overworld' && lighting == 'age_normal') return
|
||||
String name = lighting + (sky == 'overworld' ? '' : "_sky_${sky}")
|
||||
def definition = new groovy.json.JsonSlurper().parseText(template)
|
||||
definition.has_skylight = lighting in ['age_normal', 'age_bright']
|
||||
definition.ambient_light = lighting in ['age_bright', 'age_cave_bright'] ? 1.0 : 0.0
|
||||
// 26.3's lightmap reads this attribute, not ambient_light.
|
||||
if (definition.ambient_light == 1.0)
|
||||
definition.attributes['minecraft:visual/ambient_light_color'] = '#ffffff'
|
||||
if (sky != 'overworld') {
|
||||
definition.skybox = sky == 'end' ? 'end' : 'none'
|
||||
definition.attributes['minecraft:visual/fog_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#181318' : '#6b2725'
|
||||
definition.attributes['minecraft:visual/sky_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#000000' : '#6b2725'
|
||||
definition.attributes['minecraft:visual/cloud_color'] = '#00000000'
|
||||
if (sky == 'nether') {
|
||||
definition.attributes['minecraft:visual/fog_start_distance'] = 10.0
|
||||
definition.attributes['minecraft:visual/fog_end_distance'] = 96.0
|
||||
}
|
||||
definition.timelines = '#rivencraft:age_sky'
|
||||
}
|
||||
File result = new File(outputDir.get().asFile, "data/rivencraft/dimension_type/${name}.json")
|
||||
result.parentFile.mkdirs()
|
||||
result.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(definition)) + '\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def generatePortDimensionTypes = tasks.register('generatePortDimensionTypes', GeneratePortDimensionTypes) {
|
||||
dependsOn 'createMinecraftArtifacts'
|
||||
minecraftJar.set(layout.buildDirectory.file("moddev/artifacts/minecraft-patched-${neo_version}.jar"))
|
||||
outputDir.set(layout.buildDirectory.dir('generated/resources/dimensionTypes'))
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortDimensionTypes
|
||||
|
||||
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.39-beta.1
|
||||
mod_version=1.0.1.71-beta.1
|
||||
@@ -6,19 +6,33 @@
|
||||
"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.sky.overworld.description": "Use the native Overworld sky. Terrain and lighting remain independently authored.",
|
||||
"symbol.rivencraft.sky.nether.description": "Use a Nether-style sky and reddish fog, without Nether terrain or gameplay rules.",
|
||||
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop, without End terrain or a dragon fight.",
|
||||
"symbol.rivencraft.sky.static.description": "An animated gray static sky surrounds the Age.",
|
||||
"symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness. Cave terrain remains without skylight.",
|
||||
"symbol.rivencraft.lighting.dark.description": "Remove skylight from this Age; placed light sources still work.",
|
||||
"symbol.rivencraft.lighting.bright.description": "Provide full ambient brightness and effective light level 15. Vanilla light-based spawn rules apply, including in Cave terrain without skylight.",
|
||||
"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 +47,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,21 @@ 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),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortLighting.CODEC)
|
||||
.optionalFieldOf("lighting", Map.of()).forGetter(index -> index.lighting),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortSkyStyle.CODEC)
|
||||
.optionalFieldOf("sky", Map.of()).forGetter(index -> index.sky)
|
||||
).apply(instance, AgeLevelIndex::new));
|
||||
|
||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||
@@ -38,6 +52,13 @@ 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 final Map<Identifier, PortLighting> lighting;
|
||||
private final Map<Identifier, PortSkyStyle> sky;
|
||||
|
||||
private AgeLevelIndex() {
|
||||
this.ages = new LinkedHashMap<>();
|
||||
@@ -45,17 +66,38 @@ 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<>();
|
||||
this.lighting = new LinkedHashMap<>();
|
||||
this.sky = 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,
|
||||
Map<Identifier, PortLighting> lighting,
|
||||
Map<Identifier, PortSkyStyle> sky) {
|
||||
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);
|
||||
this.lighting = new LinkedHashMap<>(lighting);
|
||||
this.sky = new LinkedHashMap<>(sky);
|
||||
}
|
||||
|
||||
Map<Identifier, Long> entries() {
|
||||
@@ -81,13 +123,30 @@ 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); }
|
||||
PortLighting lighting(Identifier id) { return lighting.getOrDefault(id, PortLighting.NORMAL); }
|
||||
PortSkyStyle sky(Identifier id) { return sky.getOrDefault(id, PortSkyStyle.OVERWORLD); }
|
||||
|
||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light, PortSkyStyle skyStyle) {
|
||||
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);
|
||||
if (light != PortLighting.NORMAL) lighting.put(id, light);
|
||||
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
@@ -96,16 +155,33 @@ 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);
|
||||
lighting.remove(id);
|
||||
sky.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, PortLighting light,
|
||||
PortSkyStyle skyStyle) {
|
||||
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);
|
||||
if (light != PortLighting.NORMAL) lighting.put(id, light);
|
||||
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
|
||||
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,48 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("age")
|
||||
.then(Commands.literal("create_sky")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("sky", StringArgumentType.word())
|
||||
.then(Commands.argument("lighting", StringArgumentType.word())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::createSkyAge)))))))
|
||||
.then(Commands.literal("sky_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeSky)))
|
||||
.then(Commands.literal("environment_probe")
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeEnvironment))))
|
||||
.then(Commands.literal("create_lighting")
|
||||
.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(PortLighting.values())
|
||||
.map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder))
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::createLightingAge))))))
|
||||
.then(Commands.literal("lighting_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeLighting)))
|
||||
.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())
|
||||
@@ -112,6 +156,72 @@ public final class PortAgeCommands {
|
||||
: 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);
|
||||
var environment = PortEnvironmentSelection.resolve(java.util.List.of(), seed, selected.terrain());
|
||||
int created = createAge(context, selected.terrain(), selected.biomes(),
|
||||
null, null, 0, null, null, environment);
|
||||
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Random Age plan: terrain=" + selected.terrain()
|
||||
+ " biomes=" + selected.biomes()
|
||||
+ " environment=" + environment), 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());
|
||||
@@ -215,6 +325,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())
|
||||
@@ -222,18 +347,416 @@ public final class PortAgeCommands {
|
||||
.executes(PortAgeCommands::sampleBiome)))))));
|
||||
}
|
||||
|
||||
private static int createSkyAge(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
PortSkyStyle sky = PortSkyStyle.valueOf(StringArgumentType.getString(context, "sky").toUpperCase(Locale.ROOT));
|
||||
PortLighting lighting = PortLighting.valueOf(StringArgumentType.getString(context, "lighting").toUpperCase(Locale.ROOT));
|
||||
PortAgeTerrain terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var pages = java.util.List.of(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "sky/" + sky.name().toLowerCase(Locale.ROOT)),
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + lighting.name().toLowerCase(Locale.ROOT)),
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/off"));
|
||||
var server = context.getSource().getServer();
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
|
||||
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
|
||||
biomes, terrain, null, null, 0, null, null, PortWeatherMode.OFF, lighting, sky);
|
||||
return probeSky(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int probeSky(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
var server = context.getSource().getServer();
|
||||
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
var type = level.dimensionType();
|
||||
context.getSource().sendSuccess(() -> Component.literal("Sky probe " + id
|
||||
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
|
||||
+ " skybox=" + type.skybox() + " skylight=" + type.hasSkyLight() + " ambient=" + type.ambientLight()
|
||||
+ " fog=" + level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.FOG_COLOR,
|
||||
new net.minecraft.core.BlockPos(0, 80, 0))
|
||||
+ " ceiling=" + type.hasCeiling() + " scale=" + type.coordinateScale()
|
||||
+ " minY=" + type.minY() + " height=" + type.height() + " dragon=" + type.hasEnderDragonFight()), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int probeEnvironment(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
|
||||
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var selected = PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain);
|
||||
var explicitPages = java.util.List.of(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/storm"),
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/bright"));
|
||||
var explicit = PortEnvironmentSelection.resolve(explicitPages, seed, terrain);
|
||||
var weatherOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(0)), seed, terrain);
|
||||
var lightingOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(1)), seed, terrain);
|
||||
if (!selected.equals(PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain))
|
||||
|| explicit.lighting() != PortLighting.BRIGHT
|
||||
|| explicit.weather() != (terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF : PortWeatherMode.STORM)
|
||||
|| weatherOnly.lighting() != selected.lighting() || lightingOnly.weather() != selected.weather())
|
||||
throw new IllegalStateException("Environment selection invariant failed");
|
||||
context.getSource().sendSuccess(() -> Component.literal("Environment probe seed=" + seed
|
||||
+ " terrain=" + terrain + " selected=" + selected + " explicit=" + explicit
|
||||
+ " deterministic=true"), false);
|
||||
return 1;
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int createLightingAge(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
PortLighting mode = PortLighting.valueOf(
|
||||
StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT));
|
||||
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
|
||||
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var pages = java.util.List.of(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + mode.name().toLowerCase(Locale.ROOT)),
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
|
||||
var server = context.getSource().getServer();
|
||||
var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
|
||||
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
|
||||
biomePlan, terrain, null, null, 0, null, null, PortWeatherMode.NORMAL, mode);
|
||||
return probeLighting(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int probeLighting(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
var server = context.getSource().getServer();
|
||||
ServerLevel level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal("Lighting probe " + id
|
||||
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
|
||||
+ " skylight=" + level.dimensionType().hasSkyLight()
|
||||
+ " ambient=" + level.dimensionType().ambientLight()
|
||||
+ " undergroundBrightness=" + level.getMaxLocalRawBrightness(
|
||||
new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0))
|
||||
+ " vanillaDarkEnough=" + net.minecraft.world.entity.monster.Monster.isDarkEnoughToSpawn(
|
||||
level, new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0),
|
||||
net.minecraft.util.RandomSource.create(0))
|
||||
+ " overworldSkylight=" + server.overworld().dimensionType().hasSkyLight()
|
||||
+ " overworldAmbient=" + server.overworld().dimensionType().ambientLight()), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
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) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
|
||||
new PortEnvironmentSelection(PortWeatherMode.NORMAL, PortLighting.NORMAL));
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
|
||||
PortEnvironmentSelection environment) {
|
||||
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,
|
||||
environment.weather(), environment.lighting());
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
@@ -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,19 +23,22 @@ 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) {
|
||||
if (target.getChunkSource().getGenerator() instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
|
||||
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
|
||||
.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
|
||||
.value().noiseSettings())) {
|
||||
if (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.
|
||||
@@ -54,36 +55,50 @@ public final class PortAgeTravel {
|
||||
}
|
||||
return makePlatform(target, new BlockPos(0, 64, 0));
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
return makePlatform(target, 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) {
|
||||
@@ -106,11 +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)
|
||||
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()
|
||||
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
|
||||
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty();
|
||||
&& passable(target, foot) && passable(target, foot.above());
|
||||
}
|
||||
|
||||
private static boolean passable(ServerLevel level, BlockPos pos) {
|
||||
var state = level.getBlockState(pos);
|
||||
return state.getCollisionShape(level, pos).isEmpty() || state.is(net.minecraft.tags.BlockTags.LEAVES);
|
||||
}
|
||||
|
||||
/** Same local-then-surface safe-return search used by 1.21.1 fissures. */
|
||||
public static java.util.Optional<BlockPos> findFootedDestination(ServerLevel level, BlockPos origin) {
|
||||
var local = BlockPos.betweenClosedStream(origin.offset(-4, -2, -4), origin.offset(4, 2, 4))
|
||||
.map(BlockPos::immutable).sorted(java.util.Comparator.comparingDouble(pos -> pos.distSqr(origin)))
|
||||
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||
if (local.isPresent()) return local;
|
||||
for (int radius = 1; radius <= 64; radius++) {
|
||||
final int ring = radius;
|
||||
var found = BlockPos.betweenClosedStream(origin.offset(-ring, 0, -ring), origin.offset(ring, 0, ring))
|
||||
.filter(pos -> Math.max(Math.abs(pos.getX() - origin.getX()), Math.abs(pos.getZ() - origin.getZ())) == ring)
|
||||
.map(BlockPos::immutable)
|
||||
.flatMap(pos -> {
|
||||
BlockPos surface = new BlockPos(pos.getX(),
|
||||
level.getHeight(Heightmap.Types.MOTION_BLOCKING, pos.getX(), pos.getZ()), pos.getZ());
|
||||
return java.util.stream.Stream.of(surface, surface.above(), surface.below(),
|
||||
surface.above(2), surface.below(2));
|
||||
})
|
||||
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||
if (found.isPresent()) return found;
|
||||
}
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,28 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Seed-completes missing environment controllers once, before an Age is saved. */
|
||||
public record PortEnvironmentSelection(PortWeatherMode weather, PortLighting lighting) {
|
||||
public static PortEnvironmentSelection resolve(List<Identifier> pages, long seed, PortAgeTerrain terrain) {
|
||||
PortLighting lighting = pages.stream().map(PortLighting::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x4C49474854494E47L), PortLighting.values()));
|
||||
PortWeatherMode weather = terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF
|
||||
: pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x574541544845525FL), PortWeatherMode.values()));
|
||||
return new PortEnvironmentSelection(weather, lighting);
|
||||
}
|
||||
|
||||
private static <T> T rankedDefault(Random random, T[] choices) {
|
||||
// LegacyRankChoice: the first (Normal) option is rank 1, the rest rank 2.
|
||||
// Its rank-1 weight is the complete rare group plus one. Derive this
|
||||
// from the supported pool, rather than selecting unported weather.
|
||||
int[] weights = new int[choices.length];
|
||||
Arrays.fill(weights, 1);
|
||||
weights[0] = choices.length;
|
||||
return PortRandomAgeSelection.weighted(random, List.of(choices), weights);
|
||||
}
|
||||
}
|
||||
@@ -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,52 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.Locale;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Authored lighting choices using statically registered, client-synchronized types. */
|
||||
public enum PortLighting {
|
||||
NORMAL, DARK, BRIGHT;
|
||||
|
||||
public static final Codec<PortLighting> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(Locale.ROOT)), mode -> mode.name().toLowerCase(Locale.ROOT));
|
||||
|
||||
public static PortLighting forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "lighting/normal" -> NORMAL;
|
||||
case "lighting/dark" -> DARK;
|
||||
case "lighting/bright" -> BRIGHT;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isBright(Level level) {
|
||||
return level.dimensionTypeRegistration().unwrapKey().map(key -> {
|
||||
Identifier id = key.identifier();
|
||||
return id.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& (id.getPath().equals("age_bright") || id.getPath().equals("age_cave_bright")
|
||||
|| id.getPath().startsWith("age_bright_sky_")
|
||||
|| id.getPath().startsWith("age_cave_bright_sky_"));
|
||||
}).orElse(false);
|
||||
}
|
||||
|
||||
Holder<DimensionType> dimensionType(MinecraftServer server, PortAgeTerrain terrain,
|
||||
Holder<DimensionType> overworldType, PortSkyStyle sky) {
|
||||
boolean cave = terrain == PortAgeTerrain.CAVE;
|
||||
if (this == NORMAL && !cave && sky == PortSkyStyle.OVERWORLD) return overworldType;
|
||||
String variant = this == BRIGHT ? (cave ? "age_cave_bright" : "age_bright")
|
||||
: this == DARK || cave ? "age_dark" : "age_normal";
|
||||
if (sky != PortSkyStyle.OVERWORLD) variant += "_sky_" + sky.name().toLowerCase(Locale.ROOT);
|
||||
return server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(
|
||||
ResourceKey.create(Registries.DIMENSION_TYPE,
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, variant)));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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,31 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Persisted sky styles, including the client-rendered Legacy static background. */
|
||||
public enum PortSkyStyle {
|
||||
OVERWORLD, NETHER, END, STATIC;
|
||||
|
||||
public static final Codec<PortSkyStyle> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(Locale.ROOT)), sky -> sky.name().toLowerCase(Locale.ROOT));
|
||||
|
||||
public static PortSkyStyle forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "sky/overworld" -> OVERWORLD;
|
||||
case "sky/nether" -> NETHER;
|
||||
case "sky/end" -> END;
|
||||
case "sky/static" -> STATIC;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static PortSkyStyle fromPages(List<Identifier> pages) {
|
||||
return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElse(OVERWORLD);
|
||||
}
|
||||
}
|
||||
@@ -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,28 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -34,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;
|
||||
@@ -57,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);
|
||||
}
|
||||
@@ -75,12 +75,70 @@ 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) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, PortLighting.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,
|
||||
PortLighting lighting) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, PortSkyStyle.OVERWORLD);
|
||||
}
|
||||
|
||||
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,
|
||||
PortLighting lighting, PortSkyStyle sky) {
|
||||
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, lighting, sky);
|
||||
((PortAgeLevel) level).configureWeather(weather);
|
||||
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures, lighting, sky);
|
||||
// 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.
|
||||
@@ -100,7 +158,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;
|
||||
}
|
||||
@@ -112,18 +172,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);
|
||||
@@ -141,8 +206,15 @@ 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);
|
||||
PortLighting lighting = index.lighting(id);
|
||||
PortSkyStyle sky = index.sky(id);
|
||||
if (loaded) unloadLoaded(server, id);
|
||||
// Persist the tombstone before moving files: a crash mid-archive must
|
||||
// not let an old book silently regenerate this identity.
|
||||
@@ -159,14 +231,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, lighting, sky);
|
||||
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);
|
||||
}
|
||||
@@ -282,7 +355,17 @@ 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) {
|
||||
return load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, index(server).lighting(id), index(server).sky(id));
|
||||
}
|
||||
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortLighting lighting, PortSkyStyle sky) {
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||
|
||||
@@ -353,11 +436,12 @@ public final class RuntimeAgeBackend {
|
||||
};
|
||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||
}
|
||||
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
||||
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
|
||||
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
|
||||
LevelStem stem = new LevelStem(lighting.dimensionType(server, terrain, overworldStem.type(), sky),
|
||||
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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.blaze3d.vertex.MeshData;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalDouble;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import org.joml.Matrix4f;
|
||||
|
||||
/** Legacy's six 48x48 noise faces, uploaded once per animation frame rather than per cell. */
|
||||
public final class PortStaticSky implements AutoCloseable {
|
||||
private static final int GRID = 48;
|
||||
private final DynamicTexture texture = new DynamicTexture("Rivencraft static sky", GRID * 6, GRID, false);
|
||||
private final GpuBuffer cube = buildCube();
|
||||
private long uploadedFrame = Long.MIN_VALUE;
|
||||
|
||||
public void render(RenderTarget target, GpuBufferSlice fog, long frame) {
|
||||
if (uploadedFrame != frame) {
|
||||
RandomSource random = RandomSource.create(frame);
|
||||
var image = texture.getPixels();
|
||||
for (int face = 0; face < 6; face++) {
|
||||
for (int y = 0; y < GRID; y++) {
|
||||
for (int x = 0; x < GRID; x++) {
|
||||
int gray = 146 * (65 + random.nextInt(160)) / 255;
|
||||
image.setPixel(face * GRID + x, y, 0xff000000 | gray << 16 | gray << 8 | gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
texture.upload();
|
||||
uploadedFrame = frame;
|
||||
}
|
||||
RenderSystem.setShaderFog(fog);
|
||||
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
var indexBuffer = indices.getBuffer(36);
|
||||
var transform = RenderSystem.getDynamicUniforms().writeTransform(RenderSystem.getModelViewMatrixCopy());
|
||||
try (var pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(
|
||||
() -> "Rivencraft static sky", target.getColorTextureView(), Optional.empty(),
|
||||
target.getDepthTextureView(), OptionalDouble.empty())) {
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(RenderPipelines.END_SKY));
|
||||
RenderSystem.bindDefaultUniforms(pass);
|
||||
pass.setUniform("DynamicTransforms", transform);
|
||||
pass.setUniform("Sampler0", texture.getTextureView(), texture.getSampler());
|
||||
pass.setVertexBuffer(0, cube.slice());
|
||||
pass.setIndexBuffer(indexBuffer, indices.type());
|
||||
pass.drawIndexed(36, 1, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static GpuBuffer buildCube() {
|
||||
var format = DefaultVertexFormat.POSITION_TEX_COLOR;
|
||||
try (var bytes = ByteBufferBuilder.exactlySized(24 * format.getVertexSize())) {
|
||||
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
|
||||
for (int face = 0; face < 6; face++) {
|
||||
var pose = new Matrix4f();
|
||||
switch (face) {
|
||||
case 1 -> pose.rotationX((float) Math.PI / 2);
|
||||
case 2 -> pose.rotationX((float) -Math.PI / 2);
|
||||
case 3 -> pose.rotationX((float) Math.PI);
|
||||
case 4 -> pose.rotationZ((float) Math.PI / 2);
|
||||
case 5 -> pose.rotationZ((float) -Math.PI / 2);
|
||||
}
|
||||
float u0 = face / 6f, u1 = (face + 1) / 6f;
|
||||
builder.addVertex(pose, -100, -100, -100).setUv(u0, 0).setColor(-1);
|
||||
builder.addVertex(pose, -100, -100, 100).setUv(u0, 1).setColor(-1);
|
||||
builder.addVertex(pose, 100, -100, 100).setUv(u1, 1).setColor(-1);
|
||||
builder.addVertex(pose, 100, -100, -100).setUv(u1, 0).setColor(-1);
|
||||
}
|
||||
try (MeshData mesh = builder.buildOrThrow()) {
|
||||
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft static sky cube", 40, mesh.vertexBuffer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cube.close();
|
||||
texture.close();
|
||||
}
|
||||
}
|
||||
+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,7 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
/** Carries the extracted animation frame without consulting the live world during drawing. */
|
||||
public interface StaticSkyState {
|
||||
long rivencraft$staticFrame();
|
||||
void rivencraft$staticFrame(long frame);
|
||||
}
|
||||
@@ -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,23 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
|
||||
import net.dnorberg.rivencraft.age.PortLighting;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.lighting.LevelLightEngine;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
|
||||
/** Ambient light contributes to effective brightness, not fake block/sky propagation. */
|
||||
@Mixin(LevelLightEngine.class)
|
||||
public abstract class AgeAmbientLightMixin {
|
||||
@Shadow @Final protected LevelHeightAccessor levelHeightAccessor;
|
||||
|
||||
@ModifyReturnValue(method = "getRawBrightness", at = @At("RETURN"))
|
||||
private int rivencraft$ambientBrightness(int brightness) {
|
||||
return levelHeightAccessor instanceof Level level && PortLighting.isBright(level)
|
||||
? Math.max(brightness, 15) : brightness;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import com.mojang.blaze3d.pipeline.RenderTarget;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
|
||||
import net.dnorberg.rivencraft.client.PortStaticSky;
|
||||
import net.dnorberg.rivencraft.client.StaticSkyState;
|
||||
import net.minecraft.client.Camera;
|
||||
import net.minecraft.client.multiplayer.ClientLevel;
|
||||
import net.minecraft.client.renderer.SkyRenderer;
|
||||
import net.minecraft.client.renderer.state.level.SkyRenderState;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import org.spongepowered.asm.mixin.Final;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(SkyRenderer.class)
|
||||
public class StaticSkyRendererMixin {
|
||||
@Shadow @Final private RenderTarget renderTarget;
|
||||
@Unique private PortStaticSky rivencraft$staticSky;
|
||||
|
||||
@Inject(method = "extractRenderState", at = @At("TAIL"))
|
||||
private void rivencraft$extract(ClientLevel level, float partialTicks, Camera camera,
|
||||
SkyRenderState state, CallbackInfo ci) {
|
||||
boolean enabled = level.dimensionTypeRegistration().unwrapKey().map(key ->
|
||||
key.identifier().getNamespace().equals("rivencraft")
|
||||
&& key.identifier().getPath().endsWith("_sky_static")).orElse(false);
|
||||
((StaticSkyState) state).rivencraft$staticFrame(enabled ? level.getGameTime() / 2 : Long.MIN_VALUE);
|
||||
// Only the extracted presentation uses END to schedule the sky pass. The actual
|
||||
// dimension has NONE, so it never acquires End flashes or End gameplay behavior.
|
||||
if (enabled) state.skybox = DimensionType.Skybox.END;
|
||||
}
|
||||
|
||||
@Inject(method = "render", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$render(GpuBufferSlice fog, SkyRenderState state, CallbackInfo ci) {
|
||||
long frame = ((StaticSkyState) state).rivencraft$staticFrame();
|
||||
if (frame == Long.MIN_VALUE) return;
|
||||
if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky();
|
||||
rivencraft$staticSky.render(renderTarget, fog, frame);
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
@Inject(method = "close", at = @At("HEAD"))
|
||||
private void rivencraft$close(CallbackInfo ci) {
|
||||
if (rivencraft$staticSky != null) {
|
||||
rivencraft$staticSky.close();
|
||||
rivencraft$staticSky = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import net.dnorberg.rivencraft.client.StaticSkyState;
|
||||
import net.minecraft.client.renderer.state.level.SkyRenderState;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Unique;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
@Mixin(SkyRenderState.class)
|
||||
public class StaticSkyStateMixin implements StaticSkyState {
|
||||
@Unique private long rivencraft$frame = Long.MIN_VALUE;
|
||||
public long rivencraft$staticFrame() { return rivencraft$frame; }
|
||||
public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; }
|
||||
|
||||
@Inject(method = "reset", at = @At("TAIL"))
|
||||
private void rivencraft$reset(CallbackInfo ci) { rivencraft$frame = Long.MIN_VALUE; }
|
||||
}
|
||||
+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,22 @@ 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();
|
||||
}
|
||||
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
|
||||
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),
|
||||
environment.weather(), environment.lighting(),
|
||||
net.dnorberg.rivencraft.age.PortSkyStyle.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
-1
@@ -2,17 +2,35 @@ 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.age.PortLighting;
|
||||
import net.dnorberg.rivencraft.age.PortSkyStyle;
|
||||
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(
|
||||
@@ -25,19 +43,98 @@ public final class PortAuthoredWriting {
|
||||
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
|
||||
|| PortLighting.forPage(page) != null || PortSkyStyle.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;
|
||||
int lighting = 0;
|
||||
int sky = 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 (PortSkyStyle.forPage(page) != null && sky++ == 0) {
|
||||
// Sky presentation, independent of terrain and illumination.
|
||||
} else if (PortLighting.forPage(page) != null && lighting++ == 0) {
|
||||
// A single lighting controller; omitted choices resolve from the Age seed.
|
||||
} 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
|
||||
@@ -46,6 +143,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();
|
||||
@@ -65,6 +176,10 @@ public final class PortAuthoredWriting {
|
||||
|| 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) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
@@ -91,25 +206,132 @@ public final class PortAuthoredWriting {
|
||||
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);
|
||||
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;
|
||||
|
||||
@@ -76,13 +76,27 @@ public final class PortRegistryPages {
|
||||
|
||||
public static List<ItemStack> materialPages(boolean fluids) {
|
||||
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
|
||||
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
|
||||
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
|
||||
return materialIds(category).stream()
|
||||
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
|
||||
}
|
||||
|
||||
public static boolean isCaveBiome(net.minecraft.core.Holder.Reference<net.minecraft.world.level.biome.Biome> holder) {
|
||||
String path = holder.key().identifier().getPath();
|
||||
/** The same full-block pool used by Creative pages, in stable registry-ID order. */
|
||||
public static List<Identifier> solidMaterialIds() {
|
||||
return materialIds("rivencraft:block");
|
||||
}
|
||||
|
||||
public static List<Identifier> fluidMaterialIds() {
|
||||
return materialIds("rivencraft:fluid");
|
||||
}
|
||||
|
||||
private static List<Identifier> materialIds(String category) {
|
||||
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
|
||||
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static boolean isCaveBiome(net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> holder) {
|
||||
String path = holder.unwrapKey().map(key -> key.identifier().getPath()).orElse("");
|
||||
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|
||||
|| path.equals("deep_dark") || path.endsWith("_caves");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,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,9 @@
|
||||
{
|
||||
"required": true,
|
||||
"minVersion": "0.8",
|
||||
"package": "net.dnorberg.rivencraft.mixin",
|
||||
"compatibilityLevel": "JAVA_25",
|
||||
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin", "AgeAmbientLightMixin"],
|
||||
"client": ["StaticSkyStateMixin", "StaticSkyRendererMixin"],
|
||||
"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"
|
||||
|
||||
+167
-1
@@ -75,6 +75,20 @@ send_and_wait 'rivencraft_port age create smoke_cave 112233 cave' 'Created riven
|
||||
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"
|
||||
@@ -90,6 +104,17 @@ 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='
|
||||
@@ -98,12 +123,95 @@ send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave
|
||||
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'
|
||||
@@ -119,6 +227,7 @@ alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0
|
||||
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
|
||||
@@ -139,6 +248,34 @@ send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave
|
||||
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'
|
||||
@@ -146,6 +283,7 @@ alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 t
|
||||
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
|
||||
@@ -156,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, Void and Cave 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