Public Access
This commit is contained in:
1 parent
40770841ee
commit
f13bedd26f
20 files changed
+976
-63
No files matched your search
@@ -20,7 +20,7 @@ feature completion, and random structure selection. Nether/End noise-biome
|
|||||||
materials now follow their native rules. The beta does not yet provide full
|
materials now follow their native rules. The beta does not yet provide full
|
||||||
1.21.1 gameplay or optional-integration parity.
|
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.93-beta.1.md).
|
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.98-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
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ tracks issues #97–#103 under parent #96.
|
|||||||
|
|
||||||
## Version boundary
|
## Version boundary
|
||||||
|
|
||||||
|
Local slice `1.0.1.98-beta.1` ports the 1.21.1 Accelerated environment page:
|
||||||
|
stacking extra random block ticks in already-loaded player-nearby chunks,
|
||||||
|
eight-chunk/16,384-attempt default budgets, fair rotating section allocation,
|
||||||
|
and an Age-local saved page count. The 26.3 LOCAL config holds these limits.
|
||||||
|
Entities, machines and scheduled ticks are unaffected. Existing Ages remain
|
||||||
|
unaccelerated; random Age inclusion and instability scoring are not added.
|
||||||
|
The suspected host-seed biome zoom mismatch was checked against NeoForge's
|
||||||
|
patched ServerLevel constructor: seedOverride already replaces it correctly,
|
||||||
|
so the backend seed path was left unchanged. Native Cave policy issue #111
|
||||||
|
remains separate future work for both Minecraft tracks.
|
||||||
|
|
||||||
Minecraft 26.3/NeoForge uses Java 25, while Rivencraft 1.21.1 uses Java 21.
|
Minecraft 26.3/NeoForge uses Java 25, while Rivencraft 1.21.1 uses Java 21.
|
||||||
Build and publish separate artifacts; do not widen the Minecraft dependency
|
Build and publish separate artifacts; do not widen the Minecraft dependency
|
||||||
range of the 1.21.1 JAR. The official [26.3 release notes](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-3),
|
range of the 1.21.1 JAR. The official [26.3 release notes](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-3),
|
||||||
@@ -1801,6 +1812,144 @@ fresh chunks after full restart again used Nylium/End Stone. No full feature
|
|||||||
or entity chunk scans were run. The server was stopped and temporary world
|
or entity chunk scans were run. The server was stopped and temporary world
|
||||||
deleted. Nothing was committed, pushed or published.
|
deleted. Nothing was committed, pushed or published.
|
||||||
|
|
||||||
|
## Structure-aware biome completion
|
||||||
|
|
||||||
|
`1.0.1.94-beta.1` adapts root `AgeBiomeSelection.completeStructureBiomes` for
|
||||||
|
explicit non-Native biome controllers. It reads compatible biome IDs from
|
||||||
|
the active registered structure sets and shares this lookup with random
|
||||||
|
structure eligibility, avoiding two different compatibility definitions.
|
||||||
|
Repeated structure pages produce one requirement; Dungeons are excluded.
|
||||||
|
Single with no supplied biome chooses from the intersection of all structure
|
||||||
|
requirements and terrain-eligible biomes. No intersection leaves ordinary
|
||||||
|
seeded filling intact. An explicit Single biome is never rewritten.
|
||||||
|
|
||||||
|
Multi-biome completion adds one seeded compatible biome for each unmet
|
||||||
|
requirement where a terrain-eligible candidate exists, before ordinary minimum
|
||||||
|
filling. Root's salts, requirement order and shuffled candidate selection are
|
||||||
|
preserved. Existing biome order and duplicate weights remain, but a structure
|
||||||
|
can intentionally extend a pool beyond the usual minimum. Native and the
|
||||||
|
port's no-controller fallback remain unchanged. Impossible requirements do
|
||||||
|
not become new writing rejections: authored structure overrides still apply.
|
||||||
|
No saved format, network protocol or generated chunks are changed.
|
||||||
|
|
||||||
|
The completed-biome fixture accepts an optional structure page and checks
|
||||||
|
32 seeds for compatible Single/Cave Fortress choices, common Village/Stronghold
|
||||||
|
requirements, explicit End Single precedence, Native behavior, multi-biome
|
||||||
|
prefix preservation and avoiding unnecessary additions for already-compatible
|
||||||
|
or repeated structure pages. Existing biome plan codec checks remain in place.
|
||||||
|
|
||||||
|
The .94 build and disposable server checks passed. Village Single and Tiny
|
||||||
|
fixtures included compatible Plains; Cave Fortress Single selected Crimson
|
||||||
|
Forest. The 32-seed authoring assertions and the preexisting no-structure
|
||||||
|
completion checks passed. Saved generator pools survived unload/reload,
|
||||||
|
terrain reseed and full restart. The server was stopped and its temporary
|
||||||
|
world deleted. No structure-placement chunk scan was needed. Client book
|
||||||
|
authoring remains a manual check; no commit, push or publication occurred.
|
||||||
|
|
||||||
|
## Omitted biome-controller completion
|
||||||
|
|
||||||
|
`1.0.1.95-beta.1` ports the no-controller branch of root `AgeBiomeSelection`
|
||||||
|
and the first `AgeTraits.select` draw. The rank table and seed stream are
|
||||||
|
shared with `PortRandomAgeSelection` rather than duplicated. The production
|
||||||
|
seeded biome compiler uses that default when there is no explicit controller;
|
||||||
|
the registry-only diagnostic overload retains its exact older defaults.
|
||||||
|
Explicit controllers are unchanged. Native rolls with authored biomes, or
|
||||||
|
Single rolls with more than one biome page, fall back by authored count to
|
||||||
|
Single/Tiled/Medium. Other multi-biome rolls retain their distribution and
|
||||||
|
ordinary minimum-only filling, plus .94's structure requirements.
|
||||||
|
|
||||||
|
Void's omitted controller remains Single for empty pools and count-based
|
||||||
|
for supplied biome pages. Cave's omitted Native roll follows the port's
|
||||||
|
existing page-free seeded Single workaround; a native multi-cave source is
|
||||||
|
still not implemented. Explicit Native is not silently converted. No other
|
||||||
|
terrain, weather, lighting, color or body seed stream changes. Saved plans
|
||||||
|
remain frozen through restart and terrain reseed; old Ages are not rewritten.
|
||||||
|
|
||||||
|
The completed-biome diagnostic accepts `auto` and checks 128 seeds for all
|
||||||
|
nine distribution results, independence from unrelated weather pages,
|
||||||
|
authored prefix preservation, one/two-biome fallbacks, Void behavior and
|
||||||
|
structure-aware completion. Existing explicit-controller checks still run.
|
||||||
|
|
||||||
|
The .95 build and disposable-server checks passed. The expanded 128-seed
|
||||||
|
matrix covered all distributions, authored one/two-biome rules and Cave pools.
|
||||||
|
Created automatic Standard, Cave, Void and Cave Fortress fixtures, plus the
|
||||||
|
unchanged full-random command and explicit Single/Native Cave controls.
|
||||||
|
Generator pools survived unload/reload, reseed and full restart. A second seed
|
||||||
|
produced Small regions with three biomes, while explicit Native Cave retained
|
||||||
|
its Lush Caves source. No chunk-generation scan was needed. The server was
|
||||||
|
stopped and its temporary world deleted. Client authoring remains a manual
|
||||||
|
check; nothing was committed, pushed or published.
|
||||||
|
|
||||||
|
## Omitted terrain completion
|
||||||
|
|
||||||
|
`1.0.1.96-beta.1` adapts root `AgeRuntimeProfiles.selectForWriting`. A partly
|
||||||
|
authored book without terrain starts with the shared ranked terrain draw;
|
||||||
|
if unsupported, compatible profiles in stable ID order are indexed using
|
||||||
|
`floorMod(Long.hashCode(seed), count)`. Explicit terrain is never substituted.
|
||||||
|
Unlike root's feature-only candidate test, the port additionally preflights
|
||||||
|
registry-backed biome restrictions to avoid selecting an unloadable Cave
|
||||||
|
combination for supplied surface biomes. This is a deliberate compatibility
|
||||||
|
guard while the broader writing/instability recovery remains unported.
|
||||||
|
|
||||||
|
Binder availability checks now consider whether any supported terrain can
|
||||||
|
host an omitted-terrain book instead of assuming Standard. The server resolves
|
||||||
|
the actual terrain and biome plan once, then passes the resolved internal page
|
||||||
|
view to material/feature compilers. The inferred terrain is appended internally,
|
||||||
|
not inserted before original pages or written back to the book. Original
|
||||||
|
indices and ordered modifiers remain intact. Environment controllers use the
|
||||||
|
selected terrain, while sky colors and celestials still compile from original
|
||||||
|
pages. Page-free random books retain their original path and seed streams.
|
||||||
|
The chosen terrain and generator plans use existing saved fields, so no
|
||||||
|
network protocol change or retroactive terrain reroll is introduced.
|
||||||
|
|
||||||
|
The fixture checks 64 seeds for terrain coverage, deterministic selection,
|
||||||
|
original page prefixes, Cave-only Fortress support, implied-fluid constraints,
|
||||||
|
surface-biome compatibility, explicit terrain precedence and Binder
|
||||||
|
availability. Generator placement still uses the previously ported routines.
|
||||||
|
|
||||||
|
The .96 build and disposable server checks passed. The 64-seed assertions
|
||||||
|
covered all six terrains, deterministic fallback, page-prefix preservation,
|
||||||
|
explicit Flat, surface-biome protection and Cave-only Fortress availability.
|
||||||
|
Created Sky-only, Fortress-only, Water-only, Plains-only, explicit Flat and
|
||||||
|
full-random fixtures. A Water page retained Water as its implied lake fluid;
|
||||||
|
Fortress-only selected Cave with eligible biomes. Plans survived unload/reload,
|
||||||
|
reseed and full restart. The server was stopped and its temporary world
|
||||||
|
deleted. No chunk scan was needed; client Binder checks remain manual.
|
||||||
|
Nothing was committed, pushed or published.
|
||||||
|
|
||||||
|
## Native Cave regions
|
||||||
|
|
||||||
|
`1.0.1.97-beta.1` ports root `AgeBiomeSelection`'s full Native Cave pool and
|
||||||
|
`AgeDimensionService.createBiomeSource`'s Native-to-Tiny mapping. New Native
|
||||||
|
Cave plans retain distribution NATIVE with a saved recognized Cave/Nether
|
||||||
|
list, and the backend uses `PortScaledBiomeSource` at scale 0 (or Fixed for
|
||||||
|
a one-biome pool). Explicit Native biome pages remain prohibited. Both authored
|
||||||
|
and page-free/omitted-controller paths use the same eligibility query; the
|
||||||
|
temporary Native-to-Single Cave conversions are removed.
|
||||||
|
|
||||||
|
Native pool data is permitted only for Cave terrain at backend validation.
|
||||||
|
Other Native terrain sources are unchanged. Empty older Native Cave records
|
||||||
|
still load the old Lush source; old Single choices also remain frozen. New
|
||||||
|
Native pools are saved through the existing biome-plan codec without a new
|
||||||
|
network protocol, but earlier betas cannot decode populated Native pools, so
|
||||||
|
downgrading those saves is not supported. The current pool is passed to random
|
||||||
|
structure eligibility rather than incorrectly assuming Lush Caves.
|
||||||
|
|
||||||
|
The fixture verifies registry-derived pool ordering, old empty Native codec
|
||||||
|
defaults, the actual generator source, deterministic Tiny-region equivalence,
|
||||||
|
horizontal region variation and sampled pool membership without loading
|
||||||
|
chunks. The omitted-controller matrix now expects Native Cave to remain
|
||||||
|
Native with its full pool. This does not implement modded surface rules.
|
||||||
|
|
||||||
|
The .97 build and disposable-server checks passed: explicit and omitted
|
||||||
|
Native Cave, the full-random Native Cave path (seed 114), Native + Fortresses,
|
||||||
|
explicit Single and the legacy Lush-only control. The vanilla registry pool
|
||||||
|
contained nine biomes, including Sulfur Caves and all five Nether biomes;
|
||||||
|
the actual Native source passed Tiny-equivalence and sample-membership checks.
|
||||||
|
Pools survived unload/reload, reseed and full restart. The server was stopped
|
||||||
|
and its temporary world deleted. No chunk scan was needed. Client exploration
|
||||||
|
remains a manual check; nothing was committed, pushed or published.
|
||||||
|
|
||||||
## 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
|
||||||
|
|||||||
+123
-1
@@ -9,7 +9,7 @@ independent Age weather and lighting, multiple authored or seeded celestial
|
|||||||
bodies and eclipses, biome/material completion, and random features/structures.
|
bodies and eclipses, biome/material completion, and random features/structures.
|
||||||
Instability, crystal portals, discovery/stations, writing recovery and optional
|
Instability, crystal portals, discovery/stations, writing recovery and optional
|
||||||
integration parity remain unfinished. The latest published beta is
|
integration parity remain unfinished. The latest published beta is
|
||||||
[1.0.1.93-beta.1](RELEASE_NOTES_1.0.1.93-beta.1.md).
|
[1.0.1.98-beta.1](RELEASE_NOTES_1.0.1.98-beta.1.md).
|
||||||
Use a test world and keep a backup; do not
|
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.
|
||||||
|
|
||||||
@@ -1291,6 +1291,128 @@ and `age material_probe <name> [chunk_x chunk_z]`. The latter examines one
|
|||||||
terrain-stage chunk (default 0,0) without feature, lighting or entity generation. Nothing is committed
|
terrain-stage chunk (default 0,0) without feature, lighting or entity generation. Nothing is committed
|
||||||
or published without explicit request.
|
or published without explicit request.
|
||||||
|
|
||||||
|
The local `1.0.1.94-beta.1` slice ports **structure-aware completion of explicit
|
||||||
|
biome distributions** from 1.21.1. Single Biome without a supplied biome now
|
||||||
|
prefers a biome compatible with every authored structure page, when a common
|
||||||
|
candidate exists. Cave + Single + Nether Fortresses therefore selects an
|
||||||
|
eligible Cave/Nether biome rather than an unrelated cave biome. If there is
|
||||||
|
no common candidate, ordinary seeded filling and authored structure overrides
|
||||||
|
remain available, as in root.
|
||||||
|
|
||||||
|
For multi-biome controllers, each uncovered structure requirement can add one
|
||||||
|
compatible biome from the terrain's available pool. This is an intentional
|
||||||
|
exception to ordinary minimum-only filling: an already-full pool may gain
|
||||||
|
another biome for its structure pages. Supplied biomes, their order and
|
||||||
|
duplicate weights remain intact. A supplied Single Biome is never replaced,
|
||||||
|
Native remains unchanged, and Dungeons impose no biome requirement. The
|
||||||
|
no-controller fallback remains unchanged in this slice.
|
||||||
|
|
||||||
|
Test Single Biome + Villages without biome pages, Cave + Single Biome + Nether
|
||||||
|
Fortresses, and a multi-biome book with structures. Compare an explicit End
|
||||||
|
Single Biome + Villages to confirm that authored override still works.
|
||||||
|
`age create_completed_biomes <name> <seed> <terrain> <distribution> [structure-page-id]`
|
||||||
|
now accepts an optional structure fixture. Existing Ages are not rerolled;
|
||||||
|
restart/reseed retains completed pools. Nothing is committed or published
|
||||||
|
without explicit request.
|
||||||
|
|
||||||
|
The local `1.0.1.95-beta.1` slice seed-completes **omitted biome controllers**
|
||||||
|
in partly authored books, using 1.21.1's first AgeTraits distribution draw.
|
||||||
|
Native, Single and Medium are common; the other supported distributions are
|
||||||
|
rarer. Explicit controllers still win. Supplied biomes are never discarded:
|
||||||
|
a Native roll with authored biomes falls back by count, as does a Single roll
|
||||||
|
with multiple biome pages. Other seeded multi-biome controllers are retained,
|
||||||
|
so a lone supplied biome may receive a second biome to meet the minimum.
|
||||||
|
|
||||||
|
Void without biome pages still uses The Void; supplied Void biome pages keep
|
||||||
|
their existing count-based behavior. An omitted controller that rolls Native
|
||||||
|
on Cave terrain uses the same seeded Single fallback already used by random
|
||||||
|
Ages, since the port has no native multi-cave source. Explicit Native Cave
|
||||||
|
behavior remains unchanged. Structure-aware completion from .94 applies to
|
||||||
|
the resulting nonnative controller. Full Link-Panel-only random generation
|
||||||
|
retains its existing selector and seeds.
|
||||||
|
|
||||||
|
Test new terrain-only books without biome controllers, one/two supplied biome
|
||||||
|
pages without a controller, and Cave + Nether Fortresses without a biome
|
||||||
|
controller. Compare an explicit Single Biome book and restart/revisit.
|
||||||
|
`age create_completed_biomes <name> <seed> <terrain> auto [structure-page-id]`
|
||||||
|
provides an omitted-controller fixture. Existing Ages are not rerolled.
|
||||||
|
Nothing is committed or published without explicit request.
|
||||||
|
|
||||||
|
The local `1.0.1.96-beta.1` slice seed-completes **omitted terrain pages** in
|
||||||
|
partly authored books. It starts with the same ranked terrain draw as random
|
||||||
|
Ages, following 1.21.1's `selectForWriting` model. If that terrain cannot host
|
||||||
|
the supplied features/materials, a deterministic fallback chooses a compatible
|
||||||
|
terrain in stable profile order. The port also preflights its current biome
|
||||||
|
restrictions, avoiding an automatic Cave choice for supplied surface biomes.
|
||||||
|
|
||||||
|
Explicit terrain still wins and incompatible explicit combinations remain
|
||||||
|
rejected. Missing-terrain Fortress books can now bind using Cave terrain;
|
||||||
|
unpaired fluids use supported surface noise terrain. No extra random features
|
||||||
|
are added to partly authored books. The original book page list stays intact;
|
||||||
|
the chosen terrain is appended only to internal generation input so material
|
||||||
|
and color/controller indices do not shift. Sky/color/celestial compilation
|
||||||
|
still reads the original pages. Fully random Link-Panel-only Ages retain their
|
||||||
|
existing selector. Already-created Ages do not change terrain.
|
||||||
|
|
||||||
|
Test several new books with Sky or a feature but no terrain page, Nether
|
||||||
|
Fortresses alone, and an unpaired Water page. Compare an explicit Flat book
|
||||||
|
and a Plains biome page without terrain; the latter must not choose Cave.
|
||||||
|
`age create_auto_terrain <name> <seed> <page-id>` exercises the production
|
||||||
|
selector for one-page fixtures. Restart and revisit to confirm persistence.
|
||||||
|
Nothing is committed or published without explicit request.
|
||||||
|
|
||||||
|
The local `1.0.1.97-beta.1` slice ports **Native Cave biome distribution** from
|
||||||
|
1.21.1. A new Cave + Native book now freezes the full recognized Cave/Nether
|
||||||
|
biome pool and distributes it with the existing seeded Tiny-region sampler,
|
||||||
|
instead of forcing Lush Caves. An omitted controller or random Cave Age that
|
||||||
|
rolls Native now uses that same pool rather than converting to Single.
|
||||||
|
Random structures inspect the saved pool, so eligible Nether structures are
|
||||||
|
no longer hidden behind an assumed Lush-only source.
|
||||||
|
|
||||||
|
Existing Single Cave Ages remain Single. Old empty Native Cave records keep
|
||||||
|
their Lush fallback; they are not silently expanded on load. Other Native
|
||||||
|
terrains remain unchanged, and explicit Native still rejects biome pages.
|
||||||
|
Pools persist across restart/reseed; reseeding can still change future region
|
||||||
|
layout. Modded biome surface-rule integration remains separate pending work.
|
||||||
|
Back up before updating and do not downgrade worlds containing new Native
|
||||||
|
Cave pools to earlier beta builds.
|
||||||
|
|
||||||
|
Test a **new** Cave + Native Biomes book without individual biome pages,
|
||||||
|
explore multiple regions, then restart/revisit. Compare an explicit Single
|
||||||
|
Cave book and an existing old Cave Age. The existing
|
||||||
|
`age create_completed_biomes <name> <seed> cave native` fixture also checks
|
||||||
|
the native source against Tiny's sampler without generating chunks.
|
||||||
|
Nothing is committed or published without explicit request.
|
||||||
|
|
||||||
|
### Accelerated random block ticks
|
||||||
|
|
||||||
|
The local `1.0.1.98-beta.1` slice ports the **Accelerated** environment page
|
||||||
|
from 1.21.1. Repeated pages stack: each adds three extra random positions per
|
||||||
|
eligible block section per tick. Only already-loaded chunks within one chunk
|
||||||
|
of players are candidates; no extra chunks are loaded. Defaults match 1.21.1:
|
||||||
|
at most eight candidate chunks and 16,384 attempted positions per Age tick,
|
||||||
|
shared across sections with rotating selection. These limits live in
|
||||||
|
`config/rivencraft-local.toml` under `accelerated_age`; setting
|
||||||
|
`max_random_tick_attempts_per_tick` to zero disables the extra ticks.
|
||||||
|
|
||||||
|
This affects random-ticking blocks such as crops and saplings, not entities,
|
||||||
|
machines, scheduled block ticks, or the day/night clock. The authored page
|
||||||
|
count persists with the Age across unload/reload, reseeding and restart.
|
||||||
|
Existing Ages default to zero; pageless random Ages do not gain Accelerated
|
||||||
|
automatically. Instability costs remain part of the pending instability port.
|
||||||
|
|
||||||
|
Test two otherwise comparable new Ages, one with a single Accelerated page
|
||||||
|
and one with repeated pages. Plant crops/saplings with suitable light and
|
||||||
|
space, remain nearby, and compare growth. Revisit after restart. The operator
|
||||||
|
command `/rivencraft_port age acceleration <name>` reports the saved count
|
||||||
|
and current budgets. Disposable-server fixture
|
||||||
|
`age create_accelerated <name> <seed> <pages>` checks authoring and bounded
|
||||||
|
attempt arithmetic and creates a Flat Age; it does not prove live growth.
|
||||||
|
Build and disposable dedicated-server checks passed for counts 0, 1 and 32,
|
||||||
|
overflow/disabled budget arithmetic, unload/reload, reseed and full restart.
|
||||||
|
No player was connected for the server checks, so live growth remains an
|
||||||
|
in-game acceptance check. The test server was stopped and its world removed.
|
||||||
|
|
||||||
From the repository root:
|
From the repository root:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Rivencraft 26.3 beta 1.0.1.98-beta.1
|
||||||
|
|
||||||
|
Collects the player-validated port slices .94–.98 since 1.0.1.93-beta.1.
|
||||||
|
This beta is not yet feature-complete with Minecraft 1.21.1.
|
||||||
|
|
||||||
|
## Seeded terrain and biome completion
|
||||||
|
|
||||||
|
- Books without a Terrain page now choose terrain using the 1.21.1 weighted
|
||||||
|
selector. Incompatible choices are replaced with a seeded compatible terrain;
|
||||||
|
explicit Terrain pages retain their authored behavior.
|
||||||
|
- Omitted biome controllers now use the seeded weighted distribution selector.
|
||||||
|
Supplied biome pages are retained, with missing minimum slots completed.
|
||||||
|
Void without biome pages still uses The Void. Link-Panel-only random Ages
|
||||||
|
retain their existing selection path.
|
||||||
|
- Missing nonnative biome pools account for authored structures. Single chooses
|
||||||
|
a terrain-compatible biome satisfying all structure requirements where one
|
||||||
|
exists. Multiple-biome distributions add compatible biomes for unmet structure
|
||||||
|
requirements before completing their ordinary minimum. Explicit Single
|
||||||
|
choices are never replaced.
|
||||||
|
- New Native Cave Ages freeze the full recognized Cave/Nether biome pool and
|
||||||
|
distribute it using seeded Tiny regions, matching the 1.21.1 implementation.
|
||||||
|
This includes recognized modded cave biomes. Existing empty Native Cave
|
||||||
|
profiles retain their previous Lush Caves fallback; explicit Single is unchanged.
|
||||||
|
- Removing Nether-style biomes from the default Native Cave pool is separate
|
||||||
|
planned work for both versions in issue #111, not part of this release.
|
||||||
|
|
||||||
|
## Accelerated pages
|
||||||
|
|
||||||
|
- Ported the Accelerated environment page. Repeated pages stack, each adding
|
||||||
|
three extra random positions per eligible block section per tick near players.
|
||||||
|
- Defaults match 1.21.1: at most eight already-loaded candidate chunks and
|
||||||
|
16,384 extra random-tick attempts per Age tick, shared fairly across sections.
|
||||||
|
No extra chunks are force-loaded.
|
||||||
|
- Limits are configurable in `config/rivencraft-local.toml`, under
|
||||||
|
`accelerated_age`. A zero attempt budget disables the extra ticks.
|
||||||
|
- Page counts persist across unload/reload, reseeding and restart. Existing
|
||||||
|
Ages default to zero. Pageless random Ages do not gain Accelerated automatically.
|
||||||
|
- This currently affects random-ticking blocks such as crops and saplings.
|
||||||
|
Machine/block-entity acceleration is deferred for both versions under issue
|
||||||
|
#112; entities and scheduled block ticks are also unaffected. Instability
|
||||||
|
scoring remains part of the unfinished instability port.
|
||||||
|
- Operator command `/rivencraft_port age acceleration <name>` reports the
|
||||||
|
saved page count and current budgets.
|
||||||
|
|
||||||
|
## Compatibility and validation
|
||||||
|
|
||||||
|
- Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
|
||||||
|
Dimensions and Bad Packets are not required. Minecraft 1.21.1 is unchanged.
|
||||||
|
- Catalog protocol remains **10**. Use matching client/server builds.
|
||||||
|
- Player in-game checks passed. Builds and targeted disposable-server checks
|
||||||
|
covered seeded completion, Native Cave sampling, tick-budget arithmetic,
|
||||||
|
unload/reload, reseeding and restart. Test servers were stopped and their
|
||||||
|
worlds removed. This was not a full automated GameTest run.
|
||||||
|
- Back up worlds before updating. New populated Native Cave profiles cannot
|
||||||
|
be read by earlier beta builds; do not downgrade those saves. Keep Minecraft
|
||||||
|
1.21.1 and 26.3 saves separate.
|
||||||
|
- Full writing recovery, discovery/stations, instability, crystal portals and
|
||||||
|
optional integration parity remain unfinished. Random-page eligibility config
|
||||||
|
and modded biome surface-rule integration also remain pending. Creative page
|
||||||
|
availability does not imply full support.
|
||||||
|
|
||||||
|
Report issues and help improve the wiki at
|
||||||
|
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||||
@@ -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.93-beta.1
|
mod_version=1.0.1.98-beta.1
|
||||||
@@ -85,10 +85,11 @@
|
|||||||
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
|
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
|
||||||
"symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.",
|
"symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.",
|
||||||
"symbol.rivencraft.block.generated.description": "Modify the next compatible feature. Without a following feature, choose a compatible feature from the Age seed.",
|
"symbol.rivencraft.block.generated.description": "Modify the next compatible feature. Without a following feature, choose a compatible feature from the Age seed.",
|
||||||
"symbol.rivencraft.biome.native.description": "Use the terrain's native biome source; do not include Biome pages.",
|
"symbol.rivencraft.biome.native.description": "Use the terrain's default biome source. Cave uses Tiny regions from recognized Cave/Nether biomes. Do not include Biome pages.",
|
||||||
"symbol.rivencraft.feature.dungeons.description": "Add monster-room dungeon attempts throughout surface noise or Cave terrain, including Nether cave biomes. Not Flat or Void.",
|
"symbol.rivencraft.feature.dungeons.description": "Add monster-room dungeon attempts throughout surface noise or Cave terrain, including Nether cave biomes. Not Flat or Void.",
|
||||||
"symbol.rivencraft.biome.single.description": "Use one biome across the Age. If omitted, choose an eligible biome from the Age seed.",
|
"symbol.rivencraft.biome.single.description": "Use one biome across the Age. If omitted, choose an eligible biome from the Age seed.",
|
||||||
"symbol.rivencraft.terrain.flat.description": "A level, biome-decorated landscape with its foundation at Y=0.",
|
"symbol.rivencraft.terrain.flat.description": "A level, biome-decorated landscape with its foundation at Y=0.",
|
||||||
|
"symbol.rivencraft.environment.accelerated.description": "Adds extra random block ticks near players in this Age. Repeated pages stack within server tick budgets; does not speed up entities or machines.",
|
||||||
"symbol.rivencraft.feature.floating_islands.description": "Add high floating islands alongside noise, Flat, or Void terrain; not Cave terrain.",
|
"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.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.ravines.description": "Add canyon carving to noise terrain; not Flat or Void. Skips Nether biomes.",
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package net.dnorberg.rivencraft;
|
||||||
|
|
||||||
|
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||||
|
|
||||||
|
/** Server-side limits shared with the 1.21.1 Accelerated implementation. */
|
||||||
|
public final class PortConfig {
|
||||||
|
public static final ModConfigSpec SPEC;
|
||||||
|
public static final ModConfigSpec.IntValue ACCELERATED_MAX_CHUNKS;
|
||||||
|
public static final ModConfigSpec.IntValue ACCELERATED_MAX_ATTEMPTS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
var builder = new ModConfigSpec.Builder();
|
||||||
|
builder.push("accelerated_age");
|
||||||
|
ACCELERATED_MAX_CHUNKS = builder.comment("Maximum already-loaded player-nearby chunks given extra random ticks per Age tick.")
|
||||||
|
.defineInRange("max_loaded_chunks", 8, 1, 64);
|
||||||
|
ACCELERATED_MAX_ATTEMPTS = builder.comment("Maximum extra random-tick positions sampled per Age tick, shared fairly across loaded sections; zero disables Accelerated.")
|
||||||
|
.defineInRange("max_random_tick_attempts_per_tick", 16384, 0, 65536);
|
||||||
|
builder.pop();
|
||||||
|
SPEC = builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private PortConfig() {}
|
||||||
|
}
|
||||||
@@ -166,7 +166,8 @@ public final class Rivencraft {
|
|||||||
public static final Supplier<RecipeSerializer<LinkingBookRecipe>> LINKING_BOOK_RECIPE =
|
public static final Supplier<RecipeSerializer<LinkingBookRecipe>> LINKING_BOOK_RECIPE =
|
||||||
RECIPE_SERIALIZERS.register("linking_book", () -> LinkingBookRecipe.SERIALIZER);
|
RECIPE_SERIALIZERS.register("linking_book", () -> LinkingBookRecipe.SERIALIZER);
|
||||||
|
|
||||||
public Rivencraft(IEventBus modEventBus) {
|
public Rivencraft(IEventBus modEventBus, net.neoforged.fml.ModContainer container) {
|
||||||
|
container.registerConfig(net.neoforged.fml.config.ModConfig.Type.LOCAL, PortConfig.SPEC);
|
||||||
BLOCKS.register(modEventBus);
|
BLOCKS.register(modEventBus);
|
||||||
SOUNDS.register(modEventBus);
|
SOUNDS.register(modEventBus);
|
||||||
ITEMS.register(modEventBus);
|
ITEMS.register(modEventBus);
|
||||||
@@ -179,6 +180,7 @@ public final class Rivencraft {
|
|||||||
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(net.dnorberg.rivencraft.age.PortAgeWeather::onLevelTickPre);
|
||||||
|
NeoForge.EVENT_BUS.addListener(net.dnorberg.rivencraft.age.PortAcceleratedTicks::onLevelTickPost);
|
||||||
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);
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package net.dnorberg.rivencraft.age;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.dnorberg.rivencraft.PortConfig;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.neoforged.neoforge.event.tick.LevelTickEvent;
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.core.SectionPos;
|
||||||
|
import net.minecraft.server.level.ServerLevel;
|
||||||
|
import net.minecraft.world.level.ChunkPos;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
|
import net.minecraft.world.level.chunk.LevelChunkSection;
|
||||||
|
import net.minecraft.util.RandomSource;
|
||||||
|
|
||||||
|
/** Extra random block ticks from authored Accelerated pages, bounded to loaded player chunks. */
|
||||||
|
public final class PortAcceleratedTicks {
|
||||||
|
public static final Identifier PAGE = Identifier.fromNamespaceAndPath("rivencraft", "environment/accelerated");
|
||||||
|
|
||||||
|
private PortAcceleratedTicks() {}
|
||||||
|
|
||||||
|
public static int pageCount(List<Identifier> pages) {
|
||||||
|
return (int) pages.stream().filter(PAGE::equals).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void configure(ServerLevel level, int pages) {
|
||||||
|
if (!(level instanceof PortAgeLevel)) throw new IllegalArgumentException("Not a Rivencraft Age");
|
||||||
|
level.getDataStorage().computeIfAbsent(PortAccelerationSettings.TYPE).setPages(pages);
|
||||||
|
level.getDataStorage().saveAndJoin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int pages(ServerLevel level) {
|
||||||
|
return level.getDataStorage().computeIfAbsent(PortAccelerationSettings.TYPE).pages();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void onLevelTickPost(LevelTickEvent.Post event) {
|
||||||
|
if (event.getLevel() instanceof PortAgeLevel level) tick(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tick(ServerLevel level) {
|
||||||
|
int maximumAttempts = PortConfig.ACCELERATED_MAX_ATTEMPTS.get();
|
||||||
|
if (maximumAttempts == 0) return;
|
||||||
|
int pages = pages(level);
|
||||||
|
if (pages == 0) return;
|
||||||
|
|
||||||
|
List<ChunkPos> chunks = loadedPlayerChunks(level);
|
||||||
|
if (chunks.isEmpty()) return;
|
||||||
|
int maximumChunks = Math.min(chunks.size(), PortConfig.ACCELERATED_MAX_CHUNKS.get());
|
||||||
|
int first = Math.floorMod(level.getServer().getTickCount(), chunks.size());
|
||||||
|
List<TickSection> eligible = new ArrayList<>();
|
||||||
|
for (int offset = 0; offset < maximumChunks; offset++) {
|
||||||
|
ChunkPos pos = chunks.get((first + offset) % chunks.size());
|
||||||
|
LevelChunk chunk = level.getChunkSource().getChunkNow(pos.x(), pos.z());
|
||||||
|
if (chunk == null) continue;
|
||||||
|
LevelChunkSection[] sections = chunk.getSections();
|
||||||
|
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
|
||||||
|
LevelChunkSection section = sections[sectionIndex];
|
||||||
|
if (!section.isRandomlyTickingBlocks()) continue;
|
||||||
|
int baseY = SectionPos.sectionToBlockCoord(chunk.getSectionYFromSectionIndex(sectionIndex));
|
||||||
|
eligible.add(new TickSection(pos, baseY, section));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (eligible.isEmpty()) return;
|
||||||
|
|
||||||
|
// Legacy attempts three positions per random-ticking section per page. Divide
|
||||||
|
// the bounded total between sections before ticking any one of them, so a
|
||||||
|
// lower section cannot exhaust the entire budget ahead of surface crops.
|
||||||
|
int total = attemptBudget(pages, eligible.size(), maximumAttempts);
|
||||||
|
int perSection = total / eligible.size();
|
||||||
|
int remainder = total % eligible.size();
|
||||||
|
int firstSection = Math.floorMod(level.getServer().getTickCount(), eligible.size());
|
||||||
|
RandomSource random = level.getRandom();
|
||||||
|
for (int offset = 0; offset < eligible.size(); offset++) {
|
||||||
|
TickSection candidate = eligible.get((firstSection + offset) % eligible.size());
|
||||||
|
int samples = perSection + (offset < remainder ? 1 : 0);
|
||||||
|
for (int sample = 0; sample < samples; sample++) {
|
||||||
|
int x = random.nextInt(16);
|
||||||
|
int y = random.nextInt(16);
|
||||||
|
int z = random.nextInt(16);
|
||||||
|
BlockState state = candidate.section().getBlockState(x, y, z);
|
||||||
|
if (state.isRandomlyTicking()) {
|
||||||
|
state.randomTick(level, new BlockPos(candidate.chunk().getMinBlockX() + x,
|
||||||
|
candidate.baseY() + y, candidate.chunk().getMinBlockZ() + z), random);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int attemptBudget(int pages, int sections, int maximumAttempts) {
|
||||||
|
if (pages < 0 || sections < 0 || maximumAttempts < 0)
|
||||||
|
throw new IllegalArgumentException("Negative accelerated tick budget input");
|
||||||
|
if (sections == 0) return 0;
|
||||||
|
long perSection = 3L * pages;
|
||||||
|
return perSection > maximumAttempts / (long) sections
|
||||||
|
? maximumAttempts : (int) (perSection * sections);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<ChunkPos> loadedPlayerChunks(ServerLevel level) {
|
||||||
|
Set<ChunkPos> chunks = new LinkedHashSet<>();
|
||||||
|
for (var player : level.players()) {
|
||||||
|
ChunkPos center = player.chunkPosition();
|
||||||
|
for (int x = -1; x <= 1; x++) {
|
||||||
|
for (int z = -1; z <= 1; z++) {
|
||||||
|
int chunkX = center.x() + x;
|
||||||
|
int chunkZ = center.z() + z;
|
||||||
|
if (level.getChunkSource().hasChunk(chunkX, chunkZ)) {
|
||||||
|
chunks.add(new ChunkPos(chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(chunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record TickSection(ChunkPos chunk, int baseY, LevelChunkSection section) {}
|
||||||
|
}
|
||||||
@@ -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 authored page count, retained across unload, restart and reseeding. */
|
||||||
|
final class PortAccelerationSettings extends SavedData {
|
||||||
|
private static final Codec<PortAccelerationSettings> CODEC = Codec.intRange(0, Integer.MAX_VALUE)
|
||||||
|
.fieldOf("pages").codec().xmap(PortAccelerationSettings::new, settings -> settings.pages);
|
||||||
|
static final SavedDataType<PortAccelerationSettings> TYPE = new SavedDataType<>(
|
||||||
|
Identifier.fromNamespaceAndPath("rivencraft", "acceleration"),
|
||||||
|
() -> new PortAccelerationSettings(0), CODEC);
|
||||||
|
private int pages;
|
||||||
|
|
||||||
|
private PortAccelerationSettings(int pages) { this.pages = pages; }
|
||||||
|
int pages() { return pages; }
|
||||||
|
void setPages(int pages) {
|
||||||
|
if (pages < 0) throw new IllegalArgumentException("Negative Accelerated page count");
|
||||||
|
this.pages = pages;
|
||||||
|
setDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,19 @@ public final class PortAgeCommands {
|
|||||||
return 1;
|
return 1;
|
||||||
})))
|
})))
|
||||||
.then(Commands.literal("age")
|
.then(Commands.literal("age")
|
||||||
|
.then(Commands.literal("create_accelerated")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.then(Commands.argument("pages", IntegerArgumentType.integer(0, 256))
|
||||||
|
.executes(PortAgeCommands::createAccelerated)))))
|
||||||
|
.then(Commands.literal("acceleration")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.executes(PortAgeCommands::acceleration)))
|
||||||
|
.then(Commands.literal("create_auto_terrain")
|
||||||
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
|
.then(Commands.argument("page", IdentifierArgument.id())
|
||||||
|
.executes(PortAgeCommands::createAutomaticTerrain)))))
|
||||||
.then(Commands.literal("create_biome_surface")
|
.then(Commands.literal("create_biome_surface")
|
||||||
.then(Commands.argument("name", StringArgumentType.word())
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
@@ -108,7 +121,10 @@ public final class PortAgeCommands {
|
|||||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||||
.then(Commands.argument("distribution", StringArgumentType.word())
|
.then(Commands.argument("distribution", StringArgumentType.word())
|
||||||
.executes(PortAgeCommands::createCompletedBiomes))))))
|
.executes(context -> createCompletedBiomes(context, null))
|
||||||
|
.then(Commands.argument("structure", IdentifierArgument.id())
|
||||||
|
.executes(context -> createCompletedBiomes(context,
|
||||||
|
IdentifierArgument.getId(context, "structure")))))))))
|
||||||
.then(Commands.literal("biome_pool")
|
.then(Commands.literal("biome_pool")
|
||||||
.then(Commands.argument("name", StringArgumentType.word())
|
.then(Commands.argument("name", StringArgumentType.word())
|
||||||
.executes(PortAgeCommands::probeBiomePool)))
|
.executes(PortAgeCommands::probeBiomePool)))
|
||||||
@@ -692,6 +708,63 @@ public final class PortAgeCommands {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int createAutomaticTerrain(CommandContext<CommandSourceStack> context) {
|
||||||
|
try {
|
||||||
|
var server = context.getSource().getServer();
|
||||||
|
verifyAutomaticTerrain(server);
|
||||||
|
long seed = LongArgumentType.getLong(context, "seed");
|
||||||
|
var source = java.util.List.of(IdentifierArgument.getId(context, "page"));
|
||||||
|
var selected = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, source, seed);
|
||||||
|
var pages = selected.generationPages();
|
||||||
|
int created = createAge(context, selected.terrain(), selected.biomes(),
|
||||||
|
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed),
|
||||||
|
net.dnorberg.rivencraft.writing.PortAuthoredWriting.vegetationPlan(pages),
|
||||||
|
net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(pages),
|
||||||
|
net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed),
|
||||||
|
net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed));
|
||||||
|
if (created != 0) context.getSource().sendSuccess(() -> Component.literal("Automatic terrain: " + selected), false);
|
||||||
|
return created;
|
||||||
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
|
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyAutomaticTerrain(net.minecraft.server.MinecraftServer server) {
|
||||||
|
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||||
|
var sky = java.util.List.of(symbol.apply("sky/overworld"));
|
||||||
|
var water = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(Identifier.withDefaultNamespace("water"));
|
||||||
|
var plains = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("plains"));
|
||||||
|
var seen = java.util.EnumSet.noneOf(PortAgeTerrain.class);
|
||||||
|
for (long seed = 0; seed < 64; seed++) {
|
||||||
|
var selected = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, sky, seed);
|
||||||
|
seen.add(selected.terrain());
|
||||||
|
if (selected.terrain() != PortRandomAgeSelection.terrain(seed)
|
||||||
|
|| !selected.generationPages().subList(0, sky.size()).equals(sky)
|
||||||
|
|| !selected.equals(net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, sky, seed)))
|
||||||
|
throw new IllegalStateException("Automatic terrain seed/determinism/page-prefix check failed");
|
||||||
|
var fortress = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server,
|
||||||
|
java.util.List.of(symbol.apply("feature/nether_fortresses")), seed);
|
||||||
|
if (fortress.terrain() != PortAgeTerrain.CAVE)
|
||||||
|
throw new IllegalStateException("Omitted terrain cannot host a Cave-only structure");
|
||||||
|
var wet = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, java.util.List.of(water), seed);
|
||||||
|
if (wet.terrain() == PortAgeTerrain.FLAT || wet.terrain() == PortAgeTerrain.VOID || wet.terrain() == PortAgeTerrain.CAVE)
|
||||||
|
throw new IllegalStateException("Automatic terrain cannot host its implied lake");
|
||||||
|
var biome = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server, java.util.List.of(plains), seed);
|
||||||
|
if (biome.terrain() == PortAgeTerrain.CAVE || !biome.biomes().biomes().contains(Identifier.withDefaultNamespace("plains")))
|
||||||
|
throw new IllegalStateException("Automatic terrain discarded a supplied surface biome");
|
||||||
|
var explicit = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(server,
|
||||||
|
java.util.List.of(symbol.apply("terrain/flat"), sky.getFirst()), seed);
|
||||||
|
if (explicit.terrain() != PortAgeTerrain.FLAT || explicit.generationPages().size() != 2)
|
||||||
|
throw new IllegalStateException("Explicit terrain lost precedence");
|
||||||
|
}
|
||||||
|
if (seen.size() != PortAgeTerrain.values().length
|
||||||
|
|| !net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(symbol.apply("feature/nether_fortresses")))
|
||||||
|
|| net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(
|
||||||
|
symbol.apply("terrain/flat"), symbol.apply("feature/dungeons"))))
|
||||||
|
throw new IllegalStateException("Automatic terrain availability/coverage failed");
|
||||||
|
}
|
||||||
|
|
||||||
private static int createBiomeSurface(CommandContext<CommandSourceStack> context) {
|
private static int createBiomeSurface(CommandContext<CommandSourceStack> context) {
|
||||||
try {
|
try {
|
||||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||||
@@ -946,16 +1019,24 @@ public final class PortAgeCommands {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context) {
|
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context, Identifier structure) {
|
||||||
try {
|
try {
|
||||||
var server = context.getSource().getServer();
|
var server = context.getSource().getServer();
|
||||||
long seed = LongArgumentType.getLong(context, "seed");
|
long seed = LongArgumentType.getLong(context, "seed");
|
||||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||||
var distribution = PortBiomePlan.Distribution.valueOf(
|
String distributionName = StringArgumentType.getString(context, "distribution");
|
||||||
StringArgumentType.getString(context, "distribution").toUpperCase(Locale.ROOT));
|
var distribution = distributionName.equalsIgnoreCase("auto") ? null
|
||||||
|
: PortBiomePlan.Distribution.valueOf(distributionName.toUpperCase(Locale.ROOT));
|
||||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||||
var pages = java.util.List.of(symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
|
var pages = new java.util.ArrayList<Identifier>();
|
||||||
symbol.apply("biome/" + distribution.name().toLowerCase(Locale.ROOT)));
|
pages.add(symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
|
||||||
|
if (distribution != null) pages.add(symbol.apply("biome/" + distribution.name().toLowerCase(Locale.ROOT)));
|
||||||
|
else verifyAutomaticBiomes(server);
|
||||||
|
if (structure != null) {
|
||||||
|
if (!PortStructurePages.supports(structure)) throw new IllegalArgumentException("Unsupported structure page " + structure);
|
||||||
|
pages.add(structure);
|
||||||
|
verifyStructureBiomes(server);
|
||||||
|
}
|
||||||
var plan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server, seed);
|
var plan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server, seed);
|
||||||
// Controller-only and partial pools complete; already-sufficient pools never gain extras.
|
// Controller-only and partial pools complete; already-sufficient pools never gain extras.
|
||||||
var plains = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("plains"));
|
var plains = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("plains"));
|
||||||
@@ -980,7 +1061,10 @@ public final class PortAgeCommands {
|
|||||||
var encoded = PortBiomePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow();
|
var encoded = PortBiomePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow();
|
||||||
if (!plan.equals(PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
if (!plan.equals(PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||||
throw new IllegalStateException("Completed biome plan codec mismatch");
|
throw new IllegalStateException("Completed biome plan codec mismatch");
|
||||||
int created = createAge(context, terrain, plan);
|
var features = net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed);
|
||||||
|
int created = createAge(context, terrain, plan, null, null, 0, null, features);
|
||||||
|
if (created != 0 && terrain == PortAgeTerrain.CAVE && plan.distribution() == PortBiomePlan.Distribution.NATIVE)
|
||||||
|
verifyNativeCave(server, ageId(context), seed, plan);
|
||||||
if (created != 0) context.getSource().sendSuccess(() -> Component.literal("Completed biome plan: " + plan), false);
|
if (created != 0) context.getSource().sendSuccess(() -> Component.literal("Completed biome plan: " + plan), false);
|
||||||
return created == 0 ? 0 : probeBiomePool(context);
|
return created == 0 ? 0 : probeBiomePool(context);
|
||||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||||
@@ -989,6 +1073,121 @@ public final class PortAgeCommands {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void verifyAutomaticBiomes(net.minecraft.server.MinecraftServer server) {
|
||||||
|
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||||
|
var standard = symbol.apply("terrain/standard");
|
||||||
|
var end = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("the_end"));
|
||||||
|
var forest = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("forest"));
|
||||||
|
var seen = java.util.EnumSet.noneOf(PortBiomePlan.Distribution.class);
|
||||||
|
var caveIds = java.util.Set.copyOf(PortRandomAgeSelection.eligibleBiomes(server, PortAgeTerrain.CAVE));
|
||||||
|
for (long seed = 0; seed < 128; seed++) {
|
||||||
|
var expected = PortRandomAgeSelection.distribution(seed);
|
||||||
|
var selected = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard), server, seed);
|
||||||
|
seen.add(selected.distribution());
|
||||||
|
if (selected.distribution() != expected || !selected.equals(
|
||||||
|
net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
java.util.List.of(standard, symbol.apply("weather/off")), server, seed)))
|
||||||
|
throw new IllegalStateException("Missing biome controller is not seeded/independent");
|
||||||
|
var authored = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard, end, forest), server, seed);
|
||||||
|
if (authored.biomes().size() != 2 || !authored.biomes().equals(java.util.List.of(
|
||||||
|
Identifier.withDefaultNamespace("the_end"), Identifier.withDefaultNamespace("forest")))
|
||||||
|
|| authored.distribution() != (expected == PortBiomePlan.Distribution.NATIVE
|
||||||
|
|| expected == PortBiomePlan.Distribution.SINGLE ? PortBiomePlan.Distribution.TILED : expected))
|
||||||
|
throw new IllegalStateException("Automatic controller discarded or extended authored biomes");
|
||||||
|
var one = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard, end), server, seed);
|
||||||
|
if (!one.biomes().getFirst().equals(Identifier.withDefaultNamespace("the_end"))
|
||||||
|
|| one.distribution() != (expected == PortBiomePlan.Distribution.NATIVE ? PortBiomePlan.Distribution.SINGLE : expected)
|
||||||
|
|| one.biomes().size() != (one.distribution() == PortBiomePlan.Distribution.SINGLE ? 1 : 2))
|
||||||
|
throw new IllegalStateException("Automatic controller lost a supplied biome");
|
||||||
|
var cave = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
java.util.List.of(symbol.apply("terrain/cave")), server, seed);
|
||||||
|
if (cave.distribution() != expected || !caveIds.containsAll(cave.biomes())
|
||||||
|
|| expected == PortBiomePlan.Distribution.NATIVE && !java.util.Set.copyOf(cave.biomes()).equals(caveIds))
|
||||||
|
throw new IllegalStateException("Automatic Cave controller/biome pool is invalid");
|
||||||
|
var emptyVoid = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
java.util.List.of(symbol.apply("terrain/void")), server, seed);
|
||||||
|
var authoredVoid = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
java.util.List.of(symbol.apply("terrain/void"), end), server, seed);
|
||||||
|
if (emptyVoid.distribution() != PortBiomePlan.Distribution.SINGLE
|
||||||
|
|| !emptyVoid.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("the_void")))
|
||||||
|
|| !authoredVoid.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("the_end"))))
|
||||||
|
throw new IllegalStateException("Automatic Void controller changed biome fallback");
|
||||||
|
var village = symbol.apply("feature/villages");
|
||||||
|
var withStructure = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(standard, village), server, seed);
|
||||||
|
if (withStructure.distribution() != PortBiomePlan.Distribution.NATIVE
|
||||||
|
&& withStructure.biomes().stream().noneMatch(PortStructurePages.compatibleBiomes(village, server.registryAccess())::contains))
|
||||||
|
throw new IllegalStateException("Automatic controller skipped structure-aware completion");
|
||||||
|
}
|
||||||
|
if (seen.size() != PortBiomePlan.Distribution.values().length)
|
||||||
|
throw new IllegalStateException("Automatic biome controller fixture lacks distribution coverage");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyNativeCave(net.minecraft.server.MinecraftServer server, Identifier age, long seed, PortBiomePlan plan) {
|
||||||
|
var expected = PortRandomAgeSelection.caveBiomes(server.registryAccess());
|
||||||
|
if (!plan.biomes().equals(expected)) throw new IllegalStateException("Native Cave did not freeze the recognized biome pool");
|
||||||
|
var old = PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
|
||||||
|
com.google.gson.JsonParser.parseString("{\"distribution\":\"NATIVE\",\"biomes\":[]}")).getOrThrow();
|
||||||
|
if (!old.biomes().isEmpty() || old.distribution() != PortBiomePlan.Distribution.NATIVE)
|
||||||
|
throw new IllegalStateException("Old Native Cave plan was rewritten");
|
||||||
|
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, age));
|
||||||
|
var source = level.getChunkSource().getGenerator().getBiomeSource();
|
||||||
|
if (expected.size() < 2) return;
|
||||||
|
if (!(source instanceof PortScaledBiomeSource scaled)) throw new IllegalStateException("Native Cave is not using region distribution");
|
||||||
|
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
|
var holders = plan.biomes().stream().map(id -> (net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome>)
|
||||||
|
registry.getOrThrow(ResourceKey.create(Registries.BIOME, id))).toList();
|
||||||
|
var tiny = new PortScaledBiomeSource(net.minecraft.core.HolderSet.direct(holders), seed, 0);
|
||||||
|
var sampled = new java.util.HashSet<Identifier>();
|
||||||
|
for (int x = -64; x <= 64; x += 4) for (int z = -64; z <= 64; z += 4) {
|
||||||
|
var biome = scaled.getNoiseBiome(x, 0, z);
|
||||||
|
if (!biome.equals(tiny.getNoiseBiome(x, 0, z)) || !biome.equals(scaled.getNoiseBiome(x, 16, z)))
|
||||||
|
throw new IllegalStateException("Native Cave does not match root's Tiny-region sampler");
|
||||||
|
sampled.add(biome.unwrapKey().orElseThrow().identifier());
|
||||||
|
}
|
||||||
|
if (!java.util.Set.copyOf(expected).containsAll(sampled) || sampled.size() < 2)
|
||||||
|
throw new IllegalStateException("Native Cave sampler did not distribute its recognized biome pool");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void verifyStructureBiomes(net.minecraft.server.MinecraftServer server) {
|
||||||
|
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||||
|
java.util.function.Function<String, Identifier> biome = name ->
|
||||||
|
net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace(name));
|
||||||
|
var village = symbol.apply("feature/villages");
|
||||||
|
var fortress = symbol.apply("feature/nether_fortresses");
|
||||||
|
for (long seed = 0; seed < 32; seed++) {
|
||||||
|
var singlePages = java.util.List.of(symbol.apply("biome/single"), village);
|
||||||
|
var single = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(singlePages, server, seed);
|
||||||
|
var cave = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
java.util.List.of(symbol.apply("terrain/cave"), symbol.apply("biome/single"), fortress), server, seed);
|
||||||
|
if (!PortStructurePages.compatibleBiomes(village, server.registryAccess()).contains(single.biomes().getFirst())
|
||||||
|
|| !single.equals(net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(singlePages, server, seed))
|
||||||
|
|| !PortStructurePages.compatibleBiomes(fortress, server.registryAccess()).contains(cave.biomes().getFirst()))
|
||||||
|
throw new IllegalStateException("Structure-aware Single completion failed");
|
||||||
|
var stronghold = symbol.apply("feature/strongholds");
|
||||||
|
var shared = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(
|
||||||
|
symbol.apply("biome/single"), village, stronghold), server, seed);
|
||||||
|
if (!PortStructurePages.compatibleBiomes(village, server.registryAccess()).contains(shared.biomes().getFirst())
|
||||||
|
|| !PortStructurePages.compatibleBiomes(stronghold, server.registryAccess()).contains(shared.biomes().getFirst()))
|
||||||
|
throw new IllegalStateException("Single completion did not satisfy common structure requirements");
|
||||||
|
var explicit = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
java.util.List.of(symbol.apply("biome/single"), biome.apply("the_end"), village), server, seed);
|
||||||
|
var nativePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||||
|
java.util.List.of(symbol.apply("biome/native"), village), server, seed);
|
||||||
|
if (!explicit.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("the_end")))
|
||||||
|
|| nativePlan.distribution() != PortBiomePlan.Distribution.NATIVE || !nativePlan.biomes().isEmpty())
|
||||||
|
throw new IllegalStateException("Explicit Single/Native biome precedence changed");
|
||||||
|
var multiPages = java.util.List.of(symbol.apply("biome/tiny"), biome.apply("forest"), biome.apply("the_end"), village);
|
||||||
|
var multi = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(multiPages, server, seed);
|
||||||
|
if (multi.biomes().size() != 3 || !multi.biomes().subList(0, 2).equals(java.util.List.of(
|
||||||
|
Identifier.withDefaultNamespace("forest"), Identifier.withDefaultNamespace("the_end")))
|
||||||
|
|| !PortStructurePages.compatibleBiomes(village, server.registryAccess()).contains(multi.biomes().get(2)))
|
||||||
|
throw new IllegalStateException("Structure biome completion discarded authored choices");
|
||||||
|
var full = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(
|
||||||
|
symbol.apply("biome/tiny"), biome.apply("plains"), biome.apply("forest"), village, village), server, seed);
|
||||||
|
if (full.biomes().size() != 2) throw new IllegalStateException("Compatible/repeated structures added unnecessary biomes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int probeBiomePool(CommandContext<CommandSourceStack> context) {
|
private static int probeBiomePool(CommandContext<CommandSourceStack> context) {
|
||||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||||
if (level == null) {
|
if (level == null) {
|
||||||
@@ -1947,6 +2146,40 @@ public final class PortAgeCommands {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int createAccelerated(CommandContext<CommandSourceStack> context) {
|
||||||
|
int count = IntegerArgumentType.getInteger(context, "pages");
|
||||||
|
var pages = new java.util.ArrayList<Identifier>();
|
||||||
|
pages.add(Identifier.fromNamespaceAndPath("rivencraft", "terrain/flat"));
|
||||||
|
for (int i = 0; i < count; i++) pages.add(PortAcceleratedTicks.PAGE);
|
||||||
|
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|
||||||
|
|| !net.dnorberg.rivencraft.writing.PortAuthoredWriting.isPageImplemented(PortAcceleratedTicks.PAGE)
|
||||||
|
|| PortAcceleratedTicks.pageCount(pages) != count
|
||||||
|
|| PortAcceleratedTicks.attemptBudget(1, 8, 16384) != 24
|
||||||
|
|| PortAcceleratedTicks.attemptBudget(32, 8, 16384) != 768
|
||||||
|
|| PortAcceleratedTicks.attemptBudget(Integer.MAX_VALUE, Integer.MAX_VALUE, 16384) != 16384
|
||||||
|
|| PortAcceleratedTicks.attemptBudget(32, 8, 0) != 0)
|
||||||
|
throw new IllegalStateException("Accelerated writing/budget regression");
|
||||||
|
ServerLevel level = RuntimeAgeBackend.create(context.getSource().getServer(), ageId(context),
|
||||||
|
LongArgumentType.getLong(context, "seed"), null, PortAgeTerrain.FLAT);
|
||||||
|
PortAcceleratedTicks.configure(level, count);
|
||||||
|
return acceleration(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int acceleration(CommandContext<CommandSourceStack> context) {
|
||||||
|
ServerLevel level = context.getSource().getServer().getLevel(
|
||||||
|
ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||||
|
if (level == null) {
|
||||||
|
context.getSource().sendFailure(Component.literal("Age not loaded: " + ageId(context)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
context.getSource().sendSuccess(() -> Component.literal(ageId(context) + " Accelerated pages="
|
||||||
|
+ PortAcceleratedTicks.pages(level) + "; extra attempts/eligible section="
|
||||||
|
+ 3L * PortAcceleratedTicks.pages(level)
|
||||||
|
+ "; max loaded chunks=" + net.dnorberg.rivencraft.PortConfig.ACCELERATED_MAX_CHUNKS.get()
|
||||||
|
+ "; max attempts/tick=" + net.dnorberg.rivencraft.PortConfig.ACCELERATED_MAX_ATTEMPTS.get()), false);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
private static int sampleBiome(CommandContext<CommandSourceStack> context) {
|
private static int sampleBiome(CommandContext<CommandSourceStack> context) {
|
||||||
Identifier id = ageId(context);
|
Identifier id = ageId(context);
|
||||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import com.mojang.serialization.codecs.RecordCodecBuilder;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import net.minecraft.resources.Identifier;
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
/** Frozen authored biome choice; repeated entries retain their selection weight. */
|
/** Frozen biome choice; Native may carry an explicit Cave pool. Repeats retain weight. */
|
||||||
public record PortBiomePlan(Distribution distribution, List<Identifier> biomes) {
|
public record PortBiomePlan(Distribution distribution, List<Identifier> biomes) {
|
||||||
public enum Distribution { NATIVE, SINGLE, TILED, GRID, TINY, SMALL, MEDIUM, LARGE, HUGE }
|
public enum Distribution { NATIVE, SINGLE, TILED, GRID, TINY, SMALL, MEDIUM, LARGE, HUGE }
|
||||||
|
|
||||||
@@ -17,8 +17,7 @@ public record PortBiomePlan(Distribution distribution, List<Identifier> biomes)
|
|||||||
|
|
||||||
public PortBiomePlan {
|
public PortBiomePlan {
|
||||||
biomes = List.copyOf(biomes);
|
biomes = List.copyOf(biomes);
|
||||||
if (distribution == Distribution.NATIVE && !biomes.isEmpty()
|
if (distribution == null || distribution == Distribution.SINGLE && biomes.size() != 1
|
||||||
|| distribution == Distribution.SINGLE && biomes.size() != 1
|
|
||||||
|| distribution != Distribution.NATIVE && distribution != Distribution.SINGLE && biomes.size() < 2)
|
|| distribution != Distribution.NATIVE && distribution != Distribution.SINGLE && biomes.size() < 2)
|
||||||
throw new IllegalArgumentException("Invalid authored biome distribution");
|
throw new IllegalArgumentException("Invalid authored biome distribution");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,30 +19,18 @@ public final class PortRandomAgeSelection {
|
|||||||
public static Selection select(MinecraftServer server, long seed) {
|
public static Selection select(MinecraftServer server, long seed) {
|
||||||
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
|
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
|
||||||
// the other terrain families are rarer. The sorted order is deliberate.
|
// the other terrain families are rarer. The sorted order is deliberate.
|
||||||
PortAgeTerrain terrain = weighted(new Random(seed ^ 0x5445525241494E4CL),
|
PortAgeTerrain terrain = terrain(seed);
|
||||||
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
|
||||||
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
|
|
||||||
new int[]{1, 1, 5, 1, 6, 1});
|
|
||||||
if (terrain == PortAgeTerrain.VOID)
|
if (terrain == PortAgeTerrain.VOID)
|
||||||
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
||||||
List.of(Identifier.withDefaultNamespace("the_void"))));
|
List.of(Identifier.withDefaultNamespace("the_void"))));
|
||||||
|
|
||||||
PortBiomePlan.Distribution distribution = weighted(new Random(seed ^ 0x524956454E435241L),
|
PortBiomePlan.Distribution distribution = distribution(seed);
|
||||||
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
|
|
||||||
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
|
|
||||||
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
|
|
||||||
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
|
|
||||||
PortBiomePlan.Distribution.HUGE),
|
|
||||||
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
|
|
||||||
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
||||||
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
||||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||||
// 26.3 has no native multi-cave biome source; choose a seeded Cave
|
|
||||||
// biome rather than silently using Lush Caves for every random Age.
|
|
||||||
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
|
|
||||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
|
||||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||||
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
|
return new Selection(terrain, new PortBiomePlan(distribution,
|
||||||
|
terrain == PortAgeTerrain.CAVE ? eligible : List.of()));
|
||||||
|
|
||||||
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||||
if (count > 1) {
|
if (count > 1) {
|
||||||
@@ -54,14 +42,30 @@ public final class PortRandomAgeSelection {
|
|||||||
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<PortAgeTerrain> terrains() {
|
||||||
|
return List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
||||||
|
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PortAgeTerrain terrain(long seed) {
|
||||||
|
return weighted(new Random(seed ^ 0x5445525241494E4CL), terrains(), new int[]{1, 1, 5, 1, 6, 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** First AgeTraits draw in 1.21.1; shared by random and partly authored Ages. */
|
||||||
|
public static PortBiomePlan.Distribution distribution(long seed) {
|
||||||
|
return weighted(new Random(seed ^ 0x524956454E435241L),
|
||||||
|
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
|
||||||
|
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
|
||||||
|
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
|
||||||
|
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
|
||||||
|
PortBiomePlan.Distribution.HUGE),
|
||||||
|
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
|
||||||
|
}
|
||||||
|
|
||||||
public static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
public static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||||
if (terrain == PortAgeTerrain.VOID) return List.of(Identifier.withDefaultNamespace("the_void"));
|
if (terrain == PortAgeTerrain.VOID) return List.of(Identifier.withDefaultNamespace("the_void"));
|
||||||
if (terrain == PortAgeTerrain.CAVE) {
|
if (terrain == PortAgeTerrain.CAVE) {
|
||||||
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
return caveBiomes(server.registryAccess());
|
||||||
.filter(PortRegistryPages::isCaveBiome)
|
|
||||||
.map(holder -> holder.key().identifier())
|
|
||||||
.sorted().toList();
|
|
||||||
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
|
||||||
}
|
}
|
||||||
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
||||||
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
||||||
@@ -69,6 +73,12 @@ public final class PortRandomAgeSelection {
|
|||||||
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
|
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<Identifier> caveBiomes(net.minecraft.core.HolderLookup.Provider registries) {
|
||||||
|
var caves = registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||||
|
.filter(PortRegistryPages::isCaveBiome).map(holder -> holder.key().identifier()).sorted().toList();
|
||||||
|
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
||||||
|
}
|
||||||
|
|
||||||
static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
||||||
int total = 0;
|
int total = 0;
|
||||||
for (int weight : weights) total += weight;
|
for (int weight : weights) total += weight;
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ public final class PortRandomStructureSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Set<Identifier> actualBiomes(MinecraftServer server, PortAgeTerrain terrain, PortBiomePlan plan) {
|
private static Set<Identifier> actualBiomes(MinecraftServer server, PortAgeTerrain terrain, PortBiomePlan plan) {
|
||||||
if (plan != null && plan.distribution() != PortBiomePlan.Distribution.NATIVE)
|
if (plan != null && !plan.biomes().isEmpty())
|
||||||
return Set.copyOf(plan.biomes());
|
return Set.copyOf(plan.biomes());
|
||||||
if (terrain == PortAgeTerrain.FLAT) return Set.of(Identifier.withDefaultNamespace("plains"));
|
if (terrain == PortAgeTerrain.FLAT) return Set.of(Identifier.withDefaultNamespace("plains"));
|
||||||
if (terrain == PortAgeTerrain.VOID) return Set.of(Identifier.withDefaultNamespace("the_void"));
|
if (terrain == PortAgeTerrain.VOID) return Set.of(Identifier.withDefaultNamespace("the_void"));
|
||||||
|
|||||||
@@ -95,11 +95,22 @@ public final class PortStructurePages {
|
|||||||
var set = registries.lookupOrThrow(Registries.STRUCTURE_SET)
|
var set = registries.lookupOrThrow(Registries.STRUCTURE_SET)
|
||||||
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)));
|
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)));
|
||||||
if (set.isEmpty()) return false;
|
if (set.isEmpty()) return false;
|
||||||
var compatible = set.get().value().structures().stream()
|
var compatible = compatibleBiomes(set.get().value());
|
||||||
|
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<Identifier> compatibleBiomes(Identifier page, HolderLookup.Provider registries) {
|
||||||
|
if (!supports(page)) return Set.of();
|
||||||
|
return registries.lookupOrThrow(Registries.STRUCTURE_SET)
|
||||||
|
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)))
|
||||||
|
.map(holder -> compatibleBiomes(holder.value())).orElse(Set.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<Identifier> compatibleBiomes(StructureSet set) {
|
||||||
|
return set.structures().stream()
|
||||||
.flatMap(entry -> entry.structure().value().biomes().stream())
|
.flatMap(entry -> entry.structure().value().biomes().stream())
|
||||||
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
|
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
|
||||||
.collect(java.util.stream.Collectors.toSet());
|
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||||
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
|
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
|
||||||
|
|||||||
@@ -366,15 +366,14 @@ public final class RuntimeAgeBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static BiomeSource authoredBiomeSource(MinecraftServer server, PortBiomePlan plan, long seed) {
|
private static BiomeSource authoredBiomeSource(MinecraftServer server, PortBiomePlan plan, long seed) {
|
||||||
if (plan.distribution() == PortBiomePlan.Distribution.NATIVE)
|
|
||||||
throw new IllegalArgumentException("Native biome distribution has no authored source");
|
|
||||||
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||||
List<Holder<Biome>> biomes = plan.biomes().stream().map(id ->
|
List<Holder<Biome>> biomes = plan.biomes().stream().map(id ->
|
||||||
(Holder<Biome>) registry.get(ResourceKey.create(Registries.BIOME, id))
|
(Holder<Biome>) registry.get(ResourceKey.create(Registries.BIOME, id))
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + id)))
|
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + id)))
|
||||||
.toList();
|
.toList();
|
||||||
return switch (plan.distribution()) {
|
return switch (plan.distribution()) {
|
||||||
case NATIVE -> throw new IllegalStateException("Native is not an override");
|
case NATIVE -> biomes.size() == 1 ? new FixedBiomeSource(biomes.getFirst())
|
||||||
|
: new PortScaledBiomeSource(HolderSet.direct(biomes), seed, 0);
|
||||||
case SINGLE -> new FixedBiomeSource(biomes.getFirst());
|
case SINGLE -> new FixedBiomeSource(biomes.getFirst());
|
||||||
case TILED, GRID -> new CheckerboardColumnBiomeSource(HolderSet.direct(biomes),
|
case TILED, GRID -> new CheckerboardColumnBiomeSource(HolderSet.direct(biomes),
|
||||||
plan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4);
|
plan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4);
|
||||||
@@ -399,9 +398,12 @@ public final class RuntimeAgeBackend {
|
|||||||
PortAgeColors colors, PortDayCycle dayCycle) {
|
PortAgeColors colors, PortDayCycle dayCycle) {
|
||||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||||
|
if (biomePlan != null && biomePlan.distribution() == PortBiomePlan.Distribution.NATIVE
|
||||||
|
&& !biomePlan.biomes().isEmpty() && terrain != PortAgeTerrain.CAVE)
|
||||||
|
throw new IllegalArgumentException("Only Native Cave terrain may carry an explicit native biome pool");
|
||||||
|
|
||||||
// Noise terrains reuse the Overworld biome source unless a
|
// Noise terrains reuse the Overworld biome source unless a
|
||||||
// non-Native authored distribution replaces it.
|
// non-Native authored distribution or a frozen Native Cave pool replaces it.
|
||||||
// LevelStem.seedOverride controls chunk random state and getSeed().
|
// LevelStem.seedOverride controls chunk random state and getSeed().
|
||||||
LevelStem overworldStem = server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM)
|
LevelStem overworldStem = server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM)
|
||||||
.getValue(LevelStem.OVERWORLD);
|
.getValue(LevelStem.OVERWORLD);
|
||||||
@@ -453,7 +455,8 @@ public final class RuntimeAgeBackend {
|
|||||||
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biomeId);
|
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biomeId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
|
if (biomePlan != null && (biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE
|
||||||
|
|| terrain == PortAgeTerrain.CAVE && !biomePlan.biomes().isEmpty())) {
|
||||||
source = authoredBiomeSource(server, biomePlan, seed);
|
source = authoredBiomeSource(server, biomePlan, seed);
|
||||||
}
|
}
|
||||||
var settings = switch (terrain) {
|
var settings = switch (terrain) {
|
||||||
|
|||||||
@@ -110,8 +110,20 @@ public final class PortBookNetworking {
|
|||||||
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
|
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
|
||||||
long seed = player.getRandom().nextLong();
|
long seed = player.getRandom().nextLong();
|
||||||
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
|
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
|
||||||
|
PortAgeTerrain terrain;
|
||||||
|
List<Identifier> generationPages;
|
||||||
try {
|
try {
|
||||||
biomePlan = PortAuthoredWriting.biomePlan(pages, player.level().getServer(), seed);
|
if (pages.isEmpty()) {
|
||||||
|
var random = PortRandomAgeSelection.select(player.level().getServer(), seed);
|
||||||
|
terrain = random.terrain();
|
||||||
|
biomePlan = random.biomes();
|
||||||
|
generationPages = pages;
|
||||||
|
} else {
|
||||||
|
var selected = net.dnorberg.rivencraft.writing.PortAuthoredTerrainSelection.select(player.level().getServer(), pages, seed);
|
||||||
|
terrain = selected.terrain();
|
||||||
|
biomePlan = selected.biomes();
|
||||||
|
generationPages = selected.generationPages();
|
||||||
|
}
|
||||||
} catch (IllegalArgumentException exception) {
|
} catch (IllegalArgumentException exception) {
|
||||||
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
|
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
|
||||||
return null;
|
return null;
|
||||||
@@ -128,25 +140,21 @@ public final class PortBookNetworking {
|
|||||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
|
|
||||||
if (pages.isEmpty()) {
|
|
||||||
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
|
|
||||||
terrain = random.terrain();
|
|
||||||
biomePlan = random.biomes();
|
|
||||||
}
|
|
||||||
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
|
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
|
||||||
var randomFeatures = pages.isEmpty()
|
var randomFeatures = pages.isEmpty()
|
||||||
? net.dnorberg.rivencraft.age.PortRandomFeatureSelection.select(server, seed, terrain, biomePlan) : null;
|
? net.dnorberg.rivencraft.age.PortRandomFeatureSelection.select(server, seed, terrain, biomePlan) : null;
|
||||||
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
var level = RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||||
randomFeatures == null ? PortAuthoredWriting.rockPlan(pages, seed) : randomFeatures.rocks(),
|
randomFeatures == null ? PortAuthoredWriting.rockPlan(generationPages, seed) : randomFeatures.rocks(),
|
||||||
PortAuthoredWriting.vegetationPlan(pages),
|
PortAuthoredWriting.vegetationPlan(generationPages),
|
||||||
randomFeatures == null ? PortAuthoredWriting.dungeonCount(pages) : randomFeatures.dungeons(),
|
randomFeatures == null ? PortAuthoredWriting.dungeonCount(generationPages) : randomFeatures.dungeons(),
|
||||||
randomFeatures == null ? PortAuthoredWriting.lakePlan(pages, seed) : randomFeatures.lakes(),
|
randomFeatures == null ? PortAuthoredWriting.lakePlan(generationPages, seed) : randomFeatures.lakes(),
|
||||||
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(pages, seed) : randomFeatures.terrain(),
|
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(generationPages, seed) : randomFeatures.terrain(),
|
||||||
environment.weather(), environment.lighting(), environment.sky(),
|
environment.weather(), environment.lighting(), environment.sky(),
|
||||||
net.dnorberg.rivencraft.age.PortAgeColors.resolve(pages, seed),
|
net.dnorberg.rivencraft.age.PortAgeColors.resolve(pages, seed),
|
||||||
net.dnorberg.rivencraft.age.PortDayCycle.resolve(pages, seed,
|
net.dnorberg.rivencraft.age.PortDayCycle.resolve(pages, seed,
|
||||||
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
|
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
|
||||||
|
net.dnorberg.rivencraft.age.PortAcceleratedTicks.configure(level,
|
||||||
|
net.dnorberg.rivencraft.age.PortAcceleratedTicks.pageCount(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;
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package net.dnorberg.rivencraft.writing;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||||
|
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
||||||
|
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
import net.minecraft.server.MinecraftServer;
|
||||||
|
|
||||||
|
/** Root AgeRuntimeProfiles.selectForWriting, limited to currently supported port combinations. */
|
||||||
|
public final class PortAuthoredTerrainSelection {
|
||||||
|
private PortAuthoredTerrainSelection() {}
|
||||||
|
|
||||||
|
public record Selection(PortAgeTerrain terrain, List<Identifier> generationPages, PortBiomePlan biomes) {
|
||||||
|
public Selection { generationPages = List.copyOf(generationPages); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Selection select(MinecraftServer server, List<Identifier> pages, long seed) {
|
||||||
|
var explicit = PortAuthoredWriting.explicitTerrain(pages);
|
||||||
|
if (explicit != null) return compile(server, pages, seed, explicit);
|
||||||
|
var preferred = PortRandomAgeSelection.terrain(seed);
|
||||||
|
try {
|
||||||
|
return compile(server, pages, seed, preferred);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
// Match root's stable profile-ID fallback rather than rerolling the seed.
|
||||||
|
}
|
||||||
|
var compatible = new ArrayList<Selection>();
|
||||||
|
for (var terrain : PortRandomAgeSelection.terrains()) {
|
||||||
|
if (terrain == preferred) continue;
|
||||||
|
try { compatible.add(compile(server, pages, seed, terrain)); }
|
||||||
|
catch (IllegalArgumentException ignored) { }
|
||||||
|
}
|
||||||
|
if (compatible.isEmpty()) throw new IllegalArgumentException("No supported terrain can host this book's pages");
|
||||||
|
return compatible.get(Math.floorMod(Long.hashCode(seed), compatible.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Selection compile(MinecraftServer server, List<Identifier> pages, long seed, PortAgeTerrain terrain) {
|
||||||
|
if (!PortAuthoredWriting.supportsOnTerrain(pages, terrain, seed))
|
||||||
|
throw new IllegalArgumentException("Unsupported pages for " + terrain + " terrain");
|
||||||
|
var resolved = PortAuthoredWriting.withTerrain(pages, terrain);
|
||||||
|
// Also preflight registry-backed biome restrictions, so an automatic Cave
|
||||||
|
// choice cannot turn supplied surface biome pages into an unusable book.
|
||||||
|
var biomes = PortAuthoredWriting.biomePlan(resolved, server, seed);
|
||||||
|
return new Selection(terrain, resolved, biomes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,6 +57,7 @@ public final class PortAuthoredWriting {
|
|||||||
if (entry == null) return false;
|
if (entry == null) return false;
|
||||||
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|
||||||
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) return true;
|
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) return true;
|
||||||
|
if (page.equals(net.dnorberg.rivencraft.age.PortAcceleratedTicks.PAGE)) return true;
|
||||||
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null
|
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null
|
||||||
|| PortLighting.forPage(page) != null || PortSkyStyle.forPage(page) != null) return true;
|
|| PortLighting.forPage(page) != null || PortSkyStyle.forPage(page) != null) return true;
|
||||||
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
|
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
|
||||||
@@ -72,11 +73,35 @@ public final class PortAuthoredWriting {
|
|||||||
|
|
||||||
/** 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) {
|
||||||
|
if (pages.stream().noneMatch(PortAuthoredWriting::isTerrain))
|
||||||
|
return net.dnorberg.rivencraft.age.PortRandomAgeSelection.terrains().stream()
|
||||||
|
.anyMatch(terrain -> supportsOnTerrain(pages, terrain, 0));
|
||||||
try {
|
try {
|
||||||
return supportsExplicit(PortMaterialCompletion.complete(pages, 0));
|
return supportsExplicit(PortMaterialCompletion.complete(pages, 0));
|
||||||
} catch (IllegalArgumentException exception) { return false; }
|
} catch (IllegalArgumentException exception) { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static boolean supportsOnTerrain(List<Identifier> pages, PortAgeTerrain terrain, long seed) {
|
||||||
|
try {
|
||||||
|
return supportsExplicit(PortMaterialCompletion.complete(withTerrain(pages, terrain), seed));
|
||||||
|
} catch (IllegalArgumentException exception) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Identifier> withTerrain(List<Identifier> pages, PortAgeTerrain terrain) {
|
||||||
|
if (pages.stream().anyMatch(PortAuthoredWriting::isTerrain)) return List.copyOf(pages);
|
||||||
|
var resolved = new ArrayList<>(pages);
|
||||||
|
// Append internally so original material/color/controller indices stay stable.
|
||||||
|
resolved.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||||
|
"terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT)));
|
||||||
|
return List.copyOf(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
static PortAgeTerrain explicitTerrain(List<Identifier> pages) {
|
||||||
|
return pages.stream().filter(PortAuthoredWriting::isTerrain).findFirst()
|
||||||
|
.map(page -> PortAgeTerrain.valueOf(page.getPath().substring(8).toUpperCase(java.util.Locale.ROOT)))
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean supportsExplicit(List<Identifier> pages) {
|
private static boolean supportsExplicit(List<Identifier> pages) {
|
||||||
if (!net.dnorberg.rivencraft.age.PortAgeColors.validNumbers(pages)) return false;
|
if (!net.dnorberg.rivencraft.age.PortAgeColors.validNumbers(pages)) return false;
|
||||||
if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false;
|
if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false;
|
||||||
@@ -104,6 +129,8 @@ public final class PortAuthoredWriting {
|
|||||||
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|
||||||
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) {
|
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) {
|
||||||
// Shared colors target Sky, Ambient Light Color or Normal Time; trailing colors tint the sky.
|
// Shared colors target Sky, Ambient Light Color or Normal Time; trailing colors tint the sky.
|
||||||
|
} else if (page.equals(net.dnorberg.rivencraft.age.PortAcceleratedTicks.PAGE)) {
|
||||||
|
// Every Accelerated page adds random-tick attempts; it is not a controller.
|
||||||
} else if (isTerrain(page) && !terrain) {
|
} else if (isTerrain(page) && !terrain) {
|
||||||
terrain = true;
|
terrain = true;
|
||||||
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
|
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
|
||||||
@@ -321,17 +348,18 @@ public final class PortAuthoredWriting {
|
|||||||
|
|
||||||
/** Resolves all authored biomes from the active registry before creating a level. */
|
/** Resolves all authored biomes from the active registry before creating a level. */
|
||||||
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||||
return biomePlan(pages, registries, 0, null);
|
return biomePlan(pages, registries, 0, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creation-only completion; the saved plan never rerolls on load or reseed. */
|
/** Creation-only completion; the saved plan never rerolls on load or reseed. */
|
||||||
public static PortBiomePlan biomePlan(List<Identifier> pages, net.minecraft.server.MinecraftServer server, long seed) {
|
public static PortBiomePlan biomePlan(List<Identifier> pages, net.minecraft.server.MinecraftServer server, long seed) {
|
||||||
return biomePlan(pages, server.registryAccess(), seed,
|
return biomePlan(pages, server.registryAccess(), seed,
|
||||||
net.dnorberg.rivencraft.age.PortRandomAgeSelection.eligibleBiomes(server, terrain(pages)));
|
net.dnorberg.rivencraft.age.PortRandomAgeSelection.eligibleBiomes(server, terrain(pages)),
|
||||||
|
net.dnorberg.rivencraft.age.PortRandomAgeSelection.distribution(seed));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries,
|
private static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries,
|
||||||
long seed, List<Identifier> available) {
|
long seed, List<Identifier> available, PortBiomePlan.Distribution fallback) {
|
||||||
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<>();
|
||||||
@@ -364,8 +392,22 @@ public final class PortAuthoredWriting {
|
|||||||
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)
|
||||||
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
|
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
|
||||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
if (distribution == null && fallback != null) {
|
||||||
return new PortBiomePlan(distribution, List.of());
|
distribution = fallback;
|
||||||
|
// Root's authored-pool fallback never discards supplied biome pages.
|
||||||
|
if (pages.contains(VOID_TERRAIN) && biomes.isEmpty()) distribution = PortBiomePlan.Distribution.SINGLE;
|
||||||
|
if (!biomes.isEmpty() && (pages.contains(VOID_TERRAIN)
|
||||||
|
|| distribution == PortBiomePlan.Distribution.NATIVE
|
||||||
|
|| distribution == PortBiomePlan.Distribution.SINGLE && biomes.size() > 1))
|
||||||
|
distribution = biomes.size() == 1 ? PortBiomePlan.Distribution.SINGLE : biomes.size() == 2
|
||||||
|
? PortBiomePlan.Distribution.TILED : PortBiomePlan.Distribution.MEDIUM;
|
||||||
|
}
|
||||||
|
if (distribution == PortBiomePlan.Distribution.NATIVE) {
|
||||||
|
var nativePool = caveTerrain
|
||||||
|
? (available != null ? available : net.dnorberg.rivencraft.age.PortRandomAgeSelection.caveBiomes(registries))
|
||||||
|
: List.<Identifier>of();
|
||||||
|
return new PortBiomePlan(distribution, nativePool);
|
||||||
|
}
|
||||||
if (distribution != null && available != null) {
|
if (distribution != null && available != null) {
|
||||||
int minimum = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
int minimum = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||||
int target = minimum;
|
int target = minimum;
|
||||||
@@ -374,6 +416,9 @@ public final class PortAuthoredWriting {
|
|||||||
while (target < available.size() && random.nextInt(3) != 0) target++;
|
while (target < available.size() && random.nextInt(3) != 0) target++;
|
||||||
}
|
}
|
||||||
var distinct = new java.util.HashSet<>(biomes);
|
var distinct = new java.util.HashSet<>(biomes);
|
||||||
|
PortStructureBiomeCompletion.complete(pages, registries, available, !biomes.isEmpty(),
|
||||||
|
distribution, biomes, distinct, seed);
|
||||||
|
target = Math.max(target, distinct.size());
|
||||||
// Void may retain authored weather/season biomes, but must not gain
|
// Void may retain authored weather/season biomes, but must not gain
|
||||||
// an unrequested The Void biome just to fill a multi-biome controller.
|
// an unrequested The Void biome just to fill a multi-biome controller.
|
||||||
var filler = new ArrayList<>(pages.contains(VOID_TERRAIN) && !biomes.isEmpty() ? List.<Identifier>of() : available);
|
var filler = new ArrayList<>(pages.contains(VOID_TERRAIN) && !biomes.isEmpty() ? List.<Identifier>of() : available);
|
||||||
|
|||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package net.dnorberg.rivencraft.writing;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
||||||
|
import net.dnorberg.rivencraft.age.PortStructurePages;
|
||||||
|
import net.minecraft.core.HolderLookup;
|
||||||
|
import net.minecraft.resources.Identifier;
|
||||||
|
|
||||||
|
/** Root AgeBiomeSelection.completeStructureBiomes, before ordinary minimum filling. */
|
||||||
|
final class PortStructureBiomeCompletion {
|
||||||
|
private PortStructureBiomeCompletion() {}
|
||||||
|
|
||||||
|
static void complete(List<Identifier> pages, HolderLookup.Provider registries, List<Identifier> available,
|
||||||
|
boolean hasAuthoredBiomes, PortBiomePlan.Distribution distribution,
|
||||||
|
List<Identifier> selected, Set<Identifier> distinct, long seed) {
|
||||||
|
var requirements = pages.stream().filter(PortStructurePages::supports).distinct()
|
||||||
|
.map(page -> PortStructurePages.compatibleBiomes(page, registries))
|
||||||
|
.filter(requirement -> !requirement.isEmpty()).toList();
|
||||||
|
if (requirements.isEmpty()) return;
|
||||||
|
if (distribution == PortBiomePlan.Distribution.SINGLE) {
|
||||||
|
if (hasAuthoredBiomes) return;
|
||||||
|
var candidates = available.stream()
|
||||||
|
.filter(candidate -> requirements.stream().allMatch(requirement -> requirement.contains(candidate)))
|
||||||
|
.toList();
|
||||||
|
if (candidates.isEmpty()) return;
|
||||||
|
Identifier biome = choose(candidates, seed, 0x5354525543544249L);
|
||||||
|
selected.clear();
|
||||||
|
distinct.clear();
|
||||||
|
selected.add(biome);
|
||||||
|
distinct.add(biome);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < requirements.size(); index++) {
|
||||||
|
var requirement = requirements.get(index);
|
||||||
|
if (selected.stream().anyMatch(requirement::contains)) continue;
|
||||||
|
var candidates = available.stream().filter(requirement::contains)
|
||||||
|
.filter(candidate -> !distinct.contains(candidate)).toList();
|
||||||
|
if (candidates.isEmpty()) continue;
|
||||||
|
Identifier biome = choose(candidates, seed, 0x5354525543544249L + index);
|
||||||
|
selected.add(biome);
|
||||||
|
distinct.add(biome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Identifier choose(List<Identifier> candidates, long seed, long salt) {
|
||||||
|
var shuffled = new ArrayList<>(candidates);
|
||||||
|
Collections.shuffle(shuffled, new Random(seed ^ salt));
|
||||||
|
return shuffled.getFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user