Public Access
Port structures, weather, vegetation and safe surface arrivals to 26.3 beta
Build / build (push) Canceled after 0s
Build / build (push) Canceled after 0s
This commit is contained in:
1 parent
c223f3fdca
commit
48fbae9be9
38 files changed
+1474
-93
No files matched your search
@@ -13,10 +13,11 @@ the 1.12.2 mod.
|
|||||||
The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
|
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,
|
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
|
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
|
||||||
supports authored terrain/biomes and a growing set of generation features,
|
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.
|
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
|
See the [26.3 beta guide](ports/mc263/README.md) and
|
||||||
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.54-beta.1.md).
|
[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.
|
Keep the builds and their saves separate, and back up worlds before updating.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|||||||
@@ -681,6 +681,225 @@ resource packaging, and static checks pass for this slice. Generation, visual
|
|||||||
rendering, actual return travel, and restart checks are left to the player;
|
rendering, actual return travel, and restart checks are left to the player;
|
||||||
no development server is started.
|
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
|
||||||
|
|||||||
+210
-3
@@ -4,9 +4,9 @@ This is an **experimental beta port** for issues #97–#104, not gameplay parity
|
|||||||
with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and
|
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, Void, or Cave 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
|
||||||
@@ -351,6 +351,213 @@ 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
|
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.
|
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,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,9 +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/models/block/star_fissure.json'
|
||||||
include 'assets/rivencraft/blockstates/star_fissure.json'
|
include 'assets/rivencraft/blockstates/star_fissure.json'
|
||||||
include 'assets/rivencraft/sounds/linking/link-fissure.ogg'
|
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.54-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."
|
||||||
}
|
}
|
||||||
@@ -59,9 +59,17 @@ public final class Rivencraft {
|
|||||||
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 =
|
private static final DeferredRegister<SoundEvent> SOUNDS =
|
||||||
DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID);
|
DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID);
|
||||||
public static final Supplier<SoundEvent> LINK_FISSURE = SOUNDS.register(
|
public static final Supplier<SoundEvent> LINK = sound("linking.link");
|
||||||
"linking.link-fissure", () -> SoundEvent.createVariableRangeEvent(
|
public static final Supplier<SoundEvent> LINK_DISARM = sound("linking.link-disarm");
|
||||||
Identifier.fromNamespaceAndPath(MOD_ID, "linking.link-fissure")));
|
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 =
|
||||||
@@ -170,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);
|
||||||
|
|||||||
@@ -68,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())
|
||||||
@@ -302,6 +318,62 @@ 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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import net.minecraft.resources.ResourceKey;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.level.dimension.LevelStem;
|
||||||
|
import net.minecraft.world.level.saveddata.WeatherData;
|
||||||
|
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||||
|
|
||||||
|
/** Runtime Age with independent native weather, rather than 26.3's shared server cycle. */
|
||||||
|
final class PortAgeLevel extends ServerLevel {
|
||||||
|
private final WeatherData ageWeather;
|
||||||
|
private final PortWeatherSettings weatherSettings;
|
||||||
|
private final boolean cave;
|
||||||
|
|
||||||
|
PortAgeLevel(MinecraftServer server, ResourceKey<Level> key, LevelStem stem,
|
||||||
|
long biomeZoomSeed, PortAgeTerrain terrain) {
|
||||||
|
super(server, server.executor, server.storageSource,
|
||||||
|
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
|
||||||
|
key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false);
|
||||||
|
ageWeather = getDataStorage().computeIfAbsent(WeatherData.TYPE);
|
||||||
|
weatherSettings = getDataStorage().computeIfAbsent(PortWeatherSettings.TYPE);
|
||||||
|
cave = terrain == PortAgeTerrain.CAVE;
|
||||||
|
PortAgeWeather.apply(this);
|
||||||
|
// The base constructor initializes visual levels from server weather.
|
||||||
|
// Replace them before publishing this Age or sending its join packets.
|
||||||
|
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||||
|
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WeatherData getWeatherData() {
|
||||||
|
// The base constructor may query weather before our fields are initialized.
|
||||||
|
return ageWeather == null ? super.getWeatherData() : ageWeather;
|
||||||
|
}
|
||||||
|
|
||||||
|
PortWeatherMode weatherMode() { return cave ? PortWeatherMode.OFF : weatherSettings.mode(); }
|
||||||
|
|
||||||
|
void configureWeather(PortWeatherMode mode) {
|
||||||
|
weatherSettings.setMode(mode);
|
||||||
|
PortAgeWeather.apply(this);
|
||||||
|
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||||
|
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||||
|
getDataStorage().saveAndJoin();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,8 +11,6 @@ import net.minecraft.server.level.ServerLevel;
|
|||||||
import net.minecraft.server.level.ServerPlayer;
|
import net.minecraft.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 {
|
||||||
@@ -28,7 +26,9 @@ public final class PortAgeTravel {
|
|||||||
boolean fissure = target.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
|
boolean fissure = target.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
|
||||||
&& features.terrainFeatures() != null && features.terrainFeatures().starFissures();
|
&& features.terrainFeatures() != null && features.terrainFeatures().starFissures();
|
||||||
BlockPos landing = fissure ? PortStarFissureFeature.savedSpawn(target) : null;
|
BlockPos landing = fissure ? PortStarFissureFeature.savedSpawn(target) : null;
|
||||||
if (landing != null) landing = findFootedDestination(target, landing).orElse(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) 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));
|
if (fissure) PortStarFissureFeature.generateOnce(target, landing, isCaveTerrain(target));
|
||||||
@@ -38,8 +38,6 @@ public final class PortAgeTravel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static BlockPos findLanding(ServerLevel target) {
|
private static BlockPos findLanding(ServerLevel target) {
|
||||||
net.minecraft.world.level.chunk.ChunkGenerator generator = target.getChunkSource().getGenerator();
|
|
||||||
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
|
|
||||||
if (isCaveTerrain(target)) {
|
if (isCaveTerrain(target)) {
|
||||||
// The cave roof is not a valid arrival surface. Search down from
|
// The cave roof is not a valid arrival surface. Search down from
|
||||||
// Legacy's Y=64 start, then construct the normal safety platform
|
// Legacy's Y=64 start, then construct the normal safety platform
|
||||||
@@ -57,36 +55,40 @@ public final class PortAgeTravel {
|
|||||||
}
|
}
|
||||||
return makePlatform(target, new BlockPos(0, 64, 0));
|
return makePlatform(target, new BlockPos(0, 64, 0));
|
||||||
}
|
}
|
||||||
// A Flat Age has a known surface. Use it directly before a heightmap
|
BlockPos surface = findSurfaceLanding(target, 0, 0);
|
||||||
// fallback can place a platform in the air above a low superflat world.
|
if (surface != null) return surface;
|
||||||
FlatLevelGeneratorSettings flatSettings = switch (generator) {
|
// Only build above the actual highest obstruction, never in a pocket
|
||||||
case FlatLevelSource flat -> flat.settings();
|
// beneath it. Void Ages retain the usual platform floor at Y=63.
|
||||||
case PortFlatChunkGenerator flat -> flat.settings();
|
BlockPos obstruction = highestObstruction(target, 0, 0);
|
||||||
default -> null;
|
int y = obstruction == null ? 64 : Math.max(64, obstruction.getY() + 3);
|
||||||
};
|
return makePlatform(target, new BlockPos(0, y, 0));
|
||||||
if (flatSettings != null && !flatSettings.getLayersInfo().isEmpty()) {
|
}
|
||||||
int surfaceY = target.getMinY() + flatSettings.getLayersInfo().stream()
|
|
||||||
.mapToInt(layer -> layer.getHeight()).sum();
|
static BlockPos findSurfaceLanding(ServerLevel target, int originX, int originZ) {
|
||||||
BlockPos surface = new BlockPos(0, surfaceY, 0);
|
|
||||||
if (hasFooting(target, surface)) return surface;
|
|
||||||
}
|
|
||||||
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. */
|
||||||
return makePlatform(target, 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) {
|
private static boolean isCaveTerrain(ServerLevel target) {
|
||||||
@@ -119,8 +121,13 @@ 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);
|
||||||
|
return (floorState.isFaceSturdy(target, floor, Direction.UP)
|
||||||
|
|| floorState.is(net.minecraft.tags.BlockTags.LEAVES))
|
||||||
|
&& floorState.getFluidState().isEmpty()
|
||||||
&& target.getFluidState(foot).isEmpty()
|
&& target.getFluidState(foot).isEmpty()
|
||||||
&& target.getFluidState(foot.above()).isEmpty()
|
&& target.getFluidState(foot.above()).isEmpty()
|
||||||
&& passable(target, foot) && passable(target, foot.above());
|
&& passable(target, foot) && passable(target, foot.above());
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
-5
@@ -113,12 +113,25 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
|||||||
}
|
}
|
||||||
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
||||||
RandomState random, long seed) {
|
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);
|
return base.createState(sets, random, seed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasStructurePages() {
|
||||||
|
return terrainFeatures != null && !terrainFeatures.structures().isEmpty();
|
||||||
|
}
|
||||||
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||||
StructureManager structures, ChunkAccess chunk,
|
StructureManager structures, ChunkAccess chunk,
|
||||||
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||||
base.createStructures(registries, state, structures, chunk, templates, 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) {
|
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
||||||
base.createReferences(level, structures, chunk);
|
base.createReferences(level, structures, chunk);
|
||||||
@@ -250,19 +263,27 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
|||||||
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
|
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
|
||||||
Holder<Biome> biome = level.getBiome(new BlockPos(
|
Holder<Biome> biome = level.getBiome(new BlockPos(
|
||||||
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
|
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
|
||||||
if (biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)) return;
|
boolean surfaceBiome = !biome.is(BiomeTags.IS_END) && !PortRegistryPages.isCaveBiome(biome);
|
||||||
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||||
|
List<BlockPos> mushroomHosts = null;
|
||||||
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
|
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
|
||||||
Identifier page = vegetation.pages().get(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);
|
Identifier placedId = PortVegetationPlan.placedFeature(page);
|
||||||
PlacedFeature vanilla = registry.getOrThrow(
|
PlacedFeature vanilla = registry.getOrThrow(
|
||||||
ResourceKey.create(Registries.PLACED_FEATURE, placedId)).value();
|
ResourceKey.create(Registries.PLACED_FEATURE, placedId)).value();
|
||||||
PlacedFeature authored = new PlacedFeature(vanilla.feature(), vanilla.placement().stream()
|
PlacedFeature authored = new PlacedFeature(vanilla.feature(), vanilla.placement().stream()
|
||||||
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList());
|
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList());
|
||||||
RandomSource random = RandomSource.create(chunkSeed
|
|
||||||
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
|
|
||||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
|
||||||
authored.place(level, this, random, origin);
|
authored.place(level, this, random, origin);
|
||||||
|
if (PortVegetationPlan.addsLogs(page)) mushroomHosts = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ import net.dnorberg.rivencraft.Rivencraft;
|
|||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
/** Frozen terrain filters and ordered solid/surface features for an Age. */
|
/** Frozen terrain filters and ordered solid/surface features for an Age. */
|
||||||
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
|
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(
|
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
|
||||||
Rivencraft.MOD_ID, "feature/floating_islands");
|
Rivencraft.MOD_ID, "feature/floating_islands");
|
||||||
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
|
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
|
||||||
@@ -27,7 +28,9 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
|
|||||||
Codec.BOOL.optionalFieldOf("skylands", false).forGetter(PortTerrainFeaturePlan::skylands),
|
Codec.BOOL.optionalFieldOf("skylands", false).forGetter(PortTerrainFeaturePlan::skylands),
|
||||||
Codec.intRange(0, MAX_PER_FEATURE).optionalFieldOf("dense_ores", 0)
|
Codec.intRange(0, MAX_PER_FEATURE).optionalFieldOf("dense_ores", 0)
|
||||||
.forGetter(PortTerrainFeaturePlan::denseOres),
|
.forGetter(PortTerrainFeaturePlan::denseOres),
|
||||||
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures)
|
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));
|
).apply(instance, PortTerrainFeaturePlan::new));
|
||||||
|
|
||||||
public record Choice(Identifier page, Identifier material) {
|
public record Choice(Identifier page, Identifier material) {
|
||||||
@@ -46,9 +49,13 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
|
|||||||
|
|
||||||
public PortTerrainFeaturePlan {
|
public PortTerrainFeaturePlan {
|
||||||
choices = List.copyOf(choices);
|
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)
|
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
|
||||||
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
|
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
|
||||||
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures)
|
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty())
|
||||||
throw new IllegalArgumentException("Terrain feature plan must not be empty");
|
throw new IllegalArgumentException("Terrain feature plan must not be empty");
|
||||||
var counts = new HashMap<Identifier, Integer>();
|
var counts = new HashMap<Identifier, Integer>();
|
||||||
for (Choice choice : choices) {
|
for (Choice choice : choices) {
|
||||||
@@ -57,6 +64,15 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres) {
|
||||||
this(choices, skylands, denseOres, false);
|
this(choices, skylands, denseOres, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,15 @@ import java.util.List;
|
|||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
/** Ordered, frozen authored vegetation pages for surface Ages. */
|
/** Ordered, frozen plants and log decorations; not all require surface terrain. */
|
||||||
public record PortVegetationPlan(List<Identifier> pages) {
|
public record PortVegetationPlan(List<Identifier> pages) {
|
||||||
public static final Identifier FLOWERS = page("flowers");
|
public static final Identifier FLOWERS = page("flowers");
|
||||||
public static final Identifier TREES = page("trees");
|
public static final Identifier TREES = page("trees");
|
||||||
public static final Identifier HUGE_TREES = page("huge_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(
|
public static final Codec<PortVegetationPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||||
Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages)
|
Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages)
|
||||||
).apply(instance, PortVegetationPlan::new));
|
).apply(instance, PortVegetationPlan::new));
|
||||||
@@ -22,16 +26,30 @@ public record PortVegetationPlan(List<Identifier> pages) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isVegetation(Identifier page) {
|
public static boolean isVegetation(Identifier page) {
|
||||||
return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES);
|
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) {
|
public static Identifier placedFeature(Identifier page) {
|
||||||
if (page.equals(FLOWERS)) return Identifier.withDefaultNamespace("flower_default");
|
if (page.equals(FLOWERS)) return Identifier.withDefaultNamespace("flower_default");
|
||||||
if (page.equals(TREES)) return Identifier.withDefaultNamespace("trees_plains");
|
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(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);
|
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) {
|
private static Identifier page(String name) {
|
||||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,6 @@ import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
|||||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
import net.minecraft.world.level.levelgen.flat.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;
|
||||||
@@ -106,11 +105,20 @@ public final class RuntimeAgeBackend {
|
|||||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||||
PortTerrainFeaturePlan terrainFeatures) {
|
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, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||||
|
((PortAgeLevel) level).configureWeather(weather);
|
||||||
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
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
|
||||||
@@ -403,9 +411,7 @@ public final class RuntimeAgeBackend {
|
|||||||
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
|
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.
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ public final class StarFissureBlock extends EndPortalBlock {
|
|||||||
BlockPos landing = net.dnorberg.rivencraft.age.PortAgeTravel.findFootedDestination(overworld, spawn).orElse(spawn);
|
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);
|
level.playSound(null, pos, Rivencraft.LINK_FISSURE.get(), SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||||
return new TeleportTransition(overworld, Vec3.atBottomCenterOf(landing), entity.getDeltaMovement(),
|
return new TeleportTransition(overworld, Vec3.atBottomCenterOf(landing), entity.getDeltaMovement(),
|
||||||
entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET);
|
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);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,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();
|
||||||
|
}
|
||||||
@@ -139,7 +139,8 @@ public final class PortBookNetworking {
|
|||||||
PortAuthoredWriting.vegetationPlan(pages),
|
PortAuthoredWriting.vegetationPlan(pages),
|
||||||
PortAuthoredWriting.dungeonCount(pages),
|
PortAuthoredWriting.dungeonCount(pages),
|
||||||
PortAuthoredWriting.lakePlan(pages, seed),
|
PortAuthoredWriting.lakePlan(pages, seed),
|
||||||
PortAuthoredWriting.terrainFeaturePlan(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;
|
||||||
|
|||||||
+49
-10
@@ -12,6 +12,9 @@ import net.dnorberg.rivencraft.age.PortRockPlan;
|
|||||||
import net.dnorberg.rivencraft.age.PortVegetationPlan;
|
import net.dnorberg.rivencraft.age.PortVegetationPlan;
|
||||||
import net.dnorberg.rivencraft.age.PortLakePlan;
|
import net.dnorberg.rivencraft.age.PortLakePlan;
|
||||||
import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
|
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;
|
||||||
@@ -45,6 +48,23 @@ public final class PortAuthoredWriting {
|
|||||||
|
|
||||||
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;
|
||||||
@@ -58,13 +78,18 @@ public final class PortAuthoredWriting {
|
|||||||
int skylands = 0;
|
int skylands = 0;
|
||||||
int denseOres = 0;
|
int denseOres = 0;
|
||||||
int starFissures = 0;
|
int starFissures = 0;
|
||||||
|
int weather = 0;
|
||||||
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
|
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")
|
if (pendingMaterial != null && !(pendingMaterial.equals("rivencraft:block")
|
||||||
&& (page.equals(ROCKS) || PortTerrainFeaturePlan.acceptsMaterial(page))
|
&& (page.equals(ROCKS) || PortTerrainFeaturePlan.acceptsMaterial(page))
|
||||||
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
|
|| 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
|
} else if (isBlockPage(page) && pendingMaterial == null
|
||||||
@@ -91,6 +116,12 @@ public final class PortAuthoredWriting {
|
|||||||
} else if (page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|
} else if (page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|
||||||
&& starFissures < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
&& starFissures < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||||
starFissures++;
|
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) {
|
} else if (PortVegetationPlan.isVegetation(page) && vegetation < MAX_VEGETATION) {
|
||||||
vegetation++;
|
vegetation++;
|
||||||
} else if (page.equals(DUNGEONS) && dungeons < MAX_DUNGEONS) {
|
} else if (page.equals(DUNGEONS) && dungeons < MAX_DUNGEONS) {
|
||||||
@@ -103,11 +134,17 @@ public final class PortAuthoredWriting {
|
|||||||
biomes++;
|
biomes++;
|
||||||
} else return false;
|
} else return false;
|
||||||
}
|
}
|
||||||
if (pendingMaterial != null || (rocks > 0 || vegetation > 0)
|
if (pendingMaterial != null || (rocks > 0 || pages.stream().anyMatch(PortVegetationPlan::requiresSurface))
|
||||||
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||||
if ((dungeons > 0 || lakes > 0) && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|
if ((dungeons > 0 || lakes > 0) && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|
||||||
|| pages.contains(CAVE_TERRAIN))) return false;
|
|| pages.contains(CAVE_TERRAIN))) return false;
|
||||||
if (denseOres > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_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.contains(PortTerrainFeaturePlan.FLOATING_ISLANDS) && pages.contains(CAVE_TERRAIN)) return false;
|
||||||
if (pages.stream().anyMatch(PortTerrainFeaturePlan::isSurface)
|
if (pages.stream().anyMatch(PortTerrainFeaturePlan::isSurface)
|
||||||
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||||
@@ -184,8 +221,10 @@ public final class PortAuthoredWriting {
|
|||||||
boolean skylands = pages.contains(PortTerrainFeaturePlan.SKYLANDS);
|
boolean skylands = pages.contains(PortTerrainFeaturePlan.SKYLANDS);
|
||||||
int denseOres = (int) pages.stream().filter(PortTerrainFeaturePlan.DENSE_ORES::equals).count();
|
int denseOres = (int) pages.stream().filter(PortTerrainFeaturePlan.DENSE_ORES::equals).count();
|
||||||
boolean starFissures = pages.contains(PortTerrainFeaturePlan.STAR_FISSURES);
|
boolean starFissures = pages.contains(PortTerrainFeaturePlan.STAR_FISSURES);
|
||||||
return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures ? null
|
List<Identifier> structures = pages.stream().filter(PortStructurePages::supports).toList();
|
||||||
: new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures);
|
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) {
|
private static List<Identifier> solidMaterials(List<Identifier> pages, long seed, Identifier feature) {
|
||||||
@@ -255,19 +294,19 @@ public final class PortAuthoredWriting {
|
|||||||
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 caveTerrain = pages.contains(CAVE_TERRAIN);
|
||||||
boolean hasSurfaceBiome = false;
|
boolean hasSurfaceBiome = false;
|
||||||
boolean hasOreBiome = false;
|
boolean hasOreBiome = false;
|
||||||
for (Identifier page : pages) {
|
for (Identifier page : pages) {
|
||||||
if (isTerrain(page) || page.equals(ROCKS) || page.equals(DUNGEONS)
|
|
||||||
|| page.equals(PortTerrainFeaturePlan.SKYLANDS)
|
|
||||||
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|
|
||||||
|| page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|
|
||||||
|| PortLakePlan.isLake(page) || PortTerrainFeaturePlan.isSupported(page) || isBlockPage(page)
|
|
||||||
|| PortVegetationPlan.isVegetation(page)) continue;
|
|
||||||
PortBiomePlan.Distribution selected = controller(page);
|
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);
|
||||||
var holder = registry.get(ResourceKey.create(Registries.BIOME, biome))
|
var holder = registry.get(ResourceKey.create(Registries.BIOME, biome))
|
||||||
@@ -279,7 +318,7 @@ public final class PortAuthoredWriting {
|
|||||||
if (net.dnorberg.rivencraft.age.PortDenseOres.hasOres(holder)) hasOreBiome = true;
|
if (net.dnorberg.rivencraft.age.PortDenseOres.hasOres(holder)) hasOreBiome = true;
|
||||||
biomes.add(biome);
|
biomes.add(biome);
|
||||||
}
|
}
|
||||||
if (pages.stream().anyMatch(PortVegetationPlan::isVegetation)
|
if (pages.stream().anyMatch(PortVegetationPlan::requiresSurface)
|
||||||
&& !biomes.isEmpty() && !hasSurfaceBiome)
|
&& !biomes.isEmpty() && !hasSurfaceBiome)
|
||||||
throw new IllegalArgumentException("Vegetation pages need at least one surface Overworld biome");
|
throw new IllegalArgumentException("Vegetation pages need at least one surface Overworld biome");
|
||||||
if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome)
|
if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome)
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"linking.link-fissure": {
|
|
||||||
"sounds": [{"name": "rivencraft:linking/link-fissure"}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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}
|
||||||
|
]
|
||||||
|
}
|
||||||
+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"
|
||||||
|
|||||||
Reference in new issue
Block a user