Public Access
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48fbae9be9 | ||
|
|
c223f3fdca | ||
|
|
d75154fa9b | ||
|
|
9ccec18c18 | ||
|
|
d9464fad76 | ||
|
|
d6e9a58a24 |
No files matched your search
@@ -7,9 +7,19 @@ of [Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to
|
|||||||
Minecraft 1.21.1 on NeoForge. Mystcraft veterans will recognize the writing
|
Minecraft 1.21.1 on NeoForge. Mystcraft veterans will recognize the writing
|
||||||
flow, D'ni-inspired glyphs, libraries, crystals, and instability. Rivencraft
|
flow, D'ni-inspired glyphs, libraries, crystals, and instability. Rivencraft
|
||||||
also lets you author modern terrain, biomes, structures, seasons, and celestial
|
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 1.12.2 mod.
|
||||||
|
|
||||||
|
The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
|
||||||
|
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
|
||||||
|
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
|
||||||
|
supports authored terrain/biomes, structure and vegetation pages, independent
|
||||||
|
Age weather, and a growing set of generation features,
|
||||||
|
but does not yet provide full 1.21.1 gameplay or optional-integration parity.
|
||||||
|
See the [26.3 beta guide](ports/mc263/README.md) and
|
||||||
|
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.65-beta.1.md).
|
||||||
|
Keep the builds and their saves separate, and back up worlds before updating.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
|
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
|
||||||
|
|||||||
@@ -495,6 +495,411 @@ accepts the existing authored distribution pages for those terrains. New Flat
|
|||||||
chunks can vary vegetation by biome; Void keeps biome climate without adding
|
chunks can vary vegetation by biome; Void keeps biome climate without adding
|
||||||
a floor or decorations. These are beta-port slices, not full 1.21.1 parity.
|
a floor or decorations. These are beta-port slices, not full 1.21.1 parity.
|
||||||
|
|
||||||
|
The local `1.0.1.37-beta.1` slice ports Cave terrain authoring using the
|
||||||
|
26.3 caves noise preset. Cave and Nether Biome pages can be distributed
|
||||||
|
through an Age, and travel searches for footing from Y=64 downward rather
|
||||||
|
than mistaking the cave roof for the surface. Nether-style cave material
|
||||||
|
replacement from 1.21.1 remains a separate port task. The local
|
||||||
|
`1.0.1.38-beta.1` follow-up
|
||||||
|
keeps Cave Ages dry by default: the vanilla 26.3 Cave preset otherwise fills
|
||||||
|
low openings with water. This changes newly generated chunks only; a future
|
||||||
|
Flooded page will deliberately set an authored water level in both maintained
|
||||||
|
versions.
|
||||||
|
The local `1.0.1.39-beta.1` follow-up makes the 26.3 landing search reject
|
||||||
|
fluid at feet and head height and prevents a submerged fallback platform.
|
||||||
|
|
||||||
|
The local `1.0.1.40-beta.1` slice gives pageless Descriptive Books a
|
||||||
|
seed-selected terrain and biome plan. It follows the 1.21.1 rank weights for
|
||||||
|
terrain and biome distribution, selects registered Cave/Nether biomes for Cave
|
||||||
|
Ages and overworld-source biomes for other non-Void terrains, and persists the
|
||||||
|
resolved choices. Other random Age traits remain a later port task.
|
||||||
|
|
||||||
|
The local `1.0.1.41-beta.1` slice adds the first Age-scoped feature wrapper to
|
||||||
|
the 26.3 generator. A Block page followed by Rocks now places clusters of the
|
||||||
|
chosen block in new Standard, Amplified, Island, or Flat terrain chunks. The
|
||||||
|
Age index persists that selection, and the wrapper delegates all vanilla
|
||||||
|
terrain, biome, and structure generation. Cave and Void are intentionally
|
||||||
|
excluded until their feature placement rules are ported. This is not yet the
|
||||||
|
full 1.21.1 feature grammar or instability system.
|
||||||
|
|
||||||
|
The local `1.0.1.42-beta.1` follow-up resolves unmodified Rocks to a
|
||||||
|
seed-shuffled, full-block material and accepts up to 32 ordered Rocks pages.
|
||||||
|
Each page adds an independent, deterministic surface placement attempt.
|
||||||
|
Explicit Block modifiers still bind only to the immediately following Rocks
|
||||||
|
page. The material sequence persists in the Age index; the codec also reads
|
||||||
|
the single-material format from the previous beta. The remaining feature
|
||||||
|
families and instability scoring are not yet ported.
|
||||||
|
|
||||||
|
The local `1.0.1.43-beta.1` slice uses 26.3's `minecraft:forest_rock`
|
||||||
|
placed-feature rules and `BlockBlobFeature` for authored Rocks, supplying the
|
||||||
|
Age's selected material and removing only the vanilla biome filter. This
|
||||||
|
replaces the port's temporary 3×3 pile algorithm and follows the 1.21.1
|
||||||
|
generator's authored-placement approach. It affects new chunks only.
|
||||||
|
|
||||||
|
The local `1.0.1.44-beta.1` slice extends this Age-scoped feature wrapper to
|
||||||
|
Flowers, Trees, and Huge Trees. Ordered authored pages retain independent
|
||||||
|
attempts and use vanilla 26.3 placed-feature rules after biome decoration.
|
||||||
|
The vegetation plan is persisted alongside terrain, biome, and Rocks plans;
|
||||||
|
the dedicated-server smoke compares generated flowers and logs against a
|
||||||
|
same-seed Flat control and checks persistence after restart.
|
||||||
|
|
||||||
|
The local `1.0.1.45-beta.1` slice adds authored Dungeons separately from
|
||||||
|
surface vegetation. Its 26.3 placed-feature JSON mirrors the 1.21.1 eight
|
||||||
|
monster-room attempts over the world height; ordered duplicate pages add
|
||||||
|
attempts. The count persists in the Age index and generator codec. The server
|
||||||
|
smoke creates and samples a dungeon Age, counts monster spawners across a
|
||||||
|
4×4-chunk area, and verifies the count after restart. Room appearance and
|
||||||
|
normal book authoring remain in-game checks.
|
||||||
|
|
||||||
|
The local `1.0.1.46-beta.1` slice ports Surface Lakes, Deep Lakes, and Small
|
||||||
|
Lakes, including ordered Fluid modifiers. The 26.3 LakeFeature retains its
|
||||||
|
vanilla shape and replacement predicates while using the Age's frozen fluid
|
||||||
|
provider. Surface placement keeps Rivencraft 1.21.1's one-in-twelve rarity;
|
||||||
|
underground lakes use vanilla placement. Repeated unmodified pages resolve
|
||||||
|
their fluids independently from a seeded, shuffled source-fluid pool. Lake
|
||||||
|
plans participate in restore, unload/reload, reseed, and retirement rollback.
|
||||||
|
The server smoke compares water generation against a same-seed control and
|
||||||
|
checks explicit/random plans and fluid blocks across restart.
|
||||||
|
|
||||||
|
The local `1.0.1.47-beta.1` slice adapts the 1.21.1 FloatingIslandFeature
|
||||||
|
algorithm to 26.3 chunk access and the biomes already assigned to each chunk.
|
||||||
|
It retains source range 3, one-in-128 candidate rarity, Y=168..215 center
|
||||||
|
selection, radii 24..41, coordinate-noise contours, air-only filling, and the
|
||||||
|
biome-dependent soil cap. It supports Flat/Void in addition to noise terrains;
|
||||||
|
Block modifiers and independent seeded random materials are frozen in an
|
||||||
|
ordered terrain-feature plan. Rocks and islands share material resolution.
|
||||||
|
|
||||||
|
26.3's final NoiseBasedChunkGenerator merges noise, surface, and carving and
|
||||||
|
does not expose a pre-surface hook. This port appends islands to the completed
|
||||||
|
terrain future before decoration; existing terrain is preserved, but base
|
||||||
|
carvers do not cut the added islands. The smoke verifies actual high-altitude
|
||||||
|
material placement on noise, Flat, and Void terrain, distinct random material
|
||||||
|
choices, authored biome preservation, and block fingerprints across restart.
|
||||||
|
|
||||||
|
The local `1.0.1.48-beta.1` slice ports TendrilFeature's Legacy-derived
|
||||||
|
branching solid volumes using the 26.3 chunk mutation and height APIs. Source
|
||||||
|
range 8, rate 15, size 18, nested node/height selection, branch splitting, and
|
||||||
|
bedrock preservation match the 1.21.1 implementation. Chunk-local writes and
|
||||||
|
neighbor-source seeds preserve continuous paths across chunk borders.
|
||||||
|
|
||||||
|
The existing terrain-feature plan now supports Tendrils on all terrain types,
|
||||||
|
including Cave and Void. Compilation retains mixed feature order and applies
|
||||||
|
the 32-occurrence limit per feature. Block modifiers and seeded random choices
|
||||||
|
use the shared solid-material resolver; existing save/lifecycle handling carries
|
||||||
|
both feature types. World validation is manual at the player's request; no
|
||||||
|
development server is started for this slice.
|
||||||
|
|
||||||
|
The local `1.0.1.49-beta.1` slice ports the Legacy-derived Skylands filter
|
||||||
|
from Rivencraft 1.21.1 with 26.3 chunk height/mutation APIs. The cutoff uses
|
||||||
|
the same salt, seven octaves, initial cell width 128, amplitude 12, and base
|
||||||
|
Y=76. Solid/liquid removal and world-height coverage match the reference.
|
||||||
|
It runs once after base terrain construction and before authored solid
|
||||||
|
volumes, retaining the feature ordering used by the 1.21.1 implementation.
|
||||||
|
|
||||||
|
Skylands is a material-free feature, accepted alongside every terrain type.
|
||||||
|
Repeated pages retain the same idempotent effect. The existing terrain-feature
|
||||||
|
plan stores an optional Skylands flag, defaulting to false for earlier beta
|
||||||
|
Ages; no separate backend/lifecycle path is introduced. A Skylands-only plan
|
||||||
|
can contain no material choices. Build and static checks are automated; world
|
||||||
|
generation and restart validation are left to the player.
|
||||||
|
|
||||||
|
The local `1.0.1.50-beta.1` slice adapts the 1.21.1 SpheresFeature and
|
||||||
|
SpikesFeature to 26.3. Spheres retain their source range 8, five-percent
|
||||||
|
source chance, nested Y selection starting at 32, node geometry, and bedrock
|
||||||
|
protection. They run after the base terrain/Skylands filter and work on every
|
||||||
|
terrain type. Spikes retain their one-in-18 chunk chance, 6..37 height roll,
|
||||||
|
width 1..4, solid-footprint checks, and chunk-local writes during decoration.
|
||||||
|
They remain unavailable on Cave and Void terrain, matching 1.21.1.
|
||||||
|
|
||||||
|
Both page types use ordered saved solid-feature choices, preceding Block
|
||||||
|
modifiers, and independent seeded materials/occurrences. No backend lifecycle
|
||||||
|
branch or generated artwork is added. Compilation and static review are the
|
||||||
|
automated checks; the player handles generation and restart validation.
|
||||||
|
|
||||||
|
The local `1.0.1.51-beta.1` slice ports ObelisksFeature using the 26.3 chunk
|
||||||
|
coordinates and world-height APIs. The site salt, seed mixing, one-in-128
|
||||||
|
chance, chunk-local site selection, 4×4 plinth, and 2×12×2 shaft match 1.21.1.
|
||||||
|
Every footing column must be sturdy, dry, and free of block entities; the
|
||||||
|
base surface can vary by at most two blocks and the full shaft must be open.
|
||||||
|
No partial structure is written when a placement check fails.
|
||||||
|
|
||||||
|
The page joins the existing material-feature plan and shared material resolver.
|
||||||
|
It runs during surface decoration, with its own occurrence counter, on all
|
||||||
|
surface terrain types including Flat; Cave and Void remain unsupported.
|
||||||
|
Spikes and Obelisks use shared stage/restriction classification. Build/static
|
||||||
|
checks are automated; generation and restart checks remain with the player.
|
||||||
|
|
||||||
|
The local `1.0.1.52-beta.1` slice ports CrystalFormationsFeature directly
|
||||||
|
from 1.21.1 with only registry, ChunkPos, and height API substitutions. It
|
||||||
|
retains the site salt, one-in-fifteen rarity, 3×3 ground/clearance checks,
|
||||||
|
three-block center height, randomized outer heights, budding hearts, and
|
||||||
|
initial small buds. Existing blue crystal growth, sprites, drops, and sounds
|
||||||
|
are reused without adding assets.
|
||||||
|
|
||||||
|
Crystal Formations is an ordered surface feature with fixed crystal material,
|
||||||
|
not a consumer of Block/Fluid modifiers. It shares the existing saved feature
|
||||||
|
plan and surface-terrain restrictions (including Flat, excluding Cave/Void),
|
||||||
|
with independent occurrence seeds and the current 32-page cap. No backend
|
||||||
|
lifecycle changes are needed. The Obelisks slice passed the player's in-game
|
||||||
|
check; Crystal Formations build/static checks pass, with generation, growth,
|
||||||
|
and restart validation handed to the player. No test server is started.
|
||||||
|
|
||||||
|
The local `1.0.1.53-beta.1` slice adapts 1.21.1 AgeDenseOres to modern
|
||||||
|
`AbstractOreFeature` targets and `FeaturePlacer.placeWithBiomeCheck`. It keeps
|
||||||
|
the common ore tag/name fallback, explicit `rivencraft:dense_ores` placed-
|
||||||
|
feature opt-in tag, nearby biome discovery, stable registry-ID ordering,
|
||||||
|
and independent per-occurrence seeds. Registered modded ore features remain
|
||||||
|
eligible without depending on an ore-providing mod. Biomes and ore placements
|
||||||
|
are collected once per chunk rather than once per repeated page.
|
||||||
|
|
||||||
|
The optional Dense Ores count is saved in the existing terrain-feature plan;
|
||||||
|
older plans default to zero. Grammar accepts 1–32 occurrences on noise terrain
|
||||||
|
(including Cave), rejects Flat/Void and material modifiers, and checks explicit
|
||||||
|
biome pools for at least one eligible ore feature. No extra lifecycle path or
|
||||||
|
asset is introduced. Random feature selection and instability remain later
|
||||||
|
port work. Crystal Formations passed the player's in-game check; this slice
|
||||||
|
passes build/static checks, with ore generation and restart validation manual.
|
||||||
|
|
||||||
|
The local `1.0.1.54-beta.1` slice ports Star Fissures. Its footprint matches
|
||||||
|
1.21.1 exactly (including Legacy attribution); carving uses the dimension's
|
||||||
|
minimum Y, clears fluids, and protects Cave terrain's roof. The authored flag
|
||||||
|
is optional in the existing feature-plan codec. Duplicate pages retain the
|
||||||
|
1.21.1 single-fissure result; all terrains are accepted, with no material
|
||||||
|
modifier. Random feature selection is not introduced in this slice.
|
||||||
|
|
||||||
|
First arrival generates the fissure and stores its spawn/completion in per-Age
|
||||||
|
SavedData. Re-entry/restart does not repeat carving; an obstructed saved arrival
|
||||||
|
is rechecked for safe footing. The return block ports modern entity-inside and
|
||||||
|
TeleportTransition APIs, has a registered Rivencraft block-entity type, and
|
||||||
|
uses Minecraft's starfield renderer with 1.21.1 face offsets. Shared Overworld
|
||||||
|
spawn safety search and fallback match 1.21.1, rather than respawning at a bed.
|
||||||
|
The existing fissure OGG/model are reused. Client renderer registration stays
|
||||||
|
client-only; the block has no item/drop and is inert in the Overworld.
|
||||||
|
|
||||||
|
Dense Ores passed the player's in-game check. Build, footprint comparison,
|
||||||
|
resource packaging, and static checks pass for this slice. Generation, visual
|
||||||
|
rendering, actual return travel, and restart checks are left to the player;
|
||||||
|
no development server is started.
|
||||||
|
|
||||||
|
The local `1.0.1.55-beta.1` slice begins the authored structure-set port with
|
||||||
|
Villages, Mineshafts, and Strongholds. Saved page occurrences use the existing
|
||||||
|
feature plan's new optional structure list. Terrain gates, random-spread
|
||||||
|
frequency/spacing scaling, and bounded concentric-ring count/spread follow
|
||||||
|
1.21.1. Modern `AbstractSpreadingStructurePlacement` accessors replace the old
|
||||||
|
placement-class accessors. Scaled holder copies never mutate global registries.
|
||||||
|
|
||||||
|
Structure-state planning includes authored eligibility biomes while continuing
|
||||||
|
to sample the actual terrain source. A scoped Structure mixin carries over
|
||||||
|
1.21.1's authored biome override. The wrapper handles structure starts itself
|
||||||
|
when a structure plan exists, allowing the hook to recognize the authored Age;
|
||||||
|
unrelated/unplanned generators remain delegated. Only these three controlled
|
||||||
|
families are gated in this slice; remaining structure families, random picks,
|
||||||
|
and biome-mismatch instability remain unported. Existing plans without the
|
||||||
|
optional list retain their previous structure path. Mod metadata now correctly
|
||||||
|
calls this target Beta rather than Alpha.
|
||||||
|
|
||||||
|
The player passed Star Fissures. Build/static checks pass for this new slice.
|
||||||
|
A disposable dedicated-server startup reached `Done` without mixin-loading
|
||||||
|
errors. Its console `/stop` command hit a null-level CommandSourceStack error;
|
||||||
|
the exact test JVM was then stopped through SIGTERM and saved/shut down. This
|
||||||
|
was a startup check, not authored generation or density validation. The
|
||||||
|
disposable world is removed; client authoring, actual structures, density,
|
||||||
|
biome overrides, and restart checks are handed to the player.
|
||||||
|
|
||||||
|
The local `1.0.1.56-beta.1` follow-up corrects the structure biome override.
|
||||||
|
Client logs confirmed the build-55 mixin was applied, but 26.3 JigsawPlacement
|
||||||
|
checks `GenerationContext.couldStructureExistInColumn` inside generation,
|
||||||
|
before the final check bypassed by the 1.21.1 hook. The replacement ModifyArg
|
||||||
|
changes only the biome predicate passed to the GenerationContext constructor
|
||||||
|
in Structure.generate for explicitly authored Age structures. Both early
|
||||||
|
and final checks now see the override; all other generation constraints and
|
||||||
|
unrelated generators retain their existing behavior.
|
||||||
|
|
||||||
|
Build/static checks pass. An isolated `--initSettings` bootstrap initialized
|
||||||
|
settings without a mixin application error, then FML exited nonzero because
|
||||||
|
that settings-only mode creates no server thread. This is not a full server
|
||||||
|
or generation pass. No world was created, and the temporary settings directory
|
||||||
|
is removed. In-game validation requires a new Age or ungenerated chunks;
|
||||||
|
previously rejected chunks are not regenerated. No commit/release is made.
|
||||||
|
|
||||||
|
The local `1.0.1.57-beta.1` slice completes the existing 1.21.1 structure-page
|
||||||
|
mapping (Dungeons remains its separate placed feature) and enables 26.3's
|
||||||
|
Abandoned Camps page. The shared planner now recognizes 15 structure sets.
|
||||||
|
Page/terrain restrictions match AgeStructureSelection for the 1.21.1 families;
|
||||||
|
Abandoned Camps follows surface noise terrain. No material modifiers are added.
|
||||||
|
|
||||||
|
The separate Fortress/Bastion set resources are reused directly from 1.21.1.
|
||||||
|
The controlled-set gate includes the vanilla combined Nether set, preventing
|
||||||
|
unrequested cross-family placement. All structure pages share the previously
|
||||||
|
validated saved list, density logic, and corrected context-level biome override.
|
||||||
|
Unknown non-spreading placements are left unchanged rather than cast unsafely.
|
||||||
|
Unmodified/no-authored-structure Ages retain their prior native path; previously
|
||||||
|
saved authored plans now gate all supported families in new chunks. Instability
|
||||||
|
and random feature selection remain pending.
|
||||||
|
|
||||||
|
The player passed the build-56 biome fix. Build, diff checks, all 15 registry
|
||||||
|
and Creative-page mappings, and target structure resource existence checks pass.
|
||||||
|
Packaged Fortress/Bastion definitions match 1.21.1 byte-for-byte. No server is
|
||||||
|
started; actual structure placement, terrain-specific limits, repeated density,
|
||||||
|
and restart validation are handed to the player. Changes remain local.
|
||||||
|
|
||||||
|
The local `1.0.1.58-beta.1` slice ports Caves/Ravines feature pages separately
|
||||||
|
from Cave terrain. A new optional saved carver-page list participates in the
|
||||||
|
existing feature-plan lifecycle. Grammar accepts noise terrain including Cave,
|
||||||
|
rejects Flat/Void and material modifiers, and checks the modern CARVER registry.
|
||||||
|
|
||||||
|
26.3 combines carving with terrain construction and exposes carvers that emit
|
||||||
|
geometry rather than mutate blocks. A scoped NoiseBasedChunkGenerator mixin
|
||||||
|
adds authored geometry to its native CarvingMask immediately before that mask
|
||||||
|
is tested/applied. It only acts for the matching runtime Age wrapper, using
|
||||||
|
its frozen seed and actual biome resolver. Vanilla applies the combined mask
|
||||||
|
with its own aquifer/surface rules; no closed NoiseChunk is accessed afterward.
|
||||||
|
The source range, salts, Nether cave selection, and Nether ravine skip match
|
||||||
|
1.21.1. Duplicate Caves/Ravines repeat identical seeds in the baseline, so the
|
||||||
|
port avoids redundant identical mask passes while retaining authored pages.
|
||||||
|
Added terrain volumes still follow the combined terrain operation, as noted
|
||||||
|
for the earlier Floating Islands port.
|
||||||
|
|
||||||
|
The player passed structure slice 57. Build/static checks and packaging checks
|
||||||
|
for the hook, Creative entries, and three required vanilla carvers pass. A
|
||||||
|
settings-only bootstrap completed initialization without a mixin application
|
||||||
|
error; FML then returned nonzero because no server thread exists in that mode.
|
||||||
|
This is not world-generation validation. No world was created; the temporary
|
||||||
|
settings directory is removed. Actual carving and restart checks are manual,
|
||||||
|
and no commit/push/publication is performed.
|
||||||
|
|
||||||
|
The local `1.0.1.59-beta.1` slice replaces stale per-page port information
|
||||||
|
and adds the requested red hue for unsupported pages. A shared
|
||||||
|
PortAuthoredWriting.isPageImplemented classifier uses the same terrain,
|
||||||
|
controller, material/biome, and feature recognizers as the grammar, without
|
||||||
|
misclassifying context-dependent modifiers/controllers by testing them alone.
|
||||||
|
Page tooltips, the extracted item-render color, and the Binder background use
|
||||||
|
this single availability check. Tinting does not generate extra textures or
|
||||||
|
change stored page data. Corrupted-page names/descriptions remain obfuscated.
|
||||||
|
|
||||||
|
Port language overrides remove the old Standard/one-Biome claim, distinguish
|
||||||
|
implemented/unimplemented pages and invalid combinations, and correct all
|
||||||
|
15 structure-family descriptions, material adjacency, and selected terrain/
|
||||||
|
feature restrictions. The 1.21.1 language assets are unchanged. This is a UI
|
||||||
|
and information slice, not new generation functionality.
|
||||||
|
|
||||||
|
The player passed Caves/Ravines. Build, diff checks, unique language-key checks,
|
||||||
|
and packaged-language override checks pass. No server/world is started.
|
||||||
|
Inventory tint, Binder tint/layering, and tooltip readability await the
|
||||||
|
player's visual check. Changes remain local and unpublished.
|
||||||
|
|
||||||
|
The local `1.0.1.60-beta.1` slice implements the four requested 26.3-only
|
||||||
|
vegetation pages: Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf
|
||||||
|
Mushrooms. They extend PortVegetationPlan recognition, so grammar, saved plans,
|
||||||
|
Creative status, item tint, and tooltip availability agree without another
|
||||||
|
page list. The existing ordered vegetation cap and surface-terrain/biome
|
||||||
|
restrictions are retained. The 1.21.1 code/assets are unchanged.
|
||||||
|
|
||||||
|
Poplar Trees selects only the three vanilla poplar variants, retaining their
|
||||||
|
relative native weights and six placement attempts. Fallen poplars reuse the
|
||||||
|
vanilla feature but add authored in-square/surface placement before its sapling
|
||||||
|
check; the vanilla placed feature alone has no surface-height placement. Red
|
||||||
|
Shrubs directly reuses its registered patch. No tree/shrub algorithm or artwork
|
||||||
|
is duplicated.
|
||||||
|
|
||||||
|
Shelf Mushrooms is a tree decorator in 26.3, not a placed feature. The authored
|
||||||
|
page therefore applies ShelfMushroomDecorator to existing connected log hosts
|
||||||
|
at its position in the vegetation sequence. Section palettes skip log-free
|
||||||
|
sections, repeated pages reuse the host scan, and tree placements invalidate
|
||||||
|
that cache. Host ordering and page seeds are deterministic; callback writes
|
||||||
|
are chunk-local, fluid-free, supported, and height-bounded. The decorator keeps
|
||||||
|
its native standing/fallen logic and side/age/adjacency checks. This is an
|
||||||
|
authored extension using the native decorator, not a new vanilla feature ID.
|
||||||
|
|
||||||
|
Build/static and resource-reference checks pass. The four pages' descriptions
|
||||||
|
explain their actual effects and requirements. No server/world is started;
|
||||||
|
the player validates vegetation appearance, support/tint, ordering, and restart.
|
||||||
|
Changes remain local and unpublished.
|
||||||
|
|
||||||
|
The local `1.0.1.61-beta.1` follow-up separates log-hosted Shelf Mushrooms
|
||||||
|
from surface plants. A shared requiresSurface classification now governs
|
||||||
|
terrain/explicit-biome validation; decoration handles Shelf Mushrooms before
|
||||||
|
the surface-biome skip. Trees, shrubs, and flowers retain their restrictions.
|
||||||
|
Void/Cave/End-biome log hosts are no longer excluded by the vegetation path.
|
||||||
|
|
||||||
|
The host scan is a stable log-position list rather than connected tree groups.
|
||||||
|
Native tree decorators assume a meaningful base and only decorate a limited
|
||||||
|
height band, which is unsuitable for log tendrils. The authored extension
|
||||||
|
therefore checks exposed log faces at all heights, reusing the native decorator's
|
||||||
|
replaceability/water predicate and block facing/age/survival rules, with spacing
|
||||||
|
and chunk bounds. Cached scans still avoid repeated full-volume work. No tree
|
||||||
|
page or log-producing page is required, but actual log hosts must exist during
|
||||||
|
chunk generation. This supersedes slice 60's whole-tree height-band behavior.
|
||||||
|
|
||||||
|
Build/static checks pass. Manual validation: new Void + Oak Log + Tendrils +
|
||||||
|
Shelf Mushrooms Age, without any tree page; inspect exposed log faces and
|
||||||
|
restart/new-chunk generation. No server is started and nothing is published.
|
||||||
|
|
||||||
|
The player passed the local `1.0.1.62-beta.1` sound slice: the complete 1.21.1
|
||||||
|
sound manifest/OGGs are reused, book travel emits modifier-selected audio and
|
||||||
|
cloud bursts, and Star Fissures play audio after arrival as well as departure.
|
||||||
|
Portal/meteor events are registered but their gameplay ports remain separate.
|
||||||
|
|
||||||
|
The local `1.0.1.63-beta.1` slice adds authored Normal/Fast/Slow/Off/Always/Storm
|
||||||
|
weather policies. One policy is accepted per book, with availability/tint and
|
||||||
|
tooltips following the shared classifier. Cave terrain always suppresses it.
|
||||||
|
An omitted policy defaults to Normal for now; random-trait selection is pending.
|
||||||
|
|
||||||
|
Unlike 1.21.1, 26.3 ServerLevel.getWeatherData delegates to the server's shared
|
||||||
|
weather object. A scoped Runtime Age ServerLevel subclass instead loads native
|
||||||
|
WeatherData from that Age's data storage, alongside its saved policy. Native
|
||||||
|
weather ticking and client packets remain in charge; only the data owner and
|
||||||
|
Legacy-style duration policy change. The constructor replaces inherited global
|
||||||
|
rain/thunder visual levels before publishing the level. Retirement, reseeding,
|
||||||
|
reload and restart retain the policy through existing per-Age storage lifecycle.
|
||||||
|
Vanilla /weather remains a global server command; the Age policy is not a
|
||||||
|
global command override. Native biome precipitation rules still apply.
|
||||||
|
|
||||||
|
Build/diff checks pass. A disposable dedicated server created all six weather
|
||||||
|
policies: every Age reported independent native data and the expected flags/
|
||||||
|
duration ranges. Setting global thunder left the Off Age clear. Unload/reload
|
||||||
|
retained Storm, and a full stop/restart restored all six policies and their
|
||||||
|
live timers. Both servers shut down cleanly; the temporary test world is
|
||||||
|
deleted after completion. Client appearance, cross-Age travel, authoring/tint,
|
||||||
|
and Cave suppression remain manual checks.
|
||||||
|
No commit/push/publication is authorized by the player's testing pass alone.
|
||||||
|
|
||||||
|
The local `1.0.1.64-beta.1` follow-up fixes weather-only authoring. Build 63's
|
||||||
|
server weather checks bypassed the Binder's biome compiler, whose exclusion
|
||||||
|
list did not yet include weather. As a result weather/storm was incorrectly
|
||||||
|
resolved as a biome ID despite grammar acceptance. The biome compiler now
|
||||||
|
selects actual biome-category entries after full-book grammar validation;
|
||||||
|
controllers retain their separate handling. This avoids repeating the same
|
||||||
|
missed-exclusion bug as more non-biome page families are ported. The operator
|
||||||
|
weather fixture now calls that compiler before backend creation. All six
|
||||||
|
weather-only symbol lists compiled and created their expected weather states
|
||||||
|
on a fresh dedicated server. Build/diff checks pass. The server shut down
|
||||||
|
cleanly and its disposable world was removed; player GUI validation remains.
|
||||||
|
|
||||||
|
The local `1.0.1.65-beta.1` spawn correction traces non-Cave arrival columns
|
||||||
|
from maximum build height, inspecting actual fully generated chunk blocks.
|
||||||
|
The configured Flat-layer shortcut could select ground below later authored
|
||||||
|
terrain, and the old heightmap-only path did not explicitly ensure an exposed
|
||||||
|
surface. The new path stops at the first fluid/leaves/collision, requires dry
|
||||||
|
footing and headroom, and otherwise tries the next column rather than searching
|
||||||
|
below an obstruction. Chunk lookup is cached per column and a mutable position
|
||||||
|
avoids per-air-block allocations. A fallback platform goes above the actual
|
||||||
|
highest obstruction; empty Void retains floor Y=63. Surface footing accepts
|
||||||
|
leaves, rejects fluid-bearing floors, and checks build/border bounds.
|
||||||
|
|
||||||
|
Stored non-Cave Star Fissure arrivals also use the surface search rather than
|
||||||
|
accepting old underground pockets. Cave terrain keeps its Y=64-down search.
|
||||||
|
Exact bound Linking Books and the general fissure Overworld-return anchor
|
||||||
|
search are not redirected to the sky. A read-only surface probe command helps
|
||||||
|
validate the chosen column/nearby surface (chunk generation is possible).
|
||||||
|
Build/diff checks pass. A disposable dedicated-server probe selected Y=4 on
|
||||||
|
plain Flat ground, Y=141 above leaves placed at Y=140, and a neighboring dry
|
||||||
|
column instead of descending below water added above those leaves. A larger
|
||||||
|
roof-fixture fill was rejected by vanilla because some target chunks were not
|
||||||
|
loaded; that fixture is not claimed as tested. The server shut down cleanly,
|
||||||
|
and its temporary world was deleted. Actual player arrival in the affected
|
||||||
|
Age and the unchanged Cave/Linking Book paths remain manual checks.
|
||||||
|
|
||||||
## Verification of this first slice
|
## Verification of this first slice
|
||||||
|
|
||||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||||
@@ -503,3 +908,6 @@ extraction. The GameTest server ran 93 tests; 92 passed, while
|
|||||||
desks instead of one. That desk failure is outside the dimension-backend edit
|
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
|
and must be resolved or rechecked before a release. The 1.21.1 `build` task
|
||||||
passed again after the 26.3 registry slice.
|
passed again after the 26.3 registry slice.
|
||||||
|
On 2026-11-01, the default 1.21.1 GameTest server was rerun and all 93
|
||||||
|
required tests passed, including the desk test. Issue #97 was then closed;
|
||||||
|
remaining 26.3 parity work stays under #98–#104.
|
||||||
+21
-2
@@ -1,4 +1,4 @@
|
|||||||
# Publishing Rivencraft with the Modrinth and CurseForge APIs
|
# Publishing Rivencraft with the Gitea, Modrinth, and CurseForge APIs
|
||||||
|
|
||||||
The local helper is [`scripts/publish_mod_sites.py`](../scripts/publish_mod_sites.py).
|
The local helper is [`scripts/publish_mod_sites.py`](../scripts/publish_mod_sites.py).
|
||||||
It previews a release unless `--publish` is explicitly supplied. The current
|
It previews a release unless `--publish` is explicitly supplied. The current
|
||||||
@@ -8,6 +8,12 @@ is enabled for it.
|
|||||||
|
|
||||||
## One-time token setup
|
## One-time token setup
|
||||||
|
|
||||||
|
For Gitea, an existing personal access token is sufficient if it has
|
||||||
|
`write:repository` scope. That scope covers release creation and attachment
|
||||||
|
upload; do not grant `all` if a narrower token is available. Create or inspect
|
||||||
|
tokens under [Gitea user settings → Applications](https://git.paddedcell.org/user/settings/applications).
|
||||||
|
The helper checks token identity and scope without displaying the secret.
|
||||||
|
|
||||||
Create a [Modrinth personal access token](https://docs.modrinth.com/api/#authentication)
|
Create a [Modrinth personal access token](https://docs.modrinth.com/api/#authentication)
|
||||||
in account settings with `VERSION_CREATE` (upload) and `USER_READ` (read-only
|
in account settings with `VERSION_CREATE` (upload) and `USER_READ` (read-only
|
||||||
credential check) scopes. Create a
|
credential check) scopes. Create a
|
||||||
@@ -21,6 +27,7 @@ corresponding token into the hidden prompt:
|
|||||||
```sh
|
```sh
|
||||||
python3 scripts/publish_mod_sites.py store-token modrinth
|
python3 scripts/publish_mod_sites.py store-token modrinth
|
||||||
python3 scripts/publish_mod_sites.py store-token curseforge
|
python3 scripts/publish_mod_sites.py store-token curseforge
|
||||||
|
python3 scripts/publish_mod_sites.py store-token gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
The tokens are stored in the desktop Secret Service keyring under
|
The tokens are stored in the desktop Secret Service keyring under
|
||||||
@@ -34,6 +41,7 @@ Verify the credentials without uploading anything:
|
|||||||
```sh
|
```sh
|
||||||
python3 scripts/publish_mod_sites.py check-token modrinth
|
python3 scripts/publish_mod_sites.py check-token modrinth
|
||||||
python3 scripts/publish_mod_sites.py check-token curseforge
|
python3 scripts/publish_mod_sites.py check-token curseforge
|
||||||
|
python3 scripts/publish_mod_sites.py check-token gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
## Release workflow
|
## Release workflow
|
||||||
@@ -54,7 +62,18 @@ For a *new* version only, append `--publish` to upload to both sites. Use
|
|||||||
existing version number; CurseForge may not, so check its file list before any
|
existing version number; CurseForge may not, so check its file list before any
|
||||||
retry after a partial failure. CurseForge approval may take additional time.
|
retry after a partial failure. CurseForge approval may take additional time.
|
||||||
|
|
||||||
The helper uses Modrinth's documented multipart
|
The default destination is now all three sites. The Gitea release step requires
|
||||||
|
the annotated version tag to have been pushed first. Gitea's `Vx.y.z.b-mc26.3-beta.1`
|
||||||
|
tag convention is derived from the version and channel; an existing release
|
||||||
|
with different title or notes is never overwritten. If release creation
|
||||||
|
succeeds but its JAR upload fails, rerun with `--site gitea --publish` to resume
|
||||||
|
only the missing attachment. `--site both` retains the earlier Modrinth and
|
||||||
|
CurseForge-only behavior.
|
||||||
|
|
||||||
|
The helper uses Gitea's documented
|
||||||
|
[`POST /repos/{owner}/{repo}/releases`](https://docs.gitea.com/api/operations/repo-create-release/)
|
||||||
|
and [release attachment endpoint](https://docs.gitea.com/api/operations/repo-create-release-attachment/),
|
||||||
|
plus Modrinth's documented multipart
|
||||||
[`POST /version`](https://docs.modrinth.com/api/operations/createversion/) and
|
[`POST /version`](https://docs.modrinth.com/api/operations/createversion/) and
|
||||||
CurseForge's documented
|
CurseForge's documented
|
||||||
[`POST /api/projects/{projectId}/upload-file`](https://support.curseforge.com/support/solutions/articles/9000197321)
|
[`POST /api/projects/{projectId}/upload-file`](https://support.curseforge.com/support/solutions/articles/9000197321)
|
||||||
|
|||||||
+385
-4
@@ -3,10 +3,10 @@
|
|||||||
This is an **experimental beta port** for issues #97–#104, not gameplay parity
|
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
|
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
|
their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can
|
||||||
author Standard, Amplified, Island, Flat, or Void terrain with supported
|
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
|
||||||
biome-distribution pages. Other terrain families, feature/material effects,
|
biome-distribution pages, selected material/vegetation/structure features, and
|
||||||
portals, and much of
|
independent Age weather. Celestial/lighting systems, instability, crystal
|
||||||
the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
|
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.
|
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
|
The entries below record how the port was built, including earlier alpha-only
|
||||||
@@ -177,6 +177,387 @@ surface decoration in Flat. Void continues to generate no floor or biome
|
|||||||
decoration. The operator-only `age create_multi` probe creates two-biome test
|
decoration. The operator-only `age create_multi` probe creates two-biome test
|
||||||
Ages for the dedicated-server smoke test.
|
Ages for the dedicated-server smoke test.
|
||||||
|
|
||||||
|
The local `1.0.1.37-beta.1` slice adds the Cave terrain page to Binder
|
||||||
|
authoring. It uses Minecraft's caves noise settings, accepts Cave and Nether
|
||||||
|
biome pages (including multi-biome distribution), and searches below Y=64 for
|
||||||
|
a safe arrival instead of landing on the roof. The 26.3 vanilla Cave preset
|
||||||
|
defaults to water below Y=32, so this port derives a dry Cave setting with
|
||||||
|
air as its default fluid and the sea level below the world floor. This applies
|
||||||
|
to newly generated Cave chunks; it does not drain existing chunks. Cave-specific Nether surface
|
||||||
|
materials from 1.21.1 are not ported yet. The dedicated-server smoke now
|
||||||
|
creates and reloads Cave Ages alongside the other terrain families.
|
||||||
|
|
||||||
|
The dry-Cave follow-up is packaged as `1.0.1.38-beta.1`; no 26.3 release is
|
||||||
|
published until the in-game check passes and publication is explicitly requested.
|
||||||
|
|
||||||
|
The local `1.0.1.39-beta.1` follow-up rejects fluid at both feet and head
|
||||||
|
height during safe-landing searches. It also refuses to carve the fallback
|
||||||
|
platform into submerged space if no dry footing can be found.
|
||||||
|
|
||||||
|
The `1.0.1.40-beta.1` port slice seeds terrain and biome selection for
|
||||||
|
Descriptive Books with no authored symbol pages. It uses the same terrain and
|
||||||
|
biome-controller rank weights as the 1.21.1 line, freezes the resulting plan
|
||||||
|
in the Age index, and accepts the result after restart. The operator-only
|
||||||
|
`age create_random <name> <seed>` probe exercises this path. Random feature,
|
||||||
|
sky, weather, and instability choices are not yet ported; this slice covers
|
||||||
|
terrain and biomes only.
|
||||||
|
|
||||||
|
The `1.0.1.41-beta.1` slice establishes an Age-scoped decoration wrapper
|
||||||
|
for 26.3 and accepts one Block material page immediately before a Rocks feature
|
||||||
|
page. Newly generated surface chunks place small rock clusters made from that
|
||||||
|
block without modifying the shared biome registry. The selected block and
|
||||||
|
generator survive a restart. Cave and Void terrain remain unsupported for this
|
||||||
|
first surface-feature path; unsupported combinations stay in the Binder.
|
||||||
|
Random feature selection, unpaired materials, duplicate feature pages, and
|
||||||
|
instability scoring remain future port steps.
|
||||||
|
|
||||||
|
The local `1.0.1.42-beta.1` follow-up lets a Rocks page choose a deterministic
|
||||||
|
full-block material when no Block page precedes it. Repeated Rocks pages now
|
||||||
|
add independent placement attempts (up to 32) instead of being rejected. The
|
||||||
|
ordered material choices are frozen in the Age index and restored after a
|
||||||
|
restart; old single-material beta Ages still load. An explicit Block page
|
||||||
|
must still directly precede the Rocks page it modifies. Most other feature
|
||||||
|
pages and material-feature pairings remain unported.
|
||||||
|
|
||||||
|
The local `1.0.1.43-beta.1` slice replaces the temporary 3×3 Rocks piles
|
||||||
|
with Minecraft 26.3's forest-rock block-blob feature and its placement rules,
|
||||||
|
matching the 1.21.1 Age feature path more closely. The selected material and
|
||||||
|
repeat count still come from the frozen Age plan. Only newly generated chunks
|
||||||
|
use the new rock shape; previously generated chunks remain unchanged.
|
||||||
|
|
||||||
|
The local `1.0.1.44-beta.1` slice ports ordered Flowers, Trees, and Huge
|
||||||
|
Trees pages to surface Ages. Their 26.3 vanilla placed features run after
|
||||||
|
biome decoration, with the Age's own seed; repeated pages make independent
|
||||||
|
placement attempts. Cave/Void terrain and cave/Nether/End biome pools are
|
||||||
|
not treated as surface vegetation. The feature plan survives server restart;
|
||||||
|
already generated chunks do not gain new decoration.
|
||||||
|
|
||||||
|
The local `1.0.1.45-beta.1` slice ports the Dungeons page for Standard,
|
||||||
|
Amplified, and Island noise terrain. It uses the 1.21.1 placement definition
|
||||||
|
(eight monster-room attempts per page per chunk across the build height),
|
||||||
|
stores repeat count with the Age, and rejects Dungeons pages on Flat, Void,
|
||||||
|
or Cave terrain. New chunks only; existing chunks are not regenerated.
|
||||||
|
|
||||||
|
The local `1.0.1.46-beta.1` slice adds Surface Lakes, Deep Lakes, and Small
|
||||||
|
Lakes on surface noise terrain. A Fluid page immediately before a lake page
|
||||||
|
selects its fluid. Unmodified pages choose a seeded fluid from the same source-
|
||||||
|
fluid pool exposed in Creative, with repeated choices and their order saved
|
||||||
|
with the Age. Surface/Small Lakes retain the 1.21.1 one-in-twelve attempt rule;
|
||||||
|
Deep Lakes retain vanilla underground placement. These pages currently require
|
||||||
|
Standard, Amplified, or Island terrain. Existing chunks do not redecorate.
|
||||||
|
|
||||||
|
The local `1.0.1.47-beta.1` slice adds authored Floating Islands alongside
|
||||||
|
Standard, Amplified, Island, Flat, and Void terrain. A preceding Block page
|
||||||
|
sets the island core material; otherwise each occurrence resolves a seeded
|
||||||
|
material from the full-block pool. The 1.21.1 shape, high altitude, rarity,
|
||||||
|
and biome-dependent grass/dirt cap are retained. Repeated pages use independent
|
||||||
|
island seeds. Cave terrain remains unsupported for this feature.
|
||||||
|
|
||||||
|
26.3 combines base noise, surface, and cave carving in one terrain operation.
|
||||||
|
Floating Islands run after that operation and before decoration, so the added
|
||||||
|
islands are not cut by the base terrain's carvers. The saved feature plan is
|
||||||
|
used again after restart and for new chunks; existing chunks keep their blocks.
|
||||||
|
|
||||||
|
The local `1.0.1.48-beta.1` slice adds authored Tendrils on every terrain
|
||||||
|
type, including Cave and Void. A preceding Block page selects the material;
|
||||||
|
unmodified occurrences resolve seeded materials from the full-block pool.
|
||||||
|
Repeated pages use independent paths, with up to 32 occurrences per feature.
|
||||||
|
Mixed Floating Islands/Tendrils retain their authored order in the saved plan.
|
||||||
|
|
||||||
|
Tendrils retain the 1.21.1 branching path algorithm, source rates, and vertical
|
||||||
|
distribution concentrated below Y=128. They can replace existing terrain while
|
||||||
|
preserving bedrock. As with Floating Islands, they run after 26.3's combined
|
||||||
|
terrain operation and before decoration. Build checks are automated; in-game
|
||||||
|
generation, appearance, and restart validation are handed to the player.
|
||||||
|
|
||||||
|
The local `1.0.1.49-beta.1` slice adds the Skylands feature page. Its seeded
|
||||||
|
seven-octave filter removes base terrain at/below a varying cutoff near Y=76
|
||||||
|
and removes liquid above it, preserving the upper solid terrain. It accepts
|
||||||
|
all terrain types and no Block modifier. On low Flat terrain this removes
|
||||||
|
the base layers; on Void it leaves the empty base unchanged.
|
||||||
|
|
||||||
|
The filter runs once before authored Floating Islands and Tendrils, matching
|
||||||
|
1.21.1; duplicate Skylands pages do not compound the filter. Its flag is saved
|
||||||
|
with the terrain-feature plan. Existing chunks are unchanged. Try Amplified
|
||||||
|
terrain for a clear visual check; generation and restart validation are manual.
|
||||||
|
|
||||||
|
The local `1.0.1.50-beta.1` slice ports Spheres and Spikes using the 1.21.1
|
||||||
|
generation algorithms. Spheres add solid nodes on all terrain types, including
|
||||||
|
Cave and Void, while preserving bedrock. Spikes require solid surface terrain:
|
||||||
|
Standard, Amplified, Island, or Flat. Their circular footprint stays in one
|
||||||
|
chunk and must have solid ground beneath every column.
|
||||||
|
|
||||||
|
Each page accepts a preceding Block material or resolves a seeded random
|
||||||
|
material when unmodified. Repeated pages have independent placement seeds,
|
||||||
|
up to 32 per feature. Spheres run during terrain construction, after Skylands;
|
||||||
|
Spikes run after biome decoration. Both use the existing saved solid-feature
|
||||||
|
plan. New chunks only; in-game appearance and restart checks remain manual.
|
||||||
|
|
||||||
|
The local `1.0.1.51-beta.1` slice ports Obelisks. It preserves the 1.21.1
|
||||||
|
one-in-128 candidate chance per chunk/occurrence, 4×4 plinth, and 2×2 shaft
|
||||||
|
extending 12 blocks above it. Placement requires dry, sturdy ground across
|
||||||
|
the base, at most two blocks of surface variation, and clear/replaceable
|
||||||
|
space for the complete structure. The footprint stays within its chunk.
|
||||||
|
|
||||||
|
Obelisks support Standard, Amplified, Island, and Flat terrain. A preceding
|
||||||
|
Block page sets their material; otherwise a seeded full-block material is
|
||||||
|
selected. Repeated pages make independent attempts, up to 32 occurrences.
|
||||||
|
Spikes and Obelisks share surface-stage classification and preserve their
|
||||||
|
authored order within that stage. World and restart validation are manual.
|
||||||
|
|
||||||
|
The local `1.0.1.52-beta.1` slice ports Crystal Formations using the 1.21.1
|
||||||
|
surface-mound algorithm and the existing blue crystal blocks. Each occurrence
|
||||||
|
has a one-in-fifteen candidate chance per chunk; suitable dry, nearly level
|
||||||
|
ground produces a 3×3 mound with budding crystal and occasional small buds.
|
||||||
|
The existing budding blocks continue growing crystals through random ticks.
|
||||||
|
|
||||||
|
Crystal Formations accepts Standard, Amplified, Island, and Flat terrain,
|
||||||
|
but not Cave/Void terrain or Block/Fluid modifiers. Up to 32 repeated pages
|
||||||
|
make independent attempts. The ordered feature plan survives restart, and
|
||||||
|
only newly generated chunks receive formations. For a quick manual test,
|
||||||
|
bind a Flat/Plains Age with several Crystal Formations pages, inspect the
|
||||||
|
blue mounds and bud growth, then revisit after restarting the world.
|
||||||
|
|
||||||
|
The local `1.0.1.53-beta.1` slice ports Dense Ores. Each page replays eligible
|
||||||
|
ore placements from the Age's actual biomes, preserving their height ranges,
|
||||||
|
replacement rules, and biome filters. Vanilla and modded ore features are
|
||||||
|
recognized by their ore targets; custom placements can opt in through the
|
||||||
|
`rivencraft:dense_ores` placed-feature tag, just as in 1.21.1.
|
||||||
|
|
||||||
|
Up to 32 pages add independent passes on Standard, Amplified, Island, or Cave
|
||||||
|
terrain. Flat/Void and Block/Fluid modifiers are unsupported; an explicitly
|
||||||
|
authored biome pool must contain at least one ore-producing biome. The count
|
||||||
|
survives restart. Only new chunks gain extra ore, and this slice does not yet
|
||||||
|
port the later instability system or random-Age feature selection.
|
||||||
|
|
||||||
|
For manual validation, create a Standard/Plains Age with several Dense Ores
|
||||||
|
pages and inspect underground ore abundance, then restart and explore more
|
||||||
|
chunks. A Cave Age with a Nether biome can check Nether ore placements too.
|
||||||
|
|
||||||
|
The local `1.0.1.54-beta.1` slice ports the Star Fissures page. On first book/
|
||||||
|
operator arrival, it carves the 1.21.1 Legacy-shaped rift near the arrival chunk
|
||||||
|
and places a starfield portal at the dimension's minimum Y. Cave terrain keeps
|
||||||
|
its bedrock roof. All terrain types are supported; repeated pages still create
|
||||||
|
one fissure, and Block/Fluid modifiers are not accepted.
|
||||||
|
|
||||||
|
The first arrival and completed generation are saved per Age, so revisits and
|
||||||
|
restarts do not recarve the rift. Touching its bottom returns entities to a safe
|
||||||
|
location near the shared Overworld spawn, not a bed, with the original fissure
|
||||||
|
sound. The portal is unobtainable and inert in the Overworld. Random-Age feature
|
||||||
|
selection remains later work; author a Star Fissures page to test this slice.
|
||||||
|
|
||||||
|
For manual validation, create a Flat/Plains Age with Star Fissures. Look near
|
||||||
|
the arrival point (generally east/south), descend through the rift, and touch
|
||||||
|
the starfield to return. Re-enter after restarting; check that the same fissure
|
||||||
|
and its portal still work. A Cave Age checks that carving leaves the roof intact.
|
||||||
|
|
||||||
|
The local `1.0.1.55-beta.1` slice adds authored Villages, Mineshafts, and
|
||||||
|
Strongholds. Repeated pages (up to 32 per type) scale the registered placement
|
||||||
|
using the 1.21.1 frequency/spacing and ring-count rules. Copies are Age-local:
|
||||||
|
the Overworld's placements and biome source are not modified. Authored
|
||||||
|
structures can bypass their usual biome eligibility without changing the
|
||||||
|
terrain biome; their instability cost remains pending with the instability port.
|
||||||
|
|
||||||
|
Villages and Strongholds allow surface noise terrain and Flat, but not Cave
|
||||||
|
or Void. Mineshafts allow noise terrain including Cave, but not Flat or Void.
|
||||||
|
These pages do not accept Block/Fluid modifiers. Their ordered counts persist
|
||||||
|
in the feature plan. In books using these structure pages, unselected sets
|
||||||
|
from these three families are disabled; other structure families and random
|
||||||
|
feature selection remain later port work. Earlier books without this new
|
||||||
|
structure plan retain their existing native structure behavior.
|
||||||
|
|
||||||
|
For manual checks, bind a new Standard/Plains Age with Villages, Mineshafts,
|
||||||
|
and Strongholds, then use `/locate structure minecraft:village_plains`,
|
||||||
|
`/locate structure minecraft:mineshaft`, and
|
||||||
|
`/locate structure minecraft:stronghold` to inspect generated structures.
|
||||||
|
Compare one page with several copies in a different Age, and restart/revisit.
|
||||||
|
Flat/Plains with Villages and an otherwise incompatible surface biome with
|
||||||
|
Villages provide separate terrain and biome-override checks.
|
||||||
|
|
||||||
|
The local `1.0.1.56-beta.1` follow-up fixes authored structures outside their
|
||||||
|
normal biomes. Build 55 only bypassed the final biome check; 26.3's jigsaw
|
||||||
|
placement can reject a whole biome column earlier. The override now supplies
|
||||||
|
the generation context's biome predicate before either check, only for the
|
||||||
|
explicitly authored structures in that Age. Terrain/height/geometry rules and
|
||||||
|
unrelated structures keep their normal checks.
|
||||||
|
|
||||||
|
Test with a new Standard or Flat Age using a normally unsupported village
|
||||||
|
biome, such as The End, plus a Villages page. Multiple copies increase density.
|
||||||
|
Use `/locate structure #minecraft:village` rather than selecting only the
|
||||||
|
Plains variant: an override Age can choose other village styles. Old chunks
|
||||||
|
that already rejected their structure attempt are not regenerated; existing
|
||||||
|
build-55 Ages can also test the fix in previously unexplored chunks.
|
||||||
|
|
||||||
|
The local `1.0.1.57-beta.1` slice adds the remaining 1.21.1 authored structure
|
||||||
|
families: Nether Fortresses, Bastions, Trial Chambers, Buried Treasure,
|
||||||
|
Shipwrecks, Ocean Ruins, Pillager Outposts, Ruined Portals, Nether Fossils,
|
||||||
|
Ancient Cities, and Trail Ruins. It also enables the existing 26.3-only
|
||||||
|
Abandoned Camps page using Minecraft's registered camp set and variants.
|
||||||
|
|
||||||
|
All 15 supported structure-set pages share saved counts, biome overrides,
|
||||||
|
and repeated-page density scaling (up to 32 per type). Fortress and Bastion
|
||||||
|
sets reuse the separate 1.21.1 placement definitions; the combined vanilla
|
||||||
|
Nether set is disabled for authored structure plans so the pages stay separate.
|
||||||
|
Unselected supported families are gated in these plans. Already generated
|
||||||
|
chunks and Ages without an authored structure plan keep their existing path.
|
||||||
|
|
||||||
|
Only Villages/Strongholds support Flat; none support Void. Fortress, Bastion,
|
||||||
|
and Nether Fossils require Cave terrain. Villages/Strongholds, Buried Treasure,
|
||||||
|
Shipwrecks, Ocean Ruins, Outposts, Trail Ruins, and Abandoned Camps require
|
||||||
|
surface terrain. Mineshafts, Trial Chambers, Ancient Cities, and Ruined Portals
|
||||||
|
allow surface noise or Cave terrain. Physical terrain/height checks still apply
|
||||||
|
even when the authored page overrides biome eligibility. Instability penalties
|
||||||
|
and random selection of these features are not part of this slice.
|
||||||
|
|
||||||
|
For a quick in-game check, author a new Standard Age with Trial Chambers,
|
||||||
|
Trail Ruins, and Abandoned Camps. Use `/locate structure minecraft:trial_chambers`,
|
||||||
|
`/locate structure minecraft:trail_ruins`, and
|
||||||
|
`/locate structure minecraft:abandoned_camp_forest` to inspect actual generation.
|
||||||
|
Separately, test a Cave/Nether Wastes Age with Fortress and Bastion pages
|
||||||
|
(`/locate structure minecraft:fortress` and
|
||||||
|
`/locate structure minecraft:bastion_remnant`). Repeated pages and restart/
|
||||||
|
new-chunk generation remain useful checks. Structures can still fail when
|
||||||
|
their geometry or required terrain is unavailable.
|
||||||
|
|
||||||
|
The local `1.0.1.58-beta.1` slice enables Caves and Ravines feature pages.
|
||||||
|
They add carving to the chosen noise terrain (Standard, Amplified, Island,
|
||||||
|
or Cave); they are not aliases for the Cave terrain page. Flat/Void and
|
||||||
|
Block/Fluid modifiers remain unsupported, matching the 1.21.1 feature rules.
|
||||||
|
|
||||||
|
Authored paths retain 1.21.1's source range and seeded salts, using 26.3's
|
||||||
|
registered cave/Nether-cave/canyon carvers. Nether-biome sources select the
|
||||||
|
Nether cave carver and skip Ravines. The paths merge into the native carving
|
||||||
|
mask before Minecraft applies aquifer, replacement, and surface rules; no
|
||||||
|
second approximation carves raw air after terrain generation. Biomes are not
|
||||||
|
replaced. The page list is saved, and old plans default to no extra carving.
|
||||||
|
|
||||||
|
Duplicate pages are accepted up to 32 per type, but repeat the same seeded
|
||||||
|
pattern in 1.21.1; this port skips those redundant passes rather than multiplying
|
||||||
|
carving density. Random feature selection and instability remain pending.
|
||||||
|
New chunks only. For manual testing, author a Standard/Plains Age with one
|
||||||
|
Caves page and one Ravines page and inspect underground in Spectator mode.
|
||||||
|
Restart and explore more chunks. A separate Cave/Nether Wastes Age with Caves
|
||||||
|
checks the Nether variant without altering its biome distribution.
|
||||||
|
|
||||||
|
The local `1.0.1.59-beta.1` slice audits page presentation for the current port.
|
||||||
|
Each Symbol Page now reports whether its effect is implemented. Unported pages
|
||||||
|
have red-tinted item artwork and a red Binder page background, plus a red status
|
||||||
|
line. Supported pages retain their normal artwork. This indicates port
|
||||||
|
availability only: supported pages in an invalid order, terrain combination,
|
||||||
|
or excessive quantity are not individually tinted red. Link Panels are unchanged.
|
||||||
|
|
||||||
|
The stale Standard/one-Biome message is removed. Structure descriptions now
|
||||||
|
describe authored biome overrides rather than falsely requiring vanilla biome
|
||||||
|
eligibility. Material pages explain the immediate-before-feature requirement;
|
||||||
|
Flat/Caves/Ravines/Floating Islands descriptions reflect the current rules.
|
||||||
|
These overrides are specific to the 26.3 language file and do not change 1.21.1.
|
||||||
|
No new generation effect is introduced in this UI slice.
|
||||||
|
|
||||||
|
For manual validation, compare a working Villages or Caves page with an
|
||||||
|
unported celestial or lighting page in Creative inventory
|
||||||
|
and the Binder. Only the latter should be red; supported Block/Fluid modifiers
|
||||||
|
and biome controllers should stay normal even though they need other pages.
|
||||||
|
|
||||||
|
The local `1.0.1.60-beta.1` slice implements Poplar Trees, Fallen Poplar Trees,
|
||||||
|
Red Shrubs, and Shelf Mushrooms. They use the existing saved vegetation plan,
|
||||||
|
ordered attempts, surface-biome rules, and combined 32-vegetation-page limit.
|
||||||
|
They accept Standard, Amplified, Island, and Flat terrain, but not Cave/Void
|
||||||
|
or Block/Fluid modifiers. The four pages now show as supported, without a red tint.
|
||||||
|
|
||||||
|
Poplar Trees uses the vanilla red/orange/yellow leaf-litter tree variants with
|
||||||
|
their relative Dappled Forest weights and six attempts per page per chunk;
|
||||||
|
it does not also select spruce or fallen trees. Fallen Poplar Trees makes one
|
||||||
|
surface attempt per page, using the vanilla fallen-tree feature, decorators,
|
||||||
|
and sapling survival rules. Red Shrubs uses the vanilla patch placement.
|
||||||
|
All still require suitable ground and space; authoring does not bypass those
|
||||||
|
requirements, and existing chunks are not redecorated.
|
||||||
|
|
||||||
|
Shelf Mushrooms applies Minecraft's standing/fallen-log decorator to existing
|
||||||
|
log groups, not a new tree feature. It can decorate natural trees or logs from
|
||||||
|
earlier authored tree pages; no suitable logs means no mushrooms. The vanilla
|
||||||
|
side/age/spacing and water-avoidance logic is retained. Writes stay within the
|
||||||
|
current chunk and require valid support. Consecutive mushroom pages reuse the
|
||||||
|
host scan; adding trees invalidates the cached host list for subsequent pages.
|
||||||
|
|
||||||
|
For manual testing, bind a new Flat/Plains Age with Poplar Trees, Fallen Poplar
|
||||||
|
Trees, Red Shrubs, then Shelf Mushrooms. Check leaf colours, fallen trunks,
|
||||||
|
shrubs, and mushrooms on logs. The four pages should no longer be red. Restart,
|
||||||
|
revisit existing terrain, and explore new chunks to check persistence.
|
||||||
|
|
||||||
|
The local `1.0.1.61-beta.1` follow-up removes the inappropriate surface-terrain
|
||||||
|
and surface-biome gates from Shelf Mushrooms only. No tree page is required:
|
||||||
|
natural logs or generated log Tendrils/Spheres/other features are hosts on
|
||||||
|
any terrain, including Void and Cave, and in any biome. Other plants keep their
|
||||||
|
existing terrain/biome restrictions. For example, Void + Oak Log + Tendrils +
|
||||||
|
Shelf Mushrooms is valid authoring; there must still be exposed log faces.
|
||||||
|
|
||||||
|
The mushroom scan now considers log faces throughout the chunk height instead
|
||||||
|
of treating a connected tendril as a standing tree with a small base-height
|
||||||
|
decoration band. Placement retains native replaceability/water checks, valid
|
||||||
|
support, random age, a per-side chance, and spacing checks. It does not grow
|
||||||
|
hosts or continuously decorate player-built logs after chunk generation.
|
||||||
|
Test a new Void Age with the example pages and inspect tendril surfaces.
|
||||||
|
|
||||||
|
The local `1.0.1.62-beta.1` sound slice reuses the complete 1.21.1 sound manifest
|
||||||
|
and all six OGG files directly in the build. Successful Linking/Descriptive Book
|
||||||
|
travel now plays the normal, Intra-Linking, or Disarm sound at departure and
|
||||||
|
arrival, with the same modifier precedence and cloud bursts as 1.21.1. Failed
|
||||||
|
travel stays silent. Star Fissures also play their return sound after arrival,
|
||||||
|
instead of relying only on audio sent immediately before a dimension switch.
|
||||||
|
Portal and meteor sound events are bundled for their later gameplay ports;
|
||||||
|
this does not implement those systems. Binding still uses vanilla page audio.
|
||||||
|
Manual checks: normal book travel, Intra-Linking, Disarm, rejected travel, and
|
||||||
|
a Star Fissure return. Existing books/Ages do not need to be recreated.
|
||||||
|
|
||||||
|
The local `1.0.1.63-beta.1` slice ports the six active weather pages: Normal,
|
||||||
|
Fast, Slow, Off, Always, and Storm. One weather page is accepted per book and
|
||||||
|
its policy is saved in the Age's own data directory. Missing weather pages
|
||||||
|
currently use Normal; random weather selection is still part of the pending
|
||||||
|
random-trait port. Cave terrain forces Off, matching 1.21.1. Precipitation
|
||||||
|
still follows biome temperature and precipitation rules; a desert does not
|
||||||
|
start showing rain merely because Always or Storm is selected.
|
||||||
|
|
||||||
|
26.3 normally shares server WeatherData. Runtime Ages now use an isolated
|
||||||
|
native WeatherData instance with its own persisted rain/thunder timers. The
|
||||||
|
weather controller runs before the native cycle, allowing vanilla rendering,
|
||||||
|
lightning, snow and client dimension-specific packets to handle the result.
|
||||||
|
Fixed weather is refreshed before its timers expire; Fast and Slow use the
|
||||||
|
1.21.1 duration ranges and preserve saved timers on reload. Overworld weather
|
||||||
|
and other Ages are not changed. The vanilla `/weather` command still targets
|
||||||
|
the server's global weather, not the independent Age policy.
|
||||||
|
|
||||||
|
Operator diagnostics: `/rivencraft_port age create_weather <name> <seed> <mode>`
|
||||||
|
creates a Flat test Age; `/rivencraft_port age weather_probe <name>` reports
|
||||||
|
its policy, rain/thunder state and timers, data independence, and global state.
|
||||||
|
For manual testing, bind separate Storm/Always/Off books with Plains, compare
|
||||||
|
the Overworld, revisit and restart, and check that Cave + Storm remains dry.
|
||||||
|
The six weather pages should no longer be tinted red. Existing Ages get a
|
||||||
|
Normal independent cycle; author a new book to select another policy.
|
||||||
|
|
||||||
|
The local `1.0.1.64-beta.1` fix corrects the Binder's biome compiler: it now
|
||||||
|
positively selects biome-category pages instead of treating every page not
|
||||||
|
in an exclusion list as a biome. Weather pages no longer trigger an unknown
|
||||||
|
biome lookup. The weather fixture command now exercises the same page compiler
|
||||||
|
before creating an Age, covering the weather-only symbol list used by a Link
|
||||||
|
Panel + Storm book. Existing Binder contents can be reused after updating.
|
||||||
|
|
||||||
|
The local `1.0.1.65-beta.1` spawn correction removes the Flat-layer shortcut
|
||||||
|
and heightmap-only choice for non-Cave Descriptive Book arrivals. The search
|
||||||
|
starts at the actual maximum build height and traces each nearby column down
|
||||||
|
to its first colliding block, leaves, or fluid. A column without safe dry
|
||||||
|
footing/headroom is skipped instead of descending through its roof or water
|
||||||
|
into an underground pocket. Leaves are valid footing; fluids are not.
|
||||||
|
|
||||||
|
This also revalidates a stored Star Fissure arrival from above on non-Cave
|
||||||
|
terrain. Cave terrain retains its Y=64-downward search, and bound Linking
|
||||||
|
Books retain their exact coordinates. If no natural surface is safe, the
|
||||||
|
safety platform is placed above the actual highest obstruction (or floor
|
||||||
|
Y=63 in an empty Void), never buried below it. Plain Flat terrain still lands
|
||||||
|
on its ground, not an artificially elevated platform. No new Age is required.
|
||||||
|
Operator-only `/rivencraft_port age surface_probe <name> <x> <z>` reports the
|
||||||
|
actual highest obstruction and a safe nearby surface without creating a
|
||||||
|
platform or teleporting. It can generate chunks while inspecting the region.
|
||||||
|
|
||||||
From the repository root:
|
From the repository root:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Rivencraft 26.3 beta 1.0.1.39-beta.1
|
||||||
|
|
||||||
|
This beta adds Cave terrain to authored Ages on Minecraft 26.3.
|
||||||
|
|
||||||
|
- Bind Descriptive Books with the Cave terrain page and Cave or Nether biome pages, including supported multi-biome distributions.
|
||||||
|
- Cave Ages use the cave noise shape but remain dry by default. Newly generated chunks no longer flood below the vanilla cave sea level.
|
||||||
|
- Cave travel searches from Y=64 downward for dry, properly footed space instead of landing on the cave roof or underwater.
|
||||||
|
- Expanded dedicated-server checks for Cave creation, biome distribution, and persistence after restart.
|
||||||
|
|
||||||
|
The player verified the Cave and safe-landing changes in game. Existing generated chunks are not drained or regenerated. A future Flooded page will provide an authored water level; see [issue #107](https://git.paddedcell.org/dnorberg/rivencraft/issues/107).
|
||||||
|
|
||||||
|
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
|
||||||
|
|
||||||
|
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Rivencraft 26.3 beta 1.0.1.41-beta.1
|
||||||
|
|
||||||
|
This beta expands Age generation on Minecraft 26.3.
|
||||||
|
|
||||||
|
- Descriptive Books with no authored symbol pages now choose a seeded random terrain and biome distribution. The chosen plan is saved with the Age and restored after restart.
|
||||||
|
- A Block material page followed by a Rocks feature page now generates small clusters of that block in newly generated surface chunks. This first feature path supports Standard, Amplified, Island, and Flat terrain; Cave and Void combinations remain unsupported.
|
||||||
|
- Expanded dedicated-server checks cover random Age plans and authored Rocks generation across restart.
|
||||||
|
|
||||||
|
The player verified the Rocks feature in game. Random feature selection, duplicate feature pages, unpaired materials, and instability scoring are not yet ported. Existing chunks are not regenerated.
|
||||||
|
|
||||||
|
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
|
||||||
|
|
||||||
|
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Rivencraft 26.3 beta 1.0.1.54-beta.1
|
||||||
|
|
||||||
|
This beta collects the player-validated Age feature port slices since
|
||||||
|
1.0.1.41-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
|
||||||
|
|
||||||
|
## Age generation
|
||||||
|
|
||||||
|
- Rocks now use the established 1.21.1 forest-rock placement. Unmodified
|
||||||
|
material-bearing features choose seeded materials independently; a preceding
|
||||||
|
compatible Block or Fluid page selects the material explicitly.
|
||||||
|
- Added Flowers, Trees, Huge Trees, Dungeons, Surface Lakes, Deep Lakes, and
|
||||||
|
Small Lakes using the 1.21.1 placement rules adapted to 26.3.
|
||||||
|
- Added Floating Islands, Tendrils, Skylands, Spheres, Spikes, and Obelisks
|
||||||
|
using the existing Rivencraft generation algorithms.
|
||||||
|
- Added blue Crystal Formations with budding crystal and initially seeded buds.
|
||||||
|
- Added Dense Ores: repeated pages replay eligible biome ore placements,
|
||||||
|
including compatible modded ores and explicitly tagged custom placements.
|
||||||
|
- Added Star Fissures: one Legacy-shaped rift near the first arrival point,
|
||||||
|
a starfield at the dimension's minimum Y, and return travel to the shared
|
||||||
|
Overworld spawn with the original fissure sound. Cave roofs are preserved.
|
||||||
|
- Feature plans, materials, repeated occurrences, and fissure completion
|
||||||
|
persist across restart. Existing generated chunks are not redecorated.
|
||||||
|
|
||||||
|
## Current limits
|
||||||
|
|
||||||
|
- Authored features retain terrain restrictions. Most repeated features allow
|
||||||
|
up to 32 occurrences; exceeding the current authoring cap blocks assembly.
|
||||||
|
Skylands and Star Fissures retain their single-effect behavior when repeated.
|
||||||
|
- Put a material page immediately before a compatible feature. Dangling
|
||||||
|
material assignment, full Legacy grammar recovery, instability, and random
|
||||||
|
feature selection are not yet ported. Pageless books randomize terrain and
|
||||||
|
biome distribution, not the newly authored feature list.
|
||||||
|
- 26.3 combines base terrain, surface, and carving. Added solid volumes run
|
||||||
|
after that combined operation and before decoration, so base carvers do not
|
||||||
|
cut those added volumes.
|
||||||
|
- The full 1.21.1 writing/discovery loop, structures, environmental systems,
|
||||||
|
crystal portals, and optional integration parity remain in progress. The
|
||||||
|
presence of a Creative page is not a promise that its effect is ported.
|
||||||
|
|
||||||
|
Build and static checks passed. The player reported in-game passes for these
|
||||||
|
slices. Earlier server smoke coverage was expanded, but the later generation
|
||||||
|
slices were manually validated; this release does not claim a new full
|
||||||
|
dedicated-server regression pass.
|
||||||
|
|
||||||
|
Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build,
|
||||||
|
and **Java 25**. Dynamic Dimensions and Bad Packets are not required by this
|
||||||
|
26.3 build. Do not use Minecraft 1.21.1 dependencies or saves with it. Back up
|
||||||
|
the entire world before updating; beta-save compatibility is not guaranteed.
|
||||||
|
|
||||||
|
Report issues and help with documentation at
|
||||||
|
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Rivencraft 26.3 beta 1.0.1.65-beta.1
|
||||||
|
|
||||||
|
This beta collects the player-tested port slices since 1.0.1.54-beta.1.
|
||||||
|
It is not yet feature-complete with the Minecraft 1.21.1 release.
|
||||||
|
|
||||||
|
## Age generation
|
||||||
|
|
||||||
|
- Added authored structure pages: Villages, Mineshafts, Strongholds, Trial
|
||||||
|
Chambers, Buried Treasure, Shipwrecks, Ocean Ruins, Pillager Outposts,
|
||||||
|
Ruined Portals, Nether Fossils, Ancient Cities, Trail Ruins, Nether
|
||||||
|
Fortresses, Bastions, and 26.3's Abandoned Camps. Dungeons remain their
|
||||||
|
separate feature.
|
||||||
|
- Fixed the early structure-biome check so authored structures can generate
|
||||||
|
outside their native biome restrictions, within the supported terrain rules.
|
||||||
|
Repeated structure pages increase density up to the current authoring cap.
|
||||||
|
- Added Caves and Ravines feature pages through 26.3's native carving stage.
|
||||||
|
These are distinct from Cave terrain; Flat/Void support remains pending.
|
||||||
|
- Added Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf Mushrooms.
|
||||||
|
Shelf Mushrooms find exposed logs at any height, including log Tendrils in
|
||||||
|
Void terrain. No Trees page is required.
|
||||||
|
- Added Normal, Fast, Slow, Off, Always, and Storm weather pages. Each Age
|
||||||
|
has independent saved weather data and timers; its policy does not change
|
||||||
|
the Overworld or other Ages. Cave terrain keeps weather disabled.
|
||||||
|
|
||||||
|
## Fixes and feedback
|
||||||
|
|
||||||
|
- Non-Cave Descriptive Book arrivals trace actual blocks down from maximum
|
||||||
|
build height instead of using a Flat-layer shortcut or only a heightmap.
|
||||||
|
The search stops at roofs/fluids rather than selecting underground pockets.
|
||||||
|
Leaves count as footing; submerged footing is rejected. Fallback platforms
|
||||||
|
sit above obstructions, with floor Y=63 in an empty Void.
|
||||||
|
- Stored surface-Age Star Fissure arrivals are rechecked from above. Cave
|
||||||
|
terrain still searches down from Y=64, and Linking Books retain exact
|
||||||
|
bound destinations.
|
||||||
|
- Restored Legacy linking sounds and successful-link cloud bursts. Disarm
|
||||||
|
and Intra-Linking use their respective sounds. Star Fissure return audio
|
||||||
|
also plays after arrival so a dimension switch does not discard it.
|
||||||
|
- Fixed weather-only authoring, including Link Panel + Storm books: non-biome
|
||||||
|
pages no longer get interpreted as biome IDs.
|
||||||
|
- Unsupported pages now have a red tint and availability tooltip. Supported
|
||||||
|
pages and their descriptions reflect the implemented 26.3 behavior.
|
||||||
|
- Added operator weather and surface diagnostics for testing.
|
||||||
|
|
||||||
|
## Current limits
|
||||||
|
|
||||||
|
- Only the supported page/terrain combinations can be bound. Most repeated
|
||||||
|
features are capped at 32; vegetation uses a combined 32-page limit. The
|
||||||
|
full 1.21.1 grammar recovery and instability systems remain unported.
|
||||||
|
- Pageless books currently randomize terrain and biome distribution, not the
|
||||||
|
complete feature/environment list. Omitted weather currently means Normal.
|
||||||
|
- Base carvers do not carve authored solid volumes added after 26.3's combined
|
||||||
|
terrain operation. Previously generated chunks are not regenerated.
|
||||||
|
- The full writing/discovery loop, celestial/lighting systems, crystal portals,
|
||||||
|
instability, and optional-integration parity remain in progress. Creative
|
||||||
|
availability alone does not mean a page is supported; check its tooltip.
|
||||||
|
- Vanilla `/weather` targets global server weather, not the independent Age
|
||||||
|
policy. Biomes retain their normal precipitation rules.
|
||||||
|
|
||||||
|
The player reported in-game passes for these slices. Build/static checks passed.
|
||||||
|
Targeted disposable dedicated-server checks covered all six weather policies,
|
||||||
|
Overworld isolation, unload/reload and restart persistence, weather-only page
|
||||||
|
compilation, and dry surface/leaf spawn probes. This is not a claim of full
|
||||||
|
dedicated-server or optional-mod regression coverage. Test servers were stopped
|
||||||
|
and their temporary worlds removed.
|
||||||
|
|
||||||
|
Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build,
|
||||||
|
and **Java 25**. This build does not require Dynamic Dimensions or Bad Packets.
|
||||||
|
Keep it separate from the 1.21.1 build and saves. Back up entire worlds before
|
||||||
|
updating; beta-save compatibility is not guaranteed.
|
||||||
|
|
||||||
|
Report issues and help improve the wiki at
|
||||||
|
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||||
@@ -124,6 +124,12 @@ sourceSets.main.resources.srcDir generatePortLanguage
|
|||||||
tasks.named('processResources', ProcessResources).configure {
|
tasks.named('processResources', ProcessResources).configure {
|
||||||
// Reuse the existing sprite and base model without copying binary assets.
|
// Reuse the existing sprite and base model without copying binary assets.
|
||||||
from('../../src/main/resources') {
|
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/crystal_shard.png'
|
||||||
include 'assets/rivencraft/textures/item/page.png'
|
include 'assets/rivencraft/textures/item/page.png'
|
||||||
include 'assets/rivencraft/textures/item/page_link_panel.png'
|
include 'assets/rivencraft/textures/item/page_link_panel.png'
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ neo_version=26.3.0.99
|
|||||||
mod_id=rivencraft
|
mod_id=rivencraft
|
||||||
mod_name=Rivencraft
|
mod_name=Rivencraft
|
||||||
mod_license=LGPL-3.0-or-later
|
mod_license=LGPL-3.0-or-later
|
||||||
mod_version=1.0.1.36-beta.1
|
mod_version=1.0.1.65-beta.1
|
||||||
@@ -6,19 +6,26 @@
|
|||||||
"item.rivencraft.page.blank": "Blank Page",
|
"item.rivencraft.page.blank": "Blank Page",
|
||||||
"item.rivencraft.page.link_panel": "Link Panel Page",
|
"item.rivencraft.page.link_panel": "Link Panel Page",
|
||||||
"item.rivencraft.page.unresolved": "Unknown Symbol Page",
|
"item.rivencraft.page.unresolved": "Unknown Symbol Page",
|
||||||
"item.rivencraft.page.symbol.port_status": "Binder currently supports Standard terrain and one Biome page; other effects are still being ported.",
|
"item.rivencraft.page.symbol.implemented": "Implemented in 26.3; book and terrain requirements still apply.",
|
||||||
|
"symbol.rivencraft.weather.normal.description": "Use an independent normal weather cycle. Cave terrain keeps weather off.",
|
||||||
|
"symbol.rivencraft.weather.fast.description": "Use the faster Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||||
|
"symbol.rivencraft.weather.slow.description": "Use the slower Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||||
|
"symbol.rivencraft.weather.off.description": "Keep this Age clear without changing Overworld weather.",
|
||||||
|
"symbol.rivencraft.weather.always.description": "Keep this Age rainy, or snowy in cold biomes, without thunder. Cave terrain keeps weather off.",
|
||||||
|
"symbol.rivencraft.weather.storm.description": "Keep this Age rainy and thundering. Precipitation still depends on the biome; Cave terrain keeps weather off.",
|
||||||
|
"item.rivencraft.page.symbol.unimplemented": "Not yet implemented in 26.3; cannot be bound into an Age.",
|
||||||
"item.rivencraft.link_property.disarm": "Disarm",
|
"item.rivencraft.link_property.disarm": "Disarm",
|
||||||
"item.rivencraft.link_property.maintain_momentum": "Maintain Momentum",
|
"item.rivencraft.link_property.maintain_momentum": "Maintain Momentum",
|
||||||
"item.rivencraft.link_property.intra_linking": "Intra-Linking",
|
"item.rivencraft.link_property.intra_linking": "Intra-Linking",
|
||||||
"item.rivencraft.ink_vial": "Ink Vial",
|
"item.rivencraft.ink_vial": "Ink Vial",
|
||||||
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
|
"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.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.",
|
"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",
|
"block.rivencraft.book_binder": "Book Binder",
|
||||||
"container.rivencraft.book_binder": "Book Binder",
|
"container.rivencraft.book_binder": "Book Binder",
|
||||||
"screen.rivencraft.book_binder.missing_panel": "Link Panel required",
|
"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",
|
"block.rivencraft.ink_mixer": "Ink Mixer",
|
||||||
"container.rivencraft.ink_mixer": "Ink Mixer",
|
"container.rivencraft.ink_mixer": "Ink Mixer",
|
||||||
"screen.rivencraft.ink_mixer.ink": "Ink: %s/%s",
|
"screen.rivencraft.ink_mixer.ink": "Ink: %s/%s",
|
||||||
@@ -33,13 +40,33 @@
|
|||||||
"itemGroup.rivencraft": "Rivencraft",
|
"itemGroup.rivencraft": "Rivencraft",
|
||||||
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
||||||
"symbol.rivencraft.feature.poplar_trees": "Poplar Trees",
|
"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": "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": "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": "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": "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.BuddingCrystalBlock;
|
||||||
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
|
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
|
||||||
import net.dnorberg.rivencraft.block.InkMixerBlock;
|
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.PortBookBinderBlockEntity;
|
||||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
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.component.DataComponentType;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.sounds.SoundEvent;
|
||||||
import net.minecraft.network.codec.ByteBufCodecs;
|
import net.minecraft.network.codec.ByteBufCodecs;
|
||||||
import net.minecraft.world.level.block.Block;
|
import net.minecraft.world.level.block.Block;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
@@ -54,6 +57,19 @@ public final class Rivencraft {
|
|||||||
public static final Logger LOGGER = LogUtils.getLogger();
|
public static final Logger LOGGER = LogUtils.getLogger();
|
||||||
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
|
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
|
||||||
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(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 =
|
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
|
||||||
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
|
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
|
||||||
private static final DeferredRegister<MenuType<?>> MENUS =
|
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",
|
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
|
||||||
Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_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(
|
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
|
||||||
"book_binder", PortBookBinderBlock::new,
|
"book_binder", PortBookBinderBlock::new,
|
||||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||||
@@ -146,6 +168,7 @@ public final class Rivencraft {
|
|||||||
|
|
||||||
public Rivencraft(IEventBus modEventBus) {
|
public Rivencraft(IEventBus modEventBus) {
|
||||||
BLOCKS.register(modEventBus);
|
BLOCKS.register(modEventBus);
|
||||||
|
SOUNDS.register(modEventBus);
|
||||||
ITEMS.register(modEventBus);
|
ITEMS.register(modEventBus);
|
||||||
BLOCK_ENTITY_TYPES.register(modEventBus);
|
BLOCK_ENTITY_TYPES.register(modEventBus);
|
||||||
MENUS.register(modEventBus);
|
MENUS.register(modEventBus);
|
||||||
@@ -155,6 +178,7 @@ public final class Rivencraft {
|
|||||||
PortBiomeSources.register(modEventBus);
|
PortBiomeSources.register(modEventBus);
|
||||||
PortChunkGenerators.register(modEventBus);
|
PortChunkGenerators.register(modEventBus);
|
||||||
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
|
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(PortAgeCommands::register);
|
||||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
||||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
|
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
|
||||||
|
|||||||
@@ -24,7 +24,17 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
.forGetter(index -> index.biomePlans),
|
.forGetter(index -> index.biomePlans),
|
||||||
Codec.unboundedMap(Identifier.CODEC,
|
Codec.unboundedMap(Identifier.CODEC,
|
||||||
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
||||||
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain)
|
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
|
||||||
|
.forGetter(index -> index.rocks),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, PortVegetationPlan.CODEC)
|
||||||
|
.optionalFieldOf("vegetation", Map.of()).forGetter(index -> index.vegetation),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, Codec.intRange(1, 32))
|
||||||
|
.optionalFieldOf("dungeons", Map.of()).forGetter(index -> index.dungeons),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, PortLakePlan.CODEC)
|
||||||
|
.optionalFieldOf("lakes", Map.of()).forGetter(index -> index.lakes),
|
||||||
|
Codec.unboundedMap(Identifier.CODEC, PortTerrainFeaturePlan.CODEC)
|
||||||
|
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures)
|
||||||
).apply(instance, AgeLevelIndex::new));
|
).apply(instance, AgeLevelIndex::new));
|
||||||
|
|
||||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||||
@@ -38,6 +48,11 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
private final Map<Identifier, Identifier> biomeOverrides;
|
private final Map<Identifier, Identifier> biomeOverrides;
|
||||||
private final Map<Identifier, PortBiomePlan> biomePlans;
|
private final Map<Identifier, PortBiomePlan> biomePlans;
|
||||||
private final Map<Identifier, PortAgeTerrain> terrain;
|
private final Map<Identifier, PortAgeTerrain> terrain;
|
||||||
|
private final Map<Identifier, PortRockPlan> rocks;
|
||||||
|
private final Map<Identifier, PortVegetationPlan> vegetation;
|
||||||
|
private final Map<Identifier, Integer> dungeons;
|
||||||
|
private final Map<Identifier, PortLakePlan> lakes;
|
||||||
|
private final Map<Identifier, PortTerrainFeaturePlan> terrainFeatures;
|
||||||
|
|
||||||
private AgeLevelIndex() {
|
private AgeLevelIndex() {
|
||||||
this.ages = new LinkedHashMap<>();
|
this.ages = new LinkedHashMap<>();
|
||||||
@@ -45,17 +60,32 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
this.biomeOverrides = new LinkedHashMap<>();
|
this.biomeOverrides = new LinkedHashMap<>();
|
||||||
this.biomePlans = new LinkedHashMap<>();
|
this.biomePlans = new LinkedHashMap<>();
|
||||||
this.terrain = new LinkedHashMap<>();
|
this.terrain = new LinkedHashMap<>();
|
||||||
|
this.rocks = new LinkedHashMap<>();
|
||||||
|
this.vegetation = new LinkedHashMap<>();
|
||||||
|
this.dungeons = new LinkedHashMap<>();
|
||||||
|
this.lakes = new LinkedHashMap<>();
|
||||||
|
this.terrainFeatures = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||||
Map<Identifier, Identifier> biomeOverrides,
|
Map<Identifier, Identifier> biomeOverrides,
|
||||||
Map<Identifier, PortBiomePlan> biomePlans,
|
Map<Identifier, PortBiomePlan> biomePlans,
|
||||||
Map<Identifier, PortAgeTerrain> terrain) {
|
Map<Identifier, PortAgeTerrain> terrain,
|
||||||
|
Map<Identifier, PortRockPlan> rocks,
|
||||||
|
Map<Identifier, PortVegetationPlan> vegetation,
|
||||||
|
Map<Identifier, Integer> dungeons,
|
||||||
|
Map<Identifier, PortLakePlan> lakes,
|
||||||
|
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures) {
|
||||||
this.ages = new LinkedHashMap<>(ages);
|
this.ages = new LinkedHashMap<>(ages);
|
||||||
this.retired = new LinkedHashSet<>(retired);
|
this.retired = new LinkedHashSet<>(retired);
|
||||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||||
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
||||||
this.terrain = new LinkedHashMap<>(terrain);
|
this.terrain = new LinkedHashMap<>(terrain);
|
||||||
|
this.rocks = new LinkedHashMap<>(rocks);
|
||||||
|
this.vegetation = new LinkedHashMap<>(vegetation);
|
||||||
|
this.dungeons = new LinkedHashMap<>(dungeons);
|
||||||
|
this.lakes = new LinkedHashMap<>(lakes);
|
||||||
|
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<Identifier, Long> entries() {
|
Map<Identifier, Long> entries() {
|
||||||
@@ -81,13 +111,26 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
|
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
|
||||||
}
|
}
|
||||||
|
|
||||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain) {
|
PortRockPlan rocks(Identifier id) { return rocks.get(id); }
|
||||||
|
PortVegetationPlan vegetation(Identifier id) { return vegetation.get(id); }
|
||||||
|
int dungeons(Identifier id) { return dungeons.getOrDefault(id, 0); }
|
||||||
|
PortLakePlan lakes(Identifier id) { return lakes.get(id); }
|
||||||
|
PortTerrainFeaturePlan terrainFeatures(Identifier id) { return terrainFeatures.get(id); }
|
||||||
|
|
||||||
|
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
||||||
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||||
|
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||||
if (contains(id)) {
|
if (contains(id)) {
|
||||||
throw new IllegalArgumentException("Age already exists: " + id);
|
throw new IllegalArgumentException("Age already exists: " + id);
|
||||||
}
|
}
|
||||||
ages.put(id, seed);
|
ages.put(id, seed);
|
||||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||||
|
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||||
|
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||||
|
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||||
|
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||||
|
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,16 +139,28 @@ final class AgeLevelIndex extends SavedData {
|
|||||||
biomeOverrides.remove(id);
|
biomeOverrides.remove(id);
|
||||||
biomePlans.remove(id);
|
biomePlans.remove(id);
|
||||||
terrain.remove(id);
|
terrain.remove(id);
|
||||||
|
rocks.remove(id);
|
||||||
|
vegetation.remove(id);
|
||||||
|
dungeons.remove(id);
|
||||||
|
lakes.remove(id);
|
||||||
|
terrainFeatures.remove(id);
|
||||||
retired.add(id);
|
retired.add(id);
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
||||||
PortAgeTerrain ageTerrain) {
|
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
|
||||||
|
PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||||
|
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||||
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
||||||
ages.put(id, seed);
|
ages.put(id, seed);
|
||||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||||
|
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||||
|
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||||
|
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||||
|
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||||
|
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||||
setDirty();
|
setDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import net.minecraft.server.level.ServerPlayer;
|
|||||||
import net.minecraft.util.Prediction;
|
import net.minecraft.util.Prediction;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
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.neoforge.event.RegisterCommandsEvent;
|
||||||
import net.neoforged.fml.ModList;
|
import net.neoforged.fml.ModList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -66,6 +68,22 @@ public final class PortAgeCommands {
|
|||||||
return 1;
|
return 1;
|
||||||
})))
|
})))
|
||||||
.then(Commands.literal("age")
|
.then(Commands.literal("age")
|
||||||
|
.then(Commands.literal("surface_probe")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||||
|
.then(Commands.argument("z", IntegerArgumentType.integer())
|
||||||
|
.executes(PortAgeCommands::probeSurface)))))
|
||||||
|
.then(Commands.literal("create_weather")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.then(Commands.argument("mode", StringArgumentType.word())
|
||||||
|
.suggests((context, builder) -> net.minecraft.commands.SharedSuggestionProvider.suggest(
|
||||||
|
java.util.Arrays.stream(PortWeatherMode.values())
|
||||||
|
.map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder))
|
||||||
|
.executes(PortAgeCommands::createWeatherAge)))))
|
||||||
|
.then(Commands.literal("weather_probe")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.executes(PortAgeCommands::probeWeather)))
|
||||||
.then(Commands.literal("create")
|
.then(Commands.literal("create")
|
||||||
.then(Commands.argument("name", StringArgumentType.word())
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
@@ -78,7 +96,7 @@ public final class PortAgeCommands {
|
|||||||
context, "terrain").toUpperCase(Locale.ROOT));
|
context, "terrain").toUpperCase(Locale.ROOT));
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
context.getSource().sendFailure(Component.literal(
|
context.getSource().sendFailure(Component.literal(
|
||||||
"Unknown terrain; use standard, amplified, island, flat, or void"));
|
"Unknown terrain; use standard, amplified, island, cave, flat, or void"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return createAge(context, terrain);
|
return createAge(context, terrain);
|
||||||
@@ -94,19 +112,87 @@ public final class PortAgeCommands {
|
|||||||
context, "terrain").toUpperCase(Locale.ROOT));
|
context, "terrain").toUpperCase(Locale.ROOT));
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
context.getSource().sendFailure(Component.literal(
|
context.getSource().sendFailure(Component.literal(
|
||||||
"Unknown terrain; use flat or void"));
|
"Unknown terrain; use cave, flat, or void"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (terrain != PortAgeTerrain.FLAT && terrain != PortAgeTerrain.VOID) {
|
if (terrain != PortAgeTerrain.CAVE
|
||||||
|
&& terrain != PortAgeTerrain.FLAT
|
||||||
|
&& terrain != PortAgeTerrain.VOID) {
|
||||||
context.getSource().sendFailure(Component.literal(
|
context.getSource().sendFailure(Component.literal(
|
||||||
"Multi-biome probe supports flat or void only"));
|
"Multi-biome probe supports cave, flat, or void only"));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return createAge(context, terrain, new PortBiomePlan(
|
return createAge(context, terrain, new PortBiomePlan(
|
||||||
PortBiomePlan.Distribution.TILED, java.util.List.of(
|
PortBiomePlan.Distribution.TILED,
|
||||||
Identifier.withDefaultNamespace("plains"),
|
terrain == PortAgeTerrain.CAVE
|
||||||
Identifier.withDefaultNamespace("forest"))));
|
? java.util.List.of(Identifier.withDefaultNamespace("lush_caves"),
|
||||||
|
Identifier.withDefaultNamespace("dripstone_caves"))
|
||||||
|
: java.util.List.of(Identifier.withDefaultNamespace("plains"),
|
||||||
|
Identifier.withDefaultNamespace("forest"))));
|
||||||
})))))
|
})))))
|
||||||
|
.then(Commands.literal("create_random")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> {
|
||||||
|
long seed = LongArgumentType.getLong(context, "seed");
|
||||||
|
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
|
||||||
|
context.getSource().getServer(), seed);
|
||||||
|
int created = createAge(context, selected.terrain(), selected.biomes());
|
||||||
|
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Random Age plan: terrain=" + selected.terrain()
|
||||||
|
+ " biomes=" + selected.biomes()), false);
|
||||||
|
return created;
|
||||||
|
}))))
|
||||||
|
.then(Commands.literal("create_rocks")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
|
||||||
|
null, new PortRockPlan(Identifier.withDefaultNamespace("gold_block")))))))
|
||||||
|
.then(Commands.literal("create_rocks_multi")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||||
|
fourGoldRocks(LongArgumentType.getLong(context, "seed")))))))
|
||||||
|
.then(Commands.literal("create_rocks_random")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||||
|
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(
|
||||||
|
java.util.List.of(Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "feature/rocks")),
|
||||||
|
LongArgumentType.getLong(context, "seed")))))))
|
||||||
|
.then(Commands.literal("create_vegetation")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||||
|
null, testVegetation())))))
|
||||||
|
.then(Commands.literal("create_dungeons")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||||
|
null, null, null, testDungeons())))))
|
||||||
|
.then(Commands.literal("create_lakes")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||||
|
null, null, null, 0, testLakes(
|
||||||
|
LongArgumentType.getLong(context, "seed"), false))))))
|
||||||
|
.then(Commands.literal("create_lakes_random")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||||
|
null, null, null, 0, testLakes(
|
||||||
|
LongArgumentType.getLong(context, "seed"), true))))))
|
||||||
|
.then(Commands.literal("create_islands")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||||
|
.executes(context -> createIslandAge(context, false))))))
|
||||||
|
.then(Commands.literal("create_islands_random")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||||
|
.executes(context -> createIslandAge(context, true))))))
|
||||||
.then(Commands.literal("list")
|
.then(Commands.literal("list")
|
||||||
.executes(context -> {
|
.executes(context -> {
|
||||||
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
|
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
|
||||||
@@ -210,6 +296,21 @@ public final class PortAgeCommands {
|
|||||||
.then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0))
|
.then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0))
|
||||||
.executes(context -> sampleAge(context,
|
.executes(context -> sampleAge(context,
|
||||||
IntegerArgumentType.getInteger(context, "chunkOffset"))))))
|
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.literal("biome")
|
||||||
.then(Commands.argument("name", StringArgumentType.word())
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||||
@@ -217,18 +318,297 @@ public final class PortAgeCommands {
|
|||||||
.executes(PortAgeCommands::sampleBiome)))))));
|
.executes(PortAgeCommands::sampleBiome)))))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int probeSurface(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int x = IntegerArgumentType.getInteger(context, "x");
|
||||||
|
int z = IntegerArgumentType.getInteger(context, "z");
|
||||||
|
var obstruction = PortAgeTravel.highestObstruction(level, x, z);
|
||||||
|
var landing = PortAgeTravel.findSurfaceLanding(level, x, z);
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal("Surface probe " + id
|
||||||
|
+ " highest=" + obstruction + " landing=" + landing), false);
|
||||||
|
return landing == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createWeatherAge(CommandContext<CommandSourceStack> context) {
|
||||||
|
try {
|
||||||
|
PortWeatherMode mode = PortWeatherMode.valueOf(
|
||||||
|
StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT));
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
var server = context.getSource().getServer();
|
||||||
|
// Exercise the Binder's page compiler too, not just direct backend creation.
|
||||||
|
var pages = java.util.List.of(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||||
|
"weather/" + mode.name().toLowerCase(Locale.ROOT)));
|
||||||
|
var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
pages, server.registryAccess());
|
||||||
|
RuntimeAgeBackend.create(server, id,
|
||||||
|
LongArgumentType.getLong(context, "seed"), biomePlan, PortAgeTerrain.FLAT,
|
||||||
|
null, null, 0, null, null, mode);
|
||||||
|
return probeWeather(context);
|
||||||
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
|
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeWeather(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
var server = context.getSource().getServer();
|
||||||
|
ServerLevel loaded = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (!(loaded instanceof PortAgeLevel level)) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var weather = level.getWeatherData();
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal("Weather probe " + id
|
||||||
|
+ " mode=" + level.weatherMode() + " raining=" + weather.isRaining()
|
||||||
|
+ " thundering=" + weather.isThundering() + " rainTicks=" + weather.getRainTime()
|
||||||
|
+ " thunderTicks=" + weather.getThunderTime()
|
||||||
|
+ " independent=" + (weather != server.getWeatherData())
|
||||||
|
+ " overworldRaining=" + server.getWeatherData().isRaining()
|
||||||
|
+ " overworldThundering=" + server.getWeatherData().isThundering()), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
private static int createAge(CommandContext<CommandSourceStack> context, PortAgeTerrain terrain) {
|
private static int createAge(CommandContext<CommandSourceStack> context, PortAgeTerrain terrain) {
|
||||||
return createAge(context, terrain, null);
|
return createAge(context, terrain, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
PortAgeTerrain terrain, PortBiomePlan biomePlan) {
|
PortAgeTerrain terrain, PortBiomePlan biomePlan) {
|
||||||
|
return createAge(context, terrain, biomePlan, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeRocks(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|
||||||
|
|| features.rocks() == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var materials = features.rocks().materials().stream()
|
||||||
|
.map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
|
||||||
|
int count = 0;
|
||||||
|
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
||||||
|
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
|
||||||
|
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
|
||||||
|
if (materials.stream().anyMatch(state::is)) count++;
|
||||||
|
}
|
||||||
|
int placed = count;
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Rocks probe " + id + " materials=" + features.rocks().materials() + " count=" + placed), false);
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeVegetation(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int flowers = 0;
|
||||||
|
int logs = 0;
|
||||||
|
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
||||||
|
for (int y = 4; y < 32; y++) {
|
||||||
|
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
|
||||||
|
if (state.is(BlockTags.FLOWERS)) flowers++;
|
||||||
|
if (state.is(BlockTags.LOGS)) logs++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int flowerCount = flowers;
|
||||||
|
int logCount = logs;
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Vegetation probe " + id + " flowers=" + flowerCount + " logs=" + logCount), false);
|
||||||
|
return flowerCount + logCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PortVegetationPlan testVegetation() {
|
||||||
|
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/flowers"));
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/trees"));
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/huge_trees"));
|
||||||
|
}
|
||||||
|
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.vegetationPlan(pages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int testDungeons() {
|
||||||
|
Identifier page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dungeons");
|
||||||
|
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(
|
||||||
|
java.util.Collections.nCopies(8, page));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeDungeons(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age has no loaded feature generator: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int count = features.dungeons();
|
||||||
|
int spawners = 0;
|
||||||
|
for (int chunkX = 0; chunkX < 4; chunkX++) for (int chunkZ = 0; chunkZ < 4; chunkZ++) {
|
||||||
|
var chunk = level.getChunk(chunkX, chunkZ);
|
||||||
|
for (var pos : chunk.getBlockEntitiesPos()) {
|
||||||
|
if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int placed = spawners;
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Dungeons probe " + id + " pages=" + count + " spawners=" + placed), false);
|
||||||
|
return placed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PortLakePlan testLakes(long seed, boolean randomFluids) {
|
||||||
|
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
for (String kind : java.util.List.of("surface_lakes", "deep_lakes", "lava_lakes")) {
|
||||||
|
if (!randomFluids) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||||
|
Identifier.withDefaultNamespace("water")));
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + kind));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeLakes(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
PortLakePlan lakes = level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
|
||||||
|
? features.lakes() : null;
|
||||||
|
// Finish neighboring features before counting, since lake bowls can
|
||||||
|
// cross a chunk boundary. Include underground lakes in the probe.
|
||||||
|
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
|
||||||
|
int[] fluids = new int[2];
|
||||||
|
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
|
||||||
|
level.getChunk(x, z).findBlocks(state -> state.is(Blocks.WATER) || state.is(Blocks.LAVA),
|
||||||
|
(pos, state) -> fluids[state.is(Blocks.WATER) ? 0 : 1]++);
|
||||||
|
}
|
||||||
|
int waterCount = fluids[0];
|
||||||
|
int lavaCount = fluids[1];
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Lakes probe " + id + " pages=" + (lakes == null ? 0 : lakes.choices().size())
|
||||||
|
+ " fluids=" + (lakes == null ? java.util.List.of() : lakes.choices().stream()
|
||||||
|
.map(PortLakePlan.Choice::fluid).distinct().toList())
|
||||||
|
+ " plan=" + (lakes == null ? 0 : lakes.choices().hashCode())
|
||||||
|
+ " water=" + waterCount + " lava=" + lavaCount), false);
|
||||||
|
return waterCount + lavaCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createIslandAge(CommandContext<CommandSourceStack> context, boolean randomMaterials) {
|
||||||
|
PortAgeTerrain terrain;
|
||||||
|
try {
|
||||||
|
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Unknown island test terrain"));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/forest"));
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (!randomMaterials) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||||
|
Identifier.withDefaultNamespace("gold_block")));
|
||||||
|
pages.add(PortTerrainFeaturePlan.FLOATING_ISLANDS);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
long seed = LongArgumentType.getLong(context, "seed");
|
||||||
|
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
pages, context.getSource().getServer().registryAccess());
|
||||||
|
var features = net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed);
|
||||||
|
return createAge(context, terrain, biomes, null, null, 0, null, features);
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int probeIslands(CommandContext<CommandSourceStack> context) {
|
||||||
|
Identifier id = ageId(context);
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|
||||||
|
|| features.terrainFeatures() == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age has no loaded island generator: " + id));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
var choices = features.terrainFeatures().choices();
|
||||||
|
var materials = choices.stream().map(PortTerrainFeaturePlan.Choice::material).distinct().toList();
|
||||||
|
var blocks = materials.stream().map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
|
||||||
|
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
|
||||||
|
int[] count = {0};
|
||||||
|
long[] fingerprint = {0};
|
||||||
|
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
|
||||||
|
level.getChunk(x, z).findBlocks(state -> !state.isAir(), (pos, state) -> {
|
||||||
|
if (pos.getY() < 128 || pos.getY() >= 240) return;
|
||||||
|
if (blocks.stream().anyMatch(state::is)) count[0]++;
|
||||||
|
fingerprint[0] += pos.asLong() * 0x9E3779B97F4A7C15L ^ state.toString().hashCode();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(
|
||||||
|
"Islands probe " + id + " pages=" + choices.size() + " materials=" + materials
|
||||||
|
+ " count=" + count[0] + " fingerprint=" + fingerprint[0]), false);
|
||||||
|
return count[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Exercise the actual ordered writing parser, not only the generator codec. */
|
||||||
|
private static PortRockPlan fourGoldRocks(long seed) {
|
||||||
|
Identifier block = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||||
|
Identifier.withDefaultNamespace("gold_block"));
|
||||||
|
Identifier feature = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/rocks");
|
||||||
|
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
pages.add(block);
|
||||||
|
pages.add(feature);
|
||||||
|
}
|
||||||
|
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan, PortRockPlan rockPlan) {
|
||||||
|
return createAge(context, terrain, biomePlan, rockPlan, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||||
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan) {
|
||||||
|
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||||
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||||
|
int dungeons) {
|
||||||
|
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||||
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||||
|
int dungeons, PortLakePlan lakes) {
|
||||||
|
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||||
|
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||||
|
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||||
|
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) {
|
||||||
String name = StringArgumentType.getString(context, "name");
|
String name = StringArgumentType.getString(context, "name");
|
||||||
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||||
long seed = LongArgumentType.getLong(context, "seed");
|
long seed = LongArgumentType.getLong(context, "seed");
|
||||||
ServerLevel level;
|
ServerLevel level;
|
||||||
try {
|
try {
|
||||||
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan, terrain);
|
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
|
||||||
|
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures);
|
||||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.saveddata.WeatherData;
|
||||||
|
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||||
|
|
||||||
|
/** Runtime Age with independent native weather, rather than 26.3's shared server cycle. */
|
||||||
|
final class PortAgeLevel extends ServerLevel {
|
||||||
|
private final WeatherData ageWeather;
|
||||||
|
private final PortWeatherSettings weatherSettings;
|
||||||
|
private final boolean cave;
|
||||||
|
|
||||||
|
PortAgeLevel(MinecraftServer server, ResourceKey<Level> key, LevelStem stem,
|
||||||
|
long biomeZoomSeed, PortAgeTerrain terrain) {
|
||||||
|
super(server, server.executor, server.storageSource,
|
||||||
|
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
|
||||||
|
key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false);
|
||||||
|
ageWeather = getDataStorage().computeIfAbsent(WeatherData.TYPE);
|
||||||
|
weatherSettings = getDataStorage().computeIfAbsent(PortWeatherSettings.TYPE);
|
||||||
|
cave = terrain == PortAgeTerrain.CAVE;
|
||||||
|
PortAgeWeather.apply(this);
|
||||||
|
// The base constructor initializes visual levels from server weather.
|
||||||
|
// Replace them before publishing this Age or sending its join packets.
|
||||||
|
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||||
|
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WeatherData getWeatherData() {
|
||||||
|
// The base constructor may query weather before our fields are initialized.
|
||||||
|
return ageWeather == null ? super.getWeatherData() : ageWeather;
|
||||||
|
}
|
||||||
|
|
||||||
|
PortWeatherMode weatherMode() { return cave ? PortWeatherMode.OFF : weatherSettings.mode(); }
|
||||||
|
|
||||||
|
void configureWeather(PortWeatherMode mode) {
|
||||||
|
weatherSettings.setMode(mode);
|
||||||
|
PortAgeWeather.apply(this);
|
||||||
|
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||||
|
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||||
|
getDataStorage().saveAndJoin();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ public enum PortAgeTerrain {
|
|||||||
STANDARD,
|
STANDARD,
|
||||||
AMPLIFIED,
|
AMPLIFIED,
|
||||||
ISLAND,
|
ISLAND,
|
||||||
|
CAVE,
|
||||||
FLAT,
|
FLAT,
|
||||||
VOID
|
VOID
|
||||||
}
|
}
|
||||||
@@ -11,8 +11,6 @@ import net.minecraft.server.level.ServerLevel;
|
|||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.levelgen.Heightmap;
|
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. */
|
/** Shared, server-authoritative landing for the 26.3 book and operator probe. */
|
||||||
public final class PortAgeTravel {
|
public final class PortAgeTravel {
|
||||||
@@ -25,47 +23,96 @@ public final class PortAgeTravel {
|
|||||||
if (!AgeCatalogSync.ready(player, id)) {
|
if (!AgeCatalogSync.ready(player, id)) {
|
||||||
return "Age catalog not acknowledged by client; remaining in source dimension";
|
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 (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,
|
boolean moved = player.teleportTo(target, landing.getX() + 0.5, landing.getY(), landing.getZ() + 0.5,
|
||||||
Set.of(), player.getYRot(), player.getXRot(), true);
|
Set.of(), player.getYRot(), player.getXRot(), true);
|
||||||
return moved ? null : "Teleportation failed; remaining in source dimension";
|
return moved ? null : "Teleportation failed; remaining in source dimension";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static BlockPos findLanding(ServerLevel target) {
|
private static BlockPos findLanding(ServerLevel target) {
|
||||||
// A Flat Age has a known surface. Use it directly before a heightmap
|
if (isCaveTerrain(target)) {
|
||||||
// fallback can place a platform in the air above a low superflat world.
|
// The cave roof is not a valid arrival surface. Search down from
|
||||||
FlatLevelGeneratorSettings flatSettings = switch (target.getChunkSource().getGenerator()) {
|
// Legacy's Y=64 start, then construct the normal safety platform
|
||||||
case FlatLevelSource flat -> flat.settings();
|
// at that height only when no footed opening can be found.
|
||||||
case PortFlatChunkGenerator flat -> flat.settings();
|
for (int radius = 0; radius <= 16; radius++) {
|
||||||
default -> null;
|
for (int x = -radius; x <= radius; x++) {
|
||||||
};
|
for (int z = -radius; z <= radius; z++) {
|
||||||
if (flatSettings != null && !flatSettings.getLayersInfo().isEmpty()) {
|
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||||
int surfaceY = target.getMinY() + flatSettings.getLayersInfo().stream()
|
for (int y = Math.min(64, target.getMaxY() - 2); y > target.getMinY(); y--) {
|
||||||
.mapToInt(layer -> layer.getHeight()).sum();
|
BlockPos foot = new BlockPos(x, y, z);
|
||||||
BlockPos surface = new BlockPos(0, surfaceY, 0);
|
if (hasFooting(target, foot)) return foot;
|
||||||
if (hasFooting(target, surface)) return surface;
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return makePlatform(target, new BlockPos(0, 64, 0));
|
||||||
}
|
}
|
||||||
|
BlockPos surface = findSurfaceLanding(target, 0, 0);
|
||||||
|
if (surface != null) return surface;
|
||||||
|
// Only build above the actual highest obstruction, never in a pocket
|
||||||
|
// beneath it. Void Ages retain the usual platform floor at Y=63.
|
||||||
|
BlockPos obstruction = highestObstruction(target, 0, 0);
|
||||||
|
int y = obstruction == null ? 64 : Math.max(64, obstruction.getY() + 3);
|
||||||
|
return makePlatform(target, new BlockPos(0, y, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
static BlockPos findSurfaceLanding(ServerLevel target, int originX, int originZ) {
|
||||||
for (int radius = 0; radius <= 16; radius++) {
|
for (int radius = 0; radius <= 16; radius++) {
|
||||||
for (int x = -radius; x <= radius; x++) {
|
for (int x = -radius; x <= radius; x++) {
|
||||||
for (int z = -radius; z <= radius; z++) {
|
for (int z = -radius; z <= radius; z++) {
|
||||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||||
int y = target.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z);
|
BlockPos floor = highestObstruction(target, originX + x, originZ + z);
|
||||||
BlockPos foot = new BlockPos(x, y, z);
|
if (floor != null && hasFooting(target, floor.above())) return floor.above();
|
||||||
if (hasFooting(target, foot)) {
|
|
||||||
return foot;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The operator/book probe needs a deterministic landing even when
|
return null;
|
||||||
// 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;
|
/** First obstructing surface seen from the sky; never continue through a roof or liquid. */
|
||||||
BlockPos foot = new BlockPos(0, y, 0);
|
static BlockPos highestObstruction(ServerLevel level, int x, int z) {
|
||||||
|
var chunk = level.getChunk(x >> 4, z >> 4);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, level.getMaxY() - 1, z);
|
||||||
|
for (int y = level.getMaxY() - 1; y >= level.getMinY(); y--) {
|
||||||
|
pos.setY(y);
|
||||||
|
var state = chunk.getBlockState(pos);
|
||||||
|
if (!state.isAir() && (!state.getFluidState().isEmpty()
|
||||||
|
|| state.is(net.minecraft.tags.BlockTags.LEAVES)
|
||||||
|
|| !state.getCollisionShape(level, pos).isEmpty())) return pos.immutable();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isCaveTerrain(ServerLevel target) {
|
||||||
|
var generator = target.getChunkSource().getGenerator();
|
||||||
|
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
|
||||||
|
return generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
|
||||||
|
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
|
||||||
|
.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||||
|
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
|
||||||
|
.value().noiseSettings());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BlockPos makePlatform(ServerLevel target, BlockPos foot) {
|
||||||
|
if (foot.getY() <= target.getMinY() || foot.getY() + 2 >= target.getMaxY()) return null;
|
||||||
|
// A carved pocket inside a flooded cave immediately refills. Refuse
|
||||||
|
// that landing instead of placing the player in submerged spawn space.
|
||||||
|
if (!target.getFluidState(foot).isEmpty() || !target.getFluidState(foot.above()).isEmpty()) return null;
|
||||||
|
for (Direction direction : Direction.Plane.HORIZONTAL) {
|
||||||
|
if (!target.getFluidState(foot.relative(direction)).isEmpty()
|
||||||
|
|| !target.getFluidState(foot.above().relative(direction)).isEmpty()) return null;
|
||||||
|
}
|
||||||
for (int x = -1; x <= 1; x++) {
|
for (int x = -1; x <= 1; x++) {
|
||||||
for (int z = -1; z <= 1; z++) {
|
for (int z = -1; z <= 1; z++) {
|
||||||
target.setBlock(new BlockPos(x, y - 1, z), Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
target.setBlock(foot.offset(x, -1, z), Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
target.setBlock(foot, Blocks.AIR.defaultBlockState(), 3);
|
target.setBlock(foot, Blocks.AIR.defaultBlockState(), 3);
|
||||||
@@ -74,9 +121,45 @@ public final class PortAgeTravel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean hasFooting(ServerLevel target, BlockPos foot) {
|
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();
|
BlockPos floor = foot.below();
|
||||||
return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
|
var floorState = target.getBlockState(floor);
|
||||||
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
|
return (floorState.isFaceSturdy(target, floor, Direction.UP)
|
||||||
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty();
|
|| floorState.is(net.minecraft.tags.BlockTags.LEAVES))
|
||||||
|
&& floorState.getFluidState().isEmpty()
|
||||||
|
&& target.getFluidState(foot).isEmpty()
|
||||||
|
&& target.getFluidState(foot.above()).isEmpty()
|
||||||
|
&& passable(target, foot) && passable(target, foot.above());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean passable(ServerLevel level, BlockPos pos) {
|
||||||
|
var state = level.getBlockState(pos);
|
||||||
|
return state.getCollisionShape(level, pos).isEmpty() || state.is(net.minecraft.tags.BlockTags.LEAVES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Same local-then-surface safe-return search used by 1.21.1 fissures. */
|
||||||
|
public static java.util.Optional<BlockPos> findFootedDestination(ServerLevel level, BlockPos origin) {
|
||||||
|
var local = BlockPos.betweenClosedStream(origin.offset(-4, -2, -4), origin.offset(4, 2, 4))
|
||||||
|
.map(BlockPos::immutable).sorted(java.util.Comparator.comparingDouble(pos -> pos.distSqr(origin)))
|
||||||
|
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||||
|
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||||
|
if (local.isPresent()) return local;
|
||||||
|
for (int radius = 1; radius <= 64; radius++) {
|
||||||
|
final int ring = radius;
|
||||||
|
var found = BlockPos.betweenClosedStream(origin.offset(-ring, 0, -ring), origin.offset(ring, 0, ring))
|
||||||
|
.filter(pos -> Math.max(Math.abs(pos.getX() - origin.getX()), Math.abs(pos.getZ() - origin.getZ())) == ring)
|
||||||
|
.map(BlockPos::immutable)
|
||||||
|
.flatMap(pos -> {
|
||||||
|
BlockPos surface = new BlockPos(pos.getX(),
|
||||||
|
level.getHeight(Heightmap.Types.MOTION_BLOCKING, pos.getX(), pos.getZ()), pos.getZ());
|
||||||
|
return java.util.stream.Stream.of(surface, surface.above(), surface.below(),
|
||||||
|
surface.above(2), surface.below(2));
|
||||||
|
})
|
||||||
|
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||||
|
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||||
|
if (found.isPresent()) return found;
|
||||||
|
}
|
||||||
|
return java.util.Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
import net.minecraft.world.level.saveddata.WeatherData;
|
||||||
|
import net.neoforged.neoforge.event.tick.LevelTickEvent;
|
||||||
|
|
||||||
|
/** 1.21.1 weather policies, applied before the independent native Age cycle advances. */
|
||||||
|
public final class PortAgeWeather {
|
||||||
|
private static final Map<PortAgeLevel, Integer> STATES = new WeakHashMap<>();
|
||||||
|
|
||||||
|
private PortAgeWeather() {}
|
||||||
|
|
||||||
|
public static void onLevelTickPre(LevelTickEvent.Pre event) {
|
||||||
|
if (event.getLevel() instanceof PortAgeLevel level) apply(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void apply(PortAgeLevel level) {
|
||||||
|
PortWeatherMode mode = level.weatherMode();
|
||||||
|
if (mode == PortWeatherMode.NORMAL) {
|
||||||
|
STATES.remove(level);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WeatherData weather = level.getWeatherData();
|
||||||
|
if (mode == PortWeatherMode.FAST || mode == PortWeatherMode.SLOW) {
|
||||||
|
int state = (weather.isRaining() ? 1 : 0) | (weather.isThundering() ? 2 : 0);
|
||||||
|
Integer previous = STATES.put(level, state);
|
||||||
|
if (previous != null && previous == state) return;
|
||||||
|
// Keep persisted timers on reload; rescale only when a cycle changes.
|
||||||
|
if (previous == null && weather.getRainTime() > 0) return;
|
||||||
|
int scale = mode == PortWeatherMode.FAST ? 1 : 4;
|
||||||
|
int duration = 6000 * scale + level.getRandom().nextInt(
|
||||||
|
(weather.isRaining() ? 6000 : 84000) * scale);
|
||||||
|
set(weather, duration, weather.isRaining(), weather.isThundering());
|
||||||
|
} else {
|
||||||
|
STATES.remove(level);
|
||||||
|
boolean rain = mode != PortWeatherMode.OFF;
|
||||||
|
boolean thunder = mode == PortWeatherMode.STORM;
|
||||||
|
if (weather.isRaining() != rain || weather.isThundering() != thunder
|
||||||
|
|| weather.getRainTime() < 200 || weather.getThunderTime() < 200
|
||||||
|
|| weather.getClearWeatherTime() != 0) set(weather, 6000, rain, thunder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void set(WeatherData data, int duration, boolean rain, boolean thunder) {
|
||||||
|
data.setClearWeatherTime(0);
|
||||||
|
data.setRainTime(duration);
|
||||||
|
data.setThunderTime(duration);
|
||||||
|
data.setRaining(rain);
|
||||||
|
data.setThundering(thunder);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.QuartPos;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.tags.BiomeTags;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.chunk.CarvingMask;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldGenerationContext;
|
||||||
|
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||||
|
import net.minecraft.world.level.levelgen.carver.WorldCarver;
|
||||||
|
|
||||||
|
/** Authored Caves/Ravines, independent of the terrain page, with 1.21.1 source seeds. */
|
||||||
|
public final class PortCarverPages {
|
||||||
|
public static final Identifier CAVES = Identifier.fromNamespaceAndPath("rivencraft", "feature/caves");
|
||||||
|
public static final Identifier RAVINES = Identifier.fromNamespaceAndPath("rivencraft", "feature/ravines");
|
||||||
|
private static final long CAVE_SALT = 0x4341564553464541L;
|
||||||
|
private static final long RAVINE_SALT = 0x524156494E45534CL;
|
||||||
|
|
||||||
|
private PortCarverPages() {}
|
||||||
|
|
||||||
|
public static boolean supports(Identifier page) { return CAVES.equals(page) || RAVINES.equals(page); }
|
||||||
|
|
||||||
|
public static void validateCounts(List<Identifier> pages) {
|
||||||
|
Map<Identifier, Integer> counts = new HashMap<>();
|
||||||
|
for (Identifier page : pages) if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32)
|
||||||
|
throw new IllegalArgumentException("Carver plan requires Caves/Ravines pages, at most 32 per type");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void validateRegistry(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||||
|
if (pages.stream().noneMatch(PortCarverPages::supports)) return;
|
||||||
|
var carvers = registries.lookupOrThrow(Registries.CARVER);
|
||||||
|
for (String name : pages.contains(CAVES) ? List.of("cave", "nether_cave", "canyon") : List.of("canyon")) {
|
||||||
|
if (name.equals("canyon") && !pages.contains(RAVINES)) continue;
|
||||||
|
carvers.get(ResourceKey.create(Registries.CARVER, Identifier.withDefaultNamespace(name)))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Missing authored carver minecraft:" + name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void carve(ChunkGenerator generator, ChunkAccess chunk, RandomState randomState,
|
||||||
|
HolderLookup.Provider registries, List<Identifier> pages, long seed, CarvingMask mask) {
|
||||||
|
if (pages.isEmpty()) return;
|
||||||
|
var registry = registries.lookupOrThrow(Registries.CARVER);
|
||||||
|
var biomes = generator.getBiomeSource().createUncachedResolver(randomState);
|
||||||
|
var context = new WorldGenerationContext(generator, chunk.getHeightAccessorForGeneration());
|
||||||
|
var random = new WorldgenRandom(new LegacyRandomSource(0L));
|
||||||
|
ChunkPos target = chunk.getPos();
|
||||||
|
// 1.21.1 repeats identical seeds for duplicate Caves/Ravines pages.
|
||||||
|
// Their union is identical; avoid replaying the same mask up to 32 times.
|
||||||
|
for (Identifier page : pages.stream().distinct().toList()) {
|
||||||
|
boolean ravine = page.equals(RAVINES);
|
||||||
|
WorldCarver overworld = registry.getOrThrow(ResourceKey.create(Registries.CARVER,
|
||||||
|
Identifier.withDefaultNamespace(ravine ? "canyon" : "cave"))).value();
|
||||||
|
WorldCarver nether = ravine ? null : registry.getOrThrow(ResourceKey.create(Registries.CARVER,
|
||||||
|
Identifier.withDefaultNamespace("nether_cave"))).value();
|
||||||
|
for (int dx = -8; dx <= 8; dx++) {
|
||||||
|
for (int dz = -8; dz <= 8; dz++) {
|
||||||
|
ChunkPos source = new ChunkPos(target.x() + dx, target.z() + dz);
|
||||||
|
boolean isNether = biomes.getNoiseBiome(QuartPos.fromBlock(source.getMinBlockX()), 0,
|
||||||
|
QuartPos.fromBlock(source.getMinBlockZ())).is(BiomeTags.IS_NETHER);
|
||||||
|
if (ravine && isNether) continue;
|
||||||
|
WorldCarver carver = isNether ? nether : overworld;
|
||||||
|
random.setLargeFeatureSeed(seed ^ (ravine ? RAVINE_SALT : CAVE_SALT), source.x(), source.z());
|
||||||
|
if (carver.isStartChunk(random)) carver.carve(context, random, target, source, mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/** 26.3 port of the 1.21.1 surface crystal mound with budding hearts and seeded buds. */
|
||||||
|
final class PortCrystalFormationsFeature {
|
||||||
|
private static final long SALT = 0x4352595354414C53L;
|
||||||
|
private static final int CHANCE = 15;
|
||||||
|
|
||||||
|
private PortCrystalFormationsFeature() {}
|
||||||
|
|
||||||
|
record Site(int x, int z, long seed) {}
|
||||||
|
|
||||||
|
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||||
|
long seed = mix(ageSeed ^ SALT ^ (long) chunk.x() * 341873128712L
|
||||||
|
^ (long) chunk.z() * 132897987541L ^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||||
|
RandomSource random = RandomSource.create(seed);
|
||||||
|
if (random.nextInt(CHANCE) != 0) return null;
|
||||||
|
return new Site(chunk.getMinBlockX() + 5 + random.nextInt(6),
|
||||||
|
chunk.getMinBlockZ() + 5 + random.nextInt(6), seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, int occurrence) {
|
||||||
|
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||||
|
if (site == null) return;
|
||||||
|
RandomSource random = RandomSource.create(site.seed() ^ 0x42554444494E474CL);
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, site.x(), site.z());
|
||||||
|
if (y <= level.getMinY() + 1 || y + 4 >= level.getMaxY()) return;
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
int x = site.x() + dx;
|
||||||
|
int z = site.z() + dz;
|
||||||
|
if (Math.abs(level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - y) > 1) return;
|
||||||
|
BlockState floor = level.getBlockState(pos.set(x, y - 1, z));
|
||||||
|
if (!floor.isFaceSturdy(level, pos, Direction.UP) || !floor.getFluidState().isEmpty()
|
||||||
|
|| floor.hasBlockEntity()) return;
|
||||||
|
for (int h = 0; h < 4; h++) {
|
||||||
|
BlockState existing = level.getBlockState(pos.set(x, y + h, z));
|
||||||
|
if (!existing.isAir() && !existing.canBeReplaced()) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
for (int dz = -1; dz <= 1; dz++) {
|
||||||
|
boolean center = dx == 0 && dz == 0;
|
||||||
|
int height = center ? 3 : random.nextInt(3) == 0 ? 2 : 1;
|
||||||
|
for (int h = 0; h < height; h++) {
|
||||||
|
boolean budding = h == height - 1 && (center || random.nextBoolean());
|
||||||
|
level.setBlock(pos.set(site.x() + dx, y + h, site.z() + dz),
|
||||||
|
budding ? Rivencraft.BUDDING_CRYSTAL.get().defaultBlockState()
|
||||||
|
: Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 2);
|
||||||
|
}
|
||||||
|
if (level.getBlockState(pos.set(site.x() + dx, y + height - 1, site.z() + dz))
|
||||||
|
.is(Rivencraft.BUDDING_CRYSTAL) && random.nextBoolean()) {
|
||||||
|
level.setBlock(pos.set(site.x() + dx, y + height, site.z() + dz),
|
||||||
|
Rivencraft.SMALL_CRYSTAL_BUD.get().defaultBlockState()
|
||||||
|
.setValue(AmethystClusterBlock.FACING, Direction.UP), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.AbstractOreFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.FeaturePlacer;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||||
|
import net.neoforged.neoforge.common.Tags;
|
||||||
|
|
||||||
|
/** Replays each eligible biome ore placement once per page, matching Rivencraft 1.21.1. */
|
||||||
|
public final class PortDenseOres {
|
||||||
|
public static final TagKey<PlacedFeature> EXTRA_ORES = TagKey.create(Registries.PLACED_FEATURE,
|
||||||
|
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dense_ores"));
|
||||||
|
|
||||||
|
private PortDenseOres() {}
|
||||||
|
|
||||||
|
public static boolean hasOres(Holder<Biome> biome) {
|
||||||
|
return oreFeatures(biome).anyMatch(PortDenseOres::isOre);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modern ore features carry their targets directly rather than an OreConfiguration. */
|
||||||
|
public static boolean isOre(Holder<PlacedFeature> holder) {
|
||||||
|
if (holder.is(EXTRA_ORES)) return true;
|
||||||
|
if (!(holder.value().feature().value() instanceof AbstractOreFeature ore)) return false;
|
||||||
|
return ore.targetStates().stream().anyMatch(target -> isOreBlock(target.state()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isOreBlock(BlockState state) {
|
||||||
|
if (state.is(Tags.BlockItems.ORES.block())) return true;
|
||||||
|
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||||
|
return path.endsWith("_ore") || path.contains("_ore_");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static java.util.stream.Stream<Holder<PlacedFeature>> oreFeatures(Holder<Biome> biome) {
|
||||||
|
return biome.value().getGenerationSettings().features().stream().flatMap(HolderSet::stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void place(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator generator,
|
||||||
|
long ageSeed, int count) {
|
||||||
|
if (count == 0) return;
|
||||||
|
Set<Holder<Biome>> biomes = new HashSet<>();
|
||||||
|
ChunkPos.rangeClosed(chunk.getPos(), 1).forEach(pos -> {
|
||||||
|
for (var section : level.getChunk(pos.x(), pos.z()).getSections())
|
||||||
|
section.getBiomes().getAll(biomes::add);
|
||||||
|
});
|
||||||
|
biomes.retainAll(generator.getBiomeSource().possibleBiomes());
|
||||||
|
// Collect once per chunk, not once per repeated page. Stable registry-ID
|
||||||
|
// order and per-occurrence seeds retain the 1.21.1 placement sequence.
|
||||||
|
Map<Identifier, PlacedFeature> ores = new TreeMap<>();
|
||||||
|
for (Holder<Biome> biome : biomes) {
|
||||||
|
oreFeatures(biome).filter(PortDenseOres::isOre).forEach(feature ->
|
||||||
|
feature.unwrapKey().ifPresent(key -> ores.putIfAbsent(key.identifier(), feature.value())));
|
||||||
|
}
|
||||||
|
BlockPos origin = new BlockPos(chunk.getPos().getMinBlockX(), level.getMinY(),
|
||||||
|
chunk.getPos().getMinBlockZ());
|
||||||
|
FeaturePlacer placer = new FeaturePlacer(level, generator);
|
||||||
|
for (int occurrence = 0; occurrence < count; occurrence++) {
|
||||||
|
for (var entry : ores.entrySet()) {
|
||||||
|
long seed = mix(ageSeed ^ (long) origin.getX() * 341873128712L
|
||||||
|
^ (long) origin.getZ() * 132897987541L
|
||||||
|
^ (long) entry.getKey().hashCode() * 0x9E3779B97F4A7C15L
|
||||||
|
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
placer.placeWithBiomeCheck(entry.getValue(), RandomSource.create(seed), origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.core.registries.BuiltInRegistries;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
|
import net.minecraft.tags.BiomeTags;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.NoiseColumn;
|
||||||
|
import net.minecraft.world.level.StructureManager;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||||
|
import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.BlockBlobFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.LakeFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.BiomeFilter;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||||
|
import net.minecraft.world.level.levelgen.placement.RarityFilter;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
|
||||||
|
/** Delegates terrain generation and applies frozen Age-scoped features. */
|
||||||
|
public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
||||||
|
private static final ResourceKey<PlacedFeature> FOREST_ROCK = ResourceKey.create(
|
||||||
|
Registries.PLACED_FEATURE, Identifier.withDefaultNamespace("forest_rock"));
|
||||||
|
private static final ResourceKey<PlacedFeature> DUNGEONS = ResourceKey.create(
|
||||||
|
Registries.PLACED_FEATURE, Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dungeons"));
|
||||||
|
private static final TagKey<Block> ROCK_GROUND = TagKey.create(
|
||||||
|
Registries.BLOCK, Identifier.withDefaultNamespace("forest_rock_can_place_on"));
|
||||||
|
public static final MapCodec<PortFeatureChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||||
|
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
|
||||||
|
PortRockPlan.CODEC.optionalFieldOf("rocks").forGetter(generator -> Optional.ofNullable(generator.rocks)),
|
||||||
|
PortVegetationPlan.CODEC.optionalFieldOf("vegetation")
|
||||||
|
.forGetter(generator -> Optional.ofNullable(generator.vegetation)),
|
||||||
|
com.mojang.serialization.Codec.intRange(0, 32).optionalFieldOf("dungeons", 0)
|
||||||
|
.forGetter(generator -> generator.dungeons),
|
||||||
|
PortLakePlan.CODEC.optionalFieldOf("lakes")
|
||||||
|
.forGetter(generator -> Optional.ofNullable(generator.lakes)),
|
||||||
|
PortTerrainFeaturePlan.CODEC.optionalFieldOf("terrain_features")
|
||||||
|
.forGetter(generator -> Optional.ofNullable(generator.terrainFeatures)),
|
||||||
|
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
|
||||||
|
).apply(instance, (base, rocks, vegetation, dungeons, lakes, terrainFeatures, seed) ->
|
||||||
|
new PortFeatureChunkGenerator(base, rocks.orElse(null), vegetation.orElse(null),
|
||||||
|
dungeons, lakes.orElse(null), terrainFeatures.orElse(null), seed)));
|
||||||
|
|
||||||
|
private final ChunkGenerator base;
|
||||||
|
private final PortRockPlan rocks;
|
||||||
|
private final PortVegetationPlan vegetation;
|
||||||
|
private final int dungeons;
|
||||||
|
private final PortLakePlan lakes;
|
||||||
|
private final PortTerrainFeaturePlan terrainFeatures;
|
||||||
|
private final long ageSeed;
|
||||||
|
|
||||||
|
public PortFeatureChunkGenerator(ChunkGenerator base, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||||
|
PortTerrainFeaturePlan terrainFeatures, long ageSeed) {
|
||||||
|
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
|
||||||
|
this.base = base;
|
||||||
|
this.rocks = rocks;
|
||||||
|
this.vegetation = vegetation;
|
||||||
|
this.dungeons = dungeons;
|
||||||
|
this.lakes = lakes;
|
||||||
|
this.terrainFeatures = terrainFeatures;
|
||||||
|
this.ageSeed = ageSeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChunkGenerator base() { return base; }
|
||||||
|
public PortRockPlan rocks() { return rocks; }
|
||||||
|
public PortVegetationPlan vegetation() { return vegetation; }
|
||||||
|
public int dungeons() { return dungeons; }
|
||||||
|
public PortLakePlan lakes() { return lakes; }
|
||||||
|
public PortTerrainFeaturePlan terrainFeatures() { return terrainFeatures; }
|
||||||
|
|
||||||
|
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
|
||||||
|
@Override public void validate() { base.validate(); }
|
||||||
|
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
|
||||||
|
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
|
||||||
|
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
|
||||||
|
StructureManager structures, ChunkAccess chunk) {
|
||||||
|
return base.createBiomes(random, blender, structures, chunk);
|
||||||
|
}
|
||||||
|
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
||||||
|
RandomState random, long seed) {
|
||||||
|
if (hasStructurePages()) {
|
||||||
|
var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource());
|
||||||
|
var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures());
|
||||||
|
if (base instanceof PortFlatChunkGenerator || base instanceof net.minecraft.world.level.levelgen.FlatLevelSource)
|
||||||
|
return ChunkGeneratorStructureState.createForFlat(random, seed, getOrigin(random), planning,
|
||||||
|
lookup.listElements().map(holder -> (Holder<StructureSet>) holder));
|
||||||
|
return ChunkGeneratorStructureState.createForNormal(random, seed, getOrigin(random), planning, lookup);
|
||||||
|
}
|
||||||
|
return base.createState(sets, random, seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasStructurePages() {
|
||||||
|
return terrainFeatures != null && !terrainFeatures.structures().isEmpty();
|
||||||
|
}
|
||||||
|
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||||
|
StructureManager structures, ChunkAccess chunk,
|
||||||
|
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||||
|
if (hasStructurePages()) super.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||||
|
else base.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||||
|
}
|
||||||
|
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
||||||
|
base.createReferences(level, structures, chunk);
|
||||||
|
}
|
||||||
|
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
|
||||||
|
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
|
||||||
|
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
|
||||||
|
}
|
||||||
|
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
|
||||||
|
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
|
||||||
|
@Override public int getGenDepth() { return base.getGenDepth(); }
|
||||||
|
@Override public int getSeaLevel() { return base.getSeaLevel(); }
|
||||||
|
@Override public int getMinY() { return base.getMinY(); }
|
||||||
|
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
|
||||||
|
RandomState random, StructureManager structures, BiomeManager biomes,
|
||||||
|
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
||||||
|
CompletableFuture<ChunkAccess> built = base.buildTerrain(
|
||||||
|
chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
|
||||||
|
if (terrainFeatures == null || !terrainFeatures.skylands() && terrainFeatures.choices().isEmpty()) return built;
|
||||||
|
return built.thenApply(result -> {
|
||||||
|
// Match 1.21.1: filter the base terrain once, then add the authored
|
||||||
|
// solid volumes, irrespective of the Skylands page's written position.
|
||||||
|
if (terrainFeatures.skylands()) PortSkylandsFeature.filter(result, ageSeed);
|
||||||
|
java.util.Map<Identifier, Integer> occurrences = new java.util.HashMap<>();
|
||||||
|
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||||
|
if (PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||||
|
int occurrence = occurrences.merge(choice.page(), 1, Integer::sum) - 1;
|
||||||
|
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
|
||||||
|
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||||
|
if (choice.page().equals(PortTerrainFeaturePlan.FLOATING_ISLANDS))
|
||||||
|
PortFloatingIslandFeature.generate(result, featureSeed, material);
|
||||||
|
else if (choice.page().equals(PortTerrainFeaturePlan.TENDRILS))
|
||||||
|
PortTendrilFeature.generate(result, featureSeed, material);
|
||||||
|
else if (choice.page().equals(PortTerrainFeaturePlan.SPHERES))
|
||||||
|
PortSpheresFeature.generate(result, featureSeed, material);
|
||||||
|
else throw new IllegalStateException("Unsupported terrain feature: " + choice.page());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||||
|
LevelHeightAccessor height, RandomState random) {
|
||||||
|
return base.getBaseHeight(x, z, type, height, random);
|
||||||
|
}
|
||||||
|
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
|
||||||
|
return base.getBaseColumn(x, z, height, random);
|
||||||
|
}
|
||||||
|
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
|
||||||
|
BlockPos pos, SamplerContext context) {
|
||||||
|
base.addDebugScreenInfo(info, random, pos, context);
|
||||||
|
}
|
||||||
|
@Override public void refreshFeaturesPerStep() {
|
||||||
|
base.refreshFeaturesPerStep();
|
||||||
|
super.refreshFeaturesPerStep();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
|
||||||
|
StructureManager structures) {
|
||||||
|
base.applyBiomeDecoration(level, chunk, structures);
|
||||||
|
if (terrainFeatures != null) {
|
||||||
|
PortDenseOres.place(level, chunk, this, ageSeed, terrainFeatures.denseOres());
|
||||||
|
int spikeOccurrence = 0;
|
||||||
|
int obeliskOccurrence = 0;
|
||||||
|
int crystalOccurrence = 0;
|
||||||
|
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||||
|
if (!PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||||
|
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||||
|
if (choice.page().equals(PortTerrainFeaturePlan.SPIKES))
|
||||||
|
PortSpikesFeature.generate(level, chunk, ageSeed, material, spikeOccurrence++);
|
||||||
|
else if (choice.page().equals(PortTerrainFeaturePlan.OBELISKS))
|
||||||
|
PortObelisksFeature.generate(level, chunk, ageSeed, material, obeliskOccurrence++);
|
||||||
|
else if (choice.page().equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS))
|
||||||
|
PortCrystalFormationsFeature.generate(level, chunk, ageSeed, crystalOccurrence++);
|
||||||
|
else throw new IllegalStateException("Unsupported surface feature: " + choice.page());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChunkPos chunkPos = chunk.getPos();
|
||||||
|
long chunkSeed = ageSeed ^ (long) chunkPos.x() * 341873128712L
|
||||||
|
^ (long) chunkPos.z() * 132897987541L;
|
||||||
|
BlockPos origin = new BlockPos(chunkPos.getMinBlockX(), level.getMinY(), chunkPos.getMinBlockZ());
|
||||||
|
if (rocks != null) {
|
||||||
|
PlacedFeature vanilla = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||||
|
.getOrThrow(FOREST_ROCK).value();
|
||||||
|
var placement = vanilla.placement().stream()
|
||||||
|
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList();
|
||||||
|
for (int occurrence = 0; occurrence < rocks.materials().size(); occurrence++) {
|
||||||
|
RandomSource random = RandomSource.create(chunkSeed
|
||||||
|
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
BlockState material = BuiltInRegistries.BLOCK.getValue(
|
||||||
|
rocks.materials().get(occurrence)).defaultBlockState();
|
||||||
|
PlacedFeature authored = new PlacedFeature(Holder.direct(new BlockBlobFeature(
|
||||||
|
material, BlockPredicate.matchesTag(ROCK_GROUND))), placement);
|
||||||
|
authored.place(level, this, random, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dungeons > 0) {
|
||||||
|
PlacedFeature authored = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||||
|
.getOrThrow(DUNGEONS).value();
|
||||||
|
for (int occurrence = 0; occurrence < dungeons; occurrence++) {
|
||||||
|
RandomSource random = RandomSource.create(chunkSeed
|
||||||
|
^ 0x44554E47454F4E53L ^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
authored.place(level, this, random, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lakes != null) {
|
||||||
|
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||||
|
java.util.Map<PortLakePlan.Choice, Integer> occurrences = new java.util.HashMap<>();
|
||||||
|
for (PortLakePlan.Choice choice : lakes.choices()) {
|
||||||
|
PlacedFeature vanilla = registry.getOrThrow(ResourceKey.create(
|
||||||
|
Registries.PLACED_FEATURE, PortLakePlan.placedFeature(choice.page()))).value();
|
||||||
|
if (!(vanilla.feature().value() instanceof LakeFeature lake))
|
||||||
|
throw new IllegalStateException("Lake placement no longer references a LakeFeature: " + choice.page());
|
||||||
|
BlockState fluid = BuiltInRegistries.BLOCK.getValue(choice.fluid()).defaultBlockState();
|
||||||
|
LakeFeature feature = new LakeFeature(BlockStateProvider.holderOf(fluid), lake.barrier(),
|
||||||
|
lake.canPlaceFeature(), lake.canReplaceWithAirOrFluid(), lake.canReplaceWithBarrier());
|
||||||
|
PlacedFeature authored = new PlacedFeature(Holder.direct(feature), vanilla.placement().stream()
|
||||||
|
.filter(modifier -> !(modifier instanceof BiomeFilter))
|
||||||
|
.map(modifier -> !PortLakePlan.isDeep(choice.page()) && modifier instanceof RarityFilter
|
||||||
|
? RarityFilter.onAverageOnceEvery(12) : modifier)
|
||||||
|
.toList());
|
||||||
|
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
|
||||||
|
RandomSource random = RandomSource.create(chunkSeed
|
||||||
|
^ (long) choice.hashCode() * 0x9E3779B97F4A7C15L
|
||||||
|
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
authored.place(level, this, random, origin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vegetation != null) {
|
||||||
|
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
|
||||||
|
Holder<Biome> biome = level.getBiome(new BlockPos(
|
||||||
|
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
|
||||||
|
boolean surfaceBiome = !biome.is(BiomeTags.IS_END) && !PortRegistryPages.isCaveBiome(biome);
|
||||||
|
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||||
|
List<BlockPos> mushroomHosts = null;
|
||||||
|
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
|
||||||
|
Identifier page = vegetation.pages().get(occurrence);
|
||||||
|
RandomSource random = RandomSource.create(chunkSeed
|
||||||
|
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
|
||||||
|
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||||
|
if (page.equals(PortVegetationPlan.SHELF_MUSHROOMS)) {
|
||||||
|
if (mushroomHosts == null) mushroomHosts = PortShelfMushrooms.findHosts(chunk);
|
||||||
|
PortShelfMushrooms.decorate(level, chunk, mushroomHosts, random);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!surfaceBiome) continue;
|
||||||
|
Identifier placedId = PortVegetationPlan.placedFeature(page);
|
||||||
|
PlacedFeature vanilla = registry.getOrThrow(
|
||||||
|
ResourceKey.create(Registries.PLACED_FEATURE, placedId)).value();
|
||||||
|
PlacedFeature authored = new PlacedFeature(vanilla.feature(), vanilla.placement().stream()
|
||||||
|
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList());
|
||||||
|
authored.place(level, this, random, origin);
|
||||||
|
if (PortVegetationPlan.addsLogs(page)) mushroomHosts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.QuartPos;
|
||||||
|
import net.minecraft.tags.BiomeTags;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||||
|
|
||||||
|
/** 1.21.1 floating-island shape adapted to the 26.3 chunk terrain stage. */
|
||||||
|
final class PortFloatingIslandFeature {
|
||||||
|
private static final long SALT = 0x464C4F4154494E47L;
|
||||||
|
private static final int SOURCE_RANGE = 3;
|
||||||
|
private static final int CHANCE = 128;
|
||||||
|
|
||||||
|
private PortFloatingIslandFeature() {}
|
||||||
|
|
||||||
|
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||||
|
var target = chunk.getPos();
|
||||||
|
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
|
||||||
|
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
|
||||||
|
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||||
|
^ (long) sourceX * 341873128712L ^ (long) sourceZ * 132897987541L));
|
||||||
|
if (random.nextInt(CHANCE) != 0) continue;
|
||||||
|
int centerX = sourceX * 16 + random.nextInt(16);
|
||||||
|
int centerZ = sourceZ * 16 + random.nextInt(16);
|
||||||
|
int centerY = Math.min(chunk.getMaxY() - 30, 168 + random.nextInt(48));
|
||||||
|
if (centerY < chunk.getMinY() + 24) continue;
|
||||||
|
int radiusX = 24 + random.nextInt(18);
|
||||||
|
int radiusZ = 24 + random.nextInt(18);
|
||||||
|
fill(chunk, ageSeed, material, centerX, centerY, centerZ, radiusX, radiusZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fill(ChunkAccess chunk, long ageSeed, BlockState material,
|
||||||
|
int centerX, int centerY, int centerZ, int radiusX, int radiusZ) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
|
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int localX = 0; localX < 16; localX++) {
|
||||||
|
int worldX = originX + localX;
|
||||||
|
double dx = (worldX - centerX) / (double) radiusX;
|
||||||
|
for (int localZ = 0; localZ < 16; localZ++) {
|
||||||
|
int worldZ = originZ + localZ;
|
||||||
|
double dz = (worldZ - centerZ) / (double) radiusZ;
|
||||||
|
double distance = dx * dx + dz * dz;
|
||||||
|
if (distance >= 1.0) continue;
|
||||||
|
long detail = mix(ageSeed ^ (long) centerX * 341873128712L
|
||||||
|
^ (long) centerZ * 132897987541L ^ (long) worldX * 42317861L
|
||||||
|
^ (long) worldZ * 374761393L);
|
||||||
|
int top = centerY + (int) Math.round(3 * (1 - distance)) + Math.floorMod(detail, 3) - 1;
|
||||||
|
int depth = Math.max(1, (int) Math.round(3 + 17 * Math.pow(1 - distance, 1.5)));
|
||||||
|
// Biomes are already populated by createBiomes in 26.3, so
|
||||||
|
// sampling this chunk preserves its authored biome distribution.
|
||||||
|
var biome = chunk.getNoiseBiome(QuartPos.fromBlock(worldX), QuartPos.fromBlock(top),
|
||||||
|
QuartPos.fromBlock(worldZ));
|
||||||
|
BlockState soil = biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)
|
||||||
|
? material : Blocks.DIRT.defaultBlockState();
|
||||||
|
BlockState cap = soil == material ? material : Blocks.GRASS_BLOCK.defaultBlockState();
|
||||||
|
for (int y = Math.max(top - depth, chunk.getMinY()); y <= top; y++) {
|
||||||
|
pos.set(worldX, y, worldZ);
|
||||||
|
if (!chunk.getBlockState(pos).isAir()) continue;
|
||||||
|
BlockState state = y == top ? cap : y >= top - 3 ? soil : material;
|
||||||
|
chunk.setBlockState(pos, state);
|
||||||
|
surface.update(localX, y, localZ, state);
|
||||||
|
floor.update(localX, y, localZ, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** Ordered lake pages with fluid choices frozen when the Age is bound. */
|
||||||
|
public record PortLakePlan(List<Choice> choices) {
|
||||||
|
public static final Codec<PortLakePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortLakePlan::choices)
|
||||||
|
).apply(instance, PortLakePlan::new));
|
||||||
|
|
||||||
|
public record Choice(Identifier page, Identifier fluid) {
|
||||||
|
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
|
||||||
|
Identifier.CODEC.fieldOf("fluid").forGetter(Choice::fluid)
|
||||||
|
).apply(instance, Choice::new));
|
||||||
|
|
||||||
|
public Choice {
|
||||||
|
if (!isLake(page) || fluid == null) throw new IllegalArgumentException("Invalid lake choice");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortLakePlan {
|
||||||
|
choices = List.copyOf(choices);
|
||||||
|
if (choices.isEmpty() || choices.size() > 32)
|
||||||
|
throw new IllegalArgumentException("Lake plan must contain 1–32 choices");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLake(Identifier page) {
|
||||||
|
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||||
|
return switch (page.getPath()) {
|
||||||
|
case "feature/surface_lakes", "feature/deep_lakes", "feature/lava_lakes" -> true;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDeep(Identifier page) { return page.getPath().equals("feature/deep_lakes"); }
|
||||||
|
|
||||||
|
public static Identifier placedFeature(Identifier page) {
|
||||||
|
if (!isLake(page)) throw new IllegalArgumentException("Unsupported lake page " + page);
|
||||||
|
return Identifier.withDefaultNamespace(isDeep(page) ? "lake_lava_underground" : "lake_lava_surface");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/** 26.3 port of Rivencraft 1.21.1's Legacy-style 4×4 plinth and 2×12×2 shaft. */
|
||||||
|
final class PortObelisksFeature {
|
||||||
|
private static final long SALT = 0x4F42454C49534B53L;
|
||||||
|
private static final int CHANCE = 128;
|
||||||
|
private static final int BASE = 4;
|
||||||
|
private static final int HEIGHT = 12;
|
||||||
|
|
||||||
|
private PortObelisksFeature() {}
|
||||||
|
|
||||||
|
record Site(int x, int z) {}
|
||||||
|
|
||||||
|
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||||
|
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||||
|
^ (long) chunk.x() * 341873128712L ^ (long) chunk.z() * 132897987541L
|
||||||
|
^ (long) occurrence * 0x9E3779B97F4A7C15L));
|
||||||
|
if (random.nextInt(CHANCE) != 0) return null;
|
||||||
|
return new Site(chunk.getMinBlockX() + 4 + random.nextInt(8),
|
||||||
|
chunk.getMinBlockZ() + 4 + random.nextInt(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||||
|
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||||
|
if (site == null) return;
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
int minSurface = Integer.MAX_VALUE;
|
||||||
|
int maxSurface = Integer.MIN_VALUE;
|
||||||
|
int[][] surface = new int[BASE][BASE];
|
||||||
|
for (int dx = 0; dx < BASE; dx++) {
|
||||||
|
for (int dz = 0; dz < BASE; dz++) {
|
||||||
|
int x = site.x() + dx;
|
||||||
|
int z = site.z() + dz;
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||||
|
if (y <= level.getMinY() + 1) return;
|
||||||
|
BlockState ground = level.getBlockState(pos.set(x, y - 1, z));
|
||||||
|
if (!ground.isFaceSturdy(level, pos, Direction.UP)
|
||||||
|
|| !ground.getFluidState().isEmpty() || ground.hasBlockEntity()) return;
|
||||||
|
surface[dx][dz] = y;
|
||||||
|
minSurface = Math.min(minSurface, y);
|
||||||
|
maxSurface = Math.max(maxSurface, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxSurface - minSurface > 2 || maxSurface + HEIGHT >= level.getMaxY()) return;
|
||||||
|
for (int dx = 0; dx < BASE; dx++) {
|
||||||
|
for (int dz = 0; dz < BASE; dz++) {
|
||||||
|
for (int y = surface[dx][dz]; y <= maxSurface; y++) {
|
||||||
|
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int dx = 1; dx <= 2; dx++) {
|
||||||
|
for (int dz = 1; dz <= 2; dz++) {
|
||||||
|
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++) {
|
||||||
|
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int dx = 0; dx < BASE; dx++) {
|
||||||
|
for (int dz = 0; dz < BASE; dz++) {
|
||||||
|
for (int y = surface[dx][dz]; y <= maxSurface; y++)
|
||||||
|
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int dx = 1; dx <= 2; dx++) {
|
||||||
|
for (int dz = 1; dz <= 2; dz++) {
|
||||||
|
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++)
|
||||||
|
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean open(BlockState state) {
|
||||||
|
return state.getFluidState().isEmpty() && (state.isAir() || state.canBeReplaced());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
|
||||||
|
/** Seed-frozen terrain and biome choices for a book with no authored symbols. */
|
||||||
|
public final class PortRandomAgeSelection {
|
||||||
|
private PortRandomAgeSelection() {}
|
||||||
|
|
||||||
|
public record Selection(PortAgeTerrain terrain, PortBiomePlan biomes) {}
|
||||||
|
|
||||||
|
public static Selection select(MinecraftServer server, long seed) {
|
||||||
|
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
|
||||||
|
// the other terrain families are rarer. The sorted order is deliberate.
|
||||||
|
PortAgeTerrain terrain = weighted(new Random(seed ^ 0x5445525241494E4CL),
|
||||||
|
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
||||||
|
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
|
||||||
|
new int[]{1, 1, 5, 1, 6, 1});
|
||||||
|
if (terrain == PortAgeTerrain.VOID)
|
||||||
|
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
||||||
|
List.of(Identifier.withDefaultNamespace("the_void"))));
|
||||||
|
|
||||||
|
PortBiomePlan.Distribution distribution = weighted(new Random(seed ^ 0x524956454E435241L),
|
||||||
|
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
|
||||||
|
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
|
||||||
|
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
|
||||||
|
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
|
||||||
|
PortBiomePlan.Distribution.HUGE),
|
||||||
|
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
|
||||||
|
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
||||||
|
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
||||||
|
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||||
|
// 26.3 has no native multi-cave biome source; choose a seeded Cave
|
||||||
|
// biome rather than silently using Lush Caves for every random Age.
|
||||||
|
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
|
||||||
|
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||||
|
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||||
|
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
|
||||||
|
|
||||||
|
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||||
|
if (count > 1) {
|
||||||
|
Random countRandom = new Random(seed ^ 0x42494F4D45434F55L);
|
||||||
|
while (count < eligible.size() && countRandom.nextInt(3) != 0) count++;
|
||||||
|
}
|
||||||
|
List<Identifier> shuffled = new ArrayList<>(eligible);
|
||||||
|
Collections.shuffle(shuffled, new Random(seed ^ 0x42494F4D45504F4FL));
|
||||||
|
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||||
|
if (terrain == PortAgeTerrain.CAVE) {
|
||||||
|
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||||
|
.filter(PortRegistryPages::isCaveBiome)
|
||||||
|
.map(holder -> holder.key().identifier())
|
||||||
|
.sorted().toList();
|
||||||
|
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
||||||
|
}
|
||||||
|
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
||||||
|
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
||||||
|
.map(key -> key.identifier()).distinct().sorted(Comparator.naturalOrder()).toList();
|
||||||
|
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
||||||
|
int total = 0;
|
||||||
|
for (int weight : weights) total += weight;
|
||||||
|
int ticket = random.nextInt(total);
|
||||||
|
for (int index = 0; index < choices.size(); index++) {
|
||||||
|
ticket -= weights[index];
|
||||||
|
if (ticket < 0) return choices.get(index);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Random Age ticket was not consumed");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** Frozen, ordered Rocks materials. Old single-material beta Ages remain readable. */
|
||||||
|
public record PortRockPlan(List<Identifier> materials) {
|
||||||
|
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Identifier.CODEC.optionalFieldOf("material").forGetter(plan -> Optional.empty()),
|
||||||
|
Identifier.CODEC.listOf().optionalFieldOf("materials", List.of()).forGetter(PortRockPlan::materials)
|
||||||
|
).apply(instance, (old, current) -> new PortRockPlan(current.isEmpty()
|
||||||
|
? old.map(List::of).orElse(List.of()) : current)));
|
||||||
|
|
||||||
|
public PortRockPlan {
|
||||||
|
materials = List.copyOf(materials);
|
||||||
|
if (materials.isEmpty() || materials.size() > 32)
|
||||||
|
throw new IllegalArgumentException("Rocks plan must contain 1–32 materials");
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortRockPlan(Identifier material) { this(List.of(material)); }
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
import net.minecraft.tags.BlockTags;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.ShelfMushroomBlock;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.treedecorators.ShelfMushroomDecorator;
|
||||||
|
import net.minecraft.world.level.levelgen.feature.treedecorators.TreeDecorator;
|
||||||
|
|
||||||
|
/** Shelf mushrooms on exposed log faces, regardless of how the logs were generated. */
|
||||||
|
final class PortShelfMushrooms {
|
||||||
|
private PortShelfMushrooms() {}
|
||||||
|
|
||||||
|
static List<BlockPos> findHosts(ChunkAccess chunk) {
|
||||||
|
List<BlockPos> logs = new ArrayList<>();
|
||||||
|
var sections = chunk.getSections();
|
||||||
|
for (int index = 0; index < sections.length; index++) {
|
||||||
|
var section = sections[index];
|
||||||
|
if (!section.maybeHas(state -> state.is(BlockTags.LOGS))) continue;
|
||||||
|
int baseY = chunk.getSectionYFromSectionIndex(index) << 4;
|
||||||
|
for (int y = 0; y < 16; y++) for (int z = 0; z < 16; z++) for (int x = 0; x < 16; x++) {
|
||||||
|
if (section.getBlockState(x, y, z).is(BlockTags.LOGS))
|
||||||
|
logs.add(new BlockPos(chunk.getPos().getMinBlockX() + x, baseY + y,
|
||||||
|
chunk.getPos().getMinBlockZ() + z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void decorate(WorldGenLevel level, ChunkAccess chunk, List<BlockPos> logs, RandomSource random) {
|
||||||
|
// Reuse vanilla replaceability/water checks, but not its tree-base height
|
||||||
|
// band: a log tendril or sphere has no meaningful tree base.
|
||||||
|
TreeDecorator.Context context = new TreeDecorator.Context(level,
|
||||||
|
(pos, state) -> level.setBlock(pos, state, 2), random, Set.of(), Set.of(), Set.of());
|
||||||
|
for (BlockPos log : logs) {
|
||||||
|
if (!level.getBlockState(log).is(BlockTags.LOGS)) continue;
|
||||||
|
Direction first = Direction.Plane.HORIZONTAL.getRandomDirection(random);
|
||||||
|
for (Direction facing : List.of(first, first.getClockWise())) {
|
||||||
|
if (random.nextFloat() > 0.25F) continue;
|
||||||
|
BlockPos pos = log.relative(facing);
|
||||||
|
if ((pos.getX() >> 4) != chunk.getPos().x() || (pos.getZ() >> 4) != chunk.getPos().z()) continue;
|
||||||
|
if (pos.getY() < level.getMinY() || pos.getY() >= level.getMaxY()) continue;
|
||||||
|
if (!level.getFluidState(pos).isEmpty()
|
||||||
|
|| !ShelfMushroomDecorator.isBlockReplaceableWithShelfMushroom(context, pos)
|
||||||
|
|| level.getBlockState(pos.below()).is(Blocks.SHELF_MUSHROOM)) continue;
|
||||||
|
boolean adjacent = false;
|
||||||
|
for (Direction side : Direction.Plane.HORIZONTAL) {
|
||||||
|
if (level.getBlockState(pos.relative(side)).is(Blocks.SHELF_MUSHROOM)) {
|
||||||
|
adjacent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (adjacent) continue;
|
||||||
|
var mushroom = Blocks.SHELF_MUSHROOM.defaultBlockState()
|
||||||
|
.setValue(ShelfMushroomBlock.FACING, facing)
|
||||||
|
.setValue(ShelfMushroomBlock.AGE, random.nextInt(2));
|
||||||
|
if (!mushroom.canSurvive(level, pos)) continue;
|
||||||
|
context.setBlock(pos, mushroom);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 26.3 port of Rivencraft's subtractive Skylands filter, adapted from
|
||||||
|
* Mystcraft Legacy's SymbolSkylands (LGPL-3.0-or-later). Keeps upper solid
|
||||||
|
* terrain above a seven-octave cutoff near Y=76 and removes liquids.
|
||||||
|
*/
|
||||||
|
final class PortSkylandsFeature {
|
||||||
|
private static final long SALT = 0x534B594C414E4453L;
|
||||||
|
private static final int BASE_CUTOFF = 76;
|
||||||
|
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||||
|
|
||||||
|
private PortSkylandsFeature() {}
|
||||||
|
|
||||||
|
static void filter(ChunkAccess chunk, long ageSeed) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
int minY = chunk.getMinY();
|
||||||
|
int maxY = chunk.getMaxY();
|
||||||
|
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
|
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int localX = 0; localX < 16; localX++) {
|
||||||
|
for (int localZ = 0; localZ < 16; localZ++) {
|
||||||
|
int cutoff = cutoff(ageSeed, originX + localX, originZ + localZ);
|
||||||
|
for (int y = minY; y < maxY; y++) {
|
||||||
|
pos.set(originX + localX, y, originZ + localZ);
|
||||||
|
BlockState state = chunk.getBlockState(pos);
|
||||||
|
if (!removes(state, y, cutoff)) continue;
|
||||||
|
chunk.setBlockState(pos, AIR);
|
||||||
|
surface.update(localX, y, localZ, AIR);
|
||||||
|
floor.update(localX, y, localZ, AIR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean removes(BlockState state, int y, int cutoff) {
|
||||||
|
return !state.isAir() && (y <= cutoff || state.liquid());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cutoff(long ageSeed, int x, int z) {
|
||||||
|
double variation = 0.0D;
|
||||||
|
int cellSize = 128;
|
||||||
|
double amplitude = 12.0D;
|
||||||
|
for (int octave = 0; octave < 7; octave++) {
|
||||||
|
variation += valueNoise(ageSeed ^ SALT ^ (long) octave * 0x9E3779B97F4A7C15L,
|
||||||
|
x, z, cellSize) * amplitude;
|
||||||
|
cellSize /= 2;
|
||||||
|
amplitude /= 2.0D;
|
||||||
|
}
|
||||||
|
return BASE_CUTOFF + (int) Math.round(variation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double valueNoise(long seed, int x, int z, int cellSize) {
|
||||||
|
int cellX = Math.floorDiv(x, cellSize);
|
||||||
|
int cellZ = Math.floorDiv(z, cellSize);
|
||||||
|
double u = smooth((x - cellX * cellSize) / (double) cellSize);
|
||||||
|
double v = smooth((z - cellZ * cellSize) / (double) cellSize);
|
||||||
|
double north = lerp(value(seed, cellX, cellZ), value(seed, cellX + 1, cellZ), u);
|
||||||
|
double south = lerp(value(seed, cellX, cellZ + 1), value(seed, cellX + 1, cellZ + 1), u);
|
||||||
|
return lerp(north, south, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double value(long seed, int x, int z) {
|
||||||
|
long hash = mix(seed ^ (long) x * 0xD1B54A32D192ED03L ^ (long) z * 0xABC98388FB8FAC03L);
|
||||||
|
return (hash >>> 11) * 0x1.0p-52 - 1.0D;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double smooth(double value) { return value * value * (3.0D - 2.0D * value); }
|
||||||
|
|
||||||
|
private static double lerp(double first, double second, double amount) {
|
||||||
|
return first + (second - first) * amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long mix(long value) {
|
||||||
|
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||||
|
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||||
|
return value ^ value >>> 31;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 26.3 adaptation of Rivencraft 1.21.1's solid sphere nodes, derived from
|
||||||
|
* Mystcraft Legacy's MapGenSpheresMyst (Mystcraft contributors,
|
||||||
|
* LGPL-3.0-or-later, commit 9bc8ddc). Writes stay within the target chunk.
|
||||||
|
*/
|
||||||
|
final class PortSpheresFeature {
|
||||||
|
private static final int SOURCE_RANGE = 8;
|
||||||
|
private static final long SALT = 0x53504845524553L;
|
||||||
|
|
||||||
|
private PortSpheresFeature() {}
|
||||||
|
|
||||||
|
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||||
|
var target = chunk.getPos();
|
||||||
|
Random master = new Random(ageSeed ^ SALT);
|
||||||
|
long xSeed = master.nextLong();
|
||||||
|
long zSeed = master.nextLong();
|
||||||
|
for (int sx = target.x() - SOURCE_RANGE; sx <= target.x() + SOURCE_RANGE; sx++) {
|
||||||
|
for (int sz = target.z() - SOURCE_RANGE; sz <= target.z() + SOURCE_RANGE; sz++) {
|
||||||
|
Random source = new Random((long) sx * xSeed ^ (long) sz * zSeed ^ ageSeed ^ SALT);
|
||||||
|
if (source.nextFloat() > 0.05F) continue;
|
||||||
|
double x = sx * 16 + source.nextInt(16);
|
||||||
|
double y = source.nextInt(source.nextInt(192) + 1) + 32;
|
||||||
|
double z = sz * 16 + source.nextInt(16);
|
||||||
|
trace(chunk, material, source.nextLong(), x, y, z,
|
||||||
|
1.0F + source.nextFloat() * 4.0F, 0, 0, -1, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||||
|
double x, double y, double z, float radius, float yaw, float pitch,
|
||||||
|
int step, int length) {
|
||||||
|
Random random = new Random(seed);
|
||||||
|
if (length <= 0) {
|
||||||
|
int maximum = SOURCE_RANGE * 16 - 16;
|
||||||
|
length = maximum - random.nextInt(maximum / 4);
|
||||||
|
}
|
||||||
|
boolean singleNode = step == -1;
|
||||||
|
if (singleNode) step = length / 2;
|
||||||
|
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||||
|
boolean slowPitch = random.nextInt(6) == 0;
|
||||||
|
float yawDrift = 0;
|
||||||
|
float pitchDrift = 0;
|
||||||
|
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||||
|
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||||
|
for (; step < length; step++) {
|
||||||
|
double nodeRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||||
|
float horizontal = Mth.cos(pitch);
|
||||||
|
x += Mth.cos(yaw) * horizontal;
|
||||||
|
y += Mth.sin(pitch);
|
||||||
|
z += Mth.sin(yaw) * horizontal;
|
||||||
|
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||||
|
pitch += pitchDrift * 0.1F;
|
||||||
|
yaw += yawDrift * 0.1F;
|
||||||
|
pitchDrift = pitchDrift * 0.9F
|
||||||
|
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||||
|
yawDrift = yawDrift * 0.75F
|
||||||
|
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||||
|
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||||
|
trace(chunk, material, random.nextLong(), x, y, z,
|
||||||
|
random.nextFloat() * 0.5F + 0.5F, yaw - Mth.HALF_PI, pitch / 3, step, length);
|
||||||
|
trace(chunk, material, random.nextLong(), x, y, z,
|
||||||
|
random.nextFloat() * 0.5F + 0.5F, yaw + Mth.HALF_PI, pitch / 3, step, length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||||
|
double dx = x - middleX;
|
||||||
|
double dz = z - middleZ;
|
||||||
|
double remaining = length - step;
|
||||||
|
double reach = radius + 18;
|
||||||
|
if (dx * dx + dz * dz - remaining * remaining > reach * reach) return;
|
||||||
|
if (Math.abs(dx) <= 16 + nodeRadius * 2 && Math.abs(dz) <= 16 + nodeRadius * 2)
|
||||||
|
fillNode(chunk, material, x, y, z, nodeRadius);
|
||||||
|
if (singleNode) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fillNode(ChunkAccess chunk, BlockState material,
|
||||||
|
double centerX, double centerY, double centerZ, double radius) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
int minX = Math.max(0, Mth.floor(centerX - radius) - originX - 1);
|
||||||
|
int maxX = Math.min(16, Mth.floor(centerX + radius) - originX + 2);
|
||||||
|
int minZ = Math.max(0, Mth.floor(centerZ - radius) - originZ - 1);
|
||||||
|
int maxZ = Math.min(16, Mth.floor(centerZ + radius) - originZ + 2);
|
||||||
|
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - radius) - 1);
|
||||||
|
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + radius) + 2);
|
||||||
|
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
|
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||||
|
double dy = (worldY + 0.5D - centerY) / radius;
|
||||||
|
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||||
|
double dz = (originZ + localZ + 0.5D - centerZ) / radius;
|
||||||
|
for (int localX = minX; localX < maxX; localX++) {
|
||||||
|
double dx = (originX + localX + 0.5D - centerX) / radius;
|
||||||
|
if (!insideSphere(dx, dy, dz)) continue;
|
||||||
|
pos.set(originX + localX, worldY, originZ + localZ);
|
||||||
|
BlockState previous = chunk.getBlockState(pos);
|
||||||
|
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||||
|
chunk.setBlockState(pos, material);
|
||||||
|
surface.update(localX, worldY, localZ, material);
|
||||||
|
floor.update(localX, worldY, localZ, material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean insideSphere(double dx, double dy, double dz) {
|
||||||
|
return dx * dx + dy * dy + dz * dz < 1.0D;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
import net.minecraft.world.level.WorldGenLevel;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 26.3 port of Rivencraft 1.21.1's rare surface spikes, adapted from
|
||||||
|
* Mystcraft Legacy's WorldGenSpikesAdv (Mystcraft contributors,
|
||||||
|
* LGPL-3.0-or-later, commit 9bc8ddc). The footprint stays within one chunk.
|
||||||
|
*/
|
||||||
|
final class PortSpikesFeature {
|
||||||
|
private static final long SALT = 0x5350494B4553L;
|
||||||
|
|
||||||
|
private PortSpikesFeature() {}
|
||||||
|
|
||||||
|
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
RandomSource random = RandomSource.create(ageSeed ^ SALT
|
||||||
|
^ (long) originX * 341873128712L ^ (long) originZ * 132897987541L
|
||||||
|
^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||||
|
if (random.nextInt(18) != 0) return;
|
||||||
|
int x = originX + 4 + random.nextInt(8);
|
||||||
|
int z = originZ + 4 + random.nextInt(8);
|
||||||
|
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||||
|
int height = 6 + random.nextInt(32);
|
||||||
|
int width = 1 + random.nextInt(4);
|
||||||
|
if (y < level.getMinY() + 1 || y + height >= level.getMaxY()) return;
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
if (!level.isEmptyBlock(pos.set(x, y, z))) return;
|
||||||
|
for (int px = x - width; px <= x + width; px++) {
|
||||||
|
for (int pz = z - width; pz <= z + width; pz++) {
|
||||||
|
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||||
|
if (!level.getBlockState(pos.set(px, y - 1, pz)).isSolid()) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int px = x - width; px <= x + width; px++) {
|
||||||
|
for (int pz = z - width; pz <= z + width; pz++) {
|
||||||
|
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||||
|
int top = y + 1 + random.nextInt(random.nextInt(height) + 1);
|
||||||
|
for (int py = y; py < top; py++) {
|
||||||
|
pos.set(px, py, pz);
|
||||||
|
if (level.isEmptyBlock(pos)) level.setBlock(pos, material, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static boolean insideFootprint(int dx, int dz, int width) {
|
||||||
|
return dx * dx + dz * dz <= width * width + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
import net.minecraft.world.level.saveddata.SavedData;
|
||||||
|
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ports Rivencraft 1.21.1's Legacy fissure footprint/carving.
|
||||||
|
* Derived from Mystcraft's WorldGenMystStarFissure, LGPL-3.0,
|
||||||
|
* copyright XCompWiz and Mystcraft contributors.
|
||||||
|
*/
|
||||||
|
public final class PortStarFissureFeature {
|
||||||
|
private static final long SALT = 0x5354415246495353L;
|
||||||
|
|
||||||
|
private PortStarFissureFeature() {}
|
||||||
|
|
||||||
|
/** Per-Age data prevents revisits/restarts from carving the same fissure again. */
|
||||||
|
private static final class Arrival extends SavedData {
|
||||||
|
private static final Codec<Arrival> CODEC = BlockPos.CODEC.optionalFieldOf("spawn").codec()
|
||||||
|
.xmap(Arrival::new, data -> Optional.ofNullable(data.spawn));
|
||||||
|
private static final SavedDataType<Arrival> TYPE = new SavedDataType<>(
|
||||||
|
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "star_fissure_arrival"),
|
||||||
|
() -> new Arrival(Optional.empty()), CODEC);
|
||||||
|
private BlockPos spawn;
|
||||||
|
|
||||||
|
private Arrival(Optional<BlockPos> spawn) { this.spawn = spawn.orElse(null); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static BlockPos savedSpawn(ServerLevel level) {
|
||||||
|
return level.getDataStorage().computeIfAbsent(Arrival.TYPE).spawn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void generateOnce(ServerLevel level, BlockPos spawn, boolean caveTerrain) {
|
||||||
|
Arrival data = level.getDataStorage().computeIfAbsent(Arrival.TYPE);
|
||||||
|
if (data.spawn != null) return;
|
||||||
|
generate(level, level.getSeed(), spawn, caveTerrain);
|
||||||
|
data.spawn = spawn.immutable();
|
||||||
|
data.setDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legacy chooses 8–23 blocks after the spawn chunk, then grows along +Z. */
|
||||||
|
public static List<BlockPos> footprint(long ageSeed, BlockPos spawn) {
|
||||||
|
long chunkX = spawn.getX() >> 4;
|
||||||
|
long chunkZ = spawn.getZ() >> 4;
|
||||||
|
Random random = new Random(ageSeed ^ SALT ^ chunkX ^ (chunkZ << 32));
|
||||||
|
int originX = ((int) chunkX << 4) + random.nextInt(16) + 8;
|
||||||
|
int originZ = ((int) chunkZ << 4) + random.nextInt(16) + 8;
|
||||||
|
int length = random.nextInt(8) + 10;
|
||||||
|
int[][] intervals = new int[length][2];
|
||||||
|
List<BlockPos> columns = new ArrayList<>();
|
||||||
|
addRow(columns, originX, originZ, 0, 0, 0);
|
||||||
|
for (int row = 1; row < length; row++) {
|
||||||
|
int range = length + Math.min(row, length - row) + 1;
|
||||||
|
int right = (random.nextInt(range) - (range >> 1) + intervals[row - 1][1]) / 2;
|
||||||
|
int left = (random.nextInt(range) - (range >> 1) + intervals[row - 1][0]) / 2;
|
||||||
|
if (left > right) {
|
||||||
|
int swap = left;
|
||||||
|
left = right;
|
||||||
|
right = swap;
|
||||||
|
}
|
||||||
|
left = Math.min(left, intervals[row - 1][1]);
|
||||||
|
right = Math.max(right, intervals[row - 1][0]);
|
||||||
|
intervals[row][0] = left;
|
||||||
|
intervals[row][1] = right;
|
||||||
|
addRow(columns, originX, originZ, row, left, right);
|
||||||
|
}
|
||||||
|
return List.copyOf(columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addRow(List<BlockPos> columns, int originX, int originZ, int row, int left, int right) {
|
||||||
|
for (int x = left; x <= right; x++) {
|
||||||
|
// Only X/Z are used; the real floor is the dimension's minimum Y.
|
||||||
|
columns.add(new BlockPos(originX + x, 0, originZ + row * 2));
|
||||||
|
columns.add(new BlockPos(originX + x, 0, originZ + row * 2 + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void generate(ServerLevel level, long ageSeed, BlockPos spawn, boolean caveTerrain) {
|
||||||
|
int floorY = level.getMinY();
|
||||||
|
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
|
||||||
|
for (BlockPos column : footprint(ageSeed, spawn)) {
|
||||||
|
int top = level.getHeight(Heightmap.Types.WORLD_SURFACE, column.getX(), column.getZ());
|
||||||
|
if (caveTerrain) {
|
||||||
|
for (int y = Math.max(spawn.getY(), floorY + 1); y < top; y++) {
|
||||||
|
if (level.getBlockState(mutable.set(column.getX(), y, column.getZ())).is(Blocks.BEDROCK)) {
|
||||||
|
top = y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int y = floorY + 1; y < top; y++)
|
||||||
|
level.setBlock(mutable.set(column.getX(), y, column.getZ()), Blocks.AIR.defaultBlockState(), 2);
|
||||||
|
level.setBlock(mutable.set(column.getX(), floorY, column.getZ()),
|
||||||
|
Rivencraft.STAR_FISSURE.get().defaultBlockState(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import net.dnorberg.rivencraft.mixin.StructurePlacementAccessor;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.core.HolderOwner;
|
||||||
|
import net.minecraft.core.HolderSet;
|
||||||
|
import net.minecraft.core.registries.Registries;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
|
import net.minecraft.world.level.biome.Climate;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||||
|
|
||||||
|
/** 1.21.1 structure planning/density rules adapted to modern placement classes. */
|
||||||
|
public final class PortStructurePages {
|
||||||
|
private static final Map<Identifier, Identifier> SETS = Map.ofEntries(
|
||||||
|
vanillaSet("villages", "villages"),
|
||||||
|
vanillaSet("mineshafts", "mineshafts"),
|
||||||
|
vanillaSet("strongholds", "strongholds"),
|
||||||
|
vanillaSet("trial_chambers", "trial_chambers"),
|
||||||
|
vanillaSet("buried_treasure", "buried_treasures"),
|
||||||
|
vanillaSet("shipwrecks", "shipwrecks"),
|
||||||
|
vanillaSet("ocean_ruins", "ocean_ruins"),
|
||||||
|
vanillaSet("pillager_outposts", "pillager_outposts"),
|
||||||
|
vanillaSet("ruined_portals", "ruined_portals"),
|
||||||
|
vanillaSet("nether_fossils", "nether_fossils"),
|
||||||
|
vanillaSet("ancient_cities", "ancient_cities"),
|
||||||
|
vanillaSet("trail_ruins", "trail_ruins"),
|
||||||
|
vanillaSet("abandoned_camps", "abandoned_camp"),
|
||||||
|
Map.entry(page("nether_fortresses"), Identifier.fromNamespaceAndPath("rivencraft", "fortresses")),
|
||||||
|
Map.entry(page("bastions"), Identifier.fromNamespaceAndPath("rivencraft", "bastions")));
|
||||||
|
// Suppress vanilla's combined Nether set so an authored Fortress page does
|
||||||
|
// not also introduce Bastions (or vice versa).
|
||||||
|
private static final Set<Identifier> CONTROLLED_SETS = Stream.concat(SETS.values().stream(),
|
||||||
|
Stream.of(Identifier.withDefaultNamespace("nether_complexes")))
|
||||||
|
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||||
|
private static final Set<Identifier> CAVE_ONLY = Set.of(
|
||||||
|
page("nether_fortresses"), page("bastions"), page("nether_fossils"));
|
||||||
|
private static final Set<Identifier> SURFACE_ONLY = Set.of(
|
||||||
|
page("villages"), page("strongholds"), page("buried_treasure"), page("shipwrecks"),
|
||||||
|
page("ocean_ruins"), page("pillager_outposts"), page("trail_ruins"), page("abandoned_camps"));
|
||||||
|
private static final Set<Identifier> FLAT_ALLOWED = Set.of(page("villages"), page("strongholds"));
|
||||||
|
|
||||||
|
private PortStructurePages() {}
|
||||||
|
|
||||||
|
private static Identifier page(String name) {
|
||||||
|
return Identifier.fromNamespaceAndPath("rivencraft", "feature/" + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map.Entry<Identifier, Identifier> vanillaSet(String page, String set) {
|
||||||
|
return Map.entry(page(page), Identifier.withDefaultNamespace(set));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supports(Identifier page) { return SETS.containsKey(page); }
|
||||||
|
|
||||||
|
public static void validateCounts(List<Identifier> pages) {
|
||||||
|
Map<Identifier, Integer> counts = new HashMap<>();
|
||||||
|
for (Identifier page : pages) {
|
||||||
|
if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32)
|
||||||
|
throw new IllegalArgumentException("Structure plan requires supported pages, at most 32 per feature");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supportsTerrain(Identifier page, PortAgeTerrain terrain) {
|
||||||
|
if (!supports(page) || terrain == PortAgeTerrain.VOID) return false;
|
||||||
|
if (terrain == PortAgeTerrain.FLAT && !FLAT_ALLOWED.contains(page)) return false;
|
||||||
|
if (CAVE_ONLY.contains(page)) return terrain == PortAgeTerrain.CAVE;
|
||||||
|
return terrain != PortAgeTerrain.CAVE || !SURFACE_ONLY.contains(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void validateRegistry(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||||
|
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
|
||||||
|
pages.stream().filter(PortStructurePages::supports).distinct().forEach(page ->
|
||||||
|
sets.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Missing structure set for " + page)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
|
||||||
|
Map<Identifier, Integer> result = new HashMap<>();
|
||||||
|
for (Identifier page : pages) result.merge(SETS.get(page), 1, Integer::sum);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean authored(Structure structure, HolderLookup.Provider registries, List<Identifier> pages) {
|
||||||
|
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
|
||||||
|
return enabled(pages).keySet().stream()
|
||||||
|
.map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value())
|
||||||
|
.flatMap(set -> set.structures().stream()).anyMatch(entry -> entry.structure().value() == structure);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extra possible biomes affect planning only; terrain still uses its original source. */
|
||||||
|
public static BiomeSource planningBiomes(BiomeSource actual, HolderLookup<StructureSet> sets, List<Identifier> pages) {
|
||||||
|
List<Holder<Biome>> extra = enabled(pages).keySet().stream()
|
||||||
|
.map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value())
|
||||||
|
.flatMap(set -> set.structures().stream()).flatMap(entry -> entry.structure().value().biomes().stream())
|
||||||
|
.distinct().toList();
|
||||||
|
return new BiomeSource() {
|
||||||
|
@Override protected MapCodec<? extends BiomeSource> codec() {
|
||||||
|
throw new UnsupportedOperationException("Planning biome sources are never serialized");
|
||||||
|
}
|
||||||
|
@Override protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||||
|
return Stream.concat(actual.possibleBiomes().stream(), extra.stream());
|
||||||
|
}
|
||||||
|
@Override public net.minecraft.world.level.biome.BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||||
|
return actual.createResolver(sampler);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HolderLookup<StructureSet> filtered(HolderLookup<StructureSet> source, List<Identifier> pages,
|
||||||
|
BiomeSource actual) {
|
||||||
|
Map<Identifier, Integer> counts = enabled(pages);
|
||||||
|
Map<ResourceKey<StructureSet>, Holder.Reference<StructureSet>> copies = new HashMap<>();
|
||||||
|
return new HolderLookup<>() {
|
||||||
|
@Override public Optional<Holder.Reference<StructureSet>> get(ResourceKey<StructureSet> key) {
|
||||||
|
if (CONTROLLED_SETS.contains(key.identifier()) && !counts.containsKey(key.identifier())) return Optional.empty();
|
||||||
|
return source.get(key).filter(holder -> counts.containsKey(key.identifier()) || hasBiome(holder.value(), actual))
|
||||||
|
.map(holder -> copies.computeIfAbsent(key, ignored -> {
|
||||||
|
int count = counts.getOrDefault(key.identifier(), 1);
|
||||||
|
return count == 1 ? holder : new LocalReference(key, scaled(holder.value(), count));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
@Override public Optional<HolderSet.Named<StructureSet>> get(TagKey<StructureSet> key) { return source.get(key); }
|
||||||
|
@Override public Stream<Holder.Reference<StructureSet>> listElements() {
|
||||||
|
return source.listElements().flatMap(holder -> get(holder.key()).stream());
|
||||||
|
}
|
||||||
|
@Override public Stream<HolderSet.Named<StructureSet>> listTags() { return source.listTags(); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean hasBiome(StructureSet set, BiomeSource source) {
|
||||||
|
Set<Holder<Biome>> possible = source.possibleBiomes();
|
||||||
|
return set.structures().stream().flatMap(entry -> entry.structure().value().biomes().stream()).anyMatch(possible::contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StructureSet scaled(StructureSet set, int count) {
|
||||||
|
var placement = set.placement();
|
||||||
|
if (!(placement instanceof RandomSpreadStructurePlacement)
|
||||||
|
&& !(placement instanceof ConcentricRingsStructurePlacement)) return set;
|
||||||
|
StructurePlacementAccessor settings = (StructurePlacementAccessor) placement;
|
||||||
|
if (placement instanceof RandomSpreadStructurePlacement random) {
|
||||||
|
float original = settings.rivencraft$frequency();
|
||||||
|
float frequency = Math.min(1.0F, original * count);
|
||||||
|
double frequencyFactor = original > 0 ? frequency / original : 1;
|
||||||
|
int spacing = Math.max(1, (int) Math.round(random.spacing() / Math.sqrt(count / frequencyFactor)));
|
||||||
|
int separation = spacing == 1 ? 0 : Math.min(spacing - 1,
|
||||||
|
(int) Math.round((double) random.separation() * spacing / random.spacing()));
|
||||||
|
return new StructureSet(set.structures(), new RandomSpreadStructurePlacement(placement.locateOffset(),
|
||||||
|
settings.rivencraft$frequencyReductionMethod(), frequency, settings.rivencraft$salt(),
|
||||||
|
settings.rivencraft$exclusionZone(), spacing, separation, random.spreadType()));
|
||||||
|
}
|
||||||
|
if (placement instanceof ConcentricRingsStructurePlacement rings) {
|
||||||
|
int total = (int) Math.min(4095, (long) rings.count() * count);
|
||||||
|
int spread = Math.min(total, (int) Math.min(1023, (long) rings.spread() * count));
|
||||||
|
return new StructureSet(set.structures(), new ConcentricRingsStructurePlacement(placement.locateOffset(),
|
||||||
|
settings.rivencraft$frequencyReductionMethod(), settings.rivencraft$frequency(),
|
||||||
|
settings.rivencraft$salt(), settings.rivencraft$exclusionZone(), rings.distance(), spread,
|
||||||
|
total, rings.preferredBiomes()));
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class LocalReference extends Holder.Reference<StructureSet> {
|
||||||
|
private static final HolderOwner<StructureSet> OWNER = new HolderOwner<>() {};
|
||||||
|
private LocalReference(ResourceKey<StructureSet> key, StructureSet value) { super(Type.STAND_ALONE, OWNER, key, value); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.Random;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.level.block.Blocks;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.Heightmap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 26.3 adaptation of Rivencraft 1.21.1's winding solid volumes, originally
|
||||||
|
* adapted from Mystcraft Legacy's SymbolTendrils and MapGenCavesMyst (LGPL-3.0-or-later).
|
||||||
|
* Each chunk reconstructs the neighboring source paths to preserve continuous borders.
|
||||||
|
*/
|
||||||
|
final class PortTendrilFeature {
|
||||||
|
private static final long SALT = 0x54454E4452494C53L;
|
||||||
|
private static final int SOURCE_RANGE = 8;
|
||||||
|
private static final int SOURCE_RATE = 15;
|
||||||
|
private static final int SOURCE_SIZE = 18;
|
||||||
|
|
||||||
|
private PortTendrilFeature() {}
|
||||||
|
|
||||||
|
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||||
|
var target = chunk.getPos();
|
||||||
|
Random master = new Random(ageSeed ^ SALT);
|
||||||
|
long xSeed = master.nextLong();
|
||||||
|
long zSeed = master.nextLong();
|
||||||
|
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
|
||||||
|
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
|
||||||
|
Random source = new Random((long) sourceX * xSeed ^ (long) sourceZ * zSeed ^ ageSeed ^ SALT);
|
||||||
|
int nodes = source.nextInt(source.nextInt(source.nextInt(SOURCE_SIZE) + 1) + 1);
|
||||||
|
if (source.nextInt(SOURCE_RATE) != 0) continue;
|
||||||
|
for (int node = 0; node < nodes; node++) {
|
||||||
|
double x = sourceX * 16 + source.nextInt(16);
|
||||||
|
double y = source.nextInt(source.nextInt(120) + 8);
|
||||||
|
double z = sourceZ * 16 + source.nextInt(16);
|
||||||
|
int branches = 1;
|
||||||
|
if (source.nextInt(4) == 0) {
|
||||||
|
trace(chunk, material, source.nextLong(), x, y, z,
|
||||||
|
1.0F + source.nextFloat() * 6.0F, 0.0F, 0.0F, -1, -1, 0.5D);
|
||||||
|
branches += source.nextInt(4);
|
||||||
|
}
|
||||||
|
for (int branch = 0; branch < branches; branch++) {
|
||||||
|
float yaw = source.nextFloat() * Mth.TWO_PI;
|
||||||
|
float pitch = (source.nextFloat() - 0.5F) / 4.0F;
|
||||||
|
float radius = source.nextFloat() * 2.0F + source.nextFloat();
|
||||||
|
if (source.nextInt(10) == 0)
|
||||||
|
radius *= source.nextFloat() * source.nextFloat() * 3.0F + 1.0F;
|
||||||
|
trace(chunk, material, source.nextLong(), x, y, z,
|
||||||
|
radius, yaw, pitch, 0, 0, 1.0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||||
|
double x, double y, double z, float radius, float yaw, float pitch,
|
||||||
|
int step, int length, double verticalScale) {
|
||||||
|
Random random = new Random(seed);
|
||||||
|
if (length <= 0) {
|
||||||
|
int maximum = SOURCE_RANGE * 16 - 16;
|
||||||
|
length = maximum - random.nextInt(maximum / 4);
|
||||||
|
}
|
||||||
|
boolean singleNode = step == -1;
|
||||||
|
if (singleNode) step = length / 2;
|
||||||
|
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||||
|
boolean slowPitch = random.nextInt(6) == 0;
|
||||||
|
float yawDrift = 0.0F;
|
||||||
|
float pitchDrift = 0.0F;
|
||||||
|
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||||
|
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||||
|
for (; step < length; step++) {
|
||||||
|
double horizontalRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||||
|
double verticalRadius = horizontalRadius * verticalScale;
|
||||||
|
float horizontalMotion = Mth.cos(pitch);
|
||||||
|
x += Mth.cos(yaw) * horizontalMotion;
|
||||||
|
y += Mth.sin(pitch);
|
||||||
|
z += Mth.sin(yaw) * horizontalMotion;
|
||||||
|
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||||
|
pitch += pitchDrift * 0.1F;
|
||||||
|
yaw += yawDrift * 0.1F;
|
||||||
|
pitchDrift = pitchDrift * 0.9F
|
||||||
|
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||||
|
yawDrift = yawDrift * 0.75F
|
||||||
|
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||||
|
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||||
|
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||||
|
yaw - Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||||
|
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||||
|
yaw + Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||||
|
double distanceX = x - middleX;
|
||||||
|
double distanceZ = z - middleZ;
|
||||||
|
double remaining = length - step;
|
||||||
|
double reach = radius + 18.0D;
|
||||||
|
if (distanceX * distanceX + distanceZ * distanceZ - remaining * remaining > reach * reach) return;
|
||||||
|
if (x >= middleX - 16.0D - horizontalRadius * 2.0D
|
||||||
|
&& x <= middleX + 16.0D + horizontalRadius * 2.0D
|
||||||
|
&& z >= middleZ - 16.0D - horizontalRadius * 2.0D
|
||||||
|
&& z <= middleZ + 16.0D + horizontalRadius * 2.0D) {
|
||||||
|
fillNode(chunk, material, x, y, z, horizontalRadius, verticalRadius);
|
||||||
|
}
|
||||||
|
if (singleNode) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fillNode(ChunkAccess chunk, BlockState material, double centerX, double centerY,
|
||||||
|
double centerZ, double horizontalRadius, double verticalRadius) {
|
||||||
|
int originX = chunk.getPos().getMinBlockX();
|
||||||
|
int originZ = chunk.getPos().getMinBlockZ();
|
||||||
|
int minX = Math.max(0, Mth.floor(centerX - horizontalRadius) - originX - 1);
|
||||||
|
int maxX = Math.min(16, Mth.floor(centerX + horizontalRadius) - originX + 2);
|
||||||
|
int minZ = Math.max(0, Mth.floor(centerZ - horizontalRadius) - originZ - 1);
|
||||||
|
int maxZ = Math.min(16, Mth.floor(centerZ + horizontalRadius) - originZ + 2);
|
||||||
|
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - verticalRadius) - 1);
|
||||||
|
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + verticalRadius) + 2);
|
||||||
|
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||||
|
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||||
|
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||||
|
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||||
|
double dy = (worldY + 0.5D - centerY) / verticalRadius;
|
||||||
|
if (dy <= -0.7D || dy * dy >= 1.0D) continue;
|
||||||
|
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||||
|
double dz = (originZ + localZ + 0.5D - centerZ) / horizontalRadius;
|
||||||
|
for (int localX = minX; localX < maxX; localX++) {
|
||||||
|
double dx = (originX + localX + 0.5D - centerX) / horizontalRadius;
|
||||||
|
if (dx * dx + dy * dy + dz * dz >= 1.0D) continue;
|
||||||
|
pos.set(originX + localX, worldY, originZ + localZ);
|
||||||
|
BlockState previous = chunk.getBlockState(pos);
|
||||||
|
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||||
|
chunk.setBlockState(pos, material);
|
||||||
|
surface.update(localX, worldY, localZ, material);
|
||||||
|
floor.update(localX, worldY, localZ, material);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** Frozen terrain filters and ordered solid/surface features for an Age. */
|
||||||
|
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||||
|
boolean starFissures, List<Identifier> structures, List<Identifier> carvers) {
|
||||||
|
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "feature/floating_islands");
|
||||||
|
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
|
||||||
|
public static final Identifier SKYLANDS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/skylands");
|
||||||
|
public static final Identifier SPHERES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spheres");
|
||||||
|
public static final Identifier SPIKES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spikes");
|
||||||
|
public static final Identifier OBELISKS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/obelisks");
|
||||||
|
public static final Identifier CRYSTAL_FORMATIONS = Identifier.fromNamespaceAndPath(
|
||||||
|
Rivencraft.MOD_ID, "feature/crystal_formations");
|
||||||
|
public static final Identifier CRYSTAL_MATERIAL = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "crystal_block");
|
||||||
|
public static final Identifier DENSE_ORES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dense_ores");
|
||||||
|
public static final Identifier STAR_FISSURES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/star_fissures");
|
||||||
|
public static final int MAX_PER_FEATURE = 32;
|
||||||
|
public static final Codec<PortTerrainFeaturePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortTerrainFeaturePlan::choices),
|
||||||
|
Codec.BOOL.optionalFieldOf("skylands", false).forGetter(PortTerrainFeaturePlan::skylands),
|
||||||
|
Codec.intRange(0, MAX_PER_FEATURE).optionalFieldOf("dense_ores", 0)
|
||||||
|
.forGetter(PortTerrainFeaturePlan::denseOres),
|
||||||
|
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures),
|
||||||
|
Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures),
|
||||||
|
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers)
|
||||||
|
).apply(instance, PortTerrainFeaturePlan::new));
|
||||||
|
|
||||||
|
public record Choice(Identifier page, Identifier material) {
|
||||||
|
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
|
||||||
|
Identifier.CODEC.fieldOf("material").forGetter(Choice::material)
|
||||||
|
).apply(instance, Choice::new));
|
||||||
|
|
||||||
|
public Choice {
|
||||||
|
if (!isSupported(page) || material == null)
|
||||||
|
throw new IllegalArgumentException("Invalid terrain feature choice");
|
||||||
|
if (CRYSTAL_FORMATIONS.equals(page) && !CRYSTAL_MATERIAL.equals(material))
|
||||||
|
throw new IllegalArgumentException("Crystal formations use Rivencraft crystal, not a Block modifier");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan {
|
||||||
|
choices = List.copyOf(choices);
|
||||||
|
structures = List.copyOf(structures);
|
||||||
|
carvers = List.copyOf(carvers);
|
||||||
|
PortStructurePages.validateCounts(structures);
|
||||||
|
PortCarverPages.validateCounts(carvers);
|
||||||
|
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
|
||||||
|
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
|
||||||
|
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty())
|
||||||
|
throw new IllegalArgumentException("Terrain feature plan must not be empty");
|
||||||
|
var counts = new HashMap<Identifier, Integer>();
|
||||||
|
for (Choice choice : choices) {
|
||||||
|
if (counts.merge(choice.page(), 1, Integer::sum) > MAX_PER_FEATURE)
|
||||||
|
throw new IllegalArgumentException("Terrain feature plan permits at most 32 occurrences per feature");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||||
|
boolean starFissures, List<Identifier> structures) {
|
||||||
|
this(choices, skylands, denseOres, starFissures, structures, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
|
||||||
|
this(choices, skylands, denseOres, starFissures, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres) {
|
||||||
|
this(choices, skylands, denseOres, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands) { this(choices, skylands, 0, false); }
|
||||||
|
|
||||||
|
public PortTerrainFeaturePlan(List<Choice> choices) { this(choices, false, 0, false); }
|
||||||
|
|
||||||
|
public static boolean isSupported(Identifier page) {
|
||||||
|
return FLOATING_ISLANDS.equals(page) || TENDRILS.equals(page) || SPHERES.equals(page) || isSurface(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean acceptsMaterial(Identifier page) {
|
||||||
|
return isSupported(page) && !CRYSTAL_FORMATIONS.equals(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSurface(Identifier page) {
|
||||||
|
return SPIKES.equals(page) || OBELISKS.equals(page) || CRYSTAL_FORMATIONS.equals(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** Ordered, frozen plants and log decorations; not all require surface terrain. */
|
||||||
|
public record PortVegetationPlan(List<Identifier> pages) {
|
||||||
|
public static final Identifier FLOWERS = page("flowers");
|
||||||
|
public static final Identifier TREES = page("trees");
|
||||||
|
public static final Identifier HUGE_TREES = page("huge_trees");
|
||||||
|
public static final Identifier POPLAR_TREES = page("poplar_trees");
|
||||||
|
public static final Identifier FALLEN_POPLAR_TREES = page("fallen_poplar_trees");
|
||||||
|
public static final Identifier RED_SHRUBS = page("red_shrubs");
|
||||||
|
public static final Identifier SHELF_MUSHROOMS = page("shelf_mushrooms");
|
||||||
|
public static final Codec<PortVegetationPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
|
Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages)
|
||||||
|
).apply(instance, PortVegetationPlan::new));
|
||||||
|
|
||||||
|
public PortVegetationPlan {
|
||||||
|
pages = List.copyOf(pages);
|
||||||
|
if (pages.isEmpty() || pages.size() > 32 || pages.stream().anyMatch(page -> !isVegetation(page)))
|
||||||
|
throw new IllegalArgumentException("Vegetation plan requires 1–32 supported pages");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isVegetation(Identifier page) {
|
||||||
|
return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES)
|
||||||
|
|| page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES)
|
||||||
|
|| page.equals(RED_SHRUBS) || page.equals(SHELF_MUSHROOMS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Identifier placedFeature(Identifier page) {
|
||||||
|
if (page.equals(FLOWERS)) return Identifier.withDefaultNamespace("flower_default");
|
||||||
|
if (page.equals(TREES)) return Identifier.withDefaultNamespace("trees_plains");
|
||||||
|
if (page.equals(HUGE_TREES)) return Identifier.withDefaultNamespace("trees_old_growth_spruce_taiga");
|
||||||
|
if (page.equals(POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "poplar_trees");
|
||||||
|
if (page.equals(FALLEN_POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "fallen_poplar_trees");
|
||||||
|
if (page.equals(RED_SHRUBS)) return Identifier.withDefaultNamespace("patch_red_shrub");
|
||||||
|
throw new IllegalArgumentException("Unsupported vegetation page " + page);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean addsLogs(Identifier page) {
|
||||||
|
return page.equals(TREES) || page.equals(HUGE_TREES)
|
||||||
|
|| page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean requiresSurface(Identifier page) {
|
||||||
|
return isVegetation(page) && !page.equals(SHELF_MUSHROOMS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Identifier page(String name) {
|
||||||
|
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** The six active weather pages in the 1.21.1 writing system. */
|
||||||
|
public enum PortWeatherMode {
|
||||||
|
NORMAL, FAST, SLOW, OFF, ALWAYS, STORM;
|
||||||
|
|
||||||
|
public static final Codec<PortWeatherMode> CODEC = Codec.STRING.xmap(
|
||||||
|
name -> valueOf(name.toUpperCase(java.util.Locale.ROOT)),
|
||||||
|
mode -> mode.name().toLowerCase(java.util.Locale.ROOT));
|
||||||
|
|
||||||
|
public static PortWeatherMode forPage(Identifier page) {
|
||||||
|
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||||
|
return switch (page.getPath()) {
|
||||||
|
case "weather/normal" -> NORMAL;
|
||||||
|
case "weather/fast" -> FAST;
|
||||||
|
case "weather/slow" -> SLOW;
|
||||||
|
case "weather/off" -> OFF;
|
||||||
|
case "weather/always" -> ALWAYS;
|
||||||
|
case "weather/storm" -> STORM;
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PortWeatherMode fromPages(List<Identifier> pages) {
|
||||||
|
return pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
|
||||||
|
.findFirst().orElse(NORMAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.world.level.saveddata.SavedData;
|
||||||
|
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||||
|
|
||||||
|
/** Age-local policy; native WeatherData separately persists its live cycle. */
|
||||||
|
final class PortWeatherSettings extends SavedData {
|
||||||
|
private static final Codec<PortWeatherSettings> CODEC = PortWeatherMode.CODEC
|
||||||
|
.fieldOf("mode").codec().xmap(PortWeatherSettings::new, settings -> settings.mode);
|
||||||
|
static final SavedDataType<PortWeatherSettings> TYPE = new SavedDataType<>(
|
||||||
|
Identifier.fromNamespaceAndPath("rivencraft", "weather_policy"),
|
||||||
|
() -> new PortWeatherSettings(PortWeatherMode.NORMAL), CODEC);
|
||||||
|
|
||||||
|
private PortWeatherMode mode;
|
||||||
|
|
||||||
|
private PortWeatherSettings(PortWeatherMode mode) { this.mode = mode; }
|
||||||
|
PortWeatherMode mode() { return mode; }
|
||||||
|
void setMode(PortWeatherMode mode) {
|
||||||
|
this.mode = mode;
|
||||||
|
setDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import java.util.Map;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.OptionalLong;
|
import java.util.OptionalLong;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.core.Holder;
|
import net.minecraft.core.Holder;
|
||||||
import net.minecraft.core.HolderSet;
|
import net.minecraft.core.HolderSet;
|
||||||
@@ -33,7 +34,6 @@ import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
|||||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||||
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
|
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
|
||||||
import net.minecraft.world.level.block.Blocks;
|
import net.minecraft.world.level.block.Blocks;
|
||||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
|
||||||
import net.minecraft.world.level.storage.LevelResource;
|
import net.minecraft.world.level.storage.LevelResource;
|
||||||
import net.neoforged.neoforge.common.NeoForge;
|
import net.neoforged.neoforge.common.NeoForge;
|
||||||
import net.neoforged.neoforge.common.world.chunk.ForcedChunkManager;
|
import net.neoforged.neoforge.common.world.chunk.ForcedChunkManager;
|
||||||
@@ -56,7 +56,8 @@ public final class RuntimeAgeBackend {
|
|||||||
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
||||||
try {
|
try {
|
||||||
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
|
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) {
|
} catch (RuntimeException exception) {
|
||||||
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
|
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
|
||||||
}
|
}
|
||||||
@@ -74,12 +75,51 @@ public final class RuntimeAgeBackend {
|
|||||||
|
|
||||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, rocks, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||||
|
PortTerrainFeaturePlan terrainFeatures) {
|
||||||
|
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||||
|
terrainFeatures, PortWeatherMode.NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||||
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||||
|
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather) {
|
||||||
checkServerThread(server);
|
checkServerThread(server);
|
||||||
ageName(id);
|
ageName(id);
|
||||||
AgeLevelIndex index = index(server);
|
AgeLevelIndex index = index(server);
|
||||||
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
||||||
ServerLevel level = load(server, id, seed, biomePlan, terrain);
|
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
index.add(id, seed, biomePlan, terrain);
|
((PortAgeLevel) level).configureWeather(weather);
|
||||||
|
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
// A newly bound book may be used immediately. Persist its Age identity
|
// A newly bound book may be used immediately. Persist its Age identity
|
||||||
// before publishing the catalog so a later crash cannot leave that
|
// before publishing the catalog so a later crash cannot leave that
|
||||||
// book pointing at a world omitted from the on-disk index.
|
// book pointing at a world omitted from the on-disk index.
|
||||||
@@ -99,7 +139,9 @@ public final class RuntimeAgeBackend {
|
|||||||
public static ServerLevel reload(MinecraftServer server, Identifier id) {
|
public static ServerLevel reload(MinecraftServer server, Identifier id) {
|
||||||
checkServerThread(server);
|
checkServerThread(server);
|
||||||
long seed = requireSeed(server, id);
|
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);
|
AgeCatalogSync.onCatalogChanged(server);
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
@@ -111,18 +153,23 @@ public final class RuntimeAgeBackend {
|
|||||||
long previousSeed = requireSeed(server, id);
|
long previousSeed = requireSeed(server, id);
|
||||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||||
PortAgeTerrain terrain = index.terrain(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;
|
if (previousSeed == seed) return;
|
||||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||||
if (loaded) unloadLoaded(server, id);
|
if (loaded) unloadLoaded(server, id);
|
||||||
try {
|
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);
|
index.reseed(id, seed);
|
||||||
server.overworld().getDataStorage().saveAndJoin();
|
server.overworld().getDataStorage().saveAndJoin();
|
||||||
if (loaded) AgeCatalogSync.onCatalogChanged(server);
|
if (loaded) AgeCatalogSync.onCatalogChanged(server);
|
||||||
} catch (RuntimeException exception) {
|
} catch (RuntimeException exception) {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
try {
|
try {
|
||||||
load(server, id, previousSeed, biomePlan, terrain);
|
load(server, id, previousSeed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
AgeCatalogSync.onCatalogChanged(server);
|
AgeCatalogSync.onCatalogChanged(server);
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
@@ -140,6 +187,11 @@ public final class RuntimeAgeBackend {
|
|||||||
long seed = requireSeed(server, id);
|
long seed = requireSeed(server, id);
|
||||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||||
PortAgeTerrain terrain = index.terrain(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;
|
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||||
ArchivePaths paths = archivePaths(server, name);
|
ArchivePaths paths = archivePaths(server, name);
|
||||||
if (loaded) unloadLoaded(server, id);
|
if (loaded) unloadLoaded(server, id);
|
||||||
@@ -158,14 +210,15 @@ public final class RuntimeAgeBackend {
|
|||||||
}
|
}
|
||||||
} catch (IOException | RuntimeException exception) {
|
} catch (IOException | RuntimeException exception) {
|
||||||
try {
|
try {
|
||||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain);
|
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||||
|
terrainFeatures);
|
||||||
server.overworld().getDataStorage().saveAndJoin();
|
server.overworld().getDataStorage().saveAndJoin();
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
try {
|
try {
|
||||||
load(server, id, seed, biomePlan, terrain);
|
load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
} catch (RuntimeException rollbackFailure) {
|
} catch (RuntimeException rollbackFailure) {
|
||||||
exception.addSuppressed(rollbackFailure);
|
exception.addSuppressed(rollbackFailure);
|
||||||
}
|
}
|
||||||
@@ -281,7 +334,9 @@ public final class RuntimeAgeBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||||
|
PortTerrainFeaturePlan terrainFeatures) {
|
||||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||||
|
|
||||||
@@ -325,7 +380,19 @@ public final class RuntimeAgeBackend {
|
|||||||
|| terrain != PortAgeTerrain.STANDARD) {
|
|| terrain != PortAgeTerrain.STANDARD) {
|
||||||
if (!(generator instanceof NoiseBasedChunkGenerator noise))
|
if (!(generator instanceof NoiseBasedChunkGenerator noise))
|
||||||
throw new IllegalStateException("Overworld generator cannot accept authored noise settings");
|
throw new IllegalStateException("Overworld generator cannot accept authored noise settings");
|
||||||
var source = generator.getBiomeSource();
|
var source = terrain == PortAgeTerrain.CAVE
|
||||||
|
? new FixedBiomeSource(server.registryAccess().lookupOrThrow(Registries.BIOME)
|
||||||
|
.getOrThrow(Biomes.LUSH_CAVES))
|
||||||
|
: generator.getBiomeSource();
|
||||||
|
if (terrain == PortAgeTerrain.CAVE && biomePlan != null) {
|
||||||
|
var biomes = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
|
for (Identifier biomeId : biomePlan.biomes()) {
|
||||||
|
var holder = biomes.get(ResourceKey.create(Registries.BIOME, biomeId))
|
||||||
|
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biomeId));
|
||||||
|
if (!PortRegistryPages.isCaveBiome(holder))
|
||||||
|
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biomeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
|
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
|
||||||
source = authoredBiomeSource(server, biomePlan, seed);
|
source = authoredBiomeSource(server, biomePlan, seed);
|
||||||
}
|
}
|
||||||
@@ -335,15 +402,16 @@ public final class RuntimeAgeBackend {
|
|||||||
.getOrThrow(NoiseGeneratorSettings.AMPLIFIED);
|
.getOrThrow(NoiseGeneratorSettings.AMPLIFIED);
|
||||||
case ISLAND -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
case ISLAND -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||||
.getOrThrow(NoiseGeneratorSettings.FLOATING_ISLANDS);
|
.getOrThrow(NoiseGeneratorSettings.FLOATING_ISLANDS);
|
||||||
|
case CAVE -> dryCaveSettings(server);
|
||||||
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource");
|
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource");
|
||||||
};
|
};
|
||||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||||
}
|
}
|
||||||
|
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
|
||||||
|
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
|
||||||
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
||||||
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
||||||
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
|
ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain);
|
||||||
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
|
|
||||||
key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false);
|
|
||||||
|
|
||||||
// Publish only after the constructor succeeds. If the load event fails,
|
// Publish only after the constructor succeeds. If the load event fails,
|
||||||
// roll back the map and close the newly opened chunk/entity stores.
|
// roll back the map and close the newly opened chunk/entity stores.
|
||||||
@@ -369,4 +437,14 @@ public final class RuntimeAgeBackend {
|
|||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Holder<NoiseGeneratorSettings> dryCaveSettings(MinecraftServer server) {
|
||||||
|
NoiseGeneratorSettings caves = server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||||
|
.getOrThrow(NoiseGeneratorSettings.CAVES).value();
|
||||||
|
return Holder.direct(new NoiseGeneratorSettings(
|
||||||
|
caves.noiseSettings(), caves.defaultBlock(), Blocks.AIR.defaultBlockState(),
|
||||||
|
caves.noiseRouter(), caves.materialRule(), caves.spawnTarget(),
|
||||||
|
caves.noiseSettings().minY() - 1, caves.disableMobGeneration(), Optional.empty(),
|
||||||
|
caves.useLegacyRandomSource(), caves.debugFunctions()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package net.dnorberg.rivencraft.block;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.sounds.SoundSource;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.entity.InsideBlockEffectApplier;
|
||||||
|
import net.minecraft.world.level.BlockGetter;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.block.Block;
|
||||||
|
import net.minecraft.world.level.block.EndPortalBlock;
|
||||||
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.portal.TeleportTransition;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||||
|
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||||
|
|
||||||
|
/** Non-obtainable starfield portal returning entities to the shared Overworld spawn. */
|
||||||
|
public final class StarFissureBlock extends EndPortalBlock {
|
||||||
|
private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 1.6, 16);
|
||||||
|
|
||||||
|
public StarFissureBlock(BlockBehaviour.Properties properties) { super(properties); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||||
|
return new StarFissureBlockEntity(pos, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void animateTick(BlockState state, Level level, BlockPos pos, net.minecraft.util.RandomSource random) {
|
||||||
|
// Legacy fissures have a starfield, not vanilla End portal smoke.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||||
|
return SHAPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity,
|
||||||
|
InsideBlockEffectApplier effects, boolean precise) {
|
||||||
|
// Do not inherit End credits or End-dimension travel from the vanilla block.
|
||||||
|
if (entity.canUsePortal(false)) entity.setAsInsidePortal(this, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
|
||||||
|
if (level.dimension() == Level.OVERWORLD) return null;
|
||||||
|
ServerLevel overworld = level.getServer().overworld();
|
||||||
|
if (overworld == null) return null;
|
||||||
|
BlockPos spawn = overworld.getRespawnData().pos();
|
||||||
|
BlockPos landing = net.dnorberg.rivencraft.age.PortAgeTravel.findFootedDestination(overworld, spawn).orElse(spawn);
|
||||||
|
level.playSound(null, pos, Rivencraft.LINK_FISSURE.get(), SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||||
|
return new TeleportTransition(overworld, Vec3.atBottomCenterOf(landing), entity.getDeltaMovement(),
|
||||||
|
entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET.then(arrived -> {
|
||||||
|
// Departure audio can be discarded when the client switches levels.
|
||||||
|
// Play after arrival too so the traveler hears the successful return.
|
||||||
|
arrived.level().playSound(null, arrived.blockPosition(), Rivencraft.LINK_FISSURE.get(),
|
||||||
|
SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
package net.dnorberg.rivencraft.block.entity;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
|
||||||
|
public final class StarFissureBlockEntity extends TheEndPortalBlockEntity {
|
||||||
|
public StarFissureBlockEntity(BlockPos pos, BlockState state) {
|
||||||
|
super(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), pos, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -14,6 +14,12 @@ import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
|
|||||||
public final class PortBookBinderClientEvents {
|
public final class PortBookBinderClientEvents {
|
||||||
private 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
|
@SubscribeEvent
|
||||||
public static void registerScreens(RegisterMenuScreensEvent event) {
|
public static void registerScreens(RegisterMenuScreensEvent event) {
|
||||||
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
|
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)) {
|
} else if (PageItem.isLinkPanel(page)) {
|
||||||
graphics.fill(x + 5, y + 6, x + 22, y + 17, 0xFF000000);
|
graphics.fill(x + 5, y + 6, x + 22, y + 17, 0xFF000000);
|
||||||
} else {
|
} 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
|
// Render the full-size transparent glyph through the working
|
||||||
// item pipeline, without its small parchment-backed icon.
|
// item pipeline, without its small parchment-backed icon.
|
||||||
ItemStack glyphOnly = page.copy();
|
ItemStack glyphOnly = page.copy();
|
||||||
|
|||||||
+10
-8
@@ -5,6 +5,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
|
|||||||
import com.mojang.serialization.MapCodec;
|
import com.mojang.serialization.MapCodec;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||||
@@ -18,7 +19,8 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
|
|||||||
public PageArt extractArgument(ItemStack stack) {
|
public PageArt extractArgument(ItemStack stack) {
|
||||||
Identifier symbol = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
Identifier symbol = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||||
return symbol == null ? null : new PageArt(symbol,
|
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
|
@Override
|
||||||
@@ -30,10 +32,10 @@ public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSy
|
|||||||
: PortSymbolPageTextures.forItem(art.symbol());
|
: PortSymbolPageTextures.forItem(art.symbol());
|
||||||
collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture),
|
collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture),
|
||||||
(matrix, vertices) -> {
|
(matrix, vertices) -> {
|
||||||
vertex(vertices, matrix, 0, 0, 0, 1, light, overlay);
|
vertex(vertices, matrix, 0, 0, 0, 1, light, overlay, art.color());
|
||||||
vertex(vertices, matrix, 1, 0, 1, 1, light, overlay);
|
vertex(vertices, matrix, 1, 0, 1, 1, light, overlay, art.color());
|
||||||
vertex(vertices, matrix, 1, 1, 1, 0, light, overlay);
|
vertex(vertices, matrix, 1, 1, 1, 0, light, overlay, art.color());
|
||||||
vertex(vertices, matrix, 0, 1, 0, 0, light, overlay);
|
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,
|
private static void vertex(VertexConsumer vertices, PoseStack.Pose pose,
|
||||||
float x, float y, float u, float v, int light, int overlay) {
|
float x, float y, float u, float v, int light, int overlay, int color) {
|
||||||
vertices.addVertex(pose, x, y, 0.5F).setColor(255, 255, 255, 255)
|
vertices.addVertex(pose, x, y, 0.5F).setColor(color)
|
||||||
.setUv(u, v).setOverlay(overlay).setLight(light).setNormal(pose, 0, 0, 1);
|
.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 record Unbaked() implements SpecialModelRenderer.Unbaked<PageArt> {
|
||||||
public static final Unbaked INSTANCE = new Unbaked();
|
public static final Unbaked INSTANCE = new Unbaked();
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package net.dnorberg.rivencraft.client;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||||
|
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||||
|
import net.minecraft.client.renderer.blockentity.AbstractEndPortalRenderer;
|
||||||
|
import net.minecraft.client.renderer.blockentity.state.EndPortalRenderState;
|
||||||
|
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||||
|
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||||
|
import net.minecraft.core.Direction;
|
||||||
|
|
||||||
|
/** Retains 1.21.1's UP-at-zero/DOWN-at-one planes with the modern starfield renderer. */
|
||||||
|
public final class StarFissureRenderer extends AbstractEndPortalRenderer<StarFissureBlockEntity, EndPortalRenderState> {
|
||||||
|
@Override
|
||||||
|
public EndPortalRenderState createRenderState() { return new EndPortalRenderState(); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void submit(EndPortalRenderState state, PoseStack pose, SubmitNodeCollector collector,
|
||||||
|
CameraRenderState camera) {
|
||||||
|
if (state.facesToShow.contains(Direction.UP)) {
|
||||||
|
pose.pushPose();
|
||||||
|
pose.translate(0, -1, 0);
|
||||||
|
submitCube(List.of(Direction.UP), RenderTypes.endPortal(), pose, collector);
|
||||||
|
pose.popPose();
|
||||||
|
}
|
||||||
|
if (state.facesToShow.contains(Direction.DOWN)) {
|
||||||
|
pose.pushPose();
|
||||||
|
pose.translate(0, 1, 0);
|
||||||
|
submitCube(List.of(Direction.DOWN), RenderTypes.endPortal(), pose, collector);
|
||||||
|
pose.popPose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package net.dnorberg.rivencraft.item;
|
||||||
|
|
||||||
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
|
import net.minecraft.core.particles.ParticleTypes;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.server.level.ServerPlayer;
|
||||||
|
import net.minecraft.sounds.SoundEvent;
|
||||||
|
import net.minecraft.sounds.SoundSource;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
/** Same successful-link feedback and sound precedence as the 1.21.1 target. */
|
||||||
|
public final class LinkVisualEffects {
|
||||||
|
private LinkVisualEffects() {}
|
||||||
|
|
||||||
|
public static SoundEvent sound(LinkProperties properties, boolean portal) {
|
||||||
|
if (properties.has(LinkProperties.DISARM)) return Rivencraft.LINK_DISARM.get();
|
||||||
|
if (portal) return Rivencraft.LINK_PORTAL.get();
|
||||||
|
if (properties.has(LinkProperties.INTRA_LINKING)) return Rivencraft.LINK_INTRA.get();
|
||||||
|
return Rivencraft.LINK.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void play(ServerLevel source, Vec3 departure, ServerPlayer player,
|
||||||
|
LinkProperties properties, boolean portal) {
|
||||||
|
SoundEvent event = sound(properties, portal);
|
||||||
|
burst(source, departure, event);
|
||||||
|
burst(player.level(), player.position(), event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void burst(ServerLevel level, Vec3 position, SoundEvent event) {
|
||||||
|
level.playSound(null, position.x, position.y, position.z, event,
|
||||||
|
SoundSource.PLAYERS, 0.8F, level.getRandom().nextFloat() * 0.2F + 0.9F);
|
||||||
|
level.sendParticles(ParticleTypes.CLOUD, position.x, position.y, position.z,
|
||||||
|
50, 0.5, 1.0, 0.5, 0.005);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,7 @@ public final class LinkingBookItem extends Item {
|
|||||||
/** Link Panel effects are shared by Linking and Descriptive Books. */
|
/** Link Panel effects are shared by Linking and Descriptive Books. */
|
||||||
public static void applySuccessfulLink(ServerPlayer player, LinkProperties properties,
|
public static void applySuccessfulLink(ServerPlayer player, LinkProperties properties,
|
||||||
ServerLevel source, Vec3 departure, Vec3 incoming, float destinationYaw) {
|
ServerLevel source, Vec3 departure, Vec3 incoming, float destinationYaw) {
|
||||||
|
LinkVisualEffects.play(source, departure, player, properties, false);
|
||||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) {
|
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) {
|
||||||
double speed = Math.hypot(incoming.x, incoming.z);
|
double speed = Math.hypot(incoming.x, incoming.z);
|
||||||
double yaw = Math.toRadians(destinationYaw);
|
double yaw = Math.toRadians(destinationYaw);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.item;
|
|||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||||
|
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||||
import net.minecraft.ChatFormatting;
|
import net.minecraft.ChatFormatting;
|
||||||
import net.minecraft.core.component.DataComponents;
|
import net.minecraft.core.component.DataComponents;
|
||||||
import net.minecraft.network.chat.Component;
|
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.TooltipFlag;
|
||||||
import net.minecraft.world.item.component.TooltipDisplay;
|
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 final class PageItem extends Item {
|
||||||
public PageItem(Properties properties) {
|
public PageItem(Properties properties) {
|
||||||
super(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, ChatFormatting.OBFUSCATED)
|
||||||
: description.copy().withStyle(ChatFormatting.GRAY));
|
: description.copy().withStyle(ChatFormatting.GRAY));
|
||||||
}
|
}
|
||||||
tooltip.accept(Component.translatable("item.rivencraft.page.symbol.port_status")
|
boolean implemented = PortAuthoredWriting.isPageImplemented(symbolId);
|
||||||
.withStyle(ChatFormatting.DARK_GRAY));
|
tooltip.accept(Component.translatable(implemented
|
||||||
|
? "item.rivencraft.page.symbol.implemented" : "item.rivencraft.page.symbol.unimplemented")
|
||||||
|
.withStyle(implemented ? ChatFormatting.DARK_GRAY : ChatFormatting.RED));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package net.dnorberg.rivencraft.mixin;
|
||||||
|
|
||||||
|
import com.llamalad7.mixinextras.sugar.Local;
|
||||||
|
import net.dnorberg.rivencraft.age.PortCarverPages;
|
||||||
|
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||||
|
import net.minecraft.server.level.WorldGenRegion;
|
||||||
|
import net.minecraft.world.level.biome.BiomeManager;
|
||||||
|
import net.minecraft.world.level.chunk.CarvingMask;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.NoiseChunk;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||||
|
import net.minecraft.world.level.levelgen.material.rule.MaterialRule;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
|
|
||||||
|
/** Merge authored geometry before vanilla consumes its mask with aquifer/surface rules. */
|
||||||
|
@Mixin(NoiseBasedChunkGenerator.class)
|
||||||
|
public abstract class AgeCarversMixin {
|
||||||
|
@Inject(method = "generateCarvers", at = @At(value = "INVOKE",
|
||||||
|
target = "Lnet/minecraft/world/level/chunk/CarvingMask;isEmpty()Z"))
|
||||||
|
private void rivencraft$authoredCarvers(ChunkAccess chunk, Blender blender, NoiseChunk noise,
|
||||||
|
RandomState random, BiomeManager biomes, WorldGenRegion region, MaterialRule material,
|
||||||
|
CallbackInfo ci, @Local CarvingMask mask) {
|
||||||
|
if (region == null) return;
|
||||||
|
var level = region.getLevel();
|
||||||
|
if (level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator generator
|
||||||
|
&& generator.base() == (Object) this && generator.terrainFeatures() != null) {
|
||||||
|
PortCarverPages.carve((NoiseBasedChunkGenerator) (Object) this, chunk, random,
|
||||||
|
region.registryAccess(), generator.terrainFeatures().carvers(), level.getSeed(), mask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package net.dnorberg.rivencraft.mixin;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||||
|
import net.dnorberg.rivencraft.age.PortStructurePages;
|
||||||
|
import net.minecraft.core.Holder;
|
||||||
|
import net.minecraft.core.RegistryAccess;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.LevelHeightAccessor;
|
||||||
|
import net.minecraft.world.level.biome.Biome;
|
||||||
|
import net.minecraft.world.level.biome.BiomeSource;
|
||||||
|
import net.minecraft.world.level.biome.Climate;
|
||||||
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
|
import net.minecraft.world.level.levelgen.RandomState;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.injection.At;
|
||||||
|
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||||
|
|
||||||
|
/** Matches 1.21.1's explicit-page biome override, scoped only to authored Age structures. */
|
||||||
|
@Mixin(Structure.class)
|
||||||
|
public abstract class AgeStructureBiomeMixin {
|
||||||
|
// 26.3 jigsaw placement checks biome columns inside findGenerationPoint,
|
||||||
|
// before the final isValidBiome test. Set the predicate when constructing
|
||||||
|
// the context so both checks see the explicit-page override.
|
||||||
|
@ModifyArg(method = "generate", at = @At(value = "INVOKE",
|
||||||
|
target = "Lnet/minecraft/world/level/levelgen/structure/Structure$GenerationContext;<init>(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/world/level/biome/BiomeSource;Lnet/minecraft/world/level/biome/Climate$Sampler;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager;JLnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/LevelHeightAccessor;Ljava/util/function/Predicate;)V"),
|
||||||
|
index = 9)
|
||||||
|
private Predicate<Holder<Biome>> rivencraft$authoredBiome(
|
||||||
|
RegistryAccess registries, ChunkGenerator chunkGenerator, BiomeSource biomes,
|
||||||
|
Climate.Sampler sampler, RandomState random, StructureTemplateManager templates,
|
||||||
|
long seed, ChunkPos chunk, LevelHeightAccessor height, Predicate<Holder<Biome>> original) {
|
||||||
|
if (chunkGenerator instanceof PortFeatureChunkGenerator generator
|
||||||
|
&& generator.terrainFeatures() != null
|
||||||
|
&& PortStructurePages.authored((Structure) (Object) this, registries,
|
||||||
|
generator.terrainFeatures().structures())) {
|
||||||
|
return biome -> true;
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package net.dnorberg.rivencraft.mixin;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
|
||||||
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
|
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||||
|
|
||||||
|
/** Read-only placement settings for Age-local copies, never edits global placements. */
|
||||||
|
@Mixin(AbstractSpreadingStructurePlacement.class)
|
||||||
|
public interface StructurePlacementAccessor {
|
||||||
|
@Accessor("frequencyReductionMethod")
|
||||||
|
AbstractSpreadingStructurePlacement.FrequencyReductionMethod rivencraft$frequencyReductionMethod();
|
||||||
|
@Accessor("frequency") float rivencraft$frequency();
|
||||||
|
@Accessor("salt") int rivencraft$salt();
|
||||||
|
@Accessor("exclusionZone")
|
||||||
|
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> rivencraft$exclusionZone();
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@ package net.dnorberg.rivencraft.network;
|
|||||||
|
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.age.PortAgeTravel;
|
import net.dnorberg.rivencraft.age.PortAgeTravel;
|
||||||
|
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||||
|
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
|
||||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||||
@@ -125,8 +127,20 @@ public final class PortBookNetworking {
|
|||||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
RuntimeAgeBackend.create(server, id, player.getRandom().nextLong(), biomePlan,
|
long seed = player.getRandom().nextLong();
|
||||||
PortAuthoredWriting.terrain(pages));
|
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
|
||||||
|
if (pages.isEmpty()) {
|
||||||
|
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
|
||||||
|
terrain = random.terrain();
|
||||||
|
biomePlan = random.biomes();
|
||||||
|
}
|
||||||
|
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||||
|
PortAuthoredWriting.rockPlan(pages, seed),
|
||||||
|
PortAuthoredWriting.vegetationPlan(pages),
|
||||||
|
PortAuthoredWriting.dungeonCount(pages),
|
||||||
|
PortAuthoredWriting.lakePlan(pages, seed),
|
||||||
|
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
|
||||||
|
net.dnorberg.rivencraft.age.PortWeatherMode.fromPages(pages));
|
||||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.registry;
|
|||||||
import com.mojang.serialization.MapCodec;
|
import com.mojang.serialization.MapCodec;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
|
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
|
||||||
|
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||||
import net.neoforged.bus.api.IEventBus;
|
import net.neoforged.bus.api.IEventBus;
|
||||||
@@ -15,6 +16,8 @@ public final class PortChunkGenerators {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
|
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() {}
|
private PortChunkGenerators() {}
|
||||||
|
|||||||
+223
-4
@@ -2,17 +2,33 @@ package net.dnorberg.rivencraft.writing;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
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.PortBiomePlan;
|
||||||
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||||
|
import net.dnorberg.rivencraft.age.PortRockPlan;
|
||||||
|
import net.dnorberg.rivencraft.age.PortVegetationPlan;
|
||||||
|
import net.dnorberg.rivencraft.age.PortLakePlan;
|
||||||
|
import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
|
||||||
|
import net.dnorberg.rivencraft.age.PortStructurePages;
|
||||||
|
import net.dnorberg.rivencraft.age.PortCarverPages;
|
||||||
|
import net.dnorberg.rivencraft.age.PortWeatherMode;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.minecraft.core.HolderLookup;
|
import net.minecraft.core.HolderLookup;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
import net.minecraft.resources.ResourceKey;
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.tags.BiomeTags;
|
||||||
|
|
||||||
/** Authored noise terrain and biome distribution supported by the 26.3 backend. */
|
/** Authored noise terrain and biome distribution supported by the 26.3 backend. */
|
||||||
public final class PortAuthoredWriting {
|
public final class PortAuthoredWriting {
|
||||||
private static final int MAX_AUTHORED_BIOMES = 252;
|
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(
|
private static final Identifier STANDARD_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||||
Rivencraft.MOD_ID, "terrain/standard");
|
Rivencraft.MOD_ID, "terrain/standard");
|
||||||
private static final Identifier AMPLIFIED_TERRAIN = Identifier.fromNamespaceAndPath(
|
private static final Identifier AMPLIFIED_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||||
@@ -23,19 +39,93 @@ public final class PortAuthoredWriting {
|
|||||||
Rivencraft.MOD_ID, "terrain/flat");
|
Rivencraft.MOD_ID, "terrain/flat");
|
||||||
private static final Identifier VOID_TERRAIN = Identifier.fromNamespaceAndPath(
|
private static final Identifier VOID_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||||
Rivencraft.MOD_ID, "terrain/void");
|
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() {}
|
private PortAuthoredWriting() {}
|
||||||
|
|
||||||
|
/** Port availability only, not validity of a particular ordered book. */
|
||||||
|
public static boolean isPageImplemented(Identifier page) {
|
||||||
|
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||||
|
var entry = PortSymbolPages.find(page);
|
||||||
|
if (entry == null) return false;
|
||||||
|
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null) return true;
|
||||||
|
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
|
||||||
|
if (isBlockPage(page) && (entry.category().equals("rivencraft:block")
|
||||||
|
|| entry.category().equals("rivencraft:fluid"))) return true;
|
||||||
|
return page.equals(ROCKS) || page.equals(DUNGEONS) || PortLakePlan.isLake(page)
|
||||||
|
|| PortVegetationPlan.isVegetation(page) || PortTerrainFeaturePlan.isSupported(page)
|
||||||
|
|| page.equals(PortTerrainFeaturePlan.SKYLANDS)
|
||||||
|
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|
||||||
|
|| page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|
||||||
|
|| PortStructurePages.supports(page) || PortCarverPages.supports(page);
|
||||||
|
}
|
||||||
|
|
||||||
/** A page never disappears into an Age unless this port can apply its effect. */
|
/** A page never disappears into an Age unless this port can apply its effect. */
|
||||||
public static boolean supports(List<Identifier> pages) {
|
public static boolean supports(List<Identifier> pages) {
|
||||||
boolean terrain = false;
|
boolean terrain = false;
|
||||||
int biomes = 0;
|
int biomes = 0;
|
||||||
int controllers = 0;
|
int controllers = 0;
|
||||||
|
String pendingMaterial = null;
|
||||||
|
int rocks = 0;
|
||||||
|
int vegetation = 0;
|
||||||
|
int dungeons = 0;
|
||||||
|
int lakes = 0;
|
||||||
|
int skylands = 0;
|
||||||
|
int denseOres = 0;
|
||||||
|
int starFissures = 0;
|
||||||
|
int weather = 0;
|
||||||
|
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
|
||||||
|
Map<Identifier, Integer> structures = new HashMap<>();
|
||||||
|
Map<Identifier, Integer> carvers = new HashMap<>();
|
||||||
for (Identifier page : pages) {
|
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) {
|
if (isTerrain(page) && !terrain) {
|
||||||
terrain = true;
|
terrain = true;
|
||||||
|
} else if (PortWeatherMode.forPage(page) != null && weather++ == 0) {
|
||||||
|
// A single independent weather policy for this Age.
|
||||||
} else if (controller(page) != null && controllers++ == 0) {
|
} else if (controller(page) != null && controllers++ == 0) {
|
||||||
// An explicit controller needs an authored pool.
|
// 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)
|
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
|
||||||
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
|
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
|
||||||
&& PortSymbolPages.find(page) != null
|
&& PortSymbolPages.find(page) != null
|
||||||
@@ -44,6 +134,20 @@ public final class PortAuthoredWriting {
|
|||||||
biomes++;
|
biomes++;
|
||||||
} else return false;
|
} 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) {
|
if (controllers != 0) {
|
||||||
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
|
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
|
||||||
.filter(value -> value != null).findFirst().orElseThrow();
|
.filter(value -> value != null).findFirst().orElseThrow();
|
||||||
@@ -60,7 +164,11 @@ public final class PortAuthoredWriting {
|
|||||||
private static boolean isTerrain(Identifier page) {
|
private static boolean isTerrain(Identifier page) {
|
||||||
return page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN)
|
return page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN)
|
||||||
|| page.equals(ISLAND_TERRAIN) || page.equals(FLAT_TERRAIN)
|
|| page.equals(ISLAND_TERRAIN) || page.equals(FLAT_TERRAIN)
|
||||||
|| page.equals(VOID_TERRAIN);
|
|| page.equals(VOID_TERRAIN) || page.equals(CAVE_TERRAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isBlockPage(Identifier page) {
|
||||||
|
return page.getNamespace().equals(Rivencraft.MOD_ID) && page.getPath().startsWith("block/");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PortBiomePlan.Distribution controller(Identifier page) {
|
private static PortBiomePlan.Distribution controller(Identifier page) {
|
||||||
@@ -85,25 +193,136 @@ public final class PortAuthoredWriting {
|
|||||||
if (pages.contains(ISLAND_TERRAIN)) return PortAgeTerrain.ISLAND;
|
if (pages.contains(ISLAND_TERRAIN)) return PortAgeTerrain.ISLAND;
|
||||||
if (pages.contains(FLAT_TERRAIN)) return PortAgeTerrain.FLAT;
|
if (pages.contains(FLAT_TERRAIN)) return PortAgeTerrain.FLAT;
|
||||||
if (pages.contains(VOID_TERRAIN)) return PortAgeTerrain.VOID;
|
if (pages.contains(VOID_TERRAIN)) return PortAgeTerrain.VOID;
|
||||||
|
if (pages.contains(CAVE_TERRAIN)) return PortAgeTerrain.CAVE;
|
||||||
return PortAgeTerrain.STANDARD;
|
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. */
|
/** Resolves all authored biomes from the active registry before creating a level. */
|
||||||
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||||
if (!supports(pages)) throw new IllegalArgumentException(
|
if (!supports(pages)) throw new IllegalArgumentException(
|
||||||
"Unsupported terrain or biome-page combination in this port slice");
|
"Unsupported terrain or biome-page combination in this port slice");
|
||||||
List<Identifier> biomes = new ArrayList<>();
|
List<Identifier> biomes = new ArrayList<>();
|
||||||
|
PortStructurePages.validateRegistry(pages, registries);
|
||||||
|
PortCarverPages.validateRegistry(pages, registries);
|
||||||
PortBiomePlan.Distribution distribution = null;
|
PortBiomePlan.Distribution distribution = null;
|
||||||
|
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
|
||||||
|
boolean hasSurfaceBiome = false;
|
||||||
|
boolean hasOreBiome = false;
|
||||||
for (Identifier page : pages) {
|
for (Identifier page : pages) {
|
||||||
if (isTerrain(page)) continue;
|
|
||||||
PortBiomePlan.Distribution selected = controller(page);
|
PortBiomePlan.Distribution selected = controller(page);
|
||||||
if (selected != null) { distribution = selected; continue; }
|
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);
|
Identifier biome = PortRegistryPages.biomeId(page);
|
||||||
var registry = registries.lookupOrThrow(Registries.BIOME);
|
var registry = registries.lookupOrThrow(Registries.BIOME);
|
||||||
if (registry.get(ResourceKey.create(Registries.BIOME, biome)).isEmpty())
|
var holder = registry.get(ResourceKey.create(Registries.BIOME, biome))
|
||||||
throw new IllegalArgumentException("Unknown authored 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);
|
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)
|
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||||
return new PortBiomePlan(distribution, List.of());
|
return new PortBiomePlan(distribution, List.of());
|
||||||
if (biomes.isEmpty()) return null;
|
if (biomes.isEmpty()) return null;
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ public final class PortRegistryPages {
|
|||||||
public static List<ItemStack> biomePages(HolderLookup.Provider registries, boolean caves) {
|
public static List<ItemStack> biomePages(HolderLookup.Provider registries, boolean caves) {
|
||||||
return registries.lookupOrThrow(Registries.BIOME).listElements()
|
return registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||||
.filter(holder -> !holder.key().identifier().equals(DEFERRED_PLACEHOLDER))
|
.filter(holder -> !holder.key().identifier().equals(DEFERRED_PLACEHOLDER))
|
||||||
.filter(holder -> isCave(holder) == caves)
|
.filter(holder -> isCaveBiome(holder) == caves)
|
||||||
.map(holder -> PageItem.createSymbolPage(biomePageId(holder.key().identifier())))
|
.map(holder -> PageItem.createSymbolPage(biomePageId(holder.key().identifier())))
|
||||||
.sorted((left, right) -> left.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()
|
.sorted((left, right) -> left.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()
|
||||||
.compareTo(right.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()))
|
.compareTo(right.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()))
|
||||||
@@ -76,13 +76,27 @@ public final class PortRegistryPages {
|
|||||||
|
|
||||||
public static List<ItemStack> materialPages(boolean fluids) {
|
public static List<ItemStack> materialPages(boolean fluids) {
|
||||||
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
|
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
|
||||||
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
|
return materialIds(category).stream()
|
||||||
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
|
|
||||||
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
|
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isCave(net.minecraft.core.Holder.Reference<net.minecraft.world.level.biome.Biome> holder) {
|
/** The same full-block pool used by Creative pages, in stable registry-ID order. */
|
||||||
String path = holder.key().identifier().getPath();
|
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)
|
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|
||||||
|| path.equals("deep_dark") || path.endsWith("_caves");
|
|| path.equals("deep_dark") || path.endsWith("_caves");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"replace": false,
|
||||||
|
"values": [
|
||||||
|
"minecraft:lush_caves",
|
||||||
|
"minecraft:dripstone_caves",
|
||||||
|
"minecraft:deep_dark"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"type": "minecraft:weighted_random_selector",
|
||||||
|
"features": [
|
||||||
|
{"data": "minecraft:red_poplar_leaf_litter", "weight": 200},
|
||||||
|
{"data": "minecraft:orange_poplar_leaf_litter", "weight": 240},
|
||||||
|
{"data": "minecraft:yellow_poplar_leaf_litter", "weight": 90}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"feature": "minecraft:monster_room",
|
||||||
|
"placement": [
|
||||||
|
{ "type": "minecraft:count", "count": 8 },
|
||||||
|
{ "type": "minecraft:in_square" },
|
||||||
|
{
|
||||||
|
"type": "minecraft:height_range",
|
||||||
|
"height": {
|
||||||
|
"type": "minecraft:uniform",
|
||||||
|
"min_inclusive": { "above_bottom": 1 },
|
||||||
|
"max_inclusive": { "below_top": 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"feature": "minecraft:fallen_poplar_tree",
|
||||||
|
"placement": [
|
||||||
|
{"type": "minecraft:in_square"},
|
||||||
|
{"type": "minecraft:surface_water_depth_filter", "max_water_depth": 0},
|
||||||
|
{"type": "minecraft:heightmap", "heightmap": "OCEAN_FLOOR"},
|
||||||
|
{
|
||||||
|
"type": "minecraft:block_predicate_filter",
|
||||||
|
"predicate": {"type": "minecraft:would_survive", "state": "minecraft:poplar_sapling"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"feature": "rivencraft:poplar_trees",
|
||||||
|
"placement": [
|
||||||
|
{"type": "minecraft:count", "count": 6},
|
||||||
|
{"type": "minecraft:in_square"},
|
||||||
|
{"type": "minecraft:surface_water_depth_filter", "max_water_depth": 0},
|
||||||
|
{"type": "minecraft:heightmap", "heightmap": "OCEAN_FLOOR"}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"minVersion": "0.8",
|
||||||
|
"package": "net.dnorberg.rivencraft.mixin",
|
||||||
|
"compatibilityLevel": "JAVA_25",
|
||||||
|
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin"],
|
||||||
|
"injectors": {"defaultRequire": 1}
|
||||||
|
}
|
||||||
@@ -4,15 +4,18 @@ issueTrackerURL="https://git.paddedcell.org/dnorberg/rivencraft/issues"
|
|||||||
[[mods]]
|
[[mods]]
|
||||||
modId="${mod_id}"
|
modId="${mod_id}"
|
||||||
version="${mod_version}"
|
version="${mod_version}"
|
||||||
displayName="${mod_name} 26.3 Alpha"
|
displayName="${mod_name} 26.3 Beta"
|
||||||
displayURL="https://git.paddedcell.org/dnorberg/rivencraft"
|
displayURL="https://git.paddedcell.org/dnorberg/rivencraft"
|
||||||
authors="dnorberg and contributors"
|
authors="dnorberg and contributors"
|
||||||
description='''
|
description='''
|
||||||
Experimental Minecraft 26.3 alpha. Provides a runtime Age prototype and basic
|
Experimental Minecraft 26.3 beta with books, writing stations and selected
|
||||||
Linking Book travel. Most Rivencraft gameplay has not yet been ported. Use a
|
authored Age generation. Full 1.21.1 gameplay parity is still in progress.
|
||||||
test world and keep backups; this alpha is not compatible with 1.21.1 worlds.
|
Use a test world and keep backups; 1.21.1 worlds are not compatible.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
[[mixins]]
|
||||||
|
config="rivencraft.mixins.json"
|
||||||
|
|
||||||
[[dependencies.${mod_id}]]
|
[[dependencies.${mod_id}]]
|
||||||
modId="neoforge"
|
modId="neoforge"
|
||||||
type="required"
|
type="required"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Publish a Rivencraft JAR to Modrinth and/or CurseForge.
|
"""Publish a Rivencraft JAR to Gitea, Modrinth, and/or CurseForge.
|
||||||
|
|
||||||
Preview is the default. Tokens live in the Secret Service keyring, never in git.
|
Preview is the default. Tokens live in the Secret Service keyring, never in git.
|
||||||
"""
|
"""
|
||||||
@@ -19,8 +19,9 @@ import urllib.request
|
|||||||
|
|
||||||
MODRINTH_PROJECT = "GH2xfZJ8"
|
MODRINTH_PROJECT = "GH2xfZJ8"
|
||||||
CURSEFORGE_PROJECT = "1766326"
|
CURSEFORGE_PROJECT = "1766326"
|
||||||
|
GITEA_API = "https://git.paddedcell.org/api/v1/repos/dnorberg/rivencraft"
|
||||||
USER_AGENT = "RivencraftReleaseTool/1.0 (https://git.paddedcell.org/dnorberg/rivencraft)"
|
USER_AGENT = "RivencraftReleaseTool/1.0 (https://git.paddedcell.org/dnorberg/rivencraft)"
|
||||||
SITES = ("modrinth", "curseforge")
|
SITES = ("gitea", "modrinth", "curseforge")
|
||||||
|
|
||||||
|
|
||||||
def token_for(site: str) -> str:
|
def token_for(site: str) -> str:
|
||||||
@@ -59,7 +60,13 @@ def store_token(site: str) -> None:
|
|||||||
|
|
||||||
def check_token(site: str) -> None:
|
def check_token(site: str) -> None:
|
||||||
token = token_for(site)
|
token = token_for(site)
|
||||||
if site == "modrinth":
|
if site == "gitea":
|
||||||
|
account = request_json("https://git.paddedcell.org/api/v1/token", token=f"token {token}")
|
||||||
|
scopes = account.get("scopes", [])
|
||||||
|
if "write:repository" not in scopes and "all" not in scopes:
|
||||||
|
raise RuntimeError("Gitea token is valid but lacks write:repository scope")
|
||||||
|
print(f"Gitea API access works for {account['user']['login']} (write:repository)")
|
||||||
|
elif site == "modrinth":
|
||||||
try:
|
try:
|
||||||
account = request_json("https://api.modrinth.com/v2/user", token=token)
|
account = request_json("https://api.modrinth.com/v2/user", token=token)
|
||||||
except RuntimeError as error:
|
except RuntimeError as error:
|
||||||
@@ -81,7 +88,7 @@ def check_token(site: str) -> None:
|
|||||||
|
|
||||||
def request_json(url: str, *, token: str | None = None, token_header: str = "Authorization",
|
def request_json(url: str, *, token: str | None = None, token_header: str = "Authorization",
|
||||||
payload: bytes | None = None, content_type: str | None = None,
|
payload: bytes | None = None, content_type: str | None = None,
|
||||||
method: str = "GET") -> dict | list:
|
method: str = "GET", not_found_ok: bool = False) -> dict | list | None:
|
||||||
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
|
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
|
||||||
if token:
|
if token:
|
||||||
headers[token_header] = token
|
headers[token_header] = token
|
||||||
@@ -92,12 +99,14 @@ def request_json(url: str, *, token: str | None = None, token_header: str = "Aut
|
|||||||
with urllib.request.urlopen(request, timeout=120) as response:
|
with urllib.request.urlopen(request, timeout=120) as response:
|
||||||
return json.load(response)
|
return json.load(response)
|
||||||
except urllib.error.HTTPError as error:
|
except urllib.error.HTTPError as error:
|
||||||
|
if error.code == 404 and not_found_ok:
|
||||||
|
return None
|
||||||
# Error bodies can contain server details, but never echo tokens.
|
# Error bodies can contain server details, but never echo tokens.
|
||||||
detail = error.read(600).decode("utf-8", "replace")
|
detail = error.read(600).decode("utf-8", "replace")
|
||||||
raise RuntimeError(f"{url}: HTTP {error.code}: {detail}") from None
|
raise RuntimeError(f"{url}: HTTP {error.code}: {detail}") from None
|
||||||
|
|
||||||
|
|
||||||
def multipart(fields: dict[str, str], jar: Path) -> tuple[bytes, str]:
|
def multipart(fields: dict[str, str], jar: Path, *, file_field: str = "file") -> tuple[bytes, str]:
|
||||||
boundary = "rivencraft-" + secrets.token_hex(16)
|
boundary = "rivencraft-" + secrets.token_hex(16)
|
||||||
chunks = []
|
chunks = []
|
||||||
for name, value in fields.items():
|
for name, value in fields.items():
|
||||||
@@ -108,7 +117,7 @@ def multipart(fields: dict[str, str], jar: Path) -> tuple[bytes, str]:
|
|||||||
filename = jar.name.replace('"', "")
|
filename = jar.name.replace('"', "")
|
||||||
mime = mimetypes.guess_type(filename)[0] or "application/java-archive"
|
mime = mimetypes.guess_type(filename)[0] or "application/java-archive"
|
||||||
chunks.extend((
|
chunks.extend((
|
||||||
f"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\nContent-Type: {mime}\r\n\r\n".encode(),
|
f"--{boundary}\r\nContent-Disposition: form-data; name=\"{file_field}\"; filename=\"{filename}\"\r\nContent-Type: {mime}\r\n\r\n".encode(),
|
||||||
jar.read_bytes(), b"\r\n", f"--{boundary}--\r\n".encode(),
|
jar.read_bytes(), b"\r\n", f"--{boundary}--\r\n".encode(),
|
||||||
))
|
))
|
||||||
return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
|
return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
|
||||||
@@ -140,7 +149,6 @@ def release_metadata(args: argparse.Namespace) -> tuple[dict, dict]:
|
|||||||
"gameVersionNames": [args.minecraft, "NeoForge", "Client", "Server", f"Java {args.java}"],
|
"gameVersionNames": [args.minecraft, "NeoForge", "Client", "Server", f"Java {args.java}"],
|
||||||
"releaseType": args.channel,
|
"releaseType": args.channel,
|
||||||
"isMarkedForManualRelease": False,
|
"isMarkedForManualRelease": False,
|
||||||
"relations": {"projects": []},
|
|
||||||
}
|
}
|
||||||
return modrinth, curseforge
|
return modrinth, curseforge
|
||||||
|
|
||||||
@@ -172,6 +180,53 @@ def publish_curseforge(metadata: dict, jar: Path) -> None:
|
|||||||
print(f"CurseForge: https://www.curseforge.com/minecraft/mc-mods/rivencraft/files/{result['id']} (pending approval)")
|
print(f"CurseForge: https://www.curseforge.com/minecraft/mc-mods/rivencraft/files/{result['id']} (pending approval)")
|
||||||
|
|
||||||
|
|
||||||
|
def publish_gitea(args: argparse.Namespace) -> None:
|
||||||
|
base_version, suffix = args.version.split("-", 1)
|
||||||
|
if suffix != f"{args.channel}.1":
|
||||||
|
raise RuntimeError("Gitea tag convention requires a version ending in the selected channel and .1")
|
||||||
|
tag = f"V{base_version}-mc{args.minecraft}-{suffix}"
|
||||||
|
# The release must refer to a tag already pushed by the normal Git workflow.
|
||||||
|
remote_tag = subprocess.run(
|
||||||
|
["git", "ls-remote", "origin", f"refs/tags/{tag}"],
|
||||||
|
text=True, capture_output=True, check=True,
|
||||||
|
)
|
||||||
|
if not remote_tag.stdout.strip():
|
||||||
|
raise RuntimeError(f"Push the Gitea tag {tag} before publishing")
|
||||||
|
|
||||||
|
token = f"token {token_for('gitea')}"
|
||||||
|
release_url = f"{GITEA_API}/releases/tags/{tag}"
|
||||||
|
release = request_json(release_url, token=token, not_found_ok=True)
|
||||||
|
notes = args.changelog.read_text(encoding="utf-8")
|
||||||
|
title = f"Rivencraft {args.minecraft} {args.channel} {args.version}"
|
||||||
|
if release is None:
|
||||||
|
metadata = {
|
||||||
|
"tag_name": tag,
|
||||||
|
"target_commitish": "main",
|
||||||
|
"name": title,
|
||||||
|
"body": notes,
|
||||||
|
"draft": False,
|
||||||
|
"prerelease": args.channel != "release",
|
||||||
|
}
|
||||||
|
release = request_json(
|
||||||
|
f"{GITEA_API}/releases", token=token,
|
||||||
|
payload=json.dumps(metadata).encode(), content_type="application/json", method="POST",
|
||||||
|
)
|
||||||
|
elif release.get("name") != title or release.get("body", "").strip() != notes.strip():
|
||||||
|
raise RuntimeError(f"Gitea release {tag} exists with different notes or title; inspect it before retrying")
|
||||||
|
|
||||||
|
assets = release.get("assets", [])
|
||||||
|
if any(asset.get("name") == args.jar.name for asset in assets):
|
||||||
|
raise RuntimeError(f"Gitea release {tag} already has {args.jar.name}")
|
||||||
|
body, content_type = multipart({}, args.jar, file_field="attachment")
|
||||||
|
attachment = request_json(
|
||||||
|
f"{GITEA_API}/releases/{release['id']}/assets", token=token,
|
||||||
|
payload=body, content_type=content_type, method="POST",
|
||||||
|
)
|
||||||
|
if attachment.get("name") != args.jar.name or attachment.get("size") != args.jar.stat().st_size:
|
||||||
|
raise RuntimeError("Gitea returned unexpected attachment metadata; inspect the release")
|
||||||
|
print(f"Gitea: {release['html_url']}")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
sub = parser.add_subparsers(dest="command", required=True)
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
@@ -187,7 +242,7 @@ def main() -> None:
|
|||||||
publish.add_argument("--minecraft", required=True)
|
publish.add_argument("--minecraft", required=True)
|
||||||
publish.add_argument("--java", required=True)
|
publish.add_argument("--java", required=True)
|
||||||
publish.add_argument("--channel", choices=("alpha", "beta", "release"), required=True)
|
publish.add_argument("--channel", choices=("alpha", "beta", "release"), required=True)
|
||||||
publish.add_argument("--site", choices=(*SITES, "both"), default="both")
|
publish.add_argument("--site", choices=(*SITES, "both", "all"), default="all")
|
||||||
publish.add_argument("--publish", action="store_true", help="Actually upload; omitted by default")
|
publish.add_argument("--publish", action="store_true", help="Actually upload; omitted by default")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.command == "store-token":
|
if args.command == "store-token":
|
||||||
@@ -205,13 +260,15 @@ def main() -> None:
|
|||||||
if args.jar.name != f"rivencraft-{args.minecraft}-{args.version}.jar":
|
if args.jar.name != f"rivencraft-{args.minecraft}-{args.version}.jar":
|
||||||
raise RuntimeError("JAR filename does not match Minecraft and release versions")
|
raise RuntimeError("JAR filename does not match Minecraft and release versions")
|
||||||
modrinth, curseforge = release_metadata(args)
|
modrinth, curseforge = release_metadata(args)
|
||||||
sites = SITES if args.site == "both" else (args.site,)
|
sites = SITES if args.site == "all" else (("modrinth", "curseforge") if args.site == "both" else (args.site,))
|
||||||
print(f"JAR: {args.jar.resolve()} ({args.jar.stat().st_size} bytes)")
|
print(f"JAR: {args.jar.resolve()} ({args.jar.stat().st_size} bytes)")
|
||||||
print(f"Release: {args.version} | {args.channel} | Minecraft {args.minecraft} | NeoForge | Java {args.java}")
|
print(f"Release: {args.version} | {args.channel} | Minecraft {args.minecraft} | NeoForge | Java {args.java}")
|
||||||
print(f"Destinations: {', '.join(sites)}")
|
print(f"Destinations: {', '.join(sites)}")
|
||||||
if not args.publish:
|
if not args.publish:
|
||||||
print("PREVIEW ONLY: add --publish to upload")
|
print("PREVIEW ONLY: add --publish to upload")
|
||||||
return
|
return
|
||||||
|
if "gitea" in sites:
|
||||||
|
publish_gitea(args)
|
||||||
if "modrinth" in sites:
|
if "modrinth" in sites:
|
||||||
publish_modrinth(modrinth, args.jar)
|
publish_modrinth(modrinth, args.jar)
|
||||||
if "curseforge" in sites:
|
if "curseforge" in sites:
|
||||||
|
|||||||
+180
-1
@@ -71,8 +71,24 @@ send_and_wait 'rivencraft_port age create smoke_alpha 123456789' 'Created rivenc
|
|||||||
send_and_wait 'rivencraft_port age create smoke_beta 987654321' 'Created rivencraft:age/smoke_beta with seed 987654321'
|
send_and_wait 'rivencraft_port age create smoke_beta 987654321' 'Created rivencraft:age/smoke_beta with seed 987654321'
|
||||||
send_and_wait 'rivencraft_port age create smoke_flat 24680 flat' 'Created rivencraft:age/smoke_flat with seed 24680 terrain=FLAT'
|
send_and_wait 'rivencraft_port age create smoke_flat 24680 flat' 'Created rivencraft:age/smoke_flat with seed 24680 terrain=FLAT'
|
||||||
send_and_wait 'rivencraft_port age create smoke_void 13579 void' 'Created rivencraft:age/smoke_void with seed 13579 terrain=VOID'
|
send_and_wait 'rivencraft_port age create smoke_void 13579 void' 'Created rivencraft:age/smoke_void with seed 13579 terrain=VOID'
|
||||||
|
send_and_wait 'rivencraft_port age create smoke_cave 112233 cave' 'Created rivencraft:age/smoke_cave with seed 112233 terrain=CAVE'
|
||||||
send_and_wait 'rivencraft_port age create_multi smoke_flat_multi 24681 flat' 'Created rivencraft:age/smoke_flat_multi with seed 24681 terrain=FLAT'
|
send_and_wait 'rivencraft_port age create_multi smoke_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_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
|
# The index must reach disk while the server is still running. A graceful
|
||||||
# shutdown would hide the interrupted-save regression this checks.
|
# shutdown would hide the interrupted-save regression this checks.
|
||||||
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
|
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
|
||||||
@@ -84,18 +100,118 @@ gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_alpha'
|
|||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_beta'
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_beta'
|
||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat'
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat'
|
||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void'
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void'
|
||||||
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave'
|
||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat_multi'
|
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat_multi'
|
||||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void_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_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_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave seed=112233 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_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_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_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_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_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_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 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_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'
|
send_and_wait 'execute in rivencraft:age/smoke_void if block 0 0 0 minecraft:air run say Void remains empty' 'Void remains empty'
|
||||||
@@ -105,10 +221,13 @@ send_and_wait 'execute in rivencraft:age/smoke_void_multi if block 0 0 0 minecra
|
|||||||
send_and_wait 'rivencraft_port age biome smoke_flat_multi 128 128' 'rivencraft:age/smoke_flat_multi biome at 128,128=minecraft:plains'
|
send_and_wait 'rivencraft_port age biome smoke_flat_multi 128 128' 'rivencraft:age/smoke_flat_multi biome at 128,128=minecraft:plains'
|
||||||
send_and_wait 'rivencraft_port age biome smoke_flat_multi 384 128' 'rivencraft:age/smoke_flat_multi biome at 384,128=minecraft:forest'
|
send_and_wait 'rivencraft_port age biome smoke_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_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_cave_multi 128 128' 'rivencraft:age/smoke_cave_multi biome at 128,128=minecraft:lush_caves'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
|
||||||
alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
beta_before=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
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=//')
|
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=//')
|
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
|
if [[ "$alpha_before" == "$beta_before" ]]; then
|
||||||
printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2
|
printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -125,14 +244,46 @@ send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alp
|
|||||||
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
||||||
|
send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave seed=112233 chunkOffset=0 terrain='
|
||||||
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
|
send_and_wait 'rivencraft_port age sample smoke_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_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_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_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
|
||||||
|
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
|
||||||
alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||||
beta_after=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
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=//')
|
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=//')
|
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" ]] || {
|
[[ "$alpha_before" == "$alpha_after" && "$beta_before" == "$beta_after" ]] || {
|
||||||
printf 'Age terrain changed across restart: %s/%s -> %s/%s\n' \
|
printf 'Age terrain changed across restart: %s/%s -> %s/%s\n' \
|
||||||
"$alpha_before" "$beta_before" "$alpha_after" "$beta_after" >&2
|
"$alpha_before" "$beta_before" "$alpha_after" "$beta_after" >&2
|
||||||
@@ -143,5 +294,33 @@ void_after=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain
|
|||||||
"$flat_before" "$void_before" "$flat_after" "$void_after" >&2
|
"$flat_before" "$void_before" "$flat_after" "$void_after" >&2
|
||||||
exit 1
|
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
|
stop_server
|
||||||
printf '26.3 dedicated-server smoke passed: Standard, Flat and Void Ages persisted across restart.\n'
|
printf '26.3 dedicated-server smoke passed: terrain, biomes, Rocks, vegetation, Dungeons, lakes, and Floating Islands persisted across restart.\n'
|
||||||
Reference in new issue
Block a user