Public Access
Port seeded Age completion and Accelerated pages to 26.3 beta
This commit is contained in:
1 parent
40770841ee
commit
f4e749c4d1
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
|
||||
1.21.1 gameplay or optional-integration parity.
|
||||
See the [26.3 beta guide](ports/mc263/README.md) and
|
||||
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.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.
|
||||
|
||||
## Getting started
|
||||
|
||||
@@ -10,6 +10,17 @@ tracks issues #97–#103 under parent #96.
|
||||
|
||||
## 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.
|
||||
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),
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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.
|
||||
Instability, crystal portals, discovery/stations, writing recovery and optional
|
||||
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
|
||||
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
|
||||
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:
|
||||
|
||||
```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_name=Rivencraft
|
||||
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.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.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.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.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.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.",
|
||||
|
||||
@@ -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 =
|
||||
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);
|
||||
SOUNDS.register(modEventBus);
|
||||
ITEMS.register(modEventBus);
|
||||
@@ -179,6 +180,7 @@ public final class Rivencraft {
|
||||
PortChunkGenerators.register(modEventBus);
|
||||
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
|
||||
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(AgeCatalogSync::onLogin);
|
||||
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;
|
||||
})))
|
||||
.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.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
@@ -108,7 +121,10 @@ public final class PortAgeCommands {
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", 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.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeBiomePool)))
|
||||
@@ -692,6 +708,63 @@ public final class PortAgeCommands {
|
||||
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) {
|
||||
try {
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
@@ -946,16 +1019,24 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context) {
|
||||
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context, Identifier structure) {
|
||||
try {
|
||||
var server = context.getSource().getServer();
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var distribution = PortBiomePlan.Distribution.valueOf(
|
||||
StringArgumentType.getString(context, "distribution").toUpperCase(Locale.ROOT));
|
||||
String distributionName = StringArgumentType.getString(context, "distribution");
|
||||
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);
|
||||
var pages = java.util.List.of(symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
|
||||
symbol.apply("biome/" + distribution.name().toLowerCase(Locale.ROOT)));
|
||||
var pages = new java.util.ArrayList<Identifier>();
|
||||
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);
|
||||
// Controller-only and partial pools complete; already-sufficient pools never gain extras.
|
||||
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();
|
||||
if (!plan.equals(PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||
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);
|
||||
return created == 0 ? 0 : probeBiomePool(context);
|
||||
} 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) {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) {
|
||||
@@ -1947,6 +2146,40 @@ public final class PortAgeCommands {
|
||||
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) {
|
||||
Identifier id = ageId(context);
|
||||
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 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 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 {
|
||||
biomes = List.copyOf(biomes);
|
||||
if (distribution == Distribution.NATIVE && !biomes.isEmpty()
|
||||
|| distribution == Distribution.SINGLE && biomes.size() != 1
|
||||
if (distribution == null || distribution == Distribution.SINGLE && biomes.size() != 1
|
||||
|| distribution != Distribution.NATIVE && distribution != Distribution.SINGLE && biomes.size() < 2)
|
||||
throw new IllegalArgumentException("Invalid authored biome distribution");
|
||||
}
|
||||
|
||||
@@ -19,30 +19,18 @@ public final class PortRandomAgeSelection {
|
||||
public static Selection select(MinecraftServer server, long seed) {
|
||||
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
|
||||
// the other terrain families are rarer. The sorted order is deliberate.
|
||||
PortAgeTerrain terrain = weighted(new Random(seed ^ 0x5445525241494E4CL),
|
||||
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
||||
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
|
||||
new int[]{1, 1, 5, 1, 6, 1});
|
||||
PortAgeTerrain terrain = terrain(seed);
|
||||
if (terrain == PortAgeTerrain.VOID)
|
||||
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
||||
List.of(Identifier.withDefaultNamespace("the_void"))));
|
||||
|
||||
PortBiomePlan.Distribution distribution = weighted(new Random(seed ^ 0x524956454E435241L),
|
||||
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
|
||||
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
|
||||
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
|
||||
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
|
||||
PortBiomePlan.Distribution.HUGE),
|
||||
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
|
||||
PortBiomePlan.Distribution distribution = distribution(seed);
|
||||
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
||||
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
// 26.3 has no native multi-cave biome source; choose a seeded Cave
|
||||
// biome rather than silently using Lush Caves for every random Age.
|
||||
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
|
||||
return new Selection(terrain, new PortBiomePlan(distribution,
|
||||
terrain == PortAgeTerrain.CAVE ? eligible : List.of()));
|
||||
|
||||
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
if (count > 1) {
|
||||
@@ -54,14 +42,30 @@ public final class PortRandomAgeSelection {
|
||||
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) {
|
||||
if (terrain == PortAgeTerrain.VOID) return List.of(Identifier.withDefaultNamespace("the_void"));
|
||||
if (terrain == PortAgeTerrain.CAVE) {
|
||||
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(PortRegistryPages::isCaveBiome)
|
||||
.map(holder -> holder.key().identifier())
|
||||
.sorted().toList();
|
||||
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
||||
return caveBiomes(server.registryAccess());
|
||||
}
|
||||
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
||||
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
||||
@@ -69,6 +73,12 @@ public final class PortRandomAgeSelection {
|
||||
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) {
|
||||
int total = 0;
|
||||
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) {
|
||||
if (plan != null && plan.distribution() != PortBiomePlan.Distribution.NATIVE)
|
||||
if (plan != null && !plan.biomes().isEmpty())
|
||||
return Set.copyOf(plan.biomes());
|
||||
if (terrain == PortAgeTerrain.FLAT) return Set.of(Identifier.withDefaultNamespace("plains"));
|
||||
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)
|
||||
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)));
|
||||
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(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
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) {
|
||||
if (plan.distribution() == PortBiomePlan.Distribution.NATIVE)
|
||||
throw new IllegalArgumentException("Native biome distribution has no authored source");
|
||||
var registry = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
List<Holder<Biome>> biomes = plan.biomes().stream().map(id ->
|
||||
(Holder<Biome>) registry.get(ResourceKey.create(Registries.BIOME, id))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + id)))
|
||||
.toList();
|
||||
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 TILED, GRID -> new CheckerboardColumnBiomeSource(HolderSet.direct(biomes),
|
||||
plan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4);
|
||||
@@ -399,9 +398,12 @@ public final class RuntimeAgeBackend {
|
||||
PortAgeColors colors, PortDayCycle dayCycle) {
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, 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
|
||||
// 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 overworldStem = server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM)
|
||||
.getValue(LevelStem.OVERWORLD);
|
||||
@@ -453,7 +455,8 @@ public final class RuntimeAgeBackend {
|
||||
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);
|
||||
}
|
||||
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;
|
||||
long seed = player.getRandom().nextLong();
|
||||
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
|
||||
PortAgeTerrain terrain;
|
||||
List<Identifier> generationPages;
|
||||
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) {
|
||||
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
|
||||
return null;
|
||||
@@ -128,25 +140,21 @@ public final class PortBookNetworking {
|
||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
}
|
||||
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 randomFeatures = pages.isEmpty()
|
||||
? net.dnorberg.rivencraft.age.PortRandomFeatureSelection.select(server, seed, terrain, biomePlan) : null;
|
||||
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||
randomFeatures == null ? PortAuthoredWriting.rockPlan(pages, seed) : randomFeatures.rocks(),
|
||||
PortAuthoredWriting.vegetationPlan(pages),
|
||||
randomFeatures == null ? PortAuthoredWriting.dungeonCount(pages) : randomFeatures.dungeons(),
|
||||
randomFeatures == null ? PortAuthoredWriting.lakePlan(pages, seed) : randomFeatures.lakes(),
|
||||
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(pages, seed) : randomFeatures.terrain(),
|
||||
var level = RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||
randomFeatures == null ? PortAuthoredWriting.rockPlan(generationPages, seed) : randomFeatures.rocks(),
|
||||
PortAuthoredWriting.vegetationPlan(generationPages),
|
||||
randomFeatures == null ? PortAuthoredWriting.dungeonCount(generationPages) : randomFeatures.dungeons(),
|
||||
randomFeatures == null ? PortAuthoredWriting.lakePlan(generationPages, seed) : randomFeatures.lakes(),
|
||||
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(generationPages, seed) : randomFeatures.terrain(),
|
||||
environment.weather(), environment.lighting(), environment.sky(),
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.resolve(pages, seed),
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.resolve(pages, seed,
|
||||
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) {
|
||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||
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 (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|
||||
|| 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
|
||||
|| PortLighting.forPage(page) != null || PortSkyStyle.forPage(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. */
|
||||
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 {
|
||||
return supportsExplicit(PortMaterialCompletion.complete(pages, 0));
|
||||
} 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) {
|
||||
if (!net.dnorberg.rivencraft.age.PortAgeColors.validNumbers(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)
|
||||
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) {
|
||||
// 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) {
|
||||
terrain = true;
|
||||
} 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. */
|
||||
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. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, net.minecraft.server.MinecraftServer server, long 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,
|
||||
long seed, List<Identifier> available) {
|
||||
long seed, List<Identifier> available, PortBiomePlan.Distribution fallback) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException(
|
||||
"Unsupported terrain or biome-page combination in this port slice");
|
||||
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");
|
||||
if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome)
|
||||
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new PortBiomePlan(distribution, List.of());
|
||||
if (distribution == null && fallback != null) {
|
||||
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) {
|
||||
int minimum = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
int target = minimum;
|
||||
@@ -374,6 +416,9 @@ public final class PortAuthoredWriting {
|
||||
while (target < available.size() && random.nextInt(3) != 0) target++;
|
||||
}
|
||||
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
|
||||
// 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);
|
||||
|
||||
+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