Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c223f3fdca | ||
|
|
d75154fa9b | ||
|
|
9ccec18c18 | ||
|
|
d9464fad76 | ||
|
|
d6e9a58a24 | ||
|
|
fa294be685 | ||
|
|
cb31156584 | ||
|
|
c23b057f51 |
No files matched your search
@@ -32,6 +32,10 @@ bin/
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
### Python tools ###
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
### Minecraft Modding ###
|
||||
run/
|
||||
logs/
|
||||
|
||||
@@ -7,9 +7,18 @@ of [Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to
|
||||
Minecraft 1.21.1 on NeoForge. Mystcraft veterans will recognize the writing
|
||||
flow, D'ni-inspired glyphs, libraries, crystals, and instability. Rivencraft
|
||||
also lets you author modern terrain, biomes, structures, seasons, and celestial
|
||||
bodies. It is an active pre-public-release project, not a byte-for-byte copy of
|
||||
bodies. It is an independently developed project, not a byte-for-byte copy of
|
||||
the 1.12.2 mod.
|
||||
|
||||
The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
|
||||
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
|
||||
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
|
||||
supports authored terrain/biomes and a growing set of generation features,
|
||||
but does not yet provide full 1.21.1 gameplay or optional-integration parity.
|
||||
See the [26.3 beta guide](ports/mc263/README.md) and
|
||||
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.54-beta.1.md).
|
||||
Keep the builds and their saves separate, and back up worlds before updating.
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
|
||||
|
||||
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 243 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 60 KiB |
@@ -480,6 +480,207 @@ symbol-page data and authoring pipeline, not just drawing more placeholders.
|
||||
The local `1.0.1.15-alpha.1` follow-up changes the missing-panel cue to the
|
||||
Link Panel's 16×16 item icon while preserving its red pulse and tooltip.
|
||||
|
||||
The local `1.0.1.33-beta.1` slice adds Flat and Void terrain to authored Binder
|
||||
books. Flat uses vanilla superflat layers and can use one authored Biome; Void
|
||||
generates no terrain but can use one authored Biome for environmental behavior.
|
||||
Without a Biome page, Void uses `minecraft:the_void`. Books with multiple Biome
|
||||
pages and Flat/Void terrain remain unassembled until their custom distribution
|
||||
generator is ported. The dedicated-server smoke creates, samples, and reloads
|
||||
both terrain families in a disposable world.
|
||||
|
||||
The local `1.0.1.34-beta.1` follow-up moves Flat ground to Y=0 and targets its
|
||||
surface for travel. `1.0.1.35-beta.1` enables the selected biome's decoration
|
||||
on Flat. `1.0.1.36-beta.1` ports a multi-biome Flat/Void chunk generator and
|
||||
accepts the existing authored distribution pages for those terrains. New Flat
|
||||
chunks can vary vegetation by biome; Void keeps biome climate without adding
|
||||
a floor or decorations. These are beta-port slices, not full 1.21.1 parity.
|
||||
|
||||
The local `1.0.1.37-beta.1` slice ports Cave terrain authoring using the
|
||||
26.3 caves noise preset. Cave and Nether Biome pages can be distributed
|
||||
through an Age, and travel searches for footing from Y=64 downward rather
|
||||
than mistaking the cave roof for the surface. Nether-style cave material
|
||||
replacement from 1.21.1 remains a separate port task. The local
|
||||
`1.0.1.38-beta.1` follow-up
|
||||
keeps Cave Ages dry by default: the vanilla 26.3 Cave preset otherwise fills
|
||||
low openings with water. This changes newly generated chunks only; a future
|
||||
Flooded page will deliberately set an authored water level in both maintained
|
||||
versions.
|
||||
The local `1.0.1.39-beta.1` follow-up makes the 26.3 landing search reject
|
||||
fluid at feet and head height and prevents a submerged fallback platform.
|
||||
|
||||
The local `1.0.1.40-beta.1` slice gives pageless Descriptive Books a
|
||||
seed-selected terrain and biome plan. It follows the 1.21.1 rank weights for
|
||||
terrain and biome distribution, selects registered Cave/Nether biomes for Cave
|
||||
Ages and overworld-source biomes for other non-Void terrains, and persists the
|
||||
resolved choices. Other random Age traits remain a later port task.
|
||||
|
||||
The local `1.0.1.41-beta.1` slice adds the first Age-scoped feature wrapper to
|
||||
the 26.3 generator. A Block page followed by Rocks now places clusters of the
|
||||
chosen block in new Standard, Amplified, Island, or Flat terrain chunks. The
|
||||
Age index persists that selection, and the wrapper delegates all vanilla
|
||||
terrain, biome, and structure generation. Cave and Void are intentionally
|
||||
excluded until their feature placement rules are ported. This is not yet the
|
||||
full 1.21.1 feature grammar or instability system.
|
||||
|
||||
The local `1.0.1.42-beta.1` follow-up resolves unmodified Rocks to a
|
||||
seed-shuffled, full-block material and accepts up to 32 ordered Rocks pages.
|
||||
Each page adds an independent, deterministic surface placement attempt.
|
||||
Explicit Block modifiers still bind only to the immediately following Rocks
|
||||
page. The material sequence persists in the Age index; the codec also reads
|
||||
the single-material format from the previous beta. The remaining feature
|
||||
families and instability scoring are not yet ported.
|
||||
|
||||
The local `1.0.1.43-beta.1` slice uses 26.3's `minecraft:forest_rock`
|
||||
placed-feature rules and `BlockBlobFeature` for authored Rocks, supplying the
|
||||
Age's selected material and removing only the vanilla biome filter. This
|
||||
replaces the port's temporary 3×3 pile algorithm and follows the 1.21.1
|
||||
generator's authored-placement approach. It affects new chunks only.
|
||||
|
||||
The local `1.0.1.44-beta.1` slice extends this Age-scoped feature wrapper to
|
||||
Flowers, Trees, and Huge Trees. Ordered authored pages retain independent
|
||||
attempts and use vanilla 26.3 placed-feature rules after biome decoration.
|
||||
The vegetation plan is persisted alongside terrain, biome, and Rocks plans;
|
||||
the dedicated-server smoke compares generated flowers and logs against a
|
||||
same-seed Flat control and checks persistence after restart.
|
||||
|
||||
The local `1.0.1.45-beta.1` slice adds authored Dungeons separately from
|
||||
surface vegetation. Its 26.3 placed-feature JSON mirrors the 1.21.1 eight
|
||||
monster-room attempts over the world height; ordered duplicate pages add
|
||||
attempts. The count persists in the Age index and generator codec. The server
|
||||
smoke creates and samples a dungeon Age, counts monster spawners across a
|
||||
4×4-chunk area, and verifies the count after restart. Room appearance and
|
||||
normal book authoring remain in-game checks.
|
||||
|
||||
The local `1.0.1.46-beta.1` slice ports Surface Lakes, Deep Lakes, and Small
|
||||
Lakes, including ordered Fluid modifiers. The 26.3 LakeFeature retains its
|
||||
vanilla shape and replacement predicates while using the Age's frozen fluid
|
||||
provider. Surface placement keeps Rivencraft 1.21.1's one-in-twelve rarity;
|
||||
underground lakes use vanilla placement. Repeated unmodified pages resolve
|
||||
their fluids independently from a seeded, shuffled source-fluid pool. Lake
|
||||
plans participate in restore, unload/reload, reseed, and retirement rollback.
|
||||
The server smoke compares water generation against a same-seed control and
|
||||
checks explicit/random plans and fluid blocks across restart.
|
||||
|
||||
The local `1.0.1.47-beta.1` slice adapts the 1.21.1 FloatingIslandFeature
|
||||
algorithm to 26.3 chunk access and the biomes already assigned to each chunk.
|
||||
It retains source range 3, one-in-128 candidate rarity, Y=168..215 center
|
||||
selection, radii 24..41, coordinate-noise contours, air-only filling, and the
|
||||
biome-dependent soil cap. It supports Flat/Void in addition to noise terrains;
|
||||
Block modifiers and independent seeded random materials are frozen in an
|
||||
ordered terrain-feature plan. Rocks and islands share material resolution.
|
||||
|
||||
26.3's final NoiseBasedChunkGenerator merges noise, surface, and carving and
|
||||
does not expose a pre-surface hook. This port appends islands to the completed
|
||||
terrain future before decoration; existing terrain is preserved, but base
|
||||
carvers do not cut the added islands. The smoke verifies actual high-altitude
|
||||
material placement on noise, Flat, and Void terrain, distinct random material
|
||||
choices, authored biome preservation, and block fingerprints across restart.
|
||||
|
||||
The local `1.0.1.48-beta.1` slice ports TendrilFeature's Legacy-derived
|
||||
branching solid volumes using the 26.3 chunk mutation and height APIs. Source
|
||||
range 8, rate 15, size 18, nested node/height selection, branch splitting, and
|
||||
bedrock preservation match the 1.21.1 implementation. Chunk-local writes and
|
||||
neighbor-source seeds preserve continuous paths across chunk borders.
|
||||
|
||||
The existing terrain-feature plan now supports Tendrils on all terrain types,
|
||||
including Cave and Void. Compilation retains mixed feature order and applies
|
||||
the 32-occurrence limit per feature. Block modifiers and seeded random choices
|
||||
use the shared solid-material resolver; existing save/lifecycle handling carries
|
||||
both feature types. World validation is manual at the player's request; no
|
||||
development server is started for this slice.
|
||||
|
||||
The local `1.0.1.49-beta.1` slice ports the Legacy-derived Skylands filter
|
||||
from Rivencraft 1.21.1 with 26.3 chunk height/mutation APIs. The cutoff uses
|
||||
the same salt, seven octaves, initial cell width 128, amplitude 12, and base
|
||||
Y=76. Solid/liquid removal and world-height coverage match the reference.
|
||||
It runs once after base terrain construction and before authored solid
|
||||
volumes, retaining the feature ordering used by the 1.21.1 implementation.
|
||||
|
||||
Skylands is a material-free feature, accepted alongside every terrain type.
|
||||
Repeated pages retain the same idempotent effect. The existing terrain-feature
|
||||
plan stores an optional Skylands flag, defaulting to false for earlier beta
|
||||
Ages; no separate backend/lifecycle path is introduced. A Skylands-only plan
|
||||
can contain no material choices. Build and static checks are automated; world
|
||||
generation and restart validation are left to the player.
|
||||
|
||||
The local `1.0.1.50-beta.1` slice adapts the 1.21.1 SpheresFeature and
|
||||
SpikesFeature to 26.3. Spheres retain their source range 8, five-percent
|
||||
source chance, nested Y selection starting at 32, node geometry, and bedrock
|
||||
protection. They run after the base terrain/Skylands filter and work on every
|
||||
terrain type. Spikes retain their one-in-18 chunk chance, 6..37 height roll,
|
||||
width 1..4, solid-footprint checks, and chunk-local writes during decoration.
|
||||
They remain unavailable on Cave and Void terrain, matching 1.21.1.
|
||||
|
||||
Both page types use ordered saved solid-feature choices, preceding Block
|
||||
modifiers, and independent seeded materials/occurrences. No backend lifecycle
|
||||
branch or generated artwork is added. Compilation and static review are the
|
||||
automated checks; the player handles generation and restart validation.
|
||||
|
||||
The local `1.0.1.51-beta.1` slice ports ObelisksFeature using the 26.3 chunk
|
||||
coordinates and world-height APIs. The site salt, seed mixing, one-in-128
|
||||
chance, chunk-local site selection, 4×4 plinth, and 2×12×2 shaft match 1.21.1.
|
||||
Every footing column must be sturdy, dry, and free of block entities; the
|
||||
base surface can vary by at most two blocks and the full shaft must be open.
|
||||
No partial structure is written when a placement check fails.
|
||||
|
||||
The page joins the existing material-feature plan and shared material resolver.
|
||||
It runs during surface decoration, with its own occurrence counter, on all
|
||||
surface terrain types including Flat; Cave and Void remain unsupported.
|
||||
Spikes and Obelisks use shared stage/restriction classification. Build/static
|
||||
checks are automated; generation and restart checks remain with the player.
|
||||
|
||||
The local `1.0.1.52-beta.1` slice ports CrystalFormationsFeature directly
|
||||
from 1.21.1 with only registry, ChunkPos, and height API substitutions. It
|
||||
retains the site salt, one-in-fifteen rarity, 3×3 ground/clearance checks,
|
||||
three-block center height, randomized outer heights, budding hearts, and
|
||||
initial small buds. Existing blue crystal growth, sprites, drops, and sounds
|
||||
are reused without adding assets.
|
||||
|
||||
Crystal Formations is an ordered surface feature with fixed crystal material,
|
||||
not a consumer of Block/Fluid modifiers. It shares the existing saved feature
|
||||
plan and surface-terrain restrictions (including Flat, excluding Cave/Void),
|
||||
with independent occurrence seeds and the current 32-page cap. No backend
|
||||
lifecycle changes are needed. The Obelisks slice passed the player's in-game
|
||||
check; Crystal Formations build/static checks pass, with generation, growth,
|
||||
and restart validation handed to the player. No test server is started.
|
||||
|
||||
The local `1.0.1.53-beta.1` slice adapts 1.21.1 AgeDenseOres to modern
|
||||
`AbstractOreFeature` targets and `FeaturePlacer.placeWithBiomeCheck`. It keeps
|
||||
the common ore tag/name fallback, explicit `rivencraft:dense_ores` placed-
|
||||
feature opt-in tag, nearby biome discovery, stable registry-ID ordering,
|
||||
and independent per-occurrence seeds. Registered modded ore features remain
|
||||
eligible without depending on an ore-providing mod. Biomes and ore placements
|
||||
are collected once per chunk rather than once per repeated page.
|
||||
|
||||
The optional Dense Ores count is saved in the existing terrain-feature plan;
|
||||
older plans default to zero. Grammar accepts 1–32 occurrences on noise terrain
|
||||
(including Cave), rejects Flat/Void and material modifiers, and checks explicit
|
||||
biome pools for at least one eligible ore feature. No extra lifecycle path or
|
||||
asset is introduced. Random feature selection and instability remain later
|
||||
port work. Crystal Formations passed the player's in-game check; this slice
|
||||
passes build/static checks, with ore generation and restart validation manual.
|
||||
|
||||
The local `1.0.1.54-beta.1` slice ports Star Fissures. Its footprint matches
|
||||
1.21.1 exactly (including Legacy attribution); carving uses the dimension's
|
||||
minimum Y, clears fluids, and protects Cave terrain's roof. The authored flag
|
||||
is optional in the existing feature-plan codec. Duplicate pages retain the
|
||||
1.21.1 single-fissure result; all terrains are accepted, with no material
|
||||
modifier. Random feature selection is not introduced in this slice.
|
||||
|
||||
First arrival generates the fissure and stores its spawn/completion in per-Age
|
||||
SavedData. Re-entry/restart does not repeat carving; an obstructed saved arrival
|
||||
is rechecked for safe footing. The return block ports modern entity-inside and
|
||||
TeleportTransition APIs, has a registered Rivencraft block-entity type, and
|
||||
uses Minecraft's starfield renderer with 1.21.1 face offsets. Shared Overworld
|
||||
spawn safety search and fallback match 1.21.1, rather than respawning at a bed.
|
||||
The existing fissure OGG/model are reused. Client renderer registration stays
|
||||
client-only; the block has no item/drop and is inert in the Overworld.
|
||||
|
||||
Dense Ores passed the player's in-game check. Build, footprint comparison,
|
||||
resource packaging, and static checks pass for this slice. Generation, visual
|
||||
rendering, actual return travel, and restart checks are left to the player;
|
||||
no development server is started.
|
||||
|
||||
## Verification of this first slice
|
||||
|
||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||
@@ -488,3 +689,6 @@ extraction. The GameTest server ran 93 tests; 92 passed, while
|
||||
desks instead of one. That desk failure is outside the dimension-backend edit
|
||||
and must be resolved or rechecked before a release. The 1.21.1 `build` task
|
||||
passed again after the 26.3 registry slice.
|
||||
On 2026-11-01, the default 1.21.1 GameTest server was rerun and all 93
|
||||
required tests passed, including the desk test. Issue #97 was then closed;
|
||||
remaining 26.3 parity work stays under #98–#104.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Publishing Rivencraft with the Gitea, Modrinth, and CurseForge APIs
|
||||
|
||||
The local helper is [`scripts/publish_mod_sites.py`](../scripts/publish_mod_sites.py).
|
||||
It previews a release unless `--publish` is explicitly supplied. The current
|
||||
configuration is for the Minecraft **26.3** artifact only. The 1.21.1 line needs
|
||||
its required external dependencies mapped on both sites before API publishing
|
||||
is enabled for it.
|
||||
|
||||
## One-time token setup
|
||||
|
||||
For Gitea, an existing personal access token is sufficient if it has
|
||||
`write:repository` scope. That scope covers release creation and attachment
|
||||
upload; do not grant `all` if a narrower token is available. Create or inspect
|
||||
tokens under [Gitea user settings → Applications](https://git.paddedcell.org/user/settings/applications).
|
||||
The helper checks token identity and scope without displaying the secret.
|
||||
|
||||
Create a [Modrinth personal access token](https://docs.modrinth.com/api/#authentication)
|
||||
in account settings with `VERSION_CREATE` (upload) and `USER_READ` (read-only
|
||||
credential check) scopes. Create a
|
||||
[CurseForge Upload API token](https://support.curseforge.com/support/solutions/articles/9000197321)
|
||||
from the author's API Tokens page. Treat both as passwords: never put them in
|
||||
git, chat, a release note, or a command-line argument.
|
||||
|
||||
In a terminal at the repository root, run these one at a time and paste the
|
||||
corresponding token into the hidden prompt:
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_mod_sites.py store-token modrinth
|
||||
python3 scripts/publish_mod_sites.py store-token curseforge
|
||||
python3 scripts/publish_mod_sites.py store-token gitea
|
||||
```
|
||||
|
||||
The tokens are stored in the desktop Secret Service keyring under
|
||||
`application=rivencraft-publishing`, separately by site. The script also accepts
|
||||
`RIVENCRAFT_MODRINTH_TOKEN` or `RIVENCRAFT_CURSEFORGE_TOKEN` environment variables,
|
||||
but these take precedence over keyring entries and should not be put in shell
|
||||
history or startup files.
|
||||
|
||||
Verify the credentials without uploading anything:
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_mod_sites.py check-token modrinth
|
||||
python3 scripts/publish_mod_sites.py check-token curseforge
|
||||
python3 scripts/publish_mod_sites.py check-token gitea
|
||||
```
|
||||
|
||||
## Release workflow
|
||||
|
||||
Build the JAR, write a Markdown changelog, and preview the metadata:
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_mod_sites.py release \
|
||||
--jar ports/mc263/build/libs/rivencraft-26.3-1.0.1.32-beta.1.jar \
|
||||
--changelog ports/mc263/RELEASE_NOTES_1.0.1.32-beta.1.md \
|
||||
--version 1.0.1.32-beta.1 \
|
||||
--name 'Rivencraft 1.0.1.32-beta.1 — NeoForge 26.3' \
|
||||
--minecraft 26.3 --java 25 --channel beta
|
||||
```
|
||||
|
||||
For a *new* version only, append `--publish` to upload to both sites. Use
|
||||
`--site modrinth` or `--site curseforge` to target one site. Modrinth rejects an
|
||||
existing version number; CurseForge may not, so check its file list before any
|
||||
retry after a partial failure. CurseForge approval may take additional time.
|
||||
|
||||
The default destination is now all three sites. The Gitea release step requires
|
||||
the annotated version tag to have been pushed first. Gitea's `Vx.y.z.b-mc26.3-beta.1`
|
||||
tag convention is derived from the version and channel; an existing release
|
||||
with different title or notes is never overwritten. If release creation
|
||||
succeeds but its JAR upload fails, rerun with `--site gitea --publish` to resume
|
||||
only the missing attachment. `--site both` retains the earlier Modrinth and
|
||||
CurseForge-only behavior.
|
||||
|
||||
The helper uses Gitea's documented
|
||||
[`POST /repos/{owner}/{repo}/releases`](https://docs.gitea.com/api/operations/repo-create-release/)
|
||||
and [release attachment endpoint](https://docs.gitea.com/api/operations/repo-create-release-attachment/),
|
||||
plus Modrinth's documented multipart
|
||||
[`POST /version`](https://docs.modrinth.com/api/operations/createversion/) and
|
||||
CurseForge's documented
|
||||
[`POST /api/projects/{projectId}/upload-file`](https://support.curseforge.com/support/solutions/articles/9000197321)
|
||||
with the JAR, changelog, version channel, NeoForge, Minecraft version, Java
|
||||
version, and client/server environment. It sets no external dependencies for
|
||||
26.3, which uses Rivencraft's first-party Age backend.
|
||||
@@ -1,15 +1,16 @@
|
||||
# Minecraft 26.3 port workspace
|
||||
# Minecraft 26.3 beta port workspace
|
||||
|
||||
This is an **experimental alpha port** for issues #97–#104, not gameplay parity
|
||||
with the Minecraft 1.21.1 release. It registers basic items and a runtime Age
|
||||
prototype with persisted seeds. The operator-only `rivencraft_port age`
|
||||
commands create and inspect separate vanilla-terrain levels. The current
|
||||
development slice can name an unbound Descriptive Book to create a prototype
|
||||
Age, then enter it from the book GUI; the player verified this interaction in game.
|
||||
Named Linking Book binding/travel works as a limited probe. Authored
|
||||
generation, most content, recipes, workstations, portal systems, most modifiers and
|
||||
the destination preview renderer remain unported. Use a test world and keep a
|
||||
backup; do not substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
|
||||
This is an **experimental beta port** for issues #97–#104, not gameplay parity
|
||||
with the Minecraft 1.21.1 release. The 26.3 backend persists separate Ages and
|
||||
their seeds. Books, the Book Binder, and the Ink Mixer work, and the Binder can
|
||||
author Standard, Amplified, Island, Flat, Void, or Cave terrain with supported
|
||||
biome-distribution pages. Other terrain families, feature/material effects,
|
||||
portals, and much of
|
||||
the 1.21.1 gameplay remain unported. Use a test world and keep a backup; do not
|
||||
substitute this JAR for the 1.21.1 mod or import 1.21.1 saves.
|
||||
|
||||
The entries below record how the port was built, including earlier alpha-only
|
||||
limitations that were addressed by later slices.
|
||||
|
||||
The creative tab contains the two Books, Crystal Block and Crystal Shard;
|
||||
the player verified these in game. Unbound Linking Books use the Legacy
|
||||
@@ -80,6 +81,276 @@ The local `1.0.1.15-alpha.1` Binder polish uses the Link Panel's actual 16×16
|
||||
item icon for the missing-panel cue instead of a full-size page sprite. The
|
||||
red pulse and tooltip remain confined to that icon.
|
||||
|
||||
The local `1.0.1.16-alpha.1` slice adds the bundled 1.21.1 symbol-page
|
||||
identities to the 26.3 creative menu, grouped by category. Their names and
|
||||
descriptions are reused from the 1.21.1 translations, and the Page item now
|
||||
persists each symbol ID. These pages currently use the generic symbol-page
|
||||
artwork; the Legacy-style runtime glyph compositor has not been ported.
|
||||
They cannot yet be inserted into the Binder or change Age generation. This is
|
||||
the page-data foundation for the next authored-writing slice.
|
||||
|
||||
The local `1.0.1.17-alpha.1` follow-up derives biome pages from the active
|
||||
26.3 biome registry instead of only the older bundled list. It excludes the
|
||||
TerraBlender staging placeholder and separates cave/Nether biomes. Full-block
|
||||
materials and true source-fluid blocks also receive registry-derived pages,
|
||||
covering eligible new and modded blocks. Five 26.3 feature symbols cover
|
||||
poplar trees, fallen poplars, shelf mushrooms, red shrubs, and abandoned
|
||||
camps. These feature pages are identities only until their Age-generation
|
||||
handlers are ported. Biome, material, and feature pages still use generic
|
||||
artwork and cannot yet be placed in the Binder.
|
||||
|
||||
The local `1.0.1.18-alpha.1` follow-up restores the 1.21.1 animated redaction
|
||||
for Corrupted Page names and descriptions. It does not yet port the dynamic
|
||||
glyph artwork used for other symbol pages.
|
||||
|
||||
The local `1.0.1.19-alpha.1` slice ports the runtime glyph compositor for
|
||||
symbol Page items. It uses the shared Legacy component sheet and page
|
||||
parchment with a bounded 256-texture client cache; generated textures are
|
||||
released on resource reload. Registry-derived biome and material pages use
|
||||
the same deterministic four-word poem pattern as 1.21.1. No per-symbol PNGs
|
||||
are generated in the source tree. Binder insertion and authored Age generation
|
||||
are still unported.
|
||||
|
||||
The local `1.0.1.20-alpha.1` visual follow-up enlarges symbol artwork on Page
|
||||
items, slightly thickens the component strokes, and centers number glyphs at a
|
||||
larger size so they remain readable in inventory and creative-menu icons.
|
||||
|
||||
The local `1.0.1.21-alpha.1` slice ports the Binder's ordered, scrollable
|
||||
symbol-page strip. Pages persist in its block inventory and can be inserted,
|
||||
removed, and moved via the cursor or shift-click. A Binder with symbol pages
|
||||
cannot yet produce a book: authored Age compilation is the next port step, and
|
||||
silently producing a random Age would consume the player's writing incorrectly.
|
||||
|
||||
The local `1.0.1.22-alpha.1` visual follow-up gives large Binder pages the
|
||||
transparent, full-size Legacy glyph composition. Item icons retain their
|
||||
separate parchment-backed, reduced-size artwork.
|
||||
|
||||
The local `1.0.1.23-alpha.1` follow-up places Binder glyph overlays in a later
|
||||
26.3 GUI stratum than the page atlas, preventing texture batching from drawing
|
||||
the opaque page backgrounds over the dynamic artwork.
|
||||
|
||||
The local `1.0.1.24-alpha.1` follow-up renders Binder page glyphs through
|
||||
their working special item model, scaled to the page thumbnail. Direct GUI
|
||||
blitting of the generated texture remained blank in the 26.3 client.
|
||||
|
||||
The local `1.0.1.25-alpha.1` follow-up marks temporary Binder preview stacks
|
||||
for glyph-only special-model rendering. The full-size transparent artwork now
|
||||
uses the working item pipeline without drawing a second, smaller page outline.
|
||||
|
||||
The local `1.0.1.26-alpha.1` corrects the temporary preview component's codec
|
||||
registration, which NeoForge 26.3 requires during bootstrap.
|
||||
|
||||
The local `1.0.1.27-alpha.1` slice applies the first authored Age subset:
|
||||
Standard terrain plus at most one Biome page. The selected biome is persisted
|
||||
with the Age and used by a fixed-biome Overworld noise generator on creation
|
||||
and reload. Ordered page IDs are stored on the finished book. Other page
|
||||
combinations remain in the Binder and cannot yet be assembled, preventing
|
||||
silent loss of unsupported writing. Multi-biome distribution, other terrain
|
||||
profiles, and feature/material pages still need their 26.3 generation ports.
|
||||
|
||||
The `1.0.1.28`–`1.0.1.32` slices add Tiled, Grid, Native, and five scaled
|
||||
biome distributions; multiple authored Biome pages; Amplified and Island
|
||||
terrain; and saved terrain/biome plans across reload. A Binder or Ink Mixer
|
||||
beside another block no longer hides that block's faces. Unsupported authoring
|
||||
still stays in the Binder rather than silently producing an incorrect Age.
|
||||
|
||||
The local `1.0.1.33-beta.1` slice adds Flat and Void terrain to Binder
|
||||
authoring. Flat uses vanilla's superflat layers; Void has no generated floor
|
||||
and uses the selected Biome for its environmental rules. Each accepts zero or
|
||||
one authored Biome page. Unsupported multi-biome Flat/Void books remain in the
|
||||
Binder until their custom generator is ported. The operator probe can create
|
||||
test Ages with `rivencraft_port age create <name> <seed> flat|void`.
|
||||
|
||||
The local `1.0.1.34-beta.1` follow-up starts Flat's solid layers at Y=0
|
||||
(bedrock at 0, dirt at 1–2, grass at 3) and lands players at the ground surface
|
||||
instead of creating an elevated fallback platform. This affects newly
|
||||
generated Flat chunks; previously generated Age chunks are not rewritten.
|
||||
|
||||
The local `1.0.1.35-beta.1` follow-up enables biome decoration on newly
|
||||
generated Flat chunks, allowing the selected biome's trees, vegetation, and
|
||||
other placed features to appear on the Flat surface. Void remains undecorated.
|
||||
|
||||
The local `1.0.1.36-beta.1` slice accepts multiple Biome pages with Flat and
|
||||
Void terrain. It ports the 1.21.1 authored-biome Flat generator to 26.3, so
|
||||
Tiled, Grid, and scaled regions affect climate in both terrain types and
|
||||
surface decoration in Flat. Void continues to generate no floor or biome
|
||||
decoration. The operator-only `age create_multi` probe creates two-biome test
|
||||
Ages for the dedicated-server smoke test.
|
||||
|
||||
The local `1.0.1.37-beta.1` slice adds the Cave terrain page to Binder
|
||||
authoring. It uses Minecraft's caves noise settings, accepts Cave and Nether
|
||||
biome pages (including multi-biome distribution), and searches below Y=64 for
|
||||
a safe arrival instead of landing on the roof. The 26.3 vanilla Cave preset
|
||||
defaults to water below Y=32, so this port derives a dry Cave setting with
|
||||
air as its default fluid and the sea level below the world floor. This applies
|
||||
to newly generated Cave chunks; it does not drain existing chunks. Cave-specific Nether surface
|
||||
materials from 1.21.1 are not ported yet. The dedicated-server smoke now
|
||||
creates and reloads Cave Ages alongside the other terrain families.
|
||||
|
||||
The dry-Cave follow-up is packaged as `1.0.1.38-beta.1`; no 26.3 release is
|
||||
published until the in-game check passes and publication is explicitly requested.
|
||||
|
||||
The local `1.0.1.39-beta.1` follow-up rejects fluid at both feet and head
|
||||
height during safe-landing searches. It also refuses to carve the fallback
|
||||
platform into submerged space if no dry footing can be found.
|
||||
|
||||
The `1.0.1.40-beta.1` port slice seeds terrain and biome selection for
|
||||
Descriptive Books with no authored symbol pages. It uses the same terrain and
|
||||
biome-controller rank weights as the 1.21.1 line, freezes the resulting plan
|
||||
in the Age index, and accepts the result after restart. The operator-only
|
||||
`age create_random <name> <seed>` probe exercises this path. Random feature,
|
||||
sky, weather, and instability choices are not yet ported; this slice covers
|
||||
terrain and biomes only.
|
||||
|
||||
The `1.0.1.41-beta.1` slice establishes an Age-scoped decoration wrapper
|
||||
for 26.3 and accepts one Block material page immediately before a Rocks feature
|
||||
page. Newly generated surface chunks place small rock clusters made from that
|
||||
block without modifying the shared biome registry. The selected block and
|
||||
generator survive a restart. Cave and Void terrain remain unsupported for this
|
||||
first surface-feature path; unsupported combinations stay in the Binder.
|
||||
Random feature selection, unpaired materials, duplicate feature pages, and
|
||||
instability scoring remain future port steps.
|
||||
|
||||
The local `1.0.1.42-beta.1` follow-up lets a Rocks page choose a deterministic
|
||||
full-block material when no Block page precedes it. Repeated Rocks pages now
|
||||
add independent placement attempts (up to 32) instead of being rejected. The
|
||||
ordered material choices are frozen in the Age index and restored after a
|
||||
restart; old single-material beta Ages still load. An explicit Block page
|
||||
must still directly precede the Rocks page it modifies. Most other feature
|
||||
pages and material-feature pairings remain unported.
|
||||
|
||||
The local `1.0.1.43-beta.1` slice replaces the temporary 3×3 Rocks piles
|
||||
with Minecraft 26.3's forest-rock block-blob feature and its placement rules,
|
||||
matching the 1.21.1 Age feature path more closely. The selected material and
|
||||
repeat count still come from the frozen Age plan. Only newly generated chunks
|
||||
use the new rock shape; previously generated chunks remain unchanged.
|
||||
|
||||
The local `1.0.1.44-beta.1` slice ports ordered Flowers, Trees, and Huge
|
||||
Trees pages to surface Ages. Their 26.3 vanilla placed features run after
|
||||
biome decoration, with the Age's own seed; repeated pages make independent
|
||||
placement attempts. Cave/Void terrain and cave/Nether/End biome pools are
|
||||
not treated as surface vegetation. The feature plan survives server restart;
|
||||
already generated chunks do not gain new decoration.
|
||||
|
||||
The local `1.0.1.45-beta.1` slice ports the Dungeons page for Standard,
|
||||
Amplified, and Island noise terrain. It uses the 1.21.1 placement definition
|
||||
(eight monster-room attempts per page per chunk across the build height),
|
||||
stores repeat count with the Age, and rejects Dungeons pages on Flat, Void,
|
||||
or Cave terrain. New chunks only; existing chunks are not regenerated.
|
||||
|
||||
The local `1.0.1.46-beta.1` slice adds Surface Lakes, Deep Lakes, and Small
|
||||
Lakes on surface noise terrain. A Fluid page immediately before a lake page
|
||||
selects its fluid. Unmodified pages choose a seeded fluid from the same source-
|
||||
fluid pool exposed in Creative, with repeated choices and their order saved
|
||||
with the Age. Surface/Small Lakes retain the 1.21.1 one-in-twelve attempt rule;
|
||||
Deep Lakes retain vanilla underground placement. These pages currently require
|
||||
Standard, Amplified, or Island terrain. Existing chunks do not redecorate.
|
||||
|
||||
The local `1.0.1.47-beta.1` slice adds authored Floating Islands alongside
|
||||
Standard, Amplified, Island, Flat, and Void terrain. A preceding Block page
|
||||
sets the island core material; otherwise each occurrence resolves a seeded
|
||||
material from the full-block pool. The 1.21.1 shape, high altitude, rarity,
|
||||
and biome-dependent grass/dirt cap are retained. Repeated pages use independent
|
||||
island seeds. Cave terrain remains unsupported for this feature.
|
||||
|
||||
26.3 combines base noise, surface, and cave carving in one terrain operation.
|
||||
Floating Islands run after that operation and before decoration, so the added
|
||||
islands are not cut by the base terrain's carvers. The saved feature plan is
|
||||
used again after restart and for new chunks; existing chunks keep their blocks.
|
||||
|
||||
The local `1.0.1.48-beta.1` slice adds authored Tendrils on every terrain
|
||||
type, including Cave and Void. A preceding Block page selects the material;
|
||||
unmodified occurrences resolve seeded materials from the full-block pool.
|
||||
Repeated pages use independent paths, with up to 32 occurrences per feature.
|
||||
Mixed Floating Islands/Tendrils retain their authored order in the saved plan.
|
||||
|
||||
Tendrils retain the 1.21.1 branching path algorithm, source rates, and vertical
|
||||
distribution concentrated below Y=128. They can replace existing terrain while
|
||||
preserving bedrock. As with Floating Islands, they run after 26.3's combined
|
||||
terrain operation and before decoration. Build checks are automated; in-game
|
||||
generation, appearance, and restart validation are handed to the player.
|
||||
|
||||
The local `1.0.1.49-beta.1` slice adds the Skylands feature page. Its seeded
|
||||
seven-octave filter removes base terrain at/below a varying cutoff near Y=76
|
||||
and removes liquid above it, preserving the upper solid terrain. It accepts
|
||||
all terrain types and no Block modifier. On low Flat terrain this removes
|
||||
the base layers; on Void it leaves the empty base unchanged.
|
||||
|
||||
The filter runs once before authored Floating Islands and Tendrils, matching
|
||||
1.21.1; duplicate Skylands pages do not compound the filter. Its flag is saved
|
||||
with the terrain-feature plan. Existing chunks are unchanged. Try Amplified
|
||||
terrain for a clear visual check; generation and restart validation are manual.
|
||||
|
||||
The local `1.0.1.50-beta.1` slice ports Spheres and Spikes using the 1.21.1
|
||||
generation algorithms. Spheres add solid nodes on all terrain types, including
|
||||
Cave and Void, while preserving bedrock. Spikes require solid surface terrain:
|
||||
Standard, Amplified, Island, or Flat. Their circular footprint stays in one
|
||||
chunk and must have solid ground beneath every column.
|
||||
|
||||
Each page accepts a preceding Block material or resolves a seeded random
|
||||
material when unmodified. Repeated pages have independent placement seeds,
|
||||
up to 32 per feature. Spheres run during terrain construction, after Skylands;
|
||||
Spikes run after biome decoration. Both use the existing saved solid-feature
|
||||
plan. New chunks only; in-game appearance and restart checks remain manual.
|
||||
|
||||
The local `1.0.1.51-beta.1` slice ports Obelisks. It preserves the 1.21.1
|
||||
one-in-128 candidate chance per chunk/occurrence, 4×4 plinth, and 2×2 shaft
|
||||
extending 12 blocks above it. Placement requires dry, sturdy ground across
|
||||
the base, at most two blocks of surface variation, and clear/replaceable
|
||||
space for the complete structure. The footprint stays within its chunk.
|
||||
|
||||
Obelisks support Standard, Amplified, Island, and Flat terrain. A preceding
|
||||
Block page sets their material; otherwise a seeded full-block material is
|
||||
selected. Repeated pages make independent attempts, up to 32 occurrences.
|
||||
Spikes and Obelisks share surface-stage classification and preserve their
|
||||
authored order within that stage. World and restart validation are manual.
|
||||
|
||||
The local `1.0.1.52-beta.1` slice ports Crystal Formations using the 1.21.1
|
||||
surface-mound algorithm and the existing blue crystal blocks. Each occurrence
|
||||
has a one-in-fifteen candidate chance per chunk; suitable dry, nearly level
|
||||
ground produces a 3×3 mound with budding crystal and occasional small buds.
|
||||
The existing budding blocks continue growing crystals through random ticks.
|
||||
|
||||
Crystal Formations accepts Standard, Amplified, Island, and Flat terrain,
|
||||
but not Cave/Void terrain or Block/Fluid modifiers. Up to 32 repeated pages
|
||||
make independent attempts. The ordered feature plan survives restart, and
|
||||
only newly generated chunks receive formations. For a quick manual test,
|
||||
bind a Flat/Plains Age with several Crystal Formations pages, inspect the
|
||||
blue mounds and bud growth, then revisit after restarting the world.
|
||||
|
||||
The local `1.0.1.53-beta.1` slice ports Dense Ores. Each page replays eligible
|
||||
ore placements from the Age's actual biomes, preserving their height ranges,
|
||||
replacement rules, and biome filters. Vanilla and modded ore features are
|
||||
recognized by their ore targets; custom placements can opt in through the
|
||||
`rivencraft:dense_ores` placed-feature tag, just as in 1.21.1.
|
||||
|
||||
Up to 32 pages add independent passes on Standard, Amplified, Island, or Cave
|
||||
terrain. Flat/Void and Block/Fluid modifiers are unsupported; an explicitly
|
||||
authored biome pool must contain at least one ore-producing biome. The count
|
||||
survives restart. Only new chunks gain extra ore, and this slice does not yet
|
||||
port the later instability system or random-Age feature selection.
|
||||
|
||||
For manual validation, create a Standard/Plains Age with several Dense Ores
|
||||
pages and inspect underground ore abundance, then restart and explore more
|
||||
chunks. A Cave Age with a Nether biome can check Nether ore placements too.
|
||||
|
||||
The local `1.0.1.54-beta.1` slice ports the Star Fissures page. On first book/
|
||||
operator arrival, it carves the 1.21.1 Legacy-shaped rift near the arrival chunk
|
||||
and places a starfield portal at the dimension's minimum Y. Cave terrain keeps
|
||||
its bedrock roof. All terrain types are supported; repeated pages still create
|
||||
one fissure, and Block/Fluid modifiers are not accepted.
|
||||
|
||||
The first arrival and completed generation are saved per Age, so revisits and
|
||||
restarts do not recarve the rift. Touching its bottom returns entities to a safe
|
||||
location near the shared Overworld spawn, not a bed, with the original fissure
|
||||
sound. The portal is unobtainable and inert in the Overworld. Random-Age feature
|
||||
selection remains later work; author a Star Fissures page to test this slice.
|
||||
|
||||
For manual validation, create a Flat/Plains Age with Star Fissures. Look near
|
||||
the arrival point (generally east/south), descend through the rift, and touch
|
||||
the starfield to return. Re-enter after restarting; check that the same fissure
|
||||
and its portal still work. A Cave Age checks that carving leaves the roof intact.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
@@ -94,7 +365,7 @@ Minecraft 1.21.1/Java 21 build:
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
The alpha JAR is under `ports/mc263/build/libs`. Continue the port and its
|
||||
The beta JAR is under `ports/mc263/build/libs`. Continue the port and its
|
||||
full dual-version validation in issues #98–#104 before calling 26.3 stable.
|
||||
|
||||
The dedicated development server uses `build/run-server`, with its own world
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.32-beta.1
|
||||
|
||||
This is the first 26.3 beta, focused on authored Age generation. It is not yet
|
||||
feature-complete with the separate Minecraft 1.21.1 release.
|
||||
|
||||
- Ported the Book Binder's ordered symbol-page strip and Legacy-inspired GUI,
|
||||
plus runtime symbol artwork, creative pages, and registry-derived biome pages.
|
||||
- Ported the Ink Mixer, its additive modifiers, and one-paper/one-ink-unit Link
|
||||
Panel creation.
|
||||
- Added authored Standard, Amplified, and Island terrain; fixed, Tiled, Grid,
|
||||
Native, and Tiny through Huge biome distributions; and persistent Age plans.
|
||||
- Fixed workstation block-face occlusion beside the Book Binder and Ink Mixer.
|
||||
|
||||
Feature/material writing, Cave/Flat/Void terrain, portals, and other 1.21.1
|
||||
systems are still being ported. Use a test world and keep a backup. Minecraft
|
||||
1.21.1 saves are not supported by this beta.
|
||||
|
||||
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and
|
||||
Java 25.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.36-beta.1
|
||||
|
||||
This beta adds Flat and Void terrain to authored Ages on Minecraft 26.3.
|
||||
|
||||
- Bind Descriptive Books with Flat or Void terrain, including multiple Biome pages and supported biome-distribution pages.
|
||||
- Flat ground starts at Y=0; travel lands on its surface rather than on an elevated fallback platform.
|
||||
- Flat Ages generate the selected biomes' surface decorations, including trees and vegetation. Void Ages remain empty while retaining authored biome climate.
|
||||
- Expanded dedicated-server checks for Flat and Void terrain, biome distribution, and persistence after restart.
|
||||
|
||||
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
|
||||
|
||||
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.39-beta.1
|
||||
|
||||
This beta adds Cave terrain to authored Ages on Minecraft 26.3.
|
||||
|
||||
- Bind Descriptive Books with the Cave terrain page and Cave or Nether biome pages, including supported multi-biome distributions.
|
||||
- Cave Ages use the cave noise shape but remain dry by default. Newly generated chunks no longer flood below the vanilla cave sea level.
|
||||
- Cave travel searches from Y=64 downward for dry, properly footed space instead of landing on the cave roof or underwater.
|
||||
- Expanded dedicated-server checks for Cave creation, biome distribution, and persistence after restart.
|
||||
|
||||
The player verified the Cave and safe-landing changes in game. Existing generated chunks are not drained or regenerated. A future Flooded page will provide an authored water level; see [issue #107](https://git.paddedcell.org/dnorberg/rivencraft/issues/107).
|
||||
|
||||
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
|
||||
|
||||
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.41-beta.1
|
||||
|
||||
This beta expands Age generation on Minecraft 26.3.
|
||||
|
||||
- Descriptive Books with no authored symbol pages now choose a seeded random terrain and biome distribution. The chosen plan is saved with the Age and restored after restart.
|
||||
- A Block material page followed by a Rocks feature page now generates small clusters of that block in newly generated surface chunks. This first feature path supports Standard, Amplified, Island, and Flat terrain; Cave and Void combinations remain unsupported.
|
||||
- Expanded dedicated-server checks cover random Age plans and authored Rocks generation across restart.
|
||||
|
||||
The player verified the Rocks feature in game. Random feature selection, duplicate feature pages, unpaired materials, and instability scoring are not yet ported. Existing chunks are not regenerated.
|
||||
|
||||
This 26.3 port is still experimental and does not yet include all features of the separate Minecraft 1.21.1 version. Use a test world and keep a backup. Minecraft 1.21.1 saves are not supported by this beta.
|
||||
|
||||
Requires Minecraft 26.3, NeoForge 26.3.0.99 or a compatible 26.3 build, and Java 25.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.54-beta.1
|
||||
|
||||
This beta collects the player-validated Age feature port slices since
|
||||
1.0.1.41-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Age generation
|
||||
|
||||
- Rocks now use the established 1.21.1 forest-rock placement. Unmodified
|
||||
material-bearing features choose seeded materials independently; a preceding
|
||||
compatible Block or Fluid page selects the material explicitly.
|
||||
- Added Flowers, Trees, Huge Trees, Dungeons, Surface Lakes, Deep Lakes, and
|
||||
Small Lakes using the 1.21.1 placement rules adapted to 26.3.
|
||||
- Added Floating Islands, Tendrils, Skylands, Spheres, Spikes, and Obelisks
|
||||
using the existing Rivencraft generation algorithms.
|
||||
- Added blue Crystal Formations with budding crystal and initially seeded buds.
|
||||
- Added Dense Ores: repeated pages replay eligible biome ore placements,
|
||||
including compatible modded ores and explicitly tagged custom placements.
|
||||
- Added Star Fissures: one Legacy-shaped rift near the first arrival point,
|
||||
a starfield at the dimension's minimum Y, and return travel to the shared
|
||||
Overworld spawn with the original fissure sound. Cave roofs are preserved.
|
||||
- Feature plans, materials, repeated occurrences, and fissure completion
|
||||
persist across restart. Existing generated chunks are not redecorated.
|
||||
|
||||
## Current limits
|
||||
|
||||
- Authored features retain terrain restrictions. Most repeated features allow
|
||||
up to 32 occurrences; exceeding the current authoring cap blocks assembly.
|
||||
Skylands and Star Fissures retain their single-effect behavior when repeated.
|
||||
- Put a material page immediately before a compatible feature. Dangling
|
||||
material assignment, full Legacy grammar recovery, instability, and random
|
||||
feature selection are not yet ported. Pageless books randomize terrain and
|
||||
biome distribution, not the newly authored feature list.
|
||||
- 26.3 combines base terrain, surface, and carving. Added solid volumes run
|
||||
after that combined operation and before decoration, so base carvers do not
|
||||
cut those added volumes.
|
||||
- The full 1.21.1 writing/discovery loop, structures, environmental systems,
|
||||
crystal portals, and optional integration parity remain in progress. The
|
||||
presence of a Creative page is not a promise that its effect is ported.
|
||||
|
||||
Build and static checks passed. The player reported in-game passes for these
|
||||
slices. Earlier server smoke coverage was expanded, but the later generation
|
||||
slices were manually validated; this release does not claim a new full
|
||||
dedicated-server regression pass.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build,
|
||||
and **Java 25**. Dynamic Dimensions and Bad Packets are not required by this
|
||||
26.3 build. Do not use Minecraft 1.21.1 dependencies or saves with it. Back up
|
||||
the entire world before updating; beta-save compatibility is not guaranteed.
|
||||
|
||||
Report issues and help with documentation at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -59,12 +59,80 @@ def generateModMetadata = tasks.register('generateModMetadata', ProcessResources
|
||||
sourceSets.main.resources.srcDir generateModMetadata
|
||||
neoForge.ideSyncTask generateModMetadata
|
||||
|
||||
// Keep the bundled page list in step with the 1.21.1 symbol definitions.
|
||||
abstract class GeneratePortSymbolIndex extends DefaultTask {
|
||||
@InputDirectory abstract DirectoryProperty getSymbolsDir()
|
||||
@InputDirectory abstract DirectoryProperty getPortSymbolsDir()
|
||||
@OutputDirectory abstract DirectoryProperty getIndexDir()
|
||||
|
||||
@TaskAction void generate() {
|
||||
File root = symbolsDir.get().asFile
|
||||
def definitions = new LinkedHashMap<String, Object>()
|
||||
java.nio.file.Files.walk(root.toPath()).withCloseable { paths ->
|
||||
paths.filter { java.nio.file.Files.isRegularFile(it) && it.toString().endsWith('.json') }
|
||||
.sorted().forEach { source ->
|
||||
String path = root.toPath().relativize(source).toString()
|
||||
.replace('\\', '/').replaceAll(/\.json$/, '')
|
||||
definitions["rivencraft:${path}"] = new groovy.json.JsonSlurper().parse(source.toFile())
|
||||
}
|
||||
}
|
||||
File portRoot = portSymbolsDir.get().asFile
|
||||
java.nio.file.Files.walk(portRoot.toPath()).withCloseable { paths ->
|
||||
paths.filter { java.nio.file.Files.isRegularFile(it) && it.toString().endsWith('.json') }
|
||||
.sorted().forEach { source ->
|
||||
String path = portRoot.toPath().relativize(source).toString()
|
||||
.replace('\\', '/').replaceAll(/\.json$/, '')
|
||||
definitions["rivencraft:${path}"] = new groovy.json.JsonSlurper().parse(source.toFile())
|
||||
}
|
||||
}
|
||||
File index = new File(indexDir.get().asFile, 'assets/rivencraft/creative_symbol_pages.json')
|
||||
index.parentFile.mkdirs()
|
||||
index.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(definitions)) + '\n'
|
||||
}
|
||||
}
|
||||
|
||||
def generatePortSymbolIndex = tasks.register('generatePortSymbolIndex', GeneratePortSymbolIndex) {
|
||||
symbolsDir.set(layout.projectDirectory.dir('../../src/main/resources/data/rivencraft/rivencraft/symbols'))
|
||||
portSymbolsDir.set(layout.projectDirectory.dir('src/main/resources/data/rivencraft/rivencraft/symbols'))
|
||||
indexDir.set(layout.buildDirectory.dir('generated/resources/creativeSymbols'))
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortSymbolIndex
|
||||
|
||||
// Use the established symbol names/descriptions without overwriting 26.3-only strings.
|
||||
abstract class GeneratePortLanguage extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getBaseLanguage()
|
||||
@InputFile abstract RegularFileProperty getPortLanguage()
|
||||
@OutputDirectory abstract DirectoryProperty getOutputDir()
|
||||
|
||||
@TaskAction void generate() {
|
||||
def base = new groovy.json.JsonSlurper().parse(baseLanguage.get().asFile) as Map
|
||||
def port = new groovy.json.JsonSlurper().parse(portLanguage.get().asFile) as Map
|
||||
base.putAll(port)
|
||||
File result = new File(outputDir.get().asFile, 'assets/rivencraft/lang/en_us.json')
|
||||
result.parentFile.mkdirs()
|
||||
result.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(base)) + '\n'
|
||||
}
|
||||
}
|
||||
|
||||
def generatePortLanguage = tasks.register('generatePortLanguage', GeneratePortLanguage) {
|
||||
baseLanguage.set(layout.projectDirectory.file('../../src/main/resources/assets/rivencraft/lang/en_us.json'))
|
||||
portLanguage.set(layout.projectDirectory.file('port_en_us.json'))
|
||||
outputDir.set(layout.buildDirectory.dir('generated/resources/portLanguage'))
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortLanguage
|
||||
|
||||
tasks.named('processResources', ProcessResources).configure {
|
||||
// Reuse the existing sprite and base model without copying binary assets.
|
||||
from('../../src/main/resources') {
|
||||
include 'assets/rivencraft/models/block/star_fissure.json'
|
||||
include 'assets/rivencraft/blockstates/star_fissure.json'
|
||||
include 'assets/rivencraft/sounds/linking/link-fissure.ogg'
|
||||
include 'assets/rivencraft/textures/item/crystal_shard.png'
|
||||
include 'assets/rivencraft/textures/item/page.png'
|
||||
include 'assets/rivencraft/textures/item/page_link_panel.png'
|
||||
include 'assets/rivencraft/textures/item/page_symbol.png'
|
||||
include 'assets/rivencraft/models/item/page_symbol.json'
|
||||
include 'assets/rivencraft/textures/gui/symbolcomponents.png'
|
||||
include 'assets/rivencraft/models/item/page_link_panel.json'
|
||||
include 'assets/rivencraft/textures/item/ink_vial.png'
|
||||
include 'assets/rivencraft/textures/item/ink_vial.png.mcmeta'
|
||||
|
||||
@@ -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.15-alpha.1
|
||||
mod_version=1.0.1.54-beta.1
|
||||
@@ -5,6 +5,8 @@
|
||||
"item.rivencraft.page": "Page",
|
||||
"item.rivencraft.page.blank": "Blank Page",
|
||||
"item.rivencraft.page.link_panel": "Link Panel Page",
|
||||
"item.rivencraft.page.unresolved": "Unknown Symbol Page",
|
||||
"item.rivencraft.page.symbol.port_status": "Binder currently supports Standard terrain and one Biome page; other effects are still being ported.",
|
||||
"item.rivencraft.link_property.disarm": "Disarm",
|
||||
"item.rivencraft.link_property.maintain_momentum": "Maintain Momentum",
|
||||
"item.rivencraft.link_property.intra_linking": "Intra-Linking",
|
||||
@@ -16,6 +18,7 @@
|
||||
"block.rivencraft.book_binder": "Book Binder",
|
||||
"container.rivencraft.book_binder": "Book Binder",
|
||||
"screen.rivencraft.book_binder.missing_panel": "Link Panel required",
|
||||
"screen.rivencraft.book_binder.authored_pending": "Writing not yet ported",
|
||||
"block.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"container.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"screen.rivencraft.ink_mixer.ink": "Ink: %s/%s",
|
||||
@@ -28,5 +31,15 @@
|
||||
"block.rivencraft.large_crystal_bud": "Large Crystal Bud",
|
||||
"block.rivencraft.crystal_cluster": "Crystal Cluster",
|
||||
"itemGroup.rivencraft": "Rivencraft",
|
||||
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages"
|
||||
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
||||
"symbol.rivencraft.feature.poplar_trees": "Poplar Trees",
|
||||
"symbol.rivencraft.feature.poplar_trees.description": "Poplar tree generation symbol.",
|
||||
"symbol.rivencraft.feature.fallen_poplar_trees": "Fallen Poplar Trees",
|
||||
"symbol.rivencraft.feature.fallen_poplar_trees.description": "Fallen poplar tree generation symbol.",
|
||||
"symbol.rivencraft.feature.shelf_mushrooms": "Shelf Mushrooms",
|
||||
"symbol.rivencraft.feature.shelf_mushrooms.description": "Shelf mushroom generation symbol.",
|
||||
"symbol.rivencraft.feature.red_shrubs": "Red Shrubs",
|
||||
"symbol.rivencraft.feature.red_shrubs.description": "Red shrub generation symbol.",
|
||||
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
|
||||
"symbol.rivencraft.feature.abandoned_camps.description": "Abandoned camp structure generation symbol."
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import net.dnorberg.rivencraft.age.AgeCatalogSync;
|
||||
import net.dnorberg.rivencraft.block.BuddingCrystalBlock;
|
||||
import net.dnorberg.rivencraft.block.PortBookBinderBlock;
|
||||
import net.dnorberg.rivencraft.block.InkMixerBlock;
|
||||
import net.dnorberg.rivencraft.block.StarFissureBlock;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
@@ -20,10 +22,13 @@ import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.registry.ModCreativeTabs;
|
||||
import net.dnorberg.rivencraft.registry.PortBiomeSources;
|
||||
import net.dnorberg.rivencraft.registry.PortChunkGenerators;
|
||||
import net.dnorberg.rivencraft.writing.LinkingBookRecipe;
|
||||
import net.minecraft.core.component.DataComponentType;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
@@ -52,6 +57,11 @@ public final class Rivencraft {
|
||||
public static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MOD_ID);
|
||||
private static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MOD_ID);
|
||||
private static final DeferredRegister<SoundEvent> SOUNDS =
|
||||
DeferredRegister.create(Registries.SOUND_EVENT, MOD_ID);
|
||||
public static final Supplier<SoundEvent> LINK_FISSURE = SOUNDS.register(
|
||||
"linking.link-fissure", () -> SoundEvent.createVariableRangeEvent(
|
||||
Identifier.fromNamespaceAndPath(MOD_ID, "linking.link-fissure")));
|
||||
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
|
||||
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
|
||||
private static final DeferredRegister<MenuType<?>> MENUS =
|
||||
@@ -71,6 +81,18 @@ public final class Rivencraft {
|
||||
public static final Supplier<DataComponentType<Boolean>> LINK_PANEL_PAGE = COMPONENTS.registerComponentType(
|
||||
"link_panel_page", builder -> builder.persistent(Codec.BOOL)
|
||||
.networkSynchronized(ByteBufCodecs.BOOL));
|
||||
public static final Supplier<DataComponentType<Identifier>> SYMBOL_PAGE_ID = COMPONENTS.registerComponentType(
|
||||
"symbol_page_id", builder -> builder.persistent(Identifier.CODEC)
|
||||
.networkSynchronized(Identifier.STREAM_CODEC));
|
||||
/** Ordered authored page IDs remain on the finished book for later port slices. */
|
||||
public static final Supplier<DataComponentType<java.util.List<Identifier>>> BOOK_SYMBOLS = COMPONENTS.registerComponentType(
|
||||
"book_symbols", builder -> builder.persistent(Identifier.CODEC.listOf())
|
||||
.networkSynchronized(ByteBufCodecs.<io.netty.buffer.ByteBuf, Identifier>list(252)
|
||||
.apply(Identifier.STREAM_CODEC)));
|
||||
/** Set only on temporary GUI stacks; NeoForge 26.3 requires a registered codec. */
|
||||
public static final Supplier<DataComponentType<Boolean>> PAGE_GLYPH_ONLY = COMPONENTS.registerComponentType(
|
||||
"page_glyph_only", builder -> builder.persistent(Codec.BOOL)
|
||||
.networkSynchronized(ByteBufCodecs.BOOL));
|
||||
public static final Supplier<DataComponentType<Integer>> INK_REMAINING = COMPONENTS.registerComponentType(
|
||||
"ink_remaining", builder -> builder.persistent(Codec.INT)
|
||||
.networkSynchronized(ByteBufCodecs.VAR_INT));
|
||||
@@ -80,13 +102,19 @@ public final class Rivencraft {
|
||||
|
||||
public static final DeferredBlock<Block> CRYSTAL_BLOCK = BLOCKS.registerBlock("crystal_block",
|
||||
Block::new, () -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_BLOCK));
|
||||
public static final DeferredBlock<StarFissureBlock> STAR_FISSURE =
|
||||
BLOCKS.registerBlock("star_fissure", StarFissureBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.END_PORTAL).lightLevel(state -> 6));
|
||||
public static final Supplier<BlockEntityType<StarFissureBlockEntity>>
|
||||
STAR_FISSURE_BLOCK_ENTITY = BLOCK_ENTITY_TYPES.register("star_fissure", () -> new BlockEntityType<>(
|
||||
StarFissureBlockEntity::new, STAR_FISSURE.get()));
|
||||
public static final DeferredBlock<PortBookBinderBlock> BOOK_BINDER = BLOCKS.registerBlock(
|
||||
"book_binder", PortBookBinderBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE));
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||
public static final DeferredItem<BlockItem> BOOK_BINDER_ITEM = ITEMS.registerSimpleBlockItem(BOOK_BINDER);
|
||||
public static final DeferredBlock<InkMixerBlock> INK_MIXER = BLOCKS.registerBlock(
|
||||
"ink_mixer", InkMixerBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE));
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.CRAFTING_TABLE).noOcclusion());
|
||||
public static final DeferredItem<BlockItem> INK_MIXER_ITEM = ITEMS.registerSimpleBlockItem(INK_MIXER);
|
||||
public static final Supplier<BlockEntityType<PortBookBinderBlockEntity>> BOOK_BINDER_BLOCK_ENTITY =
|
||||
BLOCK_ENTITY_TYPES.register("book_binder", () ->
|
||||
@@ -132,12 +160,15 @@ public final class Rivencraft {
|
||||
|
||||
public Rivencraft(IEventBus modEventBus) {
|
||||
BLOCKS.register(modEventBus);
|
||||
SOUNDS.register(modEventBus);
|
||||
ITEMS.register(modEventBus);
|
||||
BLOCK_ENTITY_TYPES.register(modEventBus);
|
||||
MENUS.register(modEventBus);
|
||||
COMPONENTS.register(modEventBus);
|
||||
RECIPE_SERIALIZERS.register(modEventBus);
|
||||
ModCreativeTabs.register(modEventBus);
|
||||
PortBiomeSources.register(modEventBus);
|
||||
PortChunkGenerators.register(modEventBus);
|
||||
NeoForge.EVENT_BUS.addListener(RuntimeAgeBackend::onLevelLoad);
|
||||
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
||||
|
||||
@@ -17,7 +17,24 @@ final class AgeLevelIndex extends SavedData {
|
||||
private static final Codec<AgeLevelIndex> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.unboundedMap(Identifier.CODEC, Codec.LONG).fieldOf("ages").forGetter(index -> index.ages),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("retired", List.of())
|
||||
.forGetter(index -> List.copyOf(index.retired))
|
||||
.forGetter(index -> List.copyOf(index.retired)),
|
||||
Codec.unboundedMap(Identifier.CODEC, Identifier.CODEC).optionalFieldOf("biome_overrides", Map.of())
|
||||
.forGetter(index -> index.biomeOverrides),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortBiomePlan.CODEC).optionalFieldOf("biome_plans", Map.of())
|
||||
.forGetter(index -> index.biomePlans),
|
||||
Codec.unboundedMap(Identifier.CODEC,
|
||||
Codec.STRING.xmap(PortAgeTerrain::valueOf, PortAgeTerrain::name))
|
||||
.optionalFieldOf("terrain", Map.of()).forGetter(index -> index.terrain),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortRockPlan.CODEC).optionalFieldOf("rocks", Map.of())
|
||||
.forGetter(index -> index.rocks),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortVegetationPlan.CODEC)
|
||||
.optionalFieldOf("vegetation", Map.of()).forGetter(index -> index.vegetation),
|
||||
Codec.unboundedMap(Identifier.CODEC, Codec.intRange(1, 32))
|
||||
.optionalFieldOf("dungeons", Map.of()).forGetter(index -> index.dungeons),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortLakePlan.CODEC)
|
||||
.optionalFieldOf("lakes", Map.of()).forGetter(index -> index.lakes),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortTerrainFeaturePlan.CODEC)
|
||||
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures)
|
||||
).apply(instance, AgeLevelIndex::new));
|
||||
|
||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||
@@ -28,15 +45,47 @@ final class AgeLevelIndex extends SavedData {
|
||||
|
||||
private final Map<Identifier, Long> ages;
|
||||
private final Set<Identifier> retired;
|
||||
private final Map<Identifier, Identifier> biomeOverrides;
|
||||
private final Map<Identifier, PortBiomePlan> biomePlans;
|
||||
private final Map<Identifier, PortAgeTerrain> terrain;
|
||||
private final Map<Identifier, PortRockPlan> rocks;
|
||||
private final Map<Identifier, PortVegetationPlan> vegetation;
|
||||
private final Map<Identifier, Integer> dungeons;
|
||||
private final Map<Identifier, PortLakePlan> lakes;
|
||||
private final Map<Identifier, PortTerrainFeaturePlan> terrainFeatures;
|
||||
|
||||
private AgeLevelIndex() {
|
||||
this.ages = new LinkedHashMap<>();
|
||||
this.retired = new LinkedHashSet<>();
|
||||
this.biomeOverrides = new LinkedHashMap<>();
|
||||
this.biomePlans = new LinkedHashMap<>();
|
||||
this.terrain = new LinkedHashMap<>();
|
||||
this.rocks = new LinkedHashMap<>();
|
||||
this.vegetation = new LinkedHashMap<>();
|
||||
this.dungeons = new LinkedHashMap<>();
|
||||
this.lakes = new LinkedHashMap<>();
|
||||
this.terrainFeatures = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired) {
|
||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||
Map<Identifier, Identifier> biomeOverrides,
|
||||
Map<Identifier, PortBiomePlan> biomePlans,
|
||||
Map<Identifier, PortAgeTerrain> terrain,
|
||||
Map<Identifier, PortRockPlan> rocks,
|
||||
Map<Identifier, PortVegetationPlan> vegetation,
|
||||
Map<Identifier, Integer> dungeons,
|
||||
Map<Identifier, PortLakePlan> lakes,
|
||||
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures) {
|
||||
this.ages = new LinkedHashMap<>(ages);
|
||||
this.retired = new LinkedHashSet<>(retired);
|
||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
||||
this.terrain = new LinkedHashMap<>(terrain);
|
||||
this.rocks = new LinkedHashMap<>(rocks);
|
||||
this.vegetation = new LinkedHashMap<>(vegetation);
|
||||
this.dungeons = new LinkedHashMap<>(dungeons);
|
||||
this.lakes = new LinkedHashMap<>(lakes);
|
||||
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
|
||||
}
|
||||
|
||||
Map<Identifier, Long> entries() {
|
||||
@@ -51,23 +100,67 @@ final class AgeLevelIndex extends SavedData {
|
||||
return ages.get(id);
|
||||
}
|
||||
|
||||
void add(Identifier id, long seed) {
|
||||
PortBiomePlan biomePlan(Identifier id) {
|
||||
PortBiomePlan plan = biomePlans.get(id);
|
||||
if (plan != null) return plan;
|
||||
Identifier oldBiome = biomeOverrides.get(id);
|
||||
return oldBiome == null ? null : new PortBiomePlan(PortBiomePlan.Distribution.SINGLE, List.of(oldBiome));
|
||||
}
|
||||
|
||||
PortAgeTerrain terrain(Identifier id) {
|
||||
return terrain.getOrDefault(id, PortAgeTerrain.STANDARD);
|
||||
}
|
||||
|
||||
PortRockPlan rocks(Identifier id) { return rocks.get(id); }
|
||||
PortVegetationPlan vegetation(Identifier id) { return vegetation.get(id); }
|
||||
int dungeons(Identifier id) { return dungeons.getOrDefault(id, 0); }
|
||||
PortLakePlan lakes(Identifier id) { return lakes.get(id); }
|
||||
PortTerrainFeaturePlan terrainFeatures(Identifier id) { return terrainFeatures.get(id); }
|
||||
|
||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||
if (contains(id)) {
|
||||
throw new IllegalArgumentException("Age already exists: " + id);
|
||||
}
|
||||
ages.put(id, seed);
|
||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void retire(Identifier id) {
|
||||
if (ages.remove(id) == null) throw new IllegalArgumentException("Age not indexed: " + id);
|
||||
biomeOverrides.remove(id);
|
||||
biomePlans.remove(id);
|
||||
terrain.remove(id);
|
||||
rocks.remove(id);
|
||||
vegetation.remove(id);
|
||||
dungeons.remove(id);
|
||||
lakes.remove(id);
|
||||
terrainFeatures.remove(id);
|
||||
retired.add(id);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void restoreAfterFailedRetirement(Identifier id, long seed) {
|
||||
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
||||
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
|
||||
PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
||||
ages.put(id, seed);
|
||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ package net.dnorberg.rivencraft.age;
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -16,10 +18,13 @@ import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.util.Prediction;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||
import net.neoforged.fml.ModList;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Operator-only backend and synchronized-travel probes, not normal book behavior. */
|
||||
public final class PortAgeCommands {
|
||||
@@ -64,25 +69,114 @@ public final class PortAgeCommands {
|
||||
})))
|
||||
.then(Commands.literal("age")
|
||||
.then(Commands.literal("create")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.STANDARD))
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
PortAgeTerrain terrain;
|
||||
try {
|
||||
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(
|
||||
context, "terrain").toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal(
|
||||
"Unknown terrain; use standard, amplified, island, cave, flat, or void"));
|
||||
return 0;
|
||||
}
|
||||
return createAge(context, terrain);
|
||||
})))))
|
||||
.then(Commands.literal("create_multi")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
PortAgeTerrain terrain;
|
||||
try {
|
||||
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(
|
||||
context, "terrain").toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal(
|
||||
"Unknown terrain; use cave, flat, or void"));
|
||||
return 0;
|
||||
}
|
||||
if (terrain != PortAgeTerrain.CAVE
|
||||
&& terrain != PortAgeTerrain.FLAT
|
||||
&& terrain != PortAgeTerrain.VOID) {
|
||||
context.getSource().sendFailure(Component.literal(
|
||||
"Multi-biome probe supports cave, flat, or void only"));
|
||||
return 0;
|
||||
}
|
||||
return createAge(context, terrain, new PortBiomePlan(
|
||||
PortBiomePlan.Distribution.TILED,
|
||||
terrain == PortAgeTerrain.CAVE
|
||||
? java.util.List.of(Identifier.withDefaultNamespace("lush_caves"),
|
||||
Identifier.withDefaultNamespace("dripstone_caves"))
|
||||
: java.util.List.of(Identifier.withDefaultNamespace("plains"),
|
||||
Identifier.withDefaultNamespace("forest"))));
|
||||
})))))
|
||||
.then(Commands.literal("create_random")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
String name = StringArgumentType.getString(context, "name");
|
||||
Identifier id = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "age/" + name);
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
ServerLevel level;
|
||||
try {
|
||||
level = RuntimeAgeBackend.create(
|
||||
context.getSource().getServer(), id, seed);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Created " + id + " with seed " + level.getSeed()), true);
|
||||
return 1;
|
||||
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
|
||||
context.getSource().getServer(), seed);
|
||||
int created = createAge(context, selected.terrain(), selected.biomes());
|
||||
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Random Age plan: terrain=" + selected.terrain()
|
||||
+ " biomes=" + selected.biomes()), false);
|
||||
return created;
|
||||
}))))
|
||||
.then(Commands.literal("create_rocks")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
|
||||
null, new PortRockPlan(Identifier.withDefaultNamespace("gold_block")))))))
|
||||
.then(Commands.literal("create_rocks_multi")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||
fourGoldRocks(LongArgumentType.getLong(context, "seed")))))))
|
||||
.then(Commands.literal("create_rocks_random")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(
|
||||
java.util.List.of(Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/rocks")),
|
||||
LongArgumentType.getLong(context, "seed")))))))
|
||||
.then(Commands.literal("create_vegetation")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||
null, testVegetation())))))
|
||||
.then(Commands.literal("create_dungeons")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||
null, null, null, testDungeons())))))
|
||||
.then(Commands.literal("create_lakes")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||
null, null, null, 0, testLakes(
|
||||
LongArgumentType.getLong(context, "seed"), false))))))
|
||||
.then(Commands.literal("create_lakes_random")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||
null, null, null, 0, testLakes(
|
||||
LongArgumentType.getLong(context, "seed"), true))))))
|
||||
.then(Commands.literal("create_islands")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(context -> createIslandAge(context, false))))))
|
||||
.then(Commands.literal("create_islands_random")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(context -> createIslandAge(context, true))))))
|
||||
.then(Commands.literal("list")
|
||||
.executes(context -> {
|
||||
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
|
||||
@@ -185,7 +279,271 @@ public final class PortAgeCommands {
|
||||
.executes(context -> sampleAge(context, 0))
|
||||
.then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0))
|
||||
.executes(context -> sampleAge(context,
|
||||
IntegerArgumentType.getInteger(context, "chunkOffset"))))))));
|
||||
IntegerArgumentType.getInteger(context, "chunkOffset"))))))
|
||||
.then(Commands.literal("rocks_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeRocks)))
|
||||
.then(Commands.literal("vegetation_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeVegetation)))
|
||||
.then(Commands.literal("dungeons_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeDungeons)))
|
||||
.then(Commands.literal("lakes_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeLakes)))
|
||||
.then(Commands.literal("islands_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeIslands)))
|
||||
.then(Commands.literal("biome")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||
.then(Commands.argument("z", IntegerArgumentType.integer())
|
||||
.executes(PortAgeCommands::sampleBiome)))))));
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context, PortAgeTerrain terrain) {
|
||||
return createAge(context, terrain, null);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan) {
|
||||
return createAge(context, terrain, biomePlan, null);
|
||||
}
|
||||
|
||||
private static int probeRocks(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|
||||
|| features.rocks() == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
|
||||
return 0;
|
||||
}
|
||||
var materials = features.rocks().materials().stream()
|
||||
.map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
|
||||
int count = 0;
|
||||
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
|
||||
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
|
||||
if (materials.stream().anyMatch(state::is)) count++;
|
||||
}
|
||||
int placed = count;
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Rocks probe " + id + " materials=" + features.rocks().materials() + " count=" + placed), false);
|
||||
return placed;
|
||||
}
|
||||
|
||||
private static int probeVegetation(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
int flowers = 0;
|
||||
int logs = 0;
|
||||
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
||||
for (int y = 4; y < 32; y++) {
|
||||
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
|
||||
if (state.is(BlockTags.FLOWERS)) flowers++;
|
||||
if (state.is(BlockTags.LOGS)) logs++;
|
||||
}
|
||||
}
|
||||
int flowerCount = flowers;
|
||||
int logCount = logs;
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Vegetation probe " + id + " flowers=" + flowerCount + " logs=" + logCount), false);
|
||||
return flowerCount + logCount;
|
||||
}
|
||||
|
||||
private static PortVegetationPlan testVegetation() {
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/flowers"));
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/trees"));
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/huge_trees"));
|
||||
}
|
||||
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.vegetationPlan(pages);
|
||||
}
|
||||
|
||||
private static int testDungeons() {
|
||||
Identifier page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dungeons");
|
||||
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(
|
||||
java.util.Collections.nCopies(8, page));
|
||||
}
|
||||
|
||||
private static int probeDungeons(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
|
||||
context.getSource().sendFailure(Component.literal("Age has no loaded feature generator: " + id));
|
||||
return 0;
|
||||
}
|
||||
int count = features.dungeons();
|
||||
int spawners = 0;
|
||||
for (int chunkX = 0; chunkX < 4; chunkX++) for (int chunkZ = 0; chunkZ < 4; chunkZ++) {
|
||||
var chunk = level.getChunk(chunkX, chunkZ);
|
||||
for (var pos : chunk.getBlockEntitiesPos()) {
|
||||
if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++;
|
||||
}
|
||||
}
|
||||
int placed = spawners;
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Dungeons probe " + id + " pages=" + count + " spawners=" + placed), false);
|
||||
return placed;
|
||||
}
|
||||
|
||||
private static PortLakePlan testLakes(long seed, boolean randomFluids) {
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 8; i++) {
|
||||
for (String kind : java.util.List.of("surface_lakes", "deep_lakes", "lava_lakes")) {
|
||||
if (!randomFluids) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||
Identifier.withDefaultNamespace("water")));
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + kind));
|
||||
}
|
||||
}
|
||||
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed);
|
||||
}
|
||||
|
||||
private static int probeLakes(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
PortLakePlan lakes = level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
|
||||
? features.lakes() : null;
|
||||
// Finish neighboring features before counting, since lake bowls can
|
||||
// cross a chunk boundary. Include underground lakes in the probe.
|
||||
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
|
||||
int[] fluids = new int[2];
|
||||
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
|
||||
level.getChunk(x, z).findBlocks(state -> state.is(Blocks.WATER) || state.is(Blocks.LAVA),
|
||||
(pos, state) -> fluids[state.is(Blocks.WATER) ? 0 : 1]++);
|
||||
}
|
||||
int waterCount = fluids[0];
|
||||
int lavaCount = fluids[1];
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Lakes probe " + id + " pages=" + (lakes == null ? 0 : lakes.choices().size())
|
||||
+ " fluids=" + (lakes == null ? java.util.List.of() : lakes.choices().stream()
|
||||
.map(PortLakePlan.Choice::fluid).distinct().toList())
|
||||
+ " plan=" + (lakes == null ? 0 : lakes.choices().hashCode())
|
||||
+ " water=" + waterCount + " lava=" + lavaCount), false);
|
||||
return waterCount + lavaCount;
|
||||
}
|
||||
|
||||
private static int createIslandAge(CommandContext<CommandSourceStack> context, boolean randomMaterials) {
|
||||
PortAgeTerrain terrain;
|
||||
try {
|
||||
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal("Unknown island test terrain"));
|
||||
return 0;
|
||||
}
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/forest"));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (!randomMaterials) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||
Identifier.withDefaultNamespace("gold_block")));
|
||||
pages.add(PortTerrainFeaturePlan.FLOATING_ISLANDS);
|
||||
}
|
||||
try {
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
pages, context.getSource().getServer().registryAccess());
|
||||
var features = net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed);
|
||||
return createAge(context, terrain, biomes, null, null, 0, null, features);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int probeIslands(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|
||||
|| features.terrainFeatures() == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age has no loaded island generator: " + id));
|
||||
return 0;
|
||||
}
|
||||
var choices = features.terrainFeatures().choices();
|
||||
var materials = choices.stream().map(PortTerrainFeaturePlan.Choice::material).distinct().toList();
|
||||
var blocks = materials.stream().map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
|
||||
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
|
||||
int[] count = {0};
|
||||
long[] fingerprint = {0};
|
||||
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
|
||||
level.getChunk(x, z).findBlocks(state -> !state.isAir(), (pos, state) -> {
|
||||
if (pos.getY() < 128 || pos.getY() >= 240) return;
|
||||
if (blocks.stream().anyMatch(state::is)) count[0]++;
|
||||
fingerprint[0] += pos.asLong() * 0x9E3779B97F4A7C15L ^ state.toString().hashCode();
|
||||
});
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Islands probe " + id + " pages=" + choices.size() + " materials=" + materials
|
||||
+ " count=" + count[0] + " fingerprint=" + fingerprint[0]), false);
|
||||
return count[0];
|
||||
}
|
||||
|
||||
/** Exercise the actual ordered writing parser, not only the generator codec. */
|
||||
private static PortRockPlan fourGoldRocks(long seed) {
|
||||
Identifier block = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||
Identifier.withDefaultNamespace("gold_block"));
|
||||
Identifier feature = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/rocks");
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
pages.add(block);
|
||||
pages.add(feature);
|
||||
}
|
||||
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan, PortRockPlan rockPlan) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, null);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, 0);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, null);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons, PortLakePlan lakes) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, null);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) {
|
||||
String name = StringArgumentType.getString(context, "name");
|
||||
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
ServerLevel level;
|
||||
try {
|
||||
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
|
||||
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Created " + id + " with seed " + level.getSeed() + " terrain=" + terrain), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int sampleAge(com.mojang.brigadier.context.CommandContext<net.minecraft.commands.CommandSourceStack> context,
|
||||
@@ -213,6 +571,23 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int sampleBiome(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
int x = IntegerArgumentType.getInteger(context, "x");
|
||||
int z = IntegerArgumentType.getInteger(context, "z");
|
||||
level.getChunk(x >> 4, z >> 4);
|
||||
Identifier biome = level.getBiome(new net.minecraft.core.BlockPos(x, 4, z))
|
||||
.unwrapKey().orElseThrow().identifier();
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
id + " biome at " + x + "," + z + "=" + biome), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static Identifier ageId(com.mojang.brigadier.context.CommandContext<net.minecraft.commands.CommandSourceStack> context) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"age/" + StringArgumentType.getString(context, "name"));
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
/** Terrain variants currently backed by 26.3 vanilla noise settings. */
|
||||
public enum PortAgeTerrain {
|
||||
STANDARD,
|
||||
AMPLIFIED,
|
||||
ISLAND,
|
||||
CAVE,
|
||||
FLAT,
|
||||
VOID
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
|
||||
/** Shared, server-authoritative landing for the 26.3 book and operator probe. */
|
||||
public final class PortAgeTravel {
|
||||
@@ -23,24 +25,58 @@ public final class PortAgeTravel {
|
||||
if (!AgeCatalogSync.ready(player, id)) {
|
||||
return "Age catalog not acknowledged by client; remaining in source dimension";
|
||||
}
|
||||
BlockPos landing = findLanding(target);
|
||||
boolean fissure = target.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
|
||||
&& features.terrainFeatures() != null && features.terrainFeatures().starFissures();
|
||||
BlockPos landing = fissure ? PortStarFissureFeature.savedSpawn(target) : null;
|
||||
if (landing != null) landing = findFootedDestination(target, landing).orElse(null);
|
||||
if (landing == null) landing = findLanding(target);
|
||||
if (landing == null) return "No safe test landing available; remaining in source dimension";
|
||||
if (fissure) PortStarFissureFeature.generateOnce(target, landing, isCaveTerrain(target));
|
||||
boolean moved = player.teleportTo(target, landing.getX() + 0.5, landing.getY(), landing.getZ() + 0.5,
|
||||
Set.of(), player.getYRot(), player.getXRot(), true);
|
||||
return moved ? null : "Teleportation failed; remaining in source dimension";
|
||||
}
|
||||
|
||||
private static BlockPos findLanding(ServerLevel target) {
|
||||
net.minecraft.world.level.chunk.ChunkGenerator generator = target.getChunkSource().getGenerator();
|
||||
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
|
||||
if (isCaveTerrain(target)) {
|
||||
// The cave roof is not a valid arrival surface. Search down from
|
||||
// Legacy's Y=64 start, then construct the normal safety platform
|
||||
// at that height only when no footed opening can be found.
|
||||
for (int radius = 0; radius <= 16; radius++) {
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
for (int y = Math.min(64, target.getMaxY() - 2); y > target.getMinY(); y--) {
|
||||
BlockPos foot = new BlockPos(x, y, z);
|
||||
if (hasFooting(target, foot)) return foot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return makePlatform(target, new BlockPos(0, 64, 0));
|
||||
}
|
||||
// A Flat Age has a known surface. Use it directly before a heightmap
|
||||
// fallback can place a platform in the air above a low superflat world.
|
||||
FlatLevelGeneratorSettings flatSettings = switch (generator) {
|
||||
case FlatLevelSource flat -> flat.settings();
|
||||
case PortFlatChunkGenerator flat -> flat.settings();
|
||||
default -> null;
|
||||
};
|
||||
if (flatSettings != null && !flatSettings.getLayersInfo().isEmpty()) {
|
||||
int surfaceY = target.getMinY() + flatSettings.getLayersInfo().stream()
|
||||
.mapToInt(layer -> layer.getHeight()).sum();
|
||||
BlockPos surface = new BlockPos(0, surfaceY, 0);
|
||||
if (hasFooting(target, surface)) return surface;
|
||||
}
|
||||
for (int radius = 0; radius <= 16; radius++) {
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
int y = target.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z);
|
||||
BlockPos foot = new BlockPos(x, y, z);
|
||||
BlockPos floor = foot.below();
|
||||
if (target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
|
||||
&& target.getBlockState(foot).getCollisionShape(target, foot).isEmpty()
|
||||
&& target.getBlockState(foot.above()).getCollisionShape(target, foot.above()).isEmpty()) {
|
||||
if (hasFooting(target, foot)) {
|
||||
return foot;
|
||||
}
|
||||
}
|
||||
@@ -50,14 +86,73 @@ public final class PortAgeTravel {
|
||||
// origin is ocean or foliage. Final book landing rules are ported later.
|
||||
int y = Math.max(64, target.getHeight(Heightmap.Types.MOTION_BLOCKING, 0, 0) + 2);
|
||||
if (y + 2 >= target.getMaxY()) return null;
|
||||
BlockPos foot = new BlockPos(0, y, 0);
|
||||
return makePlatform(target, new BlockPos(0, y, 0));
|
||||
}
|
||||
|
||||
private static boolean isCaveTerrain(ServerLevel target) {
|
||||
var generator = target.getChunkSource().getGenerator();
|
||||
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
|
||||
return generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
|
||||
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
|
||||
.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
|
||||
.value().noiseSettings());
|
||||
}
|
||||
|
||||
private static BlockPos makePlatform(ServerLevel target, BlockPos foot) {
|
||||
if (foot.getY() <= target.getMinY() || foot.getY() + 2 >= target.getMaxY()) return null;
|
||||
// A carved pocket inside a flooded cave immediately refills. Refuse
|
||||
// that landing instead of placing the player in submerged spawn space.
|
||||
if (!target.getFluidState(foot).isEmpty() || !target.getFluidState(foot.above()).isEmpty()) return null;
|
||||
for (Direction direction : Direction.Plane.HORIZONTAL) {
|
||||
if (!target.getFluidState(foot.relative(direction)).isEmpty()
|
||||
|| !target.getFluidState(foot.above().relative(direction)).isEmpty()) return null;
|
||||
}
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
target.setBlock(new BlockPos(x, y - 1, z), Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
||||
target.setBlock(foot.offset(x, -1, z), Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 3);
|
||||
}
|
||||
}
|
||||
target.setBlock(foot, Blocks.AIR.defaultBlockState(), 3);
|
||||
target.setBlock(foot.above(), Blocks.AIR.defaultBlockState(), 3);
|
||||
return foot;
|
||||
}
|
||||
|
||||
private static boolean hasFooting(ServerLevel target, BlockPos foot) {
|
||||
BlockPos floor = foot.below();
|
||||
return target.getBlockState(floor).isFaceSturdy(target, floor, Direction.UP)
|
||||
&& target.getFluidState(foot).isEmpty()
|
||||
&& target.getFluidState(foot.above()).isEmpty()
|
||||
&& passable(target, foot) && passable(target, foot.above());
|
||||
}
|
||||
|
||||
private static boolean passable(ServerLevel level, BlockPos pos) {
|
||||
var state = level.getBlockState(pos);
|
||||
return state.getCollisionShape(level, pos).isEmpty() || state.is(net.minecraft.tags.BlockTags.LEAVES);
|
||||
}
|
||||
|
||||
/** Same local-then-surface safe-return search used by 1.21.1 fissures. */
|
||||
public static java.util.Optional<BlockPos> findFootedDestination(ServerLevel level, BlockPos origin) {
|
||||
var local = BlockPos.betweenClosedStream(origin.offset(-4, -2, -4), origin.offset(4, 2, 4))
|
||||
.map(BlockPos::immutable).sorted(java.util.Comparator.comparingDouble(pos -> pos.distSqr(origin)))
|
||||
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||
if (local.isPresent()) return local;
|
||||
for (int radius = 1; radius <= 64; radius++) {
|
||||
final int ring = radius;
|
||||
var found = BlockPos.betweenClosedStream(origin.offset(-ring, 0, -ring), origin.offset(ring, 0, ring))
|
||||
.filter(pos -> Math.max(Math.abs(pos.getX() - origin.getX()), Math.abs(pos.getZ() - origin.getZ())) == ring)
|
||||
.map(BlockPos::immutable)
|
||||
.flatMap(pos -> {
|
||||
BlockPos surface = new BlockPos(pos.getX(),
|
||||
level.getHeight(Heightmap.Types.MOTION_BLOCKING, pos.getX(), pos.getZ()), pos.getZ());
|
||||
return java.util.stream.Stream.of(surface, surface.above(), surface.below(),
|
||||
surface.above(2), surface.below(2));
|
||||
})
|
||||
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||
if (found.isPresent()) return found;
|
||||
}
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
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. */
|
||||
public record PortBiomePlan(Distribution distribution, List<Identifier> biomes) {
|
||||
public enum Distribution { NATIVE, SINGLE, TILED, GRID, TINY, SMALL, MEDIUM, LARGE, HUGE }
|
||||
|
||||
public static final Codec<PortBiomePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.STRING.xmap(Distribution::valueOf, Distribution::name).fieldOf("distribution")
|
||||
.forGetter(PortBiomePlan::distribution),
|
||||
Identifier.CODEC.listOf().fieldOf("biomes").forGetter(PortBiomePlan::biomes)
|
||||
).apply(instance, PortBiomePlan::new));
|
||||
|
||||
public PortBiomePlan {
|
||||
biomes = List.copyOf(biomes);
|
||||
if (distribution == Distribution.NATIVE && !biomes.isEmpty()
|
||||
|| distribution == Distribution.SINGLE && biomes.size() != 1
|
||||
|| distribution != Distribution.NATIVE && distribution != Distribution.SINGLE && biomes.size() < 2)
|
||||
throw new IllegalArgumentException("Invalid authored biome distribution");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** 26.3 port of the 1.21.1 surface crystal mound with budding hearts and seeded buds. */
|
||||
final class PortCrystalFormationsFeature {
|
||||
private static final long SALT = 0x4352595354414C53L;
|
||||
private static final int CHANCE = 15;
|
||||
|
||||
private PortCrystalFormationsFeature() {}
|
||||
|
||||
record Site(int x, int z, long seed) {}
|
||||
|
||||
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||
long seed = mix(ageSeed ^ SALT ^ (long) chunk.x() * 341873128712L
|
||||
^ (long) chunk.z() * 132897987541L ^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||
RandomSource random = RandomSource.create(seed);
|
||||
if (random.nextInt(CHANCE) != 0) return null;
|
||||
return new Site(chunk.getMinBlockX() + 5 + random.nextInt(6),
|
||||
chunk.getMinBlockZ() + 5 + random.nextInt(6), seed);
|
||||
}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, int occurrence) {
|
||||
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||
if (site == null) return;
|
||||
RandomSource random = RandomSource.create(site.seed() ^ 0x42554444494E474CL);
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, site.x(), site.z());
|
||||
if (y <= level.getMinY() + 1 || y + 4 >= level.getMaxY()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
int x = site.x() + dx;
|
||||
int z = site.z() + dz;
|
||||
if (Math.abs(level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - y) > 1) return;
|
||||
BlockState floor = level.getBlockState(pos.set(x, y - 1, z));
|
||||
if (!floor.isFaceSturdy(level, pos, Direction.UP) || !floor.getFluidState().isEmpty()
|
||||
|| floor.hasBlockEntity()) return;
|
||||
for (int h = 0; h < 4; h++) {
|
||||
BlockState existing = level.getBlockState(pos.set(x, y + h, z));
|
||||
if (!existing.isAir() && !existing.canBeReplaced()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
boolean center = dx == 0 && dz == 0;
|
||||
int height = center ? 3 : random.nextInt(3) == 0 ? 2 : 1;
|
||||
for (int h = 0; h < height; h++) {
|
||||
boolean budding = h == height - 1 && (center || random.nextBoolean());
|
||||
level.setBlock(pos.set(site.x() + dx, y + h, site.z() + dz),
|
||||
budding ? Rivencraft.BUDDING_CRYSTAL.get().defaultBlockState()
|
||||
: Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 2);
|
||||
}
|
||||
if (level.getBlockState(pos.set(site.x() + dx, y + height - 1, site.z() + dz))
|
||||
.is(Rivencraft.BUDDING_CRYSTAL) && random.nextBoolean()) {
|
||||
level.setBlock(pos.set(site.x() + dx, y + height, site.z() + dz),
|
||||
Rivencraft.SMALL_CRYSTAL_BUD.get().defaultBlockState()
|
||||
.setValue(AmethystClusterBlock.FACING, Direction.UP), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.feature.AbstractOreFeature;
|
||||
import net.minecraft.world.level.levelgen.placement.FeaturePlacer;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.neoforged.neoforge.common.Tags;
|
||||
|
||||
/** Replays each eligible biome ore placement once per page, matching Rivencraft 1.21.1. */
|
||||
public final class PortDenseOres {
|
||||
public static final TagKey<PlacedFeature> EXTRA_ORES = TagKey.create(Registries.PLACED_FEATURE,
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dense_ores"));
|
||||
|
||||
private PortDenseOres() {}
|
||||
|
||||
public static boolean hasOres(Holder<Biome> biome) {
|
||||
return oreFeatures(biome).anyMatch(PortDenseOres::isOre);
|
||||
}
|
||||
|
||||
/** Modern ore features carry their targets directly rather than an OreConfiguration. */
|
||||
public static boolean isOre(Holder<PlacedFeature> holder) {
|
||||
if (holder.is(EXTRA_ORES)) return true;
|
||||
if (!(holder.value().feature().value() instanceof AbstractOreFeature ore)) return false;
|
||||
return ore.targetStates().stream().anyMatch(target -> isOreBlock(target.state()));
|
||||
}
|
||||
|
||||
private static boolean isOreBlock(BlockState state) {
|
||||
if (state.is(Tags.BlockItems.ORES.block())) return true;
|
||||
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||
return path.endsWith("_ore") || path.contains("_ore_");
|
||||
}
|
||||
|
||||
private static java.util.stream.Stream<Holder<PlacedFeature>> oreFeatures(Holder<Biome> biome) {
|
||||
return biome.value().getGenerationSettings().features().stream().flatMap(HolderSet::stream);
|
||||
}
|
||||
|
||||
public static void place(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator generator,
|
||||
long ageSeed, int count) {
|
||||
if (count == 0) return;
|
||||
Set<Holder<Biome>> biomes = new HashSet<>();
|
||||
ChunkPos.rangeClosed(chunk.getPos(), 1).forEach(pos -> {
|
||||
for (var section : level.getChunk(pos.x(), pos.z()).getSections())
|
||||
section.getBiomes().getAll(biomes::add);
|
||||
});
|
||||
biomes.retainAll(generator.getBiomeSource().possibleBiomes());
|
||||
// Collect once per chunk, not once per repeated page. Stable registry-ID
|
||||
// order and per-occurrence seeds retain the 1.21.1 placement sequence.
|
||||
Map<Identifier, PlacedFeature> ores = new TreeMap<>();
|
||||
for (Holder<Biome> biome : biomes) {
|
||||
oreFeatures(biome).filter(PortDenseOres::isOre).forEach(feature ->
|
||||
feature.unwrapKey().ifPresent(key -> ores.putIfAbsent(key.identifier(), feature.value())));
|
||||
}
|
||||
BlockPos origin = new BlockPos(chunk.getPos().getMinBlockX(), level.getMinY(),
|
||||
chunk.getPos().getMinBlockZ());
|
||||
FeaturePlacer placer = new FeaturePlacer(level, generator);
|
||||
for (int occurrence = 0; occurrence < count; occurrence++) {
|
||||
for (var entry : ores.entrySet()) {
|
||||
long seed = mix(ageSeed ^ (long) origin.getX() * 341873128712L
|
||||
^ (long) origin.getZ() * 132897987541L
|
||||
^ (long) entry.getKey().hashCode() * 0x9E3779B97F4A7C15L
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
placer.placeWithBiomeCheck(entry.getValue(), RandomSource.create(seed), origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate;
|
||||
import net.minecraft.world.level.levelgen.feature.BlockBlobFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.LakeFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider;
|
||||
import net.minecraft.world.level.levelgen.placement.BiomeFilter;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.minecraft.world.level.levelgen.placement.RarityFilter;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
/** Delegates terrain generation and applies frozen Age-scoped features. */
|
||||
public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
||||
private static final ResourceKey<PlacedFeature> FOREST_ROCK = ResourceKey.create(
|
||||
Registries.PLACED_FEATURE, Identifier.withDefaultNamespace("forest_rock"));
|
||||
private static final ResourceKey<PlacedFeature> DUNGEONS = ResourceKey.create(
|
||||
Registries.PLACED_FEATURE, Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dungeons"));
|
||||
private static final TagKey<Block> ROCK_GROUND = TagKey.create(
|
||||
Registries.BLOCK, Identifier.withDefaultNamespace("forest_rock_can_place_on"));
|
||||
public static final MapCodec<PortFeatureChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
|
||||
PortRockPlan.CODEC.optionalFieldOf("rocks").forGetter(generator -> Optional.ofNullable(generator.rocks)),
|
||||
PortVegetationPlan.CODEC.optionalFieldOf("vegetation")
|
||||
.forGetter(generator -> Optional.ofNullable(generator.vegetation)),
|
||||
com.mojang.serialization.Codec.intRange(0, 32).optionalFieldOf("dungeons", 0)
|
||||
.forGetter(generator -> generator.dungeons),
|
||||
PortLakePlan.CODEC.optionalFieldOf("lakes")
|
||||
.forGetter(generator -> Optional.ofNullable(generator.lakes)),
|
||||
PortTerrainFeaturePlan.CODEC.optionalFieldOf("terrain_features")
|
||||
.forGetter(generator -> Optional.ofNullable(generator.terrainFeatures)),
|
||||
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
|
||||
).apply(instance, (base, rocks, vegetation, dungeons, lakes, terrainFeatures, seed) ->
|
||||
new PortFeatureChunkGenerator(base, rocks.orElse(null), vegetation.orElse(null),
|
||||
dungeons, lakes.orElse(null), terrainFeatures.orElse(null), seed)));
|
||||
|
||||
private final ChunkGenerator base;
|
||||
private final PortRockPlan rocks;
|
||||
private final PortVegetationPlan vegetation;
|
||||
private final int dungeons;
|
||||
private final PortLakePlan lakes;
|
||||
private final PortTerrainFeaturePlan terrainFeatures;
|
||||
private final long ageSeed;
|
||||
|
||||
public PortFeatureChunkGenerator(ChunkGenerator base, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, long ageSeed) {
|
||||
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
|
||||
this.base = base;
|
||||
this.rocks = rocks;
|
||||
this.vegetation = vegetation;
|
||||
this.dungeons = dungeons;
|
||||
this.lakes = lakes;
|
||||
this.terrainFeatures = terrainFeatures;
|
||||
this.ageSeed = ageSeed;
|
||||
}
|
||||
|
||||
public ChunkGenerator base() { return base; }
|
||||
public PortRockPlan rocks() { return rocks; }
|
||||
public PortVegetationPlan vegetation() { return vegetation; }
|
||||
public int dungeons() { return dungeons; }
|
||||
public PortLakePlan lakes() { return lakes; }
|
||||
public PortTerrainFeaturePlan terrainFeatures() { return terrainFeatures; }
|
||||
|
||||
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
|
||||
@Override public void validate() { base.validate(); }
|
||||
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
|
||||
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
|
||||
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
|
||||
StructureManager structures, ChunkAccess chunk) {
|
||||
return base.createBiomes(random, blender, structures, chunk);
|
||||
}
|
||||
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
||||
RandomState random, long seed) {
|
||||
return base.createState(sets, random, seed);
|
||||
}
|
||||
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||
StructureManager structures, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
base.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
||||
base.createReferences(level, structures, chunk);
|
||||
}
|
||||
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
|
||||
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
|
||||
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
|
||||
}
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
|
||||
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
|
||||
@Override public int getGenDepth() { return base.getGenDepth(); }
|
||||
@Override public int getSeaLevel() { return base.getSeaLevel(); }
|
||||
@Override public int getMinY() { return base.getMinY(); }
|
||||
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
|
||||
RandomState random, StructureManager structures, BiomeManager biomes,
|
||||
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
||||
CompletableFuture<ChunkAccess> built = base.buildTerrain(
|
||||
chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
|
||||
if (terrainFeatures == null || !terrainFeatures.skylands() && terrainFeatures.choices().isEmpty()) return built;
|
||||
return built.thenApply(result -> {
|
||||
// Match 1.21.1: filter the base terrain once, then add the authored
|
||||
// solid volumes, irrespective of the Skylands page's written position.
|
||||
if (terrainFeatures.skylands()) PortSkylandsFeature.filter(result, ageSeed);
|
||||
java.util.Map<Identifier, Integer> occurrences = new java.util.HashMap<>();
|
||||
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||
if (PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||
int occurrence = occurrences.merge(choice.page(), 1, Integer::sum) - 1;
|
||||
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
|
||||
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||
if (choice.page().equals(PortTerrainFeaturePlan.FLOATING_ISLANDS))
|
||||
PortFloatingIslandFeature.generate(result, featureSeed, material);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.TENDRILS))
|
||||
PortTendrilFeature.generate(result, featureSeed, material);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.SPHERES))
|
||||
PortSpheresFeature.generate(result, featureSeed, material);
|
||||
else throw new IllegalStateException("Unsupported terrain feature: " + choice.page());
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||
LevelHeightAccessor height, RandomState random) {
|
||||
return base.getBaseHeight(x, z, type, height, random);
|
||||
}
|
||||
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
|
||||
return base.getBaseColumn(x, z, height, random);
|
||||
}
|
||||
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
|
||||
BlockPos pos, SamplerContext context) {
|
||||
base.addDebugScreenInfo(info, random, pos, context);
|
||||
}
|
||||
@Override public void refreshFeaturesPerStep() {
|
||||
base.refreshFeaturesPerStep();
|
||||
super.refreshFeaturesPerStep();
|
||||
}
|
||||
|
||||
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
|
||||
StructureManager structures) {
|
||||
base.applyBiomeDecoration(level, chunk, structures);
|
||||
if (terrainFeatures != null) {
|
||||
PortDenseOres.place(level, chunk, this, ageSeed, terrainFeatures.denseOres());
|
||||
int spikeOccurrence = 0;
|
||||
int obeliskOccurrence = 0;
|
||||
int crystalOccurrence = 0;
|
||||
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||
if (!PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||
if (choice.page().equals(PortTerrainFeaturePlan.SPIKES))
|
||||
PortSpikesFeature.generate(level, chunk, ageSeed, material, spikeOccurrence++);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.OBELISKS))
|
||||
PortObelisksFeature.generate(level, chunk, ageSeed, material, obeliskOccurrence++);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS))
|
||||
PortCrystalFormationsFeature.generate(level, chunk, ageSeed, crystalOccurrence++);
|
||||
else throw new IllegalStateException("Unsupported surface feature: " + choice.page());
|
||||
}
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
long chunkSeed = ageSeed ^ (long) chunkPos.x() * 341873128712L
|
||||
^ (long) chunkPos.z() * 132897987541L;
|
||||
BlockPos origin = new BlockPos(chunkPos.getMinBlockX(), level.getMinY(), chunkPos.getMinBlockZ());
|
||||
if (rocks != null) {
|
||||
PlacedFeature vanilla = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.getOrThrow(FOREST_ROCK).value();
|
||||
var placement = vanilla.placement().stream()
|
||||
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList();
|
||||
for (int occurrence = 0; occurrence < rocks.materials().size(); occurrence++) {
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
BlockState material = BuiltInRegistries.BLOCK.getValue(
|
||||
rocks.materials().get(occurrence)).defaultBlockState();
|
||||
PlacedFeature authored = new PlacedFeature(Holder.direct(new BlockBlobFeature(
|
||||
material, BlockPredicate.matchesTag(ROCK_GROUND))), placement);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
if (dungeons > 0) {
|
||||
PlacedFeature authored = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.getOrThrow(DUNGEONS).value();
|
||||
for (int occurrence = 0; occurrence < dungeons; occurrence++) {
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ 0x44554E47454F4E53L ^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
if (lakes != null) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||
java.util.Map<PortLakePlan.Choice, Integer> occurrences = new java.util.HashMap<>();
|
||||
for (PortLakePlan.Choice choice : lakes.choices()) {
|
||||
PlacedFeature vanilla = registry.getOrThrow(ResourceKey.create(
|
||||
Registries.PLACED_FEATURE, PortLakePlan.placedFeature(choice.page()))).value();
|
||||
if (!(vanilla.feature().value() instanceof LakeFeature lake))
|
||||
throw new IllegalStateException("Lake placement no longer references a LakeFeature: " + choice.page());
|
||||
BlockState fluid = BuiltInRegistries.BLOCK.getValue(choice.fluid()).defaultBlockState();
|
||||
LakeFeature feature = new LakeFeature(BlockStateProvider.holderOf(fluid), lake.barrier(),
|
||||
lake.canPlaceFeature(), lake.canReplaceWithAirOrFluid(), lake.canReplaceWithBarrier());
|
||||
PlacedFeature authored = new PlacedFeature(Holder.direct(feature), vanilla.placement().stream()
|
||||
.filter(modifier -> !(modifier instanceof BiomeFilter))
|
||||
.map(modifier -> !PortLakePlan.isDeep(choice.page()) && modifier instanceof RarityFilter
|
||||
? RarityFilter.onAverageOnceEvery(12) : modifier)
|
||||
.toList());
|
||||
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ (long) choice.hashCode() * 0x9E3779B97F4A7C15L
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
if (vegetation != null) {
|
||||
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
|
||||
Holder<Biome> biome = level.getBiome(new BlockPos(
|
||||
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
|
||||
if (biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)) return;
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
|
||||
Identifier page = vegetation.pages().get(occurrence);
|
||||
Identifier placedId = PortVegetationPlan.placedFeature(page);
|
||||
PlacedFeature vanilla = registry.getOrThrow(
|
||||
ResourceKey.create(Registries.PLACED_FEATURE, placedId)).value();
|
||||
PlacedFeature authored = new PlacedFeature(vanilla.feature(), vanilla.placement().stream()
|
||||
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList());
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeGenerationSettings;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.neoforged.neoforge.common.util.MemoizedFunction;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/** Flat layers with the authored biome distribution used by 1.21.1 Ages. */
|
||||
public final class PortFlatChunkGenerator extends ChunkGenerator {
|
||||
public static final MapCodec<PortFlatChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
BiomeSource.CODEC.fieldOf("biome_source").forGetter(generator -> generator.biomeSource),
|
||||
FlatLevelGeneratorSettings.CODEC.fieldOf("settings").forGetter(PortFlatChunkGenerator::settings)
|
||||
).apply(instance, PortFlatChunkGenerator::new));
|
||||
|
||||
private final FlatLevelGeneratorSettings settings;
|
||||
private final MemoizedFunction<Holder<Biome>, BiomeGenerationSettings> generationSettingsGetter;
|
||||
|
||||
public PortFlatChunkGenerator(BiomeSource biomeSource, FlatLevelGeneratorSettings settings) {
|
||||
super(biomeSource, this.generationSettingsGetter = MemoizedFunction.of(
|
||||
biome -> generationSettings(settings, biome)));
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
public FlatLevelGeneratorSettings settings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
private static BiomeGenerationSettings generationSettings(FlatLevelGeneratorSettings settings, Holder<Biome> biome) {
|
||||
// Void biomes still control climate, but must not place terrain features.
|
||||
if (settings.getLayersInfo().isEmpty()) return BiomeGenerationSettings.EMPTY;
|
||||
if (biome.equals(settings.getBiome())) return settings.adjustGenerationSettings(biome);
|
||||
BiomeGenerationSettings.PlainBuilder builder = new BiomeGenerationSettings.PlainBuilder();
|
||||
List<HolderSet<PlacedFeature>> features = biome.value().getGenerationSettings().features();
|
||||
for (int step = 0; step < features.size(); step++) {
|
||||
if (step == GenerationStep.Decoration.UNDERGROUND_STRUCTURES.ordinal()
|
||||
|| step == GenerationStep.Decoration.SURFACE_STRUCTURES.ordinal()) continue;
|
||||
for (Holder<PlacedFeature> feature : features.get(step)) builder.addFeature(step, feature);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> structures,
|
||||
RandomState random, long seed) {
|
||||
Stream<Holder<StructureSet>> entries = settings.structureOverrides()
|
||||
.map(HolderSet::stream)
|
||||
.orElseGet(() -> structures.listElements().map(holder -> (Holder<StructureSet>) holder));
|
||||
return ChunkGeneratorStructureState.createForFlat(random, seed, getOrigin(random), biomeSource, entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSpawnHeight(LevelHeightAccessor height) {
|
||||
return height.getMinY() + Math.min(height.getHeight(), settings.getLayers().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
|
||||
RandomState random, StructureManager structures, BiomeManager biomes,
|
||||
@Nullable WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
||||
List<BlockState> layers = settings.getLayers();
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
for (int layer = 0; layer < Math.min(chunk.getHeight(), layers.size()); layer++) {
|
||||
BlockState state = layers.get(layer);
|
||||
if (state == null) continue;
|
||||
int y = chunk.getMinY() + layer;
|
||||
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++) {
|
||||
chunk.setBlockState(pos.set(x, y, z), state);
|
||||
floor.update(x, y, z, state);
|
||||
surface.update(x, y, z, state);
|
||||
}
|
||||
}
|
||||
return CompletableFuture.completedFuture(chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||
LevelHeightAccessor height, RandomState random) {
|
||||
List<BlockState> layers = settings.getLayers();
|
||||
for (int layer = Math.min(layers.size() - 1, height.getHeight() - 1); layer >= 0; layer--) {
|
||||
BlockState state = layers.get(layer);
|
||||
if (state != null && type.isOpaque().test(state)) return height.getMinY() + layer + 1;
|
||||
}
|
||||
return height.getMinY();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
|
||||
return new NoiseColumn(height.getMinY(), settings.getLayers().stream().limit(height.getHeight())
|
||||
.map(state -> state == null ? Blocks.AIR.defaultBlockState() : state)
|
||||
.toArray(BlockState[]::new));
|
||||
}
|
||||
|
||||
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
|
||||
BlockPos pos, SamplerContext context) { }
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion level) { }
|
||||
@Override public int getMinY() { return 0; }
|
||||
@Override public int getGenDepth() { return 384; }
|
||||
@Override public int getSeaLevel() { return -63; }
|
||||
|
||||
@Override
|
||||
public void refreshFeaturesPerStep() {
|
||||
super.refreshFeaturesPerStep();
|
||||
generationSettingsGetter.clear();
|
||||
settings.updateLayers();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
|
||||
/** 1.21.1 floating-island shape adapted to the 26.3 chunk terrain stage. */
|
||||
final class PortFloatingIslandFeature {
|
||||
private static final long SALT = 0x464C4F4154494E47L;
|
||||
private static final int SOURCE_RANGE = 3;
|
||||
private static final int CHANCE = 128;
|
||||
|
||||
private PortFloatingIslandFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||
var target = chunk.getPos();
|
||||
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
|
||||
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
|
||||
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||
^ (long) sourceX * 341873128712L ^ (long) sourceZ * 132897987541L));
|
||||
if (random.nextInt(CHANCE) != 0) continue;
|
||||
int centerX = sourceX * 16 + random.nextInt(16);
|
||||
int centerZ = sourceZ * 16 + random.nextInt(16);
|
||||
int centerY = Math.min(chunk.getMaxY() - 30, 168 + random.nextInt(48));
|
||||
if (centerY < chunk.getMinY() + 24) continue;
|
||||
int radiusX = 24 + random.nextInt(18);
|
||||
int radiusZ = 24 + random.nextInt(18);
|
||||
fill(chunk, ageSeed, material, centerX, centerY, centerZ, radiusX, radiusZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void fill(ChunkAccess chunk, long ageSeed, BlockState material,
|
||||
int centerX, int centerY, int centerZ, int radiusX, int radiusZ) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
int worldX = originX + localX;
|
||||
double dx = (worldX - centerX) / (double) radiusX;
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
int worldZ = originZ + localZ;
|
||||
double dz = (worldZ - centerZ) / (double) radiusZ;
|
||||
double distance = dx * dx + dz * dz;
|
||||
if (distance >= 1.0) continue;
|
||||
long detail = mix(ageSeed ^ (long) centerX * 341873128712L
|
||||
^ (long) centerZ * 132897987541L ^ (long) worldX * 42317861L
|
||||
^ (long) worldZ * 374761393L);
|
||||
int top = centerY + (int) Math.round(3 * (1 - distance)) + Math.floorMod(detail, 3) - 1;
|
||||
int depth = Math.max(1, (int) Math.round(3 + 17 * Math.pow(1 - distance, 1.5)));
|
||||
// Biomes are already populated by createBiomes in 26.3, so
|
||||
// sampling this chunk preserves its authored biome distribution.
|
||||
var biome = chunk.getNoiseBiome(QuartPos.fromBlock(worldX), QuartPos.fromBlock(top),
|
||||
QuartPos.fromBlock(worldZ));
|
||||
BlockState soil = biome.is(BiomeTags.IS_END) || PortRegistryPages.isCaveBiome(biome)
|
||||
? material : Blocks.DIRT.defaultBlockState();
|
||||
BlockState cap = soil == material ? material : Blocks.GRASS_BLOCK.defaultBlockState();
|
||||
for (int y = Math.max(top - depth, chunk.getMinY()); y <= top; y++) {
|
||||
pos.set(worldX, y, worldZ);
|
||||
if (!chunk.getBlockState(pos).isAir()) continue;
|
||||
BlockState state = y == top ? cap : y >= top - 3 ? soil : material;
|
||||
chunk.setBlockState(pos, state);
|
||||
surface.update(localX, y, localZ, state);
|
||||
floor.update(localX, y, localZ, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Ordered lake pages with fluid choices frozen when the Age is bound. */
|
||||
public record PortLakePlan(List<Choice> choices) {
|
||||
public static final Codec<PortLakePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortLakePlan::choices)
|
||||
).apply(instance, PortLakePlan::new));
|
||||
|
||||
public record Choice(Identifier page, Identifier fluid) {
|
||||
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
|
||||
Identifier.CODEC.fieldOf("fluid").forGetter(Choice::fluid)
|
||||
).apply(instance, Choice::new));
|
||||
|
||||
public Choice {
|
||||
if (!isLake(page) || fluid == null) throw new IllegalArgumentException("Invalid lake choice");
|
||||
}
|
||||
}
|
||||
|
||||
public PortLakePlan {
|
||||
choices = List.copyOf(choices);
|
||||
if (choices.isEmpty() || choices.size() > 32)
|
||||
throw new IllegalArgumentException("Lake plan must contain 1–32 choices");
|
||||
}
|
||||
|
||||
public static boolean isLake(Identifier page) {
|
||||
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||
return switch (page.getPath()) {
|
||||
case "feature/surface_lakes", "feature/deep_lakes", "feature/lava_lakes" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isDeep(Identifier page) { return page.getPath().equals("feature/deep_lakes"); }
|
||||
|
||||
public static Identifier placedFeature(Identifier page) {
|
||||
if (!isLake(page)) throw new IllegalArgumentException("Unsupported lake page " + page);
|
||||
return Identifier.withDefaultNamespace(isDeep(page) ? "lake_lava_underground" : "lake_lava_surface");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** 26.3 port of Rivencraft 1.21.1's Legacy-style 4×4 plinth and 2×12×2 shaft. */
|
||||
final class PortObelisksFeature {
|
||||
private static final long SALT = 0x4F42454C49534B53L;
|
||||
private static final int CHANCE = 128;
|
||||
private static final int BASE = 4;
|
||||
private static final int HEIGHT = 12;
|
||||
|
||||
private PortObelisksFeature() {}
|
||||
|
||||
record Site(int x, int z) {}
|
||||
|
||||
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||
^ (long) chunk.x() * 341873128712L ^ (long) chunk.z() * 132897987541L
|
||||
^ (long) occurrence * 0x9E3779B97F4A7C15L));
|
||||
if (random.nextInt(CHANCE) != 0) return null;
|
||||
return new Site(chunk.getMinBlockX() + 4 + random.nextInt(8),
|
||||
chunk.getMinBlockZ() + 4 + random.nextInt(8));
|
||||
}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||
if (site == null) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
int minSurface = Integer.MAX_VALUE;
|
||||
int maxSurface = Integer.MIN_VALUE;
|
||||
int[][] surface = new int[BASE][BASE];
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
int x = site.x() + dx;
|
||||
int z = site.z() + dz;
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||
if (y <= level.getMinY() + 1) return;
|
||||
BlockState ground = level.getBlockState(pos.set(x, y - 1, z));
|
||||
if (!ground.isFaceSturdy(level, pos, Direction.UP)
|
||||
|| !ground.getFluidState().isEmpty() || ground.hasBlockEntity()) return;
|
||||
surface[dx][dz] = y;
|
||||
minSurface = Math.min(minSurface, y);
|
||||
maxSurface = Math.max(maxSurface, y);
|
||||
}
|
||||
}
|
||||
if (maxSurface - minSurface > 2 || maxSurface + HEIGHT >= level.getMaxY()) return;
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean open(BlockState state) {
|
||||
return state.getFluidState().isEmpty() && (state.isAir() || state.canBeReplaced());
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Seed-frozen terrain and biome choices for a book with no authored symbols. */
|
||||
public final class PortRandomAgeSelection {
|
||||
private PortRandomAgeSelection() {}
|
||||
|
||||
public record Selection(PortAgeTerrain terrain, PortBiomePlan biomes) {}
|
||||
|
||||
public static Selection select(MinecraftServer server, long seed) {
|
||||
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
|
||||
// the other terrain families are rarer. The sorted order is deliberate.
|
||||
PortAgeTerrain terrain = weighted(new Random(seed ^ 0x5445525241494E4CL),
|
||||
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
||||
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
|
||||
new int[]{1, 1, 5, 1, 6, 1});
|
||||
if (terrain == PortAgeTerrain.VOID)
|
||||
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
||||
List.of(Identifier.withDefaultNamespace("the_void"))));
|
||||
|
||||
PortBiomePlan.Distribution distribution = weighted(new Random(seed ^ 0x524956454E435241L),
|
||||
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
|
||||
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
|
||||
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
|
||||
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
|
||||
PortBiomePlan.Distribution.HUGE),
|
||||
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
|
||||
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
||||
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
// 26.3 has no native multi-cave biome source; choose a seeded Cave
|
||||
// biome rather than silently using Lush Caves for every random Age.
|
||||
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
|
||||
|
||||
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
if (count > 1) {
|
||||
Random countRandom = new Random(seed ^ 0x42494F4D45434F55L);
|
||||
while (count < eligible.size() && countRandom.nextInt(3) != 0) count++;
|
||||
}
|
||||
List<Identifier> shuffled = new ArrayList<>(eligible);
|
||||
Collections.shuffle(shuffled, new Random(seed ^ 0x42494F4D45504F4FL));
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
||||
}
|
||||
|
||||
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||
if (terrain == PortAgeTerrain.CAVE) {
|
||||
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(PortRegistryPages::isCaveBiome)
|
||||
.map(holder -> holder.key().identifier())
|
||||
.sorted().toList();
|
||||
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
||||
}
|
||||
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
||||
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
||||
.map(key -> key.identifier()).distinct().sorted(Comparator.naturalOrder()).toList();
|
||||
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
|
||||
}
|
||||
|
||||
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
||||
int total = 0;
|
||||
for (int weight : weights) total += weight;
|
||||
int ticket = random.nextInt(total);
|
||||
for (int index = 0; index < choices.size(); index++) {
|
||||
ticket -= weights[index];
|
||||
if (ticket < 0) return choices.get(index);
|
||||
}
|
||||
throw new IllegalStateException("Random Age ticket was not consumed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen, ordered Rocks materials. Old single-material beta Ages remain readable. */
|
||||
public record PortRockPlan(List<Identifier> materials) {
|
||||
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.optionalFieldOf("material").forGetter(plan -> Optional.empty()),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("materials", List.of()).forGetter(PortRockPlan::materials)
|
||||
).apply(instance, (old, current) -> new PortRockPlan(current.isEmpty()
|
||||
? old.map(List::of).orElse(List.of()) : current)));
|
||||
|
||||
public PortRockPlan {
|
||||
materials = List.copyOf(materials);
|
||||
if (materials.isEmpty() || materials.size() > 32)
|
||||
throw new IllegalArgumentException("Rocks plan must contain 1–32 materials");
|
||||
}
|
||||
|
||||
public PortRockPlan(Identifier material) { this(List.of(material)); }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
|
||||
/** 26.3 resolver port of the seeded 1.21.1 Mystcraft-style region distribution. */
|
||||
public final class PortScaledBiomeSource extends BiomeSource implements BiomeResolver {
|
||||
public static final MapCodec<PortScaledBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
Biome.LIST_CODEC.fieldOf("biomes").forGetter(source -> source.biomes),
|
||||
Codec.LONG.fieldOf("seed").forGetter(source -> source.seed),
|
||||
Codec.intRange(0, 4).fieldOf("scale").forGetter(source -> source.scale)
|
||||
).apply(instance, PortScaledBiomeSource::new));
|
||||
|
||||
private final HolderSet<Biome> biomes;
|
||||
private final long seed;
|
||||
private final int scale;
|
||||
private final int cellWidth;
|
||||
|
||||
public PortScaledBiomeSource(HolderSet<Biome> biomes, long seed, int scale) {
|
||||
if (biomes.size() < 2 || scale < 0 || scale > 4)
|
||||
throw new IllegalArgumentException("Scaled regions need two biomes and scale 0..4");
|
||||
this.biomes = biomes;
|
||||
this.seed = seed;
|
||||
this.scale = scale;
|
||||
// Quart coordinates; Tiny starts at 16 blocks and each scale doubles.
|
||||
this.cellWidth = 4 << scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return biomes.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z) {
|
||||
int baseX = Math.floorDiv(x, cellWidth);
|
||||
int baseZ = Math.floorDiv(z, cellWidth);
|
||||
int closestX = baseX;
|
||||
int closestZ = baseZ;
|
||||
double closestDistance = Double.POSITIVE_INFINITY;
|
||||
for (int cellX = baseX - 1; cellX <= baseX + 1; cellX++) {
|
||||
for (int cellZ = baseZ - 1; cellZ <= baseZ + 1; cellZ++) {
|
||||
long cellSeed = mix(seed, cellX, cellZ);
|
||||
double centerX = (cellX + 0.5D + jitter(cellSeed) * 0.8D) * cellWidth;
|
||||
double centerZ = (cellZ + 0.5D + jitter(mix64(cellSeed)) * 0.8D) * cellWidth;
|
||||
double deltaX = x - centerX;
|
||||
double deltaZ = z - centerZ;
|
||||
double distance = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestX = cellX;
|
||||
closestZ = cellZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
return biomes.get(Math.floorMod((int) mix(seed ^ 0x42494F4D45534CL,
|
||||
closestX, closestZ), biomes.size()));
|
||||
}
|
||||
|
||||
private static double jitter(long value) {
|
||||
return ((value >>> 11) * 0x1.0p-53) - 0.5D;
|
||||
}
|
||||
|
||||
private static long mix(long seed, int x, int z) {
|
||||
return mix64(seed ^ (long) x * 0x9E3779B97F4A7C15L ^ (long) z * 0xC2B2AE3D27D4EB4FL);
|
||||
}
|
||||
|
||||
private static long mix64(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 port of Rivencraft's subtractive Skylands filter, adapted from
|
||||
* Mystcraft Legacy's SymbolSkylands (LGPL-3.0-or-later). Keeps upper solid
|
||||
* terrain above a seven-octave cutoff near Y=76 and removes liquids.
|
||||
*/
|
||||
final class PortSkylandsFeature {
|
||||
private static final long SALT = 0x534B594C414E4453L;
|
||||
private static final int BASE_CUTOFF = 76;
|
||||
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||
|
||||
private PortSkylandsFeature() {}
|
||||
|
||||
static void filter(ChunkAccess chunk, long ageSeed) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minY = chunk.getMinY();
|
||||
int maxY = chunk.getMaxY();
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
int cutoff = cutoff(ageSeed, originX + localX, originZ + localZ);
|
||||
for (int y = minY; y < maxY; y++) {
|
||||
pos.set(originX + localX, y, originZ + localZ);
|
||||
BlockState state = chunk.getBlockState(pos);
|
||||
if (!removes(state, y, cutoff)) continue;
|
||||
chunk.setBlockState(pos, AIR);
|
||||
surface.update(localX, y, localZ, AIR);
|
||||
floor.update(localX, y, localZ, AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean removes(BlockState state, int y, int cutoff) {
|
||||
return !state.isAir() && (y <= cutoff || state.liquid());
|
||||
}
|
||||
|
||||
static int cutoff(long ageSeed, int x, int z) {
|
||||
double variation = 0.0D;
|
||||
int cellSize = 128;
|
||||
double amplitude = 12.0D;
|
||||
for (int octave = 0; octave < 7; octave++) {
|
||||
variation += valueNoise(ageSeed ^ SALT ^ (long) octave * 0x9E3779B97F4A7C15L,
|
||||
x, z, cellSize) * amplitude;
|
||||
cellSize /= 2;
|
||||
amplitude /= 2.0D;
|
||||
}
|
||||
return BASE_CUTOFF + (int) Math.round(variation);
|
||||
}
|
||||
|
||||
private static double valueNoise(long seed, int x, int z, int cellSize) {
|
||||
int cellX = Math.floorDiv(x, cellSize);
|
||||
int cellZ = Math.floorDiv(z, cellSize);
|
||||
double u = smooth((x - cellX * cellSize) / (double) cellSize);
|
||||
double v = smooth((z - cellZ * cellSize) / (double) cellSize);
|
||||
double north = lerp(value(seed, cellX, cellZ), value(seed, cellX + 1, cellZ), u);
|
||||
double south = lerp(value(seed, cellX, cellZ + 1), value(seed, cellX + 1, cellZ + 1), u);
|
||||
return lerp(north, south, v);
|
||||
}
|
||||
|
||||
private static double value(long seed, int x, int z) {
|
||||
long hash = mix(seed ^ (long) x * 0xD1B54A32D192ED03L ^ (long) z * 0xABC98388FB8FAC03L);
|
||||
return (hash >>> 11) * 0x1.0p-52 - 1.0D;
|
||||
}
|
||||
|
||||
private static double smooth(double value) { return value * value * (3.0D - 2.0D * value); }
|
||||
|
||||
private static double lerp(double first, double second, double amount) {
|
||||
return first + (second - first) * amount;
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 adaptation of Rivencraft 1.21.1's solid sphere nodes, derived from
|
||||
* Mystcraft Legacy's MapGenSpheresMyst (Mystcraft contributors,
|
||||
* LGPL-3.0-or-later, commit 9bc8ddc). Writes stay within the target chunk.
|
||||
*/
|
||||
final class PortSpheresFeature {
|
||||
private static final int SOURCE_RANGE = 8;
|
||||
private static final long SALT = 0x53504845524553L;
|
||||
|
||||
private PortSpheresFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||
var target = chunk.getPos();
|
||||
Random master = new Random(ageSeed ^ SALT);
|
||||
long xSeed = master.nextLong();
|
||||
long zSeed = master.nextLong();
|
||||
for (int sx = target.x() - SOURCE_RANGE; sx <= target.x() + SOURCE_RANGE; sx++) {
|
||||
for (int sz = target.z() - SOURCE_RANGE; sz <= target.z() + SOURCE_RANGE; sz++) {
|
||||
Random source = new Random((long) sx * xSeed ^ (long) sz * zSeed ^ ageSeed ^ SALT);
|
||||
if (source.nextFloat() > 0.05F) continue;
|
||||
double x = sx * 16 + source.nextInt(16);
|
||||
double y = source.nextInt(source.nextInt(192) + 1) + 32;
|
||||
double z = sz * 16 + source.nextInt(16);
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
1.0F + source.nextFloat() * 4.0F, 0, 0, -1, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||
double x, double y, double z, float radius, float yaw, float pitch,
|
||||
int step, int length) {
|
||||
Random random = new Random(seed);
|
||||
if (length <= 0) {
|
||||
int maximum = SOURCE_RANGE * 16 - 16;
|
||||
length = maximum - random.nextInt(maximum / 4);
|
||||
}
|
||||
boolean singleNode = step == -1;
|
||||
if (singleNode) step = length / 2;
|
||||
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||
boolean slowPitch = random.nextInt(6) == 0;
|
||||
float yawDrift = 0;
|
||||
float pitchDrift = 0;
|
||||
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||
for (; step < length; step++) {
|
||||
double nodeRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||
float horizontal = Mth.cos(pitch);
|
||||
x += Mth.cos(yaw) * horizontal;
|
||||
y += Mth.sin(pitch);
|
||||
z += Mth.sin(yaw) * horizontal;
|
||||
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||
pitch += pitchDrift * 0.1F;
|
||||
yaw += yawDrift * 0.1F;
|
||||
pitchDrift = pitchDrift * 0.9F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||
yawDrift = yawDrift * 0.75F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||
trace(chunk, material, random.nextLong(), x, y, z,
|
||||
random.nextFloat() * 0.5F + 0.5F, yaw - Mth.HALF_PI, pitch / 3, step, length);
|
||||
trace(chunk, material, random.nextLong(), x, y, z,
|
||||
random.nextFloat() * 0.5F + 0.5F, yaw + Mth.HALF_PI, pitch / 3, step, length);
|
||||
return;
|
||||
}
|
||||
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||
double dx = x - middleX;
|
||||
double dz = z - middleZ;
|
||||
double remaining = length - step;
|
||||
double reach = radius + 18;
|
||||
if (dx * dx + dz * dz - remaining * remaining > reach * reach) return;
|
||||
if (Math.abs(dx) <= 16 + nodeRadius * 2 && Math.abs(dz) <= 16 + nodeRadius * 2)
|
||||
fillNode(chunk, material, x, y, z, nodeRadius);
|
||||
if (singleNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillNode(ChunkAccess chunk, BlockState material,
|
||||
double centerX, double centerY, double centerZ, double radius) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minX = Math.max(0, Mth.floor(centerX - radius) - originX - 1);
|
||||
int maxX = Math.min(16, Mth.floor(centerX + radius) - originX + 2);
|
||||
int minZ = Math.max(0, Mth.floor(centerZ - radius) - originZ - 1);
|
||||
int maxZ = Math.min(16, Mth.floor(centerZ + radius) - originZ + 2);
|
||||
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - radius) - 1);
|
||||
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + radius) + 2);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||
double dy = (worldY + 0.5D - centerY) / radius;
|
||||
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||
double dz = (originZ + localZ + 0.5D - centerZ) / radius;
|
||||
for (int localX = minX; localX < maxX; localX++) {
|
||||
double dx = (originX + localX + 0.5D - centerX) / radius;
|
||||
if (!insideSphere(dx, dy, dz)) continue;
|
||||
pos.set(originX + localX, worldY, originZ + localZ);
|
||||
BlockState previous = chunk.getBlockState(pos);
|
||||
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||
chunk.setBlockState(pos, material);
|
||||
surface.update(localX, worldY, localZ, material);
|
||||
floor.update(localX, worldY, localZ, material);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean insideSphere(double dx, double dy, double dz) {
|
||||
return dx * dx + dy * dy + dz * dz < 1.0D;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 port of Rivencraft 1.21.1's rare surface spikes, adapted from
|
||||
* Mystcraft Legacy's WorldGenSpikesAdv (Mystcraft contributors,
|
||||
* LGPL-3.0-or-later, commit 9bc8ddc). The footprint stays within one chunk.
|
||||
*/
|
||||
final class PortSpikesFeature {
|
||||
private static final long SALT = 0x5350494B4553L;
|
||||
|
||||
private PortSpikesFeature() {}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
RandomSource random = RandomSource.create(ageSeed ^ SALT
|
||||
^ (long) originX * 341873128712L ^ (long) originZ * 132897987541L
|
||||
^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||
if (random.nextInt(18) != 0) return;
|
||||
int x = originX + 4 + random.nextInt(8);
|
||||
int z = originZ + 4 + random.nextInt(8);
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||
int height = 6 + random.nextInt(32);
|
||||
int width = 1 + random.nextInt(4);
|
||||
if (y < level.getMinY() + 1 || y + height >= level.getMaxY()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
if (!level.isEmptyBlock(pos.set(x, y, z))) return;
|
||||
for (int px = x - width; px <= x + width; px++) {
|
||||
for (int pz = z - width; pz <= z + width; pz++) {
|
||||
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||
if (!level.getBlockState(pos.set(px, y - 1, pz)).isSolid()) return;
|
||||
}
|
||||
}
|
||||
for (int px = x - width; px <= x + width; px++) {
|
||||
for (int pz = z - width; pz <= z + width; pz++) {
|
||||
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||
int top = y + 1 + random.nextInt(random.nextInt(height) + 1);
|
||||
for (int py = y; py < top; py++) {
|
||||
pos.set(px, py, pz);
|
||||
if (level.isEmptyBlock(pos)) level.setBlock(pos, material, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean insideFootprint(int dx, int dz, int width) {
|
||||
return dx * dx + dz * dz <= width * width + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/**
|
||||
* Ports Rivencraft 1.21.1's Legacy fissure footprint/carving.
|
||||
* Derived from Mystcraft's WorldGenMystStarFissure, LGPL-3.0,
|
||||
* copyright XCompWiz and Mystcraft contributors.
|
||||
*/
|
||||
public final class PortStarFissureFeature {
|
||||
private static final long SALT = 0x5354415246495353L;
|
||||
|
||||
private PortStarFissureFeature() {}
|
||||
|
||||
/** Per-Age data prevents revisits/restarts from carving the same fissure again. */
|
||||
private static final class Arrival extends SavedData {
|
||||
private static final Codec<Arrival> CODEC = BlockPos.CODEC.optionalFieldOf("spawn").codec()
|
||||
.xmap(Arrival::new, data -> Optional.ofNullable(data.spawn));
|
||||
private static final SavedDataType<Arrival> TYPE = new SavedDataType<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "star_fissure_arrival"),
|
||||
() -> new Arrival(Optional.empty()), CODEC);
|
||||
private BlockPos spawn;
|
||||
|
||||
private Arrival(Optional<BlockPos> spawn) { this.spawn = spawn.orElse(null); }
|
||||
}
|
||||
|
||||
public static BlockPos savedSpawn(ServerLevel level) {
|
||||
return level.getDataStorage().computeIfAbsent(Arrival.TYPE).spawn;
|
||||
}
|
||||
|
||||
public static void generateOnce(ServerLevel level, BlockPos spawn, boolean caveTerrain) {
|
||||
Arrival data = level.getDataStorage().computeIfAbsent(Arrival.TYPE);
|
||||
if (data.spawn != null) return;
|
||||
generate(level, level.getSeed(), spawn, caveTerrain);
|
||||
data.spawn = spawn.immutable();
|
||||
data.setDirty();
|
||||
}
|
||||
|
||||
/** Legacy chooses 8–23 blocks after the spawn chunk, then grows along +Z. */
|
||||
public static List<BlockPos> footprint(long ageSeed, BlockPos spawn) {
|
||||
long chunkX = spawn.getX() >> 4;
|
||||
long chunkZ = spawn.getZ() >> 4;
|
||||
Random random = new Random(ageSeed ^ SALT ^ chunkX ^ (chunkZ << 32));
|
||||
int originX = ((int) chunkX << 4) + random.nextInt(16) + 8;
|
||||
int originZ = ((int) chunkZ << 4) + random.nextInt(16) + 8;
|
||||
int length = random.nextInt(8) + 10;
|
||||
int[][] intervals = new int[length][2];
|
||||
List<BlockPos> columns = new ArrayList<>();
|
||||
addRow(columns, originX, originZ, 0, 0, 0);
|
||||
for (int row = 1; row < length; row++) {
|
||||
int range = length + Math.min(row, length - row) + 1;
|
||||
int right = (random.nextInt(range) - (range >> 1) + intervals[row - 1][1]) / 2;
|
||||
int left = (random.nextInt(range) - (range >> 1) + intervals[row - 1][0]) / 2;
|
||||
if (left > right) {
|
||||
int swap = left;
|
||||
left = right;
|
||||
right = swap;
|
||||
}
|
||||
left = Math.min(left, intervals[row - 1][1]);
|
||||
right = Math.max(right, intervals[row - 1][0]);
|
||||
intervals[row][0] = left;
|
||||
intervals[row][1] = right;
|
||||
addRow(columns, originX, originZ, row, left, right);
|
||||
}
|
||||
return List.copyOf(columns);
|
||||
}
|
||||
|
||||
private static void addRow(List<BlockPos> columns, int originX, int originZ, int row, int left, int right) {
|
||||
for (int x = left; x <= right; x++) {
|
||||
// Only X/Z are used; the real floor is the dimension's minimum Y.
|
||||
columns.add(new BlockPos(originX + x, 0, originZ + row * 2));
|
||||
columns.add(new BlockPos(originX + x, 0, originZ + row * 2 + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private static void generate(ServerLevel level, long ageSeed, BlockPos spawn, boolean caveTerrain) {
|
||||
int floorY = level.getMinY();
|
||||
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
|
||||
for (BlockPos column : footprint(ageSeed, spawn)) {
|
||||
int top = level.getHeight(Heightmap.Types.WORLD_SURFACE, column.getX(), column.getZ());
|
||||
if (caveTerrain) {
|
||||
for (int y = Math.max(spawn.getY(), floorY + 1); y < top; y++) {
|
||||
if (level.getBlockState(mutable.set(column.getX(), y, column.getZ())).is(Blocks.BEDROCK)) {
|
||||
top = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y = floorY + 1; y < top; y++)
|
||||
level.setBlock(mutable.set(column.getX(), y, column.getZ()), Blocks.AIR.defaultBlockState(), 2);
|
||||
level.setBlock(mutable.set(column.getX(), floorY, column.getZ()),
|
||||
Rivencraft.STAR_FISSURE.get().defaultBlockState(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 adaptation of Rivencraft 1.21.1's winding solid volumes, originally
|
||||
* adapted from Mystcraft Legacy's SymbolTendrils and MapGenCavesMyst (LGPL-3.0-or-later).
|
||||
* Each chunk reconstructs the neighboring source paths to preserve continuous borders.
|
||||
*/
|
||||
final class PortTendrilFeature {
|
||||
private static final long SALT = 0x54454E4452494C53L;
|
||||
private static final int SOURCE_RANGE = 8;
|
||||
private static final int SOURCE_RATE = 15;
|
||||
private static final int SOURCE_SIZE = 18;
|
||||
|
||||
private PortTendrilFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||
var target = chunk.getPos();
|
||||
Random master = new Random(ageSeed ^ SALT);
|
||||
long xSeed = master.nextLong();
|
||||
long zSeed = master.nextLong();
|
||||
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
|
||||
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
|
||||
Random source = new Random((long) sourceX * xSeed ^ (long) sourceZ * zSeed ^ ageSeed ^ SALT);
|
||||
int nodes = source.nextInt(source.nextInt(source.nextInt(SOURCE_SIZE) + 1) + 1);
|
||||
if (source.nextInt(SOURCE_RATE) != 0) continue;
|
||||
for (int node = 0; node < nodes; node++) {
|
||||
double x = sourceX * 16 + source.nextInt(16);
|
||||
double y = source.nextInt(source.nextInt(120) + 8);
|
||||
double z = sourceZ * 16 + source.nextInt(16);
|
||||
int branches = 1;
|
||||
if (source.nextInt(4) == 0) {
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
1.0F + source.nextFloat() * 6.0F, 0.0F, 0.0F, -1, -1, 0.5D);
|
||||
branches += source.nextInt(4);
|
||||
}
|
||||
for (int branch = 0; branch < branches; branch++) {
|
||||
float yaw = source.nextFloat() * Mth.TWO_PI;
|
||||
float pitch = (source.nextFloat() - 0.5F) / 4.0F;
|
||||
float radius = source.nextFloat() * 2.0F + source.nextFloat();
|
||||
if (source.nextInt(10) == 0)
|
||||
radius *= source.nextFloat() * source.nextFloat() * 3.0F + 1.0F;
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
radius, yaw, pitch, 0, 0, 1.0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||
double x, double y, double z, float radius, float yaw, float pitch,
|
||||
int step, int length, double verticalScale) {
|
||||
Random random = new Random(seed);
|
||||
if (length <= 0) {
|
||||
int maximum = SOURCE_RANGE * 16 - 16;
|
||||
length = maximum - random.nextInt(maximum / 4);
|
||||
}
|
||||
boolean singleNode = step == -1;
|
||||
if (singleNode) step = length / 2;
|
||||
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||
boolean slowPitch = random.nextInt(6) == 0;
|
||||
float yawDrift = 0.0F;
|
||||
float pitchDrift = 0.0F;
|
||||
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||
for (; step < length; step++) {
|
||||
double horizontalRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||
double verticalRadius = horizontalRadius * verticalScale;
|
||||
float horizontalMotion = Mth.cos(pitch);
|
||||
x += Mth.cos(yaw) * horizontalMotion;
|
||||
y += Mth.sin(pitch);
|
||||
z += Mth.sin(yaw) * horizontalMotion;
|
||||
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||
pitch += pitchDrift * 0.1F;
|
||||
yaw += yawDrift * 0.1F;
|
||||
pitchDrift = pitchDrift * 0.9F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||
yawDrift = yawDrift * 0.75F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||
yaw - Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||
yaw + Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||
return;
|
||||
}
|
||||
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||
double distanceX = x - middleX;
|
||||
double distanceZ = z - middleZ;
|
||||
double remaining = length - step;
|
||||
double reach = radius + 18.0D;
|
||||
if (distanceX * distanceX + distanceZ * distanceZ - remaining * remaining > reach * reach) return;
|
||||
if (x >= middleX - 16.0D - horizontalRadius * 2.0D
|
||||
&& x <= middleX + 16.0D + horizontalRadius * 2.0D
|
||||
&& z >= middleZ - 16.0D - horizontalRadius * 2.0D
|
||||
&& z <= middleZ + 16.0D + horizontalRadius * 2.0D) {
|
||||
fillNode(chunk, material, x, y, z, horizontalRadius, verticalRadius);
|
||||
}
|
||||
if (singleNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillNode(ChunkAccess chunk, BlockState material, double centerX, double centerY,
|
||||
double centerZ, double horizontalRadius, double verticalRadius) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minX = Math.max(0, Mth.floor(centerX - horizontalRadius) - originX - 1);
|
||||
int maxX = Math.min(16, Mth.floor(centerX + horizontalRadius) - originX + 2);
|
||||
int minZ = Math.max(0, Mth.floor(centerZ - horizontalRadius) - originZ - 1);
|
||||
int maxZ = Math.min(16, Mth.floor(centerZ + horizontalRadius) - originZ + 2);
|
||||
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - verticalRadius) - 1);
|
||||
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + verticalRadius) + 2);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||
double dy = (worldY + 0.5D - centerY) / verticalRadius;
|
||||
if (dy <= -0.7D || dy * dy >= 1.0D) continue;
|
||||
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||
double dz = (originZ + localZ + 0.5D - centerZ) / horizontalRadius;
|
||||
for (int localX = minX; localX < maxX; localX++) {
|
||||
double dx = (originX + localX + 0.5D - centerX) / horizontalRadius;
|
||||
if (dx * dx + dy * dy + dz * dz >= 1.0D) continue;
|
||||
pos.set(originX + localX, worldY, originZ + localZ);
|
||||
BlockState previous = chunk.getBlockState(pos);
|
||||
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||
chunk.setBlockState(pos, material);
|
||||
surface.update(localX, worldY, localZ, material);
|
||||
floor.update(localX, worldY, localZ, material);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen terrain filters and ordered solid/surface features for an Age. */
|
||||
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
|
||||
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/floating_islands");
|
||||
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
|
||||
public static final Identifier SKYLANDS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/skylands");
|
||||
public static final Identifier SPHERES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spheres");
|
||||
public static final Identifier SPIKES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spikes");
|
||||
public static final Identifier OBELISKS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/obelisks");
|
||||
public static final Identifier CRYSTAL_FORMATIONS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/crystal_formations");
|
||||
public static final Identifier CRYSTAL_MATERIAL = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "crystal_block");
|
||||
public static final Identifier DENSE_ORES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dense_ores");
|
||||
public static final Identifier STAR_FISSURES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/star_fissures");
|
||||
public static final int MAX_PER_FEATURE = 32;
|
||||
public static final Codec<PortTerrainFeaturePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortTerrainFeaturePlan::choices),
|
||||
Codec.BOOL.optionalFieldOf("skylands", false).forGetter(PortTerrainFeaturePlan::skylands),
|
||||
Codec.intRange(0, MAX_PER_FEATURE).optionalFieldOf("dense_ores", 0)
|
||||
.forGetter(PortTerrainFeaturePlan::denseOres),
|
||||
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures)
|
||||
).apply(instance, PortTerrainFeaturePlan::new));
|
||||
|
||||
public record Choice(Identifier page, Identifier material) {
|
||||
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
|
||||
Identifier.CODEC.fieldOf("material").forGetter(Choice::material)
|
||||
).apply(instance, Choice::new));
|
||||
|
||||
public Choice {
|
||||
if (!isSupported(page) || material == null)
|
||||
throw new IllegalArgumentException("Invalid terrain feature choice");
|
||||
if (CRYSTAL_FORMATIONS.equals(page) && !CRYSTAL_MATERIAL.equals(material))
|
||||
throw new IllegalArgumentException("Crystal formations use Rivencraft crystal, not a Block modifier");
|
||||
}
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan {
|
||||
choices = List.copyOf(choices);
|
||||
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
|
||||
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
|
||||
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures)
|
||||
throw new IllegalArgumentException("Terrain feature plan must not be empty");
|
||||
var counts = new HashMap<Identifier, Integer>();
|
||||
for (Choice choice : choices) {
|
||||
if (counts.merge(choice.page(), 1, Integer::sum) > MAX_PER_FEATURE)
|
||||
throw new IllegalArgumentException("Terrain feature plan permits at most 32 occurrences per feature");
|
||||
}
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres) {
|
||||
this(choices, skylands, denseOres, false);
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands) { this(choices, skylands, 0, false); }
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices) { this(choices, false, 0, false); }
|
||||
|
||||
public static boolean isSupported(Identifier page) {
|
||||
return FLOATING_ISLANDS.equals(page) || TENDRILS.equals(page) || SPHERES.equals(page) || isSurface(page);
|
||||
}
|
||||
|
||||
public static boolean acceptsMaterial(Identifier page) {
|
||||
return isSupported(page) && !CRYSTAL_FORMATIONS.equals(page);
|
||||
}
|
||||
|
||||
public static boolean isSurface(Identifier page) {
|
||||
return SPIKES.equals(page) || OBELISKS.equals(page) || CRYSTAL_FORMATIONS.equals(page);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Ordered, frozen authored vegetation pages for surface Ages. */
|
||||
public record PortVegetationPlan(List<Identifier> pages) {
|
||||
public static final Identifier FLOWERS = page("flowers");
|
||||
public static final Identifier TREES = page("trees");
|
||||
public static final Identifier HUGE_TREES = page("huge_trees");
|
||||
public static final Codec<PortVegetationPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages)
|
||||
).apply(instance, PortVegetationPlan::new));
|
||||
|
||||
public PortVegetationPlan {
|
||||
pages = List.copyOf(pages);
|
||||
if (pages.isEmpty() || pages.size() > 32 || pages.stream().anyMatch(page -> !isVegetation(page)))
|
||||
throw new IllegalArgumentException("Vegetation plan requires 1–32 supported pages");
|
||||
}
|
||||
|
||||
public static boolean isVegetation(Identifier page) {
|
||||
return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES);
|
||||
}
|
||||
|
||||
public static Identifier placedFeature(Identifier page) {
|
||||
if (page.equals(FLOWERS)) return Identifier.withDefaultNamespace("flower_default");
|
||||
if (page.equals(TREES)) return Identifier.withDefaultNamespace("trees_plains");
|
||||
if (page.equals(HUGE_TREES)) return Identifier.withDefaultNamespace("trees_old_growth_spruce_taiga");
|
||||
throw new IllegalArgumentException("Unsupported vegetation page " + page);
|
||||
}
|
||||
|
||||
private static Identifier page(String name) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + name);
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,13 @@ import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
@@ -18,7 +22,18 @@ import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.TicketStorage;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.FixedBiomeSource;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.biome.CheckerboardColumnBiomeSource;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.levelgen.FlatLevelSource;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.flat.FlatLayerInfo;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
import net.minecraft.world.level.storage.LevelResource;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
@@ -41,7 +56,9 @@ public final class RuntimeAgeBackend {
|
||||
AgeLevelIndex index = index(server);
|
||||
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
||||
try {
|
||||
load(server, age.getKey(), age.getValue());
|
||||
load(server, age.getKey(), age.getValue(), index.biomePlan(age.getKey()),
|
||||
index.terrain(age.getKey()), index.rocks(age.getKey()), index.vegetation(age.getKey()),
|
||||
index.dungeons(age.getKey()), index.lakes(age.getKey()), index.terrainFeatures(age.getKey()));
|
||||
} catch (RuntimeException exception) {
|
||||
Rivencraft.LOGGER.error("Could not restore Age {}. Its saved index remains intact.", age.getKey(), exception);
|
||||
}
|
||||
@@ -50,12 +67,51 @@ public final class RuntimeAgeBackend {
|
||||
|
||||
/** Creates a server level, then commits its identity only after successful construction. */
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed) {
|
||||
return create(server, id, seed, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed, PortBiomePlan biomePlan) {
|
||||
return create(server, id, seed, biomePlan, PortAgeTerrain.STANDARD);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain) {
|
||||
return create(server, id, seed, biomePlan, terrain, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, 0);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, null);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures) {
|
||||
checkServerThread(server);
|
||||
ageName(id);
|
||||
AgeLevelIndex index = index(server);
|
||||
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
|
||||
ServerLevel level = load(server, id, seed);
|
||||
index.add(id, seed);
|
||||
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
// A newly bound book may be used immediately. Persist its Age identity
|
||||
// before publishing the catalog so a later crash cannot leave that
|
||||
// book pointing at a world omitted from the on-disk index.
|
||||
@@ -75,7 +131,9 @@ public final class RuntimeAgeBackend {
|
||||
public static ServerLevel reload(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
long seed = requireSeed(server, id);
|
||||
ServerLevel level = load(server, id, seed);
|
||||
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id),
|
||||
index(server).rocks(id), index(server).vegetation(id), index(server).dungeons(id), index(server).lakes(id),
|
||||
index(server).terrainFeatures(id));
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
return level;
|
||||
}
|
||||
@@ -85,18 +143,25 @@ public final class RuntimeAgeBackend {
|
||||
checkServerThread(server);
|
||||
AgeLevelIndex index = index(server);
|
||||
long previousSeed = requireSeed(server, id);
|
||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||
PortAgeTerrain terrain = index.terrain(id);
|
||||
PortRockPlan rocks = index.rocks(id);
|
||||
PortVegetationPlan vegetation = index.vegetation(id);
|
||||
int dungeons = index.dungeons(id);
|
||||
PortLakePlan lakes = index.lakes(id);
|
||||
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
|
||||
if (previousSeed == seed) return;
|
||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||
if (loaded) unloadLoaded(server, id);
|
||||
try {
|
||||
if (loaded) load(server, id, seed);
|
||||
if (loaded) load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
index.reseed(id, seed);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
if (loaded) AgeCatalogSync.onCatalogChanged(server);
|
||||
} catch (RuntimeException exception) {
|
||||
if (loaded) {
|
||||
try {
|
||||
load(server, id, previousSeed);
|
||||
load(server, id, previousSeed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
@@ -112,6 +177,13 @@ public final class RuntimeAgeBackend {
|
||||
String name = ageName(id);
|
||||
AgeLevelIndex index = index(server);
|
||||
long seed = requireSeed(server, id);
|
||||
PortBiomePlan biomePlan = index.biomePlan(id);
|
||||
PortAgeTerrain terrain = index.terrain(id);
|
||||
PortRockPlan rocks = index.rocks(id);
|
||||
PortVegetationPlan vegetation = index.vegetation(id);
|
||||
int dungeons = index.dungeons(id);
|
||||
PortLakePlan lakes = index.lakes(id);
|
||||
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
|
||||
boolean loaded = server.getLevel(dimensionKey(id)) != null;
|
||||
ArchivePaths paths = archivePaths(server, name);
|
||||
if (loaded) unloadLoaded(server, id);
|
||||
@@ -130,14 +202,15 @@ public final class RuntimeAgeBackend {
|
||||
}
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
try {
|
||||
index.restoreAfterFailedRetirement(id, seed);
|
||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
if (loaded) {
|
||||
try {
|
||||
load(server, id, seed);
|
||||
load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
@@ -233,17 +306,102 @@ public final class RuntimeAgeBackend {
|
||||
private record ArchivePaths(Path live, Path archive) {
|
||||
}
|
||||
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, 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);
|
||||
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 SINGLE -> new FixedBiomeSource(biomes.getFirst());
|
||||
case TILED, GRID -> new CheckerboardColumnBiomeSource(HolderSet.direct(biomes),
|
||||
plan.distribution() == PortBiomePlan.Distribution.GRID ? 0 : 4);
|
||||
case TINY, SMALL, MEDIUM, LARGE, HUGE -> new PortScaledBiomeSource(
|
||||
HolderSet.direct(biomes), seed, plan.distribution().ordinal() -
|
||||
PortBiomePlan.Distribution.TINY.ordinal());
|
||||
};
|
||||
}
|
||||
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures) {
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||
|
||||
// This prototype uses vanilla Overworld terrain until issue #98 ports
|
||||
// authored ChunkGenerators. LevelStem.seedOverride controls chunk random
|
||||
// state as well as ServerLevel.getSeed() in NeoForge 26.3.
|
||||
// Noise terrains reuse the Overworld biome source unless a
|
||||
// non-Native authored distribution replaces it.
|
||||
// LevelStem.seedOverride controls chunk random state and getSeed().
|
||||
LevelStem overworldStem = server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM)
|
||||
.getValue(LevelStem.OVERWORLD);
|
||||
if (overworldStem == null) throw new IllegalStateException("Missing Overworld LevelStem");
|
||||
LevelStem stem = new LevelStem(overworldStem.type(), overworldStem.generator(), OptionalLong.of(seed));
|
||||
var generator = overworldStem.generator();
|
||||
if (terrain == PortAgeTerrain.FLAT || terrain == PortAgeTerrain.VOID) {
|
||||
var biomes = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
Holder<Biome> biome = biomes.getOrThrow(terrain == PortAgeTerrain.VOID
|
||||
? Biomes.THE_VOID : Biomes.PLAINS);
|
||||
if (biomePlan != null && !biomePlan.biomes().isEmpty()) {
|
||||
Identifier biomeId = biomePlan.biomes().getFirst();
|
||||
biome = biomes.get(ResourceKey.create(Registries.BIOME, biomeId))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biomeId));
|
||||
}
|
||||
FlatLevelGeneratorSettings settings;
|
||||
if (terrain == PortAgeTerrain.FLAT) {
|
||||
settings = FlatLevelGeneratorSettings.getDefault(biomes,
|
||||
server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET),
|
||||
server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE));
|
||||
// Vanilla superflat begins at the modern minimum build height.
|
||||
// Pad the column with air so the first solid layer begins at Y=0.
|
||||
int airBelowZero = Math.max(0, -server.overworld().getMinY());
|
||||
if (airBelowZero > 0)
|
||||
settings.getLayersInfo().addFirst(new FlatLayerInfo(airBelowZero, Blocks.AIR));
|
||||
settings = settings.withBiomeAndLayers(settings.getLayersInfo(),
|
||||
settings.structureOverrides(), biome);
|
||||
settings.setDecoration();
|
||||
} else {
|
||||
settings = new FlatLevelGeneratorSettings(Optional.empty(), biome, List.of());
|
||||
settings.updateLayers();
|
||||
}
|
||||
generator = biomePlan != null && biomePlan.biomes().size() > 1
|
||||
? new PortFlatChunkGenerator(authoredBiomeSource(server, biomePlan, seed), settings)
|
||||
: new FlatLevelSource(settings);
|
||||
} else if ((biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE)
|
||||
|| terrain != PortAgeTerrain.STANDARD) {
|
||||
if (!(generator instanceof NoiseBasedChunkGenerator noise))
|
||||
throw new IllegalStateException("Overworld generator cannot accept authored noise settings");
|
||||
var source = terrain == PortAgeTerrain.CAVE
|
||||
? new FixedBiomeSource(server.registryAccess().lookupOrThrow(Registries.BIOME)
|
||||
.getOrThrow(Biomes.LUSH_CAVES))
|
||||
: generator.getBiomeSource();
|
||||
if (terrain == PortAgeTerrain.CAVE && biomePlan != null) {
|
||||
var biomes = server.registryAccess().lookupOrThrow(Registries.BIOME);
|
||||
for (Identifier biomeId : biomePlan.biomes()) {
|
||||
var holder = biomes.get(ResourceKey.create(Registries.BIOME, biomeId))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biomeId));
|
||||
if (!PortRegistryPages.isCaveBiome(holder))
|
||||
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biomeId);
|
||||
}
|
||||
}
|
||||
if (biomePlan != null && biomePlan.distribution() != PortBiomePlan.Distribution.NATIVE) {
|
||||
source = authoredBiomeSource(server, biomePlan, seed);
|
||||
}
|
||||
var settings = switch (terrain) {
|
||||
case STANDARD -> noise.generatorSettings();
|
||||
case AMPLIFIED -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(NoiseGeneratorSettings.AMPLIFIED);
|
||||
case ISLAND -> server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(NoiseGeneratorSettings.FLOATING_ISLANDS);
|
||||
case CAVE -> dryCaveSettings(server);
|
||||
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource");
|
||||
};
|
||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||
}
|
||||
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
|
||||
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
|
||||
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
|
||||
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
||||
ServerLevel level = new ServerLevel(server, server.executor, server.storageSource,
|
||||
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
|
||||
@@ -273,4 +431,14 @@ public final class RuntimeAgeBackend {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private static Holder<NoiseGeneratorSettings> dryCaveSettings(MinecraftServer server) {
|
||||
NoiseGeneratorSettings caves = server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(NoiseGeneratorSettings.CAVES).value();
|
||||
return Holder.direct(new NoiseGeneratorSettings(
|
||||
caves.noiseSettings(), caves.defaultBlock(), Blocks.AIR.defaultBlockState(),
|
||||
caves.noiseRouter(), caves.materialRule(), caves.spawnTarget(),
|
||||
caves.noiseSettings().minY() - 1, caves.disableMobGeneration(), Optional.empty(),
|
||||
caves.useLegacyRandomSource(), caves.debugFunctions()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.InsideBlockEffectApplier;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.EndPortalBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** Non-obtainable starfield portal returning entities to the shared Overworld spawn. */
|
||||
public final class StarFissureBlock extends EndPortalBlock {
|
||||
private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 1.6, 16);
|
||||
|
||||
public StarFissureBlock(BlockBehaviour.Properties properties) { super(properties); }
|
||||
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new StarFissureBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animateTick(BlockState state, Level level, BlockPos pos, net.minecraft.util.RandomSource random) {
|
||||
// Legacy fissures have a starfield, not vanilla End portal smoke.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity,
|
||||
InsideBlockEffectApplier effects, boolean precise) {
|
||||
// Do not inherit End credits or End-dimension travel from the vanilla block.
|
||||
if (entity.canUsePortal(false)) entity.setAsInsidePortal(this, pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
|
||||
if (level.dimension() == Level.OVERWORLD) return null;
|
||||
ServerLevel overworld = level.getServer().overworld();
|
||||
if (overworld == null) return null;
|
||||
BlockPos spawn = overworld.getRespawnData().pos();
|
||||
BlockPos landing = net.dnorberg.rivencraft.age.PortAgeTravel.findFootedDestination(overworld, spawn).orElse(spawn);
|
||||
level.playSound(null, pos, Rivencraft.LINK_FISSURE.get(), SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||
return new TeleportTransition(overworld, Vec3.atBottomCenterOf(landing), entity.getDeltaMovement(),
|
||||
entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.world.Container;
|
||||
@@ -14,12 +17,14 @@ import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
/** Persistent, server-owned inputs and output for the first 26.3 Binder slice. */
|
||||
/** Persistent, server-owned Binder inputs. */
|
||||
public final class PortBookBinderBlockEntity extends BlockEntity implements Container {
|
||||
public static final int COVER = 0;
|
||||
public static final int PANEL = 1;
|
||||
public static final int OUTPUT = 2;
|
||||
public static final int SIZE = 3;
|
||||
public static final int FIRST_PAGE = 3;
|
||||
public static final int PAGE_COUNT = 252;
|
||||
public static final int SIZE = FIRST_PAGE + PAGE_COUNT;
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
private String title = "";
|
||||
|
||||
@@ -61,11 +66,61 @@ public final class PortBookBinderBlockEntity extends BlockEntity implements Cont
|
||||
setChanged();
|
||||
}
|
||||
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
|
||||
return switch (slot) {
|
||||
case COVER -> stack.is(Items.LEATHER);
|
||||
case PANEL -> PageItem.isLinkPanel(stack);
|
||||
default -> false;
|
||||
};
|
||||
if (slot == COVER) return stack.is(Items.LEATHER);
|
||||
if (slot == PANEL) return PageItem.isLinkPanel(stack);
|
||||
return slot >= FIRST_PAGE && slot < SIZE && validSymbolPage(stack);
|
||||
}
|
||||
private static boolean validSymbolPage(ItemStack stack) {
|
||||
return stack.is(Rivencraft.PAGE.get()) && !PageItem.isLinkPanel(stack)
|
||||
&& stack.has(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
}
|
||||
|
||||
public boolean hasSymbolPages() {
|
||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++)
|
||||
if (!items.get(slot).isEmpty()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<Identifier> symbolPages() {
|
||||
List<Identifier> pages = new ArrayList<>();
|
||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++) {
|
||||
Identifier symbol = items.get(slot).get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (symbol != null) pages.add(symbol);
|
||||
}
|
||||
return List.copyOf(pages);
|
||||
}
|
||||
|
||||
public void clearSymbolPages() {
|
||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++) items.set(slot, ItemStack.EMPTY);
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public int nextPageIndex() {
|
||||
for (int index = PAGE_COUNT - 1; index >= 0; index--)
|
||||
if (!items.get(FIRST_PAGE + index).isEmpty()) return index + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean insertPage(int index, ItemStack page) {
|
||||
int end = nextPageIndex();
|
||||
if (index < 0 || index > end || end >= PAGE_COUNT || page.getCount() != 1
|
||||
|| !validSymbolPage(page)) return false;
|
||||
for (int current = end; current > index; current--)
|
||||
items.set(FIRST_PAGE + current, items.get(FIRST_PAGE + current - 1));
|
||||
items.set(FIRST_PAGE + index, page.copy());
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ItemStack takePage(int index) {
|
||||
int end = nextPageIndex();
|
||||
if (index < 0 || index >= end) return ItemStack.EMPTY;
|
||||
ItemStack removed = items.get(FIRST_PAGE + index);
|
||||
for (int current = index; current < end - 1; current++)
|
||||
items.set(FIRST_PAGE + current, items.get(FIRST_PAGE + current + 1));
|
||||
items.set(FIRST_PAGE + end - 1, ItemStack.EMPTY);
|
||||
setChanged();
|
||||
return removed;
|
||||
}
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return level != null && level.getBlockEntity(worldPosition) == this
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class StarFissureBlockEntity extends TheEndPortalBlockEntity {
|
||||
public StarFissureBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), pos, state);
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,36 @@ import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
|
||||
import net.neoforged.neoforge.client.event.RegisterSpecialModelRendererEvent;
|
||||
import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
|
||||
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class PortBookBinderClientEvents {
|
||||
private PortBookBinderClientEvents() {}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerBlockEntityRenderers(
|
||||
net.neoforged.neoforge.client.event.EntityRenderersEvent.RegisterRenderers event) {
|
||||
event.registerBlockEntityRenderer(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), context -> new StarFissureRenderer());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerScreens(RegisterMenuScreensEvent event) {
|
||||
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
|
||||
event.register(Rivencraft.INK_MIXER_MENU.get(), InkMixerScreen::new);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerPageRenderer(RegisterSpecialModelRendererEvent event) {
|
||||
event.register(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "symbol_page"),
|
||||
PortSymbolPageRenderer.Unbaked.MAP_CODEC);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerPageTextureReload(AddClientReloadListenersEvent event) {
|
||||
event.addListener(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "symbol_page_textures"),
|
||||
(ResourceManagerReloadListener) manager -> PortSymbolPageTextures.clear());
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
import net.dnorberg.rivencraft.network.AssembleBinderPayload;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.network.SetBinderTitlePayload;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
@@ -28,12 +30,19 @@ public final class PortBookBinderScreen extends AbstractContainerScreen<PortBook
|
||||
private static final int PAGE_Y = 48;
|
||||
private static final int PAGE_WIDTH = 26;
|
||||
private static final int PAGE_HEIGHT = 34;
|
||||
private static final int PAGE_STEP = 28;
|
||||
private static final int VISIBLE_PAGES = 5;
|
||||
private static final int STRIP_X = 7;
|
||||
private static final int STRIP_Y = 45;
|
||||
private static final int STRIP_WIDTH = 162;
|
||||
private static final int STRIP_HEIGHT = 40;
|
||||
private static final int MISSING_PANEL_X = 27;
|
||||
private static final int MISSING_PANEL_Y = 26;
|
||||
private static final int MISSING_PANEL_SIZE = 16;
|
||||
private static final ItemStack MISSING_PANEL_ICON = PageItem.createLinkPanelPage();
|
||||
private EditBox titleInput;
|
||||
private boolean stripClickPendingRelease;
|
||||
private int firstVisible;
|
||||
|
||||
public PortBookBinderScreen(PortBookBinderMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 176, 181);
|
||||
@@ -65,19 +74,43 @@ public final class PortBookBinderScreen extends AbstractContainerScreen<PortBook
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left, top, 0, 0,
|
||||
imageWidth, imageHeight, 256, 256);
|
||||
graphics.fill(left + 7, top + 9, left + 123, top + 23, 0xFF000000);
|
||||
// The strip grows only when a page is present or the player is carrying
|
||||
// a page to insert, just as the 1.21.1 Binder does.
|
||||
boolean panelPresent = !menu.getSlot(1).getItem().isEmpty();
|
||||
if (panelPresent || PageItem.isLinkPanel(menu.getCarried())) {
|
||||
int x = left + 16;
|
||||
firstVisible = Math.min(firstVisible, maxFirstVisible());
|
||||
int visibleCount = Math.min(VISIBLE_PAGES, visiblePageCount() - firstVisible);
|
||||
for (int visible = 0; visible < visibleCount; visible++) {
|
||||
int x = left + 16 + visible * PAGE_STEP;
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, PAGE_ATLAS,
|
||||
x, top + PAGE_Y, 156, 0, PAGE_WIDTH, PAGE_HEIGHT, 256, 256);
|
||||
if (!panelPresent)
|
||||
graphics.fill(x, top + PAGE_Y, x + PAGE_WIDTH, top + PAGE_Y + PAGE_HEIGHT,
|
||||
0xAA000000);
|
||||
else graphics.fill(x + 5, top + PAGE_Y + 6, x + 22, top + PAGE_Y + 17,
|
||||
0xFF000000);
|
||||
}
|
||||
// GUI extraction batches textures within a stratum. The glyphs need a
|
||||
// later stratum so another atlas page cannot cover their transparent quad.
|
||||
graphics.nextStratum();
|
||||
for (int visible = 0; visible < visibleCount; visible++) {
|
||||
int index = firstVisible + visible;
|
||||
int x = left + 16 + visible * PAGE_STEP;
|
||||
int y = top + PAGE_Y;
|
||||
ItemStack page = pageAt(index);
|
||||
if (page.isEmpty()) {
|
||||
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0xAA000000);
|
||||
} else if (PageItem.isLinkPanel(page)) {
|
||||
graphics.fill(x + 5, y + 6, x + 22, y + 17, 0xFF000000);
|
||||
} else {
|
||||
// Render the full-size transparent glyph through the working
|
||||
// item pipeline, without its small parchment-backed icon.
|
||||
ItemStack glyphOnly = page.copy();
|
||||
glyphOnly.set(Rivencraft.PAGE_GLYPH_ONLY.get(), true);
|
||||
graphics.pose().pushMatrix();
|
||||
graphics.pose().translate(x + 1, y + 5);
|
||||
graphics.pose().scale(1.5F, 1.5F);
|
||||
graphics.fakeItem(glyphOnly, 0, 0);
|
||||
graphics.pose().popMatrix();
|
||||
}
|
||||
}
|
||||
graphics.fill(left + STRIP_X, top + STRIP_Y, left + STRIP_X + 8,
|
||||
top + STRIP_Y + STRIP_HEIGHT, firstVisible == 0 ? 0x33000000 : 0xAA000000);
|
||||
graphics.fill(left + STRIP_X + STRIP_WIDTH - 8, top + STRIP_Y,
|
||||
left + STRIP_X + STRIP_WIDTH, top + STRIP_Y + STRIP_HEIGHT,
|
||||
firstVisible >= maxFirstVisible() ? 0x33000000 : 0xAA000000);
|
||||
boolean panelPresent = !menu.getSlot(PortBookBinderBlockEntity.PANEL).getItem().isEmpty();
|
||||
if (titleInput == null || titleInput.getValue().isBlank()) {
|
||||
graphics.fill(left + 7, top + 9, left + 123, top + 10, 0xFFFF3030);
|
||||
graphics.fill(left + 7, top + 22, left + 123, top + 23, 0xFFFF3030);
|
||||
@@ -92,7 +125,10 @@ public final class PortBookBinderScreen extends AbstractContainerScreen<PortBook
|
||||
top + MISSING_PANEL_Y + MISSING_PANEL_SIZE,
|
||||
(alpha << 24) | 0x00B02020);
|
||||
}
|
||||
if (menu.getSlot(2).getItem().isEmpty() && !menu.getSlot(0).getItem().isEmpty()
|
||||
if (hasUnsupportedPages()) graphics.text(font,
|
||||
Component.translatable("screen.rivencraft.book_binder.authored_pending"),
|
||||
left + 75, top + 31, 0xFFB84B42, false);
|
||||
if (!hasUnsupportedPages() && menu.getSlot(2).getItem().isEmpty() && !menu.getSlot(0).getItem().isEmpty()
|
||||
&& !menu.getSlot(1).getItem().isEmpty() && titleInput != null
|
||||
&& !titleInput.getValue().isBlank()) {
|
||||
graphics.fakeItem(new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()), left + 152, top + 27);
|
||||
@@ -112,32 +148,74 @@ public final class PortBookBinderScreen extends AbstractContainerScreen<PortBook
|
||||
&& menu.getSlot(1).getItem().isEmpty()) {
|
||||
graphics.setTooltipForNextFrame(font,
|
||||
Component.translatable("screen.rivencraft.book_binder.missing_panel"), mouseX, mouseY);
|
||||
} else if (x >= 16 && x < 16 + PAGE_WIDTH && y >= PAGE_Y && y < PAGE_Y + PAGE_HEIGHT
|
||||
&& !menu.getSlot(1).getItem().isEmpty()) {
|
||||
graphics.setTooltipForNextFrame(font, menu.getSlot(1).getItem(), mouseX, mouseY);
|
||||
} else {
|
||||
int index = pageIndexAt(mouseX, mouseY);
|
||||
if (index >= 0 && !pageAt(index).isEmpty())
|
||||
graphics.setTooltipForNextFrame(font, pageAt(index), mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack pageAt(int index) {
|
||||
return menu.getSlot(index == 0 ? PortBookBinderBlockEntity.PANEL
|
||||
: PortBookBinderBlockEntity.FIRST_PAGE + index - 1).getItem();
|
||||
}
|
||||
|
||||
private int displayedPages() {
|
||||
for (int index = PortBookBinderBlockEntity.PAGE_COUNT; index >= 1; index--)
|
||||
if (!pageAt(index).isEmpty()) return index + 1;
|
||||
return pageAt(0).isEmpty() ? 0 : 1;
|
||||
}
|
||||
|
||||
private boolean hasUnsupportedPages() {
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
for (int index = 1; index <= PortBookBinderBlockEntity.PAGE_COUNT; index++) {
|
||||
Identifier id = pageAt(index).get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (id != null) pages.add(id);
|
||||
}
|
||||
return !PortAuthoredWriting.supports(pages);
|
||||
}
|
||||
|
||||
private int visiblePageCount() {
|
||||
return Math.min(PortBookBinderBlockEntity.PAGE_COUNT + 1,
|
||||
displayedPages() + (menu.getCarried().isEmpty() ? 0 : 1));
|
||||
}
|
||||
|
||||
private int maxFirstVisible() { return Math.max(0, visiblePageCount() - VISIBLE_PAGES); }
|
||||
|
||||
private int pageIndexAt(double mouseX, double mouseY) {
|
||||
int x = (int) mouseX - leftPos;
|
||||
int y = (int) mouseY - topPos;
|
||||
if (x < 16 || x >= STRIP_X + STRIP_WIDTH - 8 || y < STRIP_Y || y >= STRIP_Y + STRIP_HEIGHT)
|
||||
return -1;
|
||||
int visible = (x - 16) / PAGE_STEP;
|
||||
return visible < VISIBLE_PAGES ? firstVisible + visible : -1;
|
||||
}
|
||||
|
||||
@Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
int left = (width - imageWidth) / 2;
|
||||
int top = (height - imageHeight) / 2;
|
||||
if ((event.button() == InputConstants.MOUSE_BUTTON_LEFT
|
||||
|| event.button() == InputConstants.MOUSE_BUTTON_RIGHT)
|
||||
&& event.x() >= left + 16 && event.x() < left + 16 + PAGE_WIDTH
|
||||
&& event.y() >= top + PAGE_Y && event.y() < top + PAGE_Y + PAGE_HEIGHT) {
|
||||
boolean hasPanel = !menu.getSlot(1).getItem().isEmpty();
|
||||
if (hasPanel && menu.getCarried().isEmpty()
|
||||
|| !hasPanel && PageItem.isLinkPanel(menu.getCarried())) {
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
|
||||
PortBookBinderMenu.STRIP_CLICK_PANEL);
|
||||
stripClickPendingRelease = true;
|
||||
return true;
|
||||
&& event.x() >= left + STRIP_X && event.x() < left + STRIP_X + STRIP_WIDTH
|
||||
&& event.y() >= top + STRIP_Y && event.y() < top + STRIP_Y + STRIP_HEIGHT) {
|
||||
stripClickPendingRelease = true;
|
||||
if (event.x() < left + STRIP_X + 8) firstVisible = Math.max(0, firstVisible - 1);
|
||||
else if (event.x() >= left + STRIP_X + STRIP_WIDTH - 8)
|
||||
firstVisible = Math.min(maxFirstVisible(), firstVisible + 1);
|
||||
else {
|
||||
int index = pageIndexAt(event.x(), event.y());
|
||||
if (index >= 0 && index <= displayedPages()
|
||||
&& (index < displayedPages() || !menu.getCarried().isEmpty()))
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
|
||||
PortBookBinderMenu.STRIP_CLICK_BASE + index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.button() == InputConstants.MOUSE_BUTTON_LEFT
|
||||
&& event.x() >= left + 151 && event.x() < left + 170
|
||||
&& event.y() >= top + 26 && event.y() < top + 45
|
||||
&& menu.getSlot(2).getItem().isEmpty() && menu.getCarried().isEmpty()) {
|
||||
&& !hasUnsupportedPages() && menu.getSlot(2).getItem().isEmpty()
|
||||
&& menu.getCarried().isEmpty()) {
|
||||
String name = titleInput.getValue().strip();
|
||||
if (!name.isEmpty()) ClientPacketDistributor.sendToServer(new AssembleBinderPayload(name));
|
||||
return true;
|
||||
@@ -153,6 +231,15 @@ public final class PortBookBinderScreen extends AbstractContainerScreen<PortBook
|
||||
return super.mouseReleased(event);
|
||||
}
|
||||
|
||||
@Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
if (mouseX >= leftPos + STRIP_X && mouseX < leftPos + STRIP_X + STRIP_WIDTH
|
||||
&& mouseY >= topPos + STRIP_Y && mouseY < topPos + STRIP_Y + STRIP_HEIGHT) {
|
||||
firstVisible = Math.clamp(firstVisible - (int) Math.signum(scrollY), 0, maxFirstVisible());
|
||||
return true;
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
@Override public boolean keyPressed(KeyEvent event) {
|
||||
if (titleInput != null && titleInput.isFocused()) {
|
||||
if (event.isEscape()) {
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
/** Flat item geometry textured with a page composed from its symbol stack. */
|
||||
public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSymbolPageRenderer.PageArt> {
|
||||
@Override
|
||||
public PageArt extractArgument(ItemStack stack) {
|
||||
Identifier symbol = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
return symbol == null ? null : new PageArt(symbol,
|
||||
stack.getOrDefault(Rivencraft.PAGE_GLYPH_ONLY.get(), false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(PageArt art, PoseStack pose, SubmitNodeCollector collector,
|
||||
int light, int overlay, boolean hasFoil, int outlineColor) {
|
||||
if (art == null) return;
|
||||
Identifier texture = art.glyphOnly()
|
||||
? PortSymbolPageTextures.forPage(art.symbol())
|
||||
: PortSymbolPageTextures.forItem(art.symbol());
|
||||
collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture),
|
||||
(matrix, vertices) -> {
|
||||
vertex(vertices, matrix, 0, 0, 0, 1, light, overlay);
|
||||
vertex(vertices, matrix, 1, 0, 1, 1, light, overlay);
|
||||
vertex(vertices, matrix, 1, 1, 1, 0, light, overlay);
|
||||
vertex(vertices, matrix, 0, 1, 0, 0, light, overlay);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getExtents(Consumer<org.joml.Vector3fc> extents) {
|
||||
extents.accept(new Vector3f(0, 0, 0.5F));
|
||||
extents.accept(new Vector3f(1, 1, 0.5F));
|
||||
}
|
||||
|
||||
private static void vertex(VertexConsumer vertices, PoseStack.Pose pose,
|
||||
float x, float y, float u, float v, int light, int overlay) {
|
||||
vertices.addVertex(pose, x, y, 0.5F).setColor(255, 255, 255, 255)
|
||||
.setUv(u, v).setOverlay(overlay).setLight(light).setNormal(pose, 0, 0, 1);
|
||||
}
|
||||
|
||||
public record PageArt(Identifier symbol, boolean glyphOnly) {}
|
||||
|
||||
public record Unbaked() implements SpecialModelRenderer.Unbaked<PageArt> {
|
||||
public static final Unbaked INSTANCE = new Unbaked();
|
||||
public static final MapCodec<Unbaked> MAP_CODEC = MapCodec.unit(INSTANCE);
|
||||
|
||||
@Override public MapCodec<Unbaked> type() { return MAP_CODEC; }
|
||||
@Override public SpecialModelRenderer<PageArt> bake(BakingContext context) {
|
||||
return new PortSymbolPageRenderer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolNumerals;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.util.ARGB;
|
||||
|
||||
/** The 1.21.1 Legacy-style glyph compositor adapted to 26.3's texture API. */
|
||||
public final class PortSymbolPageTextures {
|
||||
private static final int SIZE = 128;
|
||||
private static final int GLYPH = 51;
|
||||
private static final int ITEM_SYMBOL_SIZE = 15;
|
||||
private static final int NUMBER_GLYPH_SIZE = 64;
|
||||
private static final int CACHE_LIMIT = 256;
|
||||
private static final Identifier PAGE = texture("item/page");
|
||||
private static final Identifier COMPONENTS = texture("gui/symbolcomponents");
|
||||
private static final Identifier FALLBACK = texture("item/page_symbol");
|
||||
private static final Map<String, Identifier> CACHE = new LinkedHashMap<>(16, .75F, true);
|
||||
private static final Map<String, int[]> WORD_CACHE = new LinkedHashMap<>(16, .75F, true);
|
||||
private static NativeImage page;
|
||||
private static NativeImage components;
|
||||
private static int nextId;
|
||||
|
||||
// WordData from Mystcraft Legacy 0.13.7.06, matching the 1.21.1 compositor.
|
||||
private static final Map<String, int[]> WORDS = Map.ofEntries(
|
||||
Map.entry("Terrain", new int[] {6,10,12,13,16,19,24,25,27}),
|
||||
Map.entry("Form", new int[] {4,6,8,9,10,11,13,14,16,23}),
|
||||
Map.entry("Tradition", new int[] {4,6,7,8,9,14,17,18,20}),
|
||||
Map.entry("Flow", new int[] {4,7,12,13,17,18}),
|
||||
Map.entry("Constraint", new int[] {5,6,9,11,13,20,22,23}),
|
||||
Map.entry("Nature", new int[] {5,6,8,10,11,12,15,16,17,22}),
|
||||
Map.entry("Sustain", new int[] {6,16,19,20,21,23}),
|
||||
Map.entry("Ethereal", new int[] {4,6,8,13,14,16,18,21,23}),
|
||||
Map.entry("Dynamic", new int[] {4,5,6,12,13,17,18,22,23}),
|
||||
Map.entry("Cycle", new int[] {4,5,7,16,17,18}),
|
||||
Map.entry("Balance", new int[] {4,6,9,10,16,19}),
|
||||
Map.entry("Transform", new int[] {4,5,6,8,11,14,18,20,21}),
|
||||
Map.entry("Time", new int[] {4,5,7,12,13,19,20,21,22,23}),
|
||||
Map.entry("System", new int[] {10,12,14,15,16,20,21,22}),
|
||||
Map.entry("Void", new int[] {4,5,6,16,17,18,23})
|
||||
);
|
||||
|
||||
private PortSymbolPageTextures() {}
|
||||
|
||||
public static Identifier forItem(Identifier id) {
|
||||
return forSymbol(id, true);
|
||||
}
|
||||
|
||||
/** Transparent, full-size glyphs layered over the Binder's page atlas. */
|
||||
public static Identifier forPage(Identifier id) {
|
||||
return forSymbol(id, false);
|
||||
}
|
||||
|
||||
private static Identifier forSymbol(Identifier id, boolean item) {
|
||||
PortSymbolPages.Entry entry = PortSymbolPages.find(id);
|
||||
if (entry == null) return FALLBACK;
|
||||
String poem = entry.poem();
|
||||
if (!poem.matches("[A-Za-z]+(?: [A-Za-z]+){3}")) poem = "";
|
||||
String key = id + "|" + poem + (item ? "|item" : "|page");
|
||||
Identifier cached = CACHE.get(key);
|
||||
if (cached != null) return cached;
|
||||
try {
|
||||
loadSources(Minecraft.getInstance().getResourceManager());
|
||||
NativeImage image = compose(id, poem, item);
|
||||
Identifier location = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"generated/page_" + nextId++);
|
||||
Minecraft.getInstance().getTextureManager().register(location,
|
||||
new DynamicTexture(() -> "Rivencraft symbol page " + id, image));
|
||||
CACHE.put(key, location);
|
||||
if (CACHE.size() > CACHE_LIMIT) {
|
||||
String eldest = CACHE.keySet().iterator().next();
|
||||
Minecraft.getInstance().getTextureManager().release(CACHE.remove(eldest));
|
||||
}
|
||||
return location;
|
||||
} catch (IOException exception) {
|
||||
Rivencraft.LOGGER.error("Could not compose symbol page {}", id, exception);
|
||||
return FALLBACK;
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
var textures = Minecraft.getInstance().getTextureManager();
|
||||
CACHE.values().forEach(textures::release);
|
||||
CACHE.clear();
|
||||
WORD_CACHE.clear();
|
||||
if (page != null) page.close();
|
||||
if (components != null) components.close();
|
||||
page = null;
|
||||
components = null;
|
||||
}
|
||||
|
||||
private static void loadSources(ResourceManager manager) throws IOException {
|
||||
if (page != null && components != null) return;
|
||||
try (InputStream pageStream = manager.getResourceOrThrow(PAGE).open();
|
||||
InputStream componentsStream = manager.getResourceOrThrow(COMPONENTS).open()) {
|
||||
page = NativeImage.read(pageStream);
|
||||
components = NativeImage.read(componentsStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeImage compose(Identifier id, String poem, boolean item) {
|
||||
NativeImage image = new NativeImage(SIZE, SIZE, true);
|
||||
if (item) {
|
||||
for (int y = 0; y < SIZE; y++) for (int x = 0; x < SIZE; x++) {
|
||||
image.setPixel(x, y, page.getPixel(x * page.getWidth() / SIZE,
|
||||
y * page.getHeight() / SIZE));
|
||||
}
|
||||
}
|
||||
String numberPath = id.getPath().startsWith("number/")
|
||||
? id.getPath().substring("number/".length()) : "";
|
||||
if (id.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& numberPath.matches("(?:[0-9]|1[0-9]|2[0-4])")) {
|
||||
int digit = Integer.parseInt(numberPath);
|
||||
int glyphSize = item ? NUMBER_GLYPH_SIZE : GLYPH;
|
||||
int origin = item ? (SIZE - glyphSize) / 2 : 38;
|
||||
for (int component : PortSymbolNumerals.componentsForDigit(digit))
|
||||
blendComponent(image, component, origin, origin, glyphSize);
|
||||
return image;
|
||||
}
|
||||
String[] words = poem.isBlank() ? generatedWords(id) : poem.split(" ");
|
||||
int[] offsetsX = {38, 77, 38, 0};
|
||||
int[] offsetsY = {0, 38, 77, 38};
|
||||
int glyphSize = item ? GLYPH * ITEM_SYMBOL_SIZE / 16 : GLYPH;
|
||||
for (int word = 0; word < 4; word++) {
|
||||
int x = item ? scaleForItem(offsetsX[word]) : offsetsX[word];
|
||||
int y = item ? scaleForItem(offsetsY[word]) : offsetsY[word];
|
||||
for (int component : glyphs(words[word]))
|
||||
blendComponent(image, component, x, y, glyphSize);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static void blendComponent(NativeImage image, int component, int x0, int y0, int size) {
|
||||
int cellX = (component % 8) * 64;
|
||||
int cellY = (component / 8) * 64;
|
||||
for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) {
|
||||
int sourceX = cellX + x * 64 / size;
|
||||
int sourceY = cellY + y * 64 / size;
|
||||
// One source-pixel of coverage keeps the thin Legacy strokes legible
|
||||
// when Minecraft shrinks this 128px composite to a 16px item icon.
|
||||
int alpha = ARGB.alpha(components.getPixel(sourceX, sourceY));
|
||||
if (sourceX > cellX) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX - 1, sourceY)));
|
||||
if (sourceX < cellX + 63) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX + 1, sourceY)));
|
||||
if (sourceY > cellY) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX, sourceY - 1)));
|
||||
if (sourceY < cellY + 63) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX, sourceY + 1)));
|
||||
if (alpha != 0) image.setPixel(x0 + x, y0 + y,
|
||||
ARGB.alphaBlend(image.getPixel(x0 + x, y0 + y),
|
||||
(alpha << 24) | 0x001A212B));
|
||||
}
|
||||
}
|
||||
|
||||
private static int scaleForItem(int coordinate) {
|
||||
return SIZE / 2 + Math.round((coordinate - SIZE / 2F) * ITEM_SYMBOL_SIZE / 16F);
|
||||
}
|
||||
|
||||
private static String[] generatedWords(Identifier id) {
|
||||
String stem = id.toString();
|
||||
return new String[] {stem + 'a', stem + 'b', stem + 'c', stem + 'd'};
|
||||
}
|
||||
|
||||
private static int[] glyphs(String word) {
|
||||
int[] known = WORDS.get(word);
|
||||
if (known != null) return known;
|
||||
String key = word.toLowerCase(Locale.ROOT);
|
||||
int[] cached = WORD_CACHE.get(key);
|
||||
if (cached != null) return cached;
|
||||
Random random = new Random(key.hashCode());
|
||||
int[] result = new int[random.nextInt(10) + 3];
|
||||
for (int i = 0; i < result.length; i++) result[i] = random.nextInt(20) + 4;
|
||||
WORD_CACHE.put(key, result);
|
||||
if (WORD_CACHE.size() > CACHE_LIMIT) WORD_CACHE.remove(WORD_CACHE.keySet().iterator().next());
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Identifier texture(String path) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/" + path + ".png");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.AbstractEndPortalRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.state.EndPortalRenderState;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.core.Direction;
|
||||
|
||||
/** Retains 1.21.1's UP-at-zero/DOWN-at-one planes with the modern starfield renderer. */
|
||||
public final class StarFissureRenderer extends AbstractEndPortalRenderer<StarFissureBlockEntity, EndPortalRenderState> {
|
||||
@Override
|
||||
public EndPortalRenderState createRenderState() { return new EndPortalRenderState(); }
|
||||
|
||||
@Override
|
||||
public void submit(EndPortalRenderState state, PoseStack pose, SubmitNodeCollector collector,
|
||||
CameraRenderState camera) {
|
||||
if (state.facesToShow.contains(Direction.UP)) {
|
||||
pose.pushPose();
|
||||
pose.translate(0, -1, 0);
|
||||
submitCube(List.of(Direction.UP), RenderTypes.endPortal(), pose, collector);
|
||||
pose.popPose();
|
||||
}
|
||||
if (state.facesToShow.contains(Direction.DOWN)) {
|
||||
pose.pushPose();
|
||||
pose.translate(0, 1, 0);
|
||||
submitCube(List.of(Direction.DOWN), RenderTypes.endPortal(), pose, collector);
|
||||
pose.popPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,11 @@ package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
@@ -30,10 +33,28 @@ public final class PageItem extends Item {
|
||||
return stack.is(Rivencraft.PAGE.get()) && stack.getOrDefault(Rivencraft.LINK_PANEL_PAGE.get(), false);
|
||||
}
|
||||
|
||||
public static ItemStack createSymbolPage(Identifier id) {
|
||||
if (PortSymbolPages.find(id) == null)
|
||||
throw new IllegalArgumentException("Unknown bundled symbol " + id);
|
||||
ItemStack stack = new ItemStack(Rivencraft.PAGE.get());
|
||||
stack.set(Rivencraft.SYMBOL_PAGE_ID.get(), id);
|
||||
return stack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getName(ItemStack stack) {
|
||||
Component customName = stack.get(DataComponents.CUSTOM_NAME);
|
||||
if (customName != null) return customName;
|
||||
Identifier symbolId = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (symbolId != null) {
|
||||
PortSymbolPages.Entry entry = PortSymbolPages.find(symbolId);
|
||||
if (entry != null) {
|
||||
Component name = Component.translatableWithFallback(
|
||||
entry.nameKey(), PortSymbolPages.fallbackName(symbolId));
|
||||
return isCorrupted(symbolId) ? name.copy().withStyle(ChatFormatting.OBFUSCATED) : name;
|
||||
}
|
||||
return Component.translatableWithFallback("item.rivencraft.page.unresolved", "Unknown Symbol Page");
|
||||
}
|
||||
return Component.translatable(isLinkPanel(stack)
|
||||
? "item.rivencraft.page.link_panel" : "item.rivencraft.page.blank");
|
||||
}
|
||||
@@ -41,6 +62,20 @@ public final class PageItem extends Item {
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
Identifier symbolId = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (symbolId != null) {
|
||||
PortSymbolPages.Entry entry = PortSymbolPages.find(symbolId);
|
||||
if (entry != null) {
|
||||
Component description = Component.translatableWithFallback(
|
||||
entry.descriptionKey(), PortSymbolPages.fallbackName(symbolId));
|
||||
tooltip.accept(isCorrupted(symbolId)
|
||||
? description.copy().withStyle(ChatFormatting.GRAY, ChatFormatting.OBFUSCATED)
|
||||
: description.copy().withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
tooltip.accept(Component.translatable("item.rivencraft.page.symbol.port_status")
|
||||
.withStyle(ChatFormatting.DARK_GRAY));
|
||||
return;
|
||||
}
|
||||
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
if (properties.has(LinkProperties.DISARM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm"));
|
||||
@@ -52,4 +87,8 @@ public final class PageItem extends Item {
|
||||
? "item.rivencraft.page.link_panel.port_status"
|
||||
: "item.rivencraft.page.blank.port_status"));
|
||||
}
|
||||
|
||||
private static boolean isCorrupted(Identifier id) {
|
||||
return id.getNamespace().equals(Rivencraft.MOD_ID) && id.getPath().startsWith("corrupted/");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -19,7 +20,7 @@ import net.minecraft.world.item.Items;
|
||||
|
||||
/** Binder inputs persist in the block; assembling places the finished book on the cursor. */
|
||||
public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
public static final int STRIP_CLICK_PANEL = 3000;
|
||||
public static final int STRIP_CLICK_BASE = 3000;
|
||||
private final Container binder;
|
||||
private final PortBookBinderBlockEntity blockEntity;
|
||||
private final String initialTitle;
|
||||
@@ -48,6 +49,14 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.OUTPUT, 152, 27) {
|
||||
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||
});
|
||||
for (int page = 0; page < PortBookBinderBlockEntity.PAGE_COUNT; page++) {
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.FIRST_PAGE + page, -1000, -1000) {
|
||||
@Override public boolean mayPlace(ItemStack stack) {
|
||||
return binder.canPlaceItem(getContainerSlot(), stack);
|
||||
}
|
||||
@Override public int getMaxStackSize() { return 1; }
|
||||
});
|
||||
}
|
||||
addStandardInventorySlots(inventory, 8, 99);
|
||||
}
|
||||
|
||||
@@ -58,19 +67,26 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
public String title() { return initialTitle; }
|
||||
|
||||
@Override public boolean clickMenuButton(Player player, int id) {
|
||||
if (id != STRIP_CLICK_PANEL || !stillValid(player)) return false;
|
||||
if (id < STRIP_CLICK_BASE || id > STRIP_CLICK_BASE + PortBookBinderBlockEntity.PAGE_COUNT
|
||||
|| !stillValid(player)) return false;
|
||||
if (player.level().isClientSide()) return true;
|
||||
int index = id - STRIP_CLICK_BASE;
|
||||
ItemStack carried = getCarried();
|
||||
if (carried.isEmpty()) {
|
||||
ItemStack panel = binder.removeItem(PortBookBinderBlockEntity.PANEL, 1);
|
||||
if (panel.isEmpty()) return false;
|
||||
setCarried(panel);
|
||||
} else {
|
||||
if (!PageItem.isLinkPanel(carried)
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty()) return false;
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, carried.copyWithCount(1));
|
||||
if (!carried.isEmpty()) {
|
||||
if (index == 0) {
|
||||
if (!PageItem.isLinkPanel(carried)
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty()) return false;
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, carried.copyWithCount(1));
|
||||
} else if (!blockEntity.insertPage(index - 1, carried.copyWithCount(1))) return false;
|
||||
ItemStack remainder = carried.copy();
|
||||
remainder.shrink(1);
|
||||
setCarried(remainder);
|
||||
} else {
|
||||
ItemStack removed = index == 0
|
||||
? binder.removeItem(PortBookBinderBlockEntity.PANEL, 1)
|
||||
: blockEntity.takePage(index - 1);
|
||||
if (removed.isEmpty()) return false;
|
||||
setCarried(removed);
|
||||
}
|
||||
broadcastChanges();
|
||||
return true;
|
||||
@@ -84,22 +100,26 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
public void assemble(ServerPlayer player, String title) {
|
||||
if (!stillValid(player) || title.isBlank() || title.length() > PortBookNetworking.MAX_BOOK_TITLE_LENGTH
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.OUTPUT).isEmpty()
|
||||
|| !PortAuthoredWriting.supports(blockEntity.symbolPages())
|
||||
|| !getCarried().isEmpty()) return;
|
||||
ItemStack cover = binder.getItem(PortBookBinderBlockEntity.COVER);
|
||||
ItemStack panel = binder.getItem(PortBookBinderBlockEntity.PANEL);
|
||||
if (!cover.is(Items.LEATHER) || !PageItem.isLinkPanel(panel)) return;
|
||||
blockEntity.setTitle(title);
|
||||
ItemStack book = new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
var id = PortBookNetworking.createAge(player, title);
|
||||
var id = PortBookNetworking.createAge(player, title, blockEntity.symbolPages());
|
||||
if (id == null) return;
|
||||
book.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
if (blockEntity.hasSymbolPages())
|
||||
book.set(Rivencraft.BOOK_SYMBOLS.get(), blockEntity.symbolPages());
|
||||
var modifiers = panel.get(Rivencraft.LINK_PROPERTIES.get());
|
||||
if (modifiers != null) book.set(Rivencraft.LINK_PROPERTIES.get(), modifiers);
|
||||
cover.shrink(1);
|
||||
panel.shrink(1);
|
||||
binder.setItem(PortBookBinderBlockEntity.COVER, cover);
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, panel);
|
||||
blockEntity.clearSymbolPages();
|
||||
setCarried(book);
|
||||
broadcastChanges();
|
||||
}
|
||||
@@ -108,6 +128,15 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY;
|
||||
Slot slot = slots.get(index);
|
||||
if (!slot.hasItem()) return ItemStack.EMPTY;
|
||||
if (index >= PortBookBinderBlockEntity.FIRST_PAGE && index < PortBookBinderBlockEntity.SIZE) {
|
||||
ItemStack page = slot.getItem().copy();
|
||||
ItemStack remainder = page.copy();
|
||||
if (!moveItemStackTo(remainder, PortBookBinderBlockEntity.SIZE, slots.size(), true)
|
||||
|| !remainder.isEmpty()) return ItemStack.EMPTY;
|
||||
blockEntity.takePage(index - PortBookBinderBlockEntity.FIRST_PAGE);
|
||||
broadcastChanges();
|
||||
return page;
|
||||
}
|
||||
ItemStack stack = slot.getItem();
|
||||
ItemStack original = stack.copy();
|
||||
boolean moved;
|
||||
@@ -119,6 +148,9 @@ public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
} else if (PageItem.isLinkPanel(stack)) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.PANEL,
|
||||
PortBookBinderBlockEntity.PANEL + 1, false);
|
||||
} else if (stack.is(Rivencraft.PAGE.get()) && stack.has(Rivencraft.SYMBOL_PAGE_ID.get())) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.FIRST_PAGE,
|
||||
PortBookBinderBlockEntity.SIZE, false);
|
||||
} else return ItemStack.EMPTY;
|
||||
if (!moved) return ItemStack.EMPTY;
|
||||
if (stack.isEmpty()) slot.setByPlayer(ItemStack.EMPTY);
|
||||
|
||||
@@ -2,11 +2,14 @@ package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTravel;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
@@ -19,6 +22,7 @@ import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
|
||||
import java.util.Locale;
|
||||
import java.util.List;
|
||||
|
||||
/** Server-authoritative handheld Linking Book binding and travel. */
|
||||
public final class PortBookNetworking {
|
||||
@@ -99,7 +103,18 @@ public final class PortBookNetworking {
|
||||
|
||||
/** Shared name resolution and Age creation for handheld books and the Binder. */
|
||||
public static Identifier createAge(ServerPlayer player, String title) {
|
||||
return createAge(player, title, List.of());
|
||||
}
|
||||
|
||||
public static Identifier createAge(ServerPlayer player, String title, List<Identifier> pages) {
|
||||
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
|
||||
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
|
||||
try {
|
||||
biomePlan = PortAuthoredWriting.biomePlan(pages, player.registryAccess());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
|
||||
return null;
|
||||
}
|
||||
String base = title.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9._-]+", "_")
|
||||
.replaceAll("^[_\\.-]+|[_\\.-]+$", "");
|
||||
if (base.isEmpty()) base = "age";
|
||||
@@ -112,7 +127,19 @@ public final class PortBookNetworking {
|
||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
}
|
||||
try {
|
||||
RuntimeAgeBackend.create(server, id, player.getRandom().nextLong());
|
||||
long seed = player.getRandom().nextLong();
|
||||
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
|
||||
if (pages.isEmpty()) {
|
||||
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
|
||||
terrain = random.terrain();
|
||||
biomePlan = random.biomes();
|
||||
}
|
||||
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||
PortAuthoredWriting.rockPlan(pages, seed),
|
||||
PortAuthoredWriting.vegetationPlan(pages),
|
||||
PortAuthoredWriting.dungeonCount(pages),
|
||||
PortAuthoredWriting.lakePlan(pages, seed),
|
||||
PortAuthoredWriting.terrainFeaturePlan(pages, seed));
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||
return null;
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.dnorberg.rivencraft.registry;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
@@ -47,6 +48,18 @@ public final class ModCreativeTabs {
|
||||
}
|
||||
})
|
||||
.build());
|
||||
for (String category : PortSymbolPages.categories()) {
|
||||
String path = category.substring(category.indexOf(':') + 1);
|
||||
TABS.register("rivencraft_pages_" + path, () -> CreativeModeTab.builder()
|
||||
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + path,
|
||||
"Rivencraft " + PortSymbolPages.fallbackName(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path))
|
||||
+ " Pages"))
|
||||
.icon(() -> PortSymbolPages.icon(category))
|
||||
.displayItems((parameters, output) ->
|
||||
PortSymbolPages.pages(category, parameters.holders()).forEach(output::accept))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private ModCreativeTabs() {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.PortScaledBiomeSource;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
/** Serializes the custom biome resolver in saved 26.3 Age level stems. */
|
||||
public final class PortBiomeSources {
|
||||
private static final DeferredRegister<MapCodec<? extends BiomeSource>> SOURCES =
|
||||
DeferredRegister.create(Registries.BIOME_SOURCE, Rivencraft.MOD_ID);
|
||||
|
||||
static {
|
||||
SOURCES.register("scaled", () -> PortScaledBiomeSource.CODEC);
|
||||
}
|
||||
|
||||
private PortBiomeSources() {}
|
||||
|
||||
public static void register(IEventBus bus) {
|
||||
SOURCES.register(bus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
|
||||
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
/** Codec for the authored Flat/Void biome source and layer combination. */
|
||||
public final class PortChunkGenerators {
|
||||
private static final DeferredRegister<MapCodec<? extends ChunkGenerator>> GENERATORS =
|
||||
DeferredRegister.create(Registries.CHUNK_GENERATOR, Rivencraft.MOD_ID);
|
||||
|
||||
static {
|
||||
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
|
||||
// Keep the beta codec ID so previously saved Rocks Ages remain readable.
|
||||
GENERATORS.register("age_rocks", () -> PortFeatureChunkGenerator.CODEC);
|
||||
}
|
||||
|
||||
private PortChunkGenerators() {}
|
||||
|
||||
public static void register(IEventBus bus) {
|
||||
GENERATORS.register(bus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Random;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import net.dnorberg.rivencraft.age.PortBiomePlan;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||
import net.dnorberg.rivencraft.age.PortRockPlan;
|
||||
import net.dnorberg.rivencraft.age.PortVegetationPlan;
|
||||
import net.dnorberg.rivencraft.age.PortLakePlan;
|
||||
import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
|
||||
/** Authored noise terrain and biome distribution supported by the 26.3 backend. */
|
||||
public final class PortAuthoredWriting {
|
||||
private static final int MAX_AUTHORED_BIOMES = 252;
|
||||
private static final int MAX_ROCKS = 32;
|
||||
private static final int MAX_VEGETATION = 32;
|
||||
private static final int MAX_DUNGEONS = 32;
|
||||
private static final int MAX_LAKES = 32;
|
||||
private static final Identifier STANDARD_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/standard");
|
||||
private static final Identifier AMPLIFIED_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/amplified");
|
||||
private static final Identifier ISLAND_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/island");
|
||||
private static final Identifier FLAT_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/flat");
|
||||
private static final Identifier VOID_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/void");
|
||||
private static final Identifier CAVE_TERRAIN = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "terrain/cave");
|
||||
private static final Identifier ROCKS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/rocks");
|
||||
private static final Identifier DUNGEONS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/dungeons");
|
||||
|
||||
private PortAuthoredWriting() {}
|
||||
|
||||
/** A page never disappears into an Age unless this port can apply its effect. */
|
||||
public static boolean supports(List<Identifier> pages) {
|
||||
boolean terrain = false;
|
||||
int biomes = 0;
|
||||
int controllers = 0;
|
||||
String pendingMaterial = null;
|
||||
int rocks = 0;
|
||||
int vegetation = 0;
|
||||
int dungeons = 0;
|
||||
int lakes = 0;
|
||||
int skylands = 0;
|
||||
int denseOres = 0;
|
||||
int starFissures = 0;
|
||||
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
|
||||
for (Identifier page : pages) {
|
||||
if (pendingMaterial != null && !(pendingMaterial.equals("rivencraft:block")
|
||||
&& (page.equals(ROCKS) || PortTerrainFeaturePlan.acceptsMaterial(page))
|
||||
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
|
||||
if (isTerrain(page) && !terrain) {
|
||||
terrain = true;
|
||||
} else if (controller(page) != null && controllers++ == 0) {
|
||||
// An explicit controller needs an authored pool.
|
||||
} else if (isBlockPage(page) && pendingMaterial == null
|
||||
&& PortSymbolPages.find(page) != null
|
||||
&& (PortSymbolPages.find(page).category().equals("rivencraft:block")
|
||||
|| PortSymbolPages.find(page).category().equals("rivencraft:fluid"))) {
|
||||
pendingMaterial = PortSymbolPages.find(page).category();
|
||||
} else if (page.equals(ROCKS) && rocks < MAX_ROCKS) {
|
||||
pendingMaterial = null;
|
||||
rocks++;
|
||||
} else if (PortLakePlan.isLake(page) && lakes < MAX_LAKES) {
|
||||
pendingMaterial = null;
|
||||
lakes++;
|
||||
} else if (PortTerrainFeaturePlan.isSupported(page)
|
||||
&& terrainFeatures.getOrDefault(page, 0) < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||
pendingMaterial = null;
|
||||
terrainFeatures.merge(page, 1, Integer::sum);
|
||||
} else if (page.equals(PortTerrainFeaturePlan.SKYLANDS)
|
||||
&& skylands < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||
skylands++;
|
||||
} else if (page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|
||||
&& denseOres < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||
denseOres++;
|
||||
} else if (page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|
||||
&& starFissures < PortTerrainFeaturePlan.MAX_PER_FEATURE) {
|
||||
starFissures++;
|
||||
} else if (PortVegetationPlan.isVegetation(page) && vegetation < MAX_VEGETATION) {
|
||||
vegetation++;
|
||||
} else if (page.equals(DUNGEONS) && dungeons < MAX_DUNGEONS) {
|
||||
dungeons++;
|
||||
} else if (page.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& page.getPath().startsWith("biome/") && biomes < MAX_AUTHORED_BIOMES
|
||||
&& PortSymbolPages.find(page) != null
|
||||
&& PortSymbolPages.find(page).category().equals("rivencraft:biome")
|
||||
&& PortRegistryPages.biomeId(page) != null) {
|
||||
biomes++;
|
||||
} else return false;
|
||||
}
|
||||
if (pendingMaterial != null || (rocks > 0 || vegetation > 0)
|
||||
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
if ((dungeons > 0 || lakes > 0) && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|
||||
|| pages.contains(CAVE_TERRAIN))) return false;
|
||||
if (denseOres > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
if (pages.contains(PortTerrainFeaturePlan.FLOATING_ISLANDS) && pages.contains(CAVE_TERRAIN)) return false;
|
||||
if (pages.stream().anyMatch(PortTerrainFeaturePlan::isSurface)
|
||||
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
if (controllers != 0) {
|
||||
PortBiomePlan.Distribution distribution = pages.stream().map(PortAuthoredWriting::controller)
|
||||
.filter(value -> value != null).findFirst().orElseThrow();
|
||||
boolean valid = switch (distribution) {
|
||||
case NATIVE -> biomes == 0;
|
||||
case SINGLE -> biomes == 1;
|
||||
default -> biomes >= 2;
|
||||
};
|
||||
return valid;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isTerrain(Identifier page) {
|
||||
return page.equals(STANDARD_TERRAIN) || page.equals(AMPLIFIED_TERRAIN)
|
||||
|| page.equals(ISLAND_TERRAIN) || page.equals(FLAT_TERRAIN)
|
||||
|| page.equals(VOID_TERRAIN) || page.equals(CAVE_TERRAIN);
|
||||
}
|
||||
|
||||
private static boolean isBlockPage(Identifier page) {
|
||||
return page.getNamespace().equals(Rivencraft.MOD_ID) && page.getPath().startsWith("block/");
|
||||
}
|
||||
|
||||
private static PortBiomePlan.Distribution controller(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "biome/native" -> PortBiomePlan.Distribution.NATIVE;
|
||||
case "biome/single" -> PortBiomePlan.Distribution.SINGLE;
|
||||
case "biome/tiled" -> PortBiomePlan.Distribution.TILED;
|
||||
case "biome/grid" -> PortBiomePlan.Distribution.GRID;
|
||||
case "biome/tiny" -> PortBiomePlan.Distribution.TINY;
|
||||
case "biome/small" -> PortBiomePlan.Distribution.SMALL;
|
||||
case "biome/medium" -> PortBiomePlan.Distribution.MEDIUM;
|
||||
case "biome/large" -> PortBiomePlan.Distribution.LARGE;
|
||||
case "biome/huge" -> PortBiomePlan.Distribution.HUGE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static PortAgeTerrain terrain(List<Identifier> pages) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
if (pages.contains(AMPLIFIED_TERRAIN)) return PortAgeTerrain.AMPLIFIED;
|
||||
if (pages.contains(ISLAND_TERRAIN)) return PortAgeTerrain.ISLAND;
|
||||
if (pages.contains(FLAT_TERRAIN)) return PortAgeTerrain.FLAT;
|
||||
if (pages.contains(VOID_TERRAIN)) return PortAgeTerrain.VOID;
|
||||
if (pages.contains(CAVE_TERRAIN)) return PortAgeTerrain.CAVE;
|
||||
return PortAgeTerrain.STANDARD;
|
||||
}
|
||||
|
||||
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
List<Identifier> materials = solidMaterials(pages, seed, ROCKS);
|
||||
return materials.isEmpty() ? null : new PortRockPlan(materials);
|
||||
}
|
||||
|
||||
public static PortTerrainFeaturePlan terrainFeaturePlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
Map<Identifier, List<Identifier>> materials = new HashMap<>();
|
||||
Map<Identifier, Integer> occurrences = new HashMap<>();
|
||||
List<PortTerrainFeaturePlan.Choice> choices = new ArrayList<>();
|
||||
for (Identifier page : pages) {
|
||||
if (!PortTerrainFeaturePlan.isSupported(page)) continue;
|
||||
if (page.equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS)) {
|
||||
choices.add(new PortTerrainFeaturePlan.Choice(page, PortTerrainFeaturePlan.CRYSTAL_MATERIAL));
|
||||
continue;
|
||||
}
|
||||
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(pages, seed, feature));
|
||||
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
|
||||
choices.add(new PortTerrainFeaturePlan.Choice(page, selected.get(occurrence)));
|
||||
}
|
||||
boolean skylands = pages.contains(PortTerrainFeaturePlan.SKYLANDS);
|
||||
int denseOres = (int) pages.stream().filter(PortTerrainFeaturePlan.DENSE_ORES::equals).count();
|
||||
boolean starFissures = pages.contains(PortTerrainFeaturePlan.STAR_FISSURES);
|
||||
return choices.isEmpty() && !skylands && denseOres == 0 && !starFissures ? null
|
||||
: new PortTerrainFeaturePlan(choices, skylands, denseOres, starFissures);
|
||||
}
|
||||
|
||||
private static List<Identifier> solidMaterials(List<Identifier> pages, long seed, Identifier feature) {
|
||||
if (!pages.contains(feature)) return List.of();
|
||||
List<Identifier> randomPool = new ArrayList<>(PortRegistryPages.solidMaterialIds());
|
||||
if (randomPool.isEmpty()) throw new IllegalStateException("No full-block materials are registered");
|
||||
Collections.shuffle(randomPool, new Random(mix(seed ^ (long) feature.hashCode() * 0x9E3779B97F4A7C15L)));
|
||||
List<Identifier> materials = new ArrayList<>();
|
||||
Identifier pending = null;
|
||||
int randomIndex = 0;
|
||||
for (Identifier page : pages) {
|
||||
if (isBlockPage(page)) pending = PortRegistryPages.blockId(page);
|
||||
else if (page.equals(feature)) {
|
||||
materials.add(pending != null ? pending : randomPool.get(randomIndex++ % randomPool.size()));
|
||||
pending = null;
|
||||
} else pending = null;
|
||||
}
|
||||
return List.copyOf(materials);
|
||||
}
|
||||
|
||||
public static PortLakePlan lakePlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
if (pages.stream().noneMatch(PortLakePlan::isLake)) return null;
|
||||
List<Identifier> fluidPool = PortRegistryPages.fluidMaterialIds();
|
||||
if (fluidPool.isEmpty()) throw new IllegalStateException("No source-fluid materials are registered");
|
||||
Map<Identifier, List<Identifier>> orders = new HashMap<>();
|
||||
Map<Identifier, Integer> occurrences = new HashMap<>();
|
||||
List<PortLakePlan.Choice> choices = new ArrayList<>();
|
||||
Identifier pending = null;
|
||||
for (Identifier page : pages) {
|
||||
if (isBlockPage(page)) pending = PortRegistryPages.blockId(page);
|
||||
else if (PortLakePlan.isLake(page)) {
|
||||
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
|
||||
List<Identifier> fluids = orders.computeIfAbsent(page, id -> {
|
||||
List<Identifier> shuffled = new ArrayList<>(fluidPool);
|
||||
Collections.shuffle(shuffled, new Random(mix(seed ^ (long) id.hashCode()
|
||||
* 0x9E3779B97F4A7C15L)));
|
||||
return shuffled;
|
||||
});
|
||||
choices.add(new PortLakePlan.Choice(page,
|
||||
pending != null ? pending : fluids.get(occurrence % fluids.size())));
|
||||
pending = null;
|
||||
} else pending = null;
|
||||
}
|
||||
return new PortLakePlan(choices);
|
||||
}
|
||||
|
||||
public static PortVegetationPlan vegetationPlan(List<Identifier> pages) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
List<Identifier> vegetation = pages.stream().filter(PortVegetationPlan::isVegetation).toList();
|
||||
return vegetation.isEmpty() ? null : new PortVegetationPlan(vegetation);
|
||||
}
|
||||
|
||||
public static int dungeonCount(List<Identifier> pages) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
return (int) pages.stream().filter(DUNGEONS::equals).count();
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
|
||||
/** Resolves all authored biomes from the active registry before creating a level. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException(
|
||||
"Unsupported terrain or biome-page combination in this port slice");
|
||||
List<Identifier> biomes = new ArrayList<>();
|
||||
PortBiomePlan.Distribution distribution = null;
|
||||
boolean caveTerrain = pages.contains(CAVE_TERRAIN);
|
||||
boolean hasSurfaceBiome = false;
|
||||
boolean hasOreBiome = false;
|
||||
for (Identifier page : pages) {
|
||||
if (isTerrain(page) || page.equals(ROCKS) || page.equals(DUNGEONS)
|
||||
|| page.equals(PortTerrainFeaturePlan.SKYLANDS)
|
||||
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES)
|
||||
|| page.equals(PortTerrainFeaturePlan.STAR_FISSURES)
|
||||
|| PortLakePlan.isLake(page) || PortTerrainFeaturePlan.isSupported(page) || isBlockPage(page)
|
||||
|| PortVegetationPlan.isVegetation(page)) continue;
|
||||
PortBiomePlan.Distribution selected = controller(page);
|
||||
if (selected != null) { distribution = selected; continue; }
|
||||
Identifier biome = PortRegistryPages.biomeId(page);
|
||||
var registry = registries.lookupOrThrow(Registries.BIOME);
|
||||
var holder = registry.get(ResourceKey.create(Registries.BIOME, biome))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown authored biome: " + biome));
|
||||
if (caveTerrain && !PortRegistryPages.isCaveBiome(holder))
|
||||
throw new IllegalArgumentException("Cave terrain requires a Cave or Nether biome: " + biome);
|
||||
if (!PortRegistryPages.isCaveBiome(holder) && !holder.is(BiomeTags.IS_END))
|
||||
hasSurfaceBiome = true;
|
||||
if (net.dnorberg.rivencraft.age.PortDenseOres.hasOres(holder)) hasOreBiome = true;
|
||||
biomes.add(biome);
|
||||
}
|
||||
if (pages.stream().anyMatch(PortVegetationPlan::isVegetation)
|
||||
&& !biomes.isEmpty() && !hasSurfaceBiome)
|
||||
throw new IllegalArgumentException("Vegetation pages need at least one surface Overworld biome");
|
||||
if (pages.contains(PortTerrainFeaturePlan.DENSE_ORES) && !biomes.isEmpty() && !hasOreBiome)
|
||||
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new PortBiomePlan(distribution, List.of());
|
||||
if (biomes.isEmpty()) return null;
|
||||
if (distribution == null) distribution = biomes.size() == 1
|
||||
? PortBiomePlan.Distribution.SINGLE : biomes.size() == 2
|
||||
? PortBiomePlan.Distribution.TILED : PortBiomePlan.Distribution.MEDIUM;
|
||||
return new PortBiomePlan(distribution, biomes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.EmptyBlockGetter;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.EntityBlock;
|
||||
|
||||
/** Registry-derived 26.3 biome and material pages, with the same IDs as 1.21.1. */
|
||||
public final class PortRegistryPages {
|
||||
private static final TagKey<net.minecraft.world.level.biome.Biome> CAVE_BIOMES = TagKey.create(
|
||||
Registries.BIOME, Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "cave_biomes"));
|
||||
private static final Identifier DEFERRED_PLACEHOLDER = Identifier.fromNamespaceAndPath(
|
||||
"terrablender", "deferred_placeholder");
|
||||
|
||||
private PortRegistryPages() {}
|
||||
|
||||
public static Identifier biomePageId(Identifier biome) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/"
|
||||
+ (biome.getNamespace().equals("minecraft") ? "" : biome.getNamespace() + "/")
|
||||
+ biome.getPath());
|
||||
}
|
||||
|
||||
public static Identifier blockPageId(Identifier block) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"block/" + block.getNamespace() + "/" + block.getPath());
|
||||
}
|
||||
|
||||
public static PortSymbolPages.Entry find(Identifier symbol) {
|
||||
if (!symbol.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
String path = symbol.getPath();
|
||||
if (path.startsWith("biome/")) {
|
||||
Identifier biome = biomeId(symbol);
|
||||
if (biome == null) return null;
|
||||
String key = biome.getNamespace().equals("minecraft")
|
||||
? "symbol.rivencraft.biome." + biome.getPath()
|
||||
: "biome." + biome.getNamespace() + "." + biome.getPath().replace('/', '.');
|
||||
return new PortSymbolPages.Entry(symbol, "rivencraft:biome", key,
|
||||
"symbol.rivencraft.biome.generated.description",
|
||||
"Nature Form " + glyphWord(biome.getPath(), "World") + " Biome");
|
||||
}
|
||||
if (path.startsWith("block/")) {
|
||||
Identifier block = blockId(symbol);
|
||||
if (block == null) return null;
|
||||
String category = materialCategory(BuiltInRegistries.BLOCK.getValue(block));
|
||||
if (category == null) return null;
|
||||
return new PortSymbolPages.Entry(symbol, category,
|
||||
"block." + block.getNamespace() + "." + block.getPath().replace('/', '_'),
|
||||
"symbol.rivencraft.block.generated.description",
|
||||
"Modifier Constraint " + glyphWord(block.getPath(), "Stone") + " Form");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<ItemStack> biomePages(HolderLookup.Provider registries, boolean caves) {
|
||||
return registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(holder -> !holder.key().identifier().equals(DEFERRED_PLACEHOLDER))
|
||||
.filter(holder -> isCaveBiome(holder) == caves)
|
||||
.map(holder -> PageItem.createSymbolPage(biomePageId(holder.key().identifier())))
|
||||
.sorted((left, right) -> left.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()
|
||||
.compareTo(right.get(Rivencraft.SYMBOL_PAGE_ID.get()).toString()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static List<ItemStack> materialPages(boolean fluids) {
|
||||
String category = fluids ? "rivencraft:fluid" : "rivencraft:block";
|
||||
return materialIds(category).stream()
|
||||
.map(id -> PageItem.createSymbolPage(blockPageId(id))).toList();
|
||||
}
|
||||
|
||||
/** The same full-block pool used by Creative pages, in stable registry-ID order. */
|
||||
public static List<Identifier> solidMaterialIds() {
|
||||
return materialIds("rivencraft:block");
|
||||
}
|
||||
|
||||
public static List<Identifier> fluidMaterialIds() {
|
||||
return materialIds("rivencraft:fluid");
|
||||
}
|
||||
|
||||
private static List<Identifier> materialIds(String category) {
|
||||
return BuiltInRegistries.BLOCK.keySet().stream().sorted()
|
||||
.filter(id -> category.equals(materialCategory(BuiltInRegistries.BLOCK.getValue(id))))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static boolean isCaveBiome(net.minecraft.core.Holder<net.minecraft.world.level.biome.Biome> holder) {
|
||||
String path = holder.unwrapKey().map(key -> key.identifier().getPath()).orElse("");
|
||||
return holder.is(CAVE_BIOMES) || holder.is(BiomeTags.IS_NETHER)
|
||||
|| path.equals("deep_dark") || path.endsWith("_caves");
|
||||
}
|
||||
|
||||
private static String materialCategory(Block block) {
|
||||
if (block == null || block == Blocks.AIR || block == Blocks.CAVE_AIR || block == Blocks.VOID_AIR
|
||||
|| block instanceof EntityBlock || block.defaultBlockState().hasBlockEntity()) return null;
|
||||
var state = block.defaultBlockState();
|
||||
var fluid = state.getFluidState();
|
||||
if (fluid.isSource() && fluid.createLegacyBlock().getBlock() == block) return "rivencraft:fluid";
|
||||
return fluid.isEmpty() && state.isCollisionShapeFullBlock(EmptyBlockGetter.INSTANCE, BlockPos.ZERO)
|
||||
? "rivencraft:block" : null;
|
||||
}
|
||||
|
||||
public static Identifier biomeId(Identifier symbol) {
|
||||
String path = symbol.getPath().substring("biome/".length());
|
||||
int slash = path.indexOf('/');
|
||||
if (slash < 0) return Identifier.fromNamespaceAndPath("minecraft", path);
|
||||
return Identifier.fromNamespaceAndPath(path.substring(0, slash), path.substring(slash + 1));
|
||||
}
|
||||
|
||||
public static Identifier blockId(Identifier symbol) {
|
||||
String path = symbol.getPath().substring("block/".length());
|
||||
int slash = path.indexOf('/');
|
||||
if (slash < 1) return null;
|
||||
return Identifier.fromNamespaceAndPath(path.substring(0, slash), path.substring(slash + 1));
|
||||
}
|
||||
|
||||
public static String fallbackName(Identifier symbol) {
|
||||
boolean biome = symbol.getPath().startsWith("biome/");
|
||||
Identifier source = biome ? biomeId(symbol) : blockId(symbol);
|
||||
if (source == null) return symbol.toString();
|
||||
String words = Arrays.stream(source.getPath().replace('/', '_').split("_"))
|
||||
.filter(word -> !word.isEmpty())
|
||||
.map(word -> word.substring(0, 1).toUpperCase(Locale.ROOT) + word.substring(1))
|
||||
.reduce((a, b) -> a + " " + b).orElse("Unknown");
|
||||
return words + (biome ? " Biome" : "rivencraft:fluid".equals(find(symbol).category())
|
||||
? " Fluid" : " Material")
|
||||
+ (source.getNamespace().equals("minecraft") ? "" : " (" + source.getNamespace() + ")");
|
||||
}
|
||||
|
||||
private static String glyphWord(String path, String fallback) {
|
||||
String word = Arrays.stream(path.replace('/', '_').split("_"))
|
||||
.map(part -> part.replaceAll("[^A-Za-z]", ""))
|
||||
.filter(part -> !part.isEmpty())
|
||||
.map(part -> part.substring(0, 1).toUpperCase(Locale.ROOT) + part.substring(1))
|
||||
.reduce("", String::concat);
|
||||
if (word.isEmpty()) word = fallback;
|
||||
return word.length() > 48 ? word.substring(0, 48) : word;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
/** Mystcraft Legacy's layered base-25 numeral components, shared with 1.21.1. */
|
||||
public final class PortSymbolNumerals {
|
||||
private PortSymbolNumerals() {}
|
||||
|
||||
public static int[] componentsForDigit(int digit) {
|
||||
if (digit < 0 || digit >= 25) throw new IllegalArgumentException("Base-25 digit must be in [0, 24]");
|
||||
if (digit == 0) return new int[] {1};
|
||||
int fifths = digit / 5;
|
||||
int remainder = digit % 5;
|
||||
if (fifths == 0) return new int[] {55 + remainder};
|
||||
if (remainder == 0) return new int[] {59 + fifths};
|
||||
return new int[] {59 + fifths, 55 + remainder};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashSet;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
|
||||
/** Bundled 1.21.1 symbol identities, shared by the port's creative pages. */
|
||||
public final class PortSymbolPages {
|
||||
public record Entry(Identifier id, String category, String nameKey, String descriptionKey, String poem) {}
|
||||
|
||||
private static final Map<Identifier, Entry> ENTRIES = load();
|
||||
|
||||
private PortSymbolPages() {}
|
||||
|
||||
public static Entry find(Identifier id) {
|
||||
Entry bundled = ENTRIES.get(id);
|
||||
return bundled != null ? bundled : PortRegistryPages.find(id);
|
||||
}
|
||||
|
||||
public static List<String> categories() {
|
||||
LinkedHashSet<String> categories = new LinkedHashSet<>();
|
||||
ENTRIES.values().stream().map(Entry::category).forEach(categories::add);
|
||||
categories.add("rivencraft:cave_biome");
|
||||
categories.add("rivencraft:block");
|
||||
categories.add("rivencraft:fluid");
|
||||
return List.copyOf(categories);
|
||||
}
|
||||
|
||||
public static List<ItemStack> pages(String category, HolderLookup.Provider registries) {
|
||||
List<ItemStack> pages = new ArrayList<>();
|
||||
ENTRIES.values().stream().filter(entry -> entry.category().equals(category))
|
||||
.filter(entry -> !category.equals("rivencraft:biome")
|
||||
|| !entry.id().getPath().startsWith("biome/"))
|
||||
.forEach(entry -> pages.add(PageItem.createSymbolPage(entry.id())));
|
||||
if (category.equals("rivencraft:biome")) pages.addAll(PortRegistryPages.biomePages(registries, false));
|
||||
if (category.equals("rivencraft:cave_biome")) pages.addAll(PortRegistryPages.biomePages(registries, true));
|
||||
if (category.equals("rivencraft:block")) pages.addAll(PortRegistryPages.materialPages(false));
|
||||
if (category.equals("rivencraft:fluid")) pages.addAll(PortRegistryPages.materialPages(true));
|
||||
return List.copyOf(pages);
|
||||
}
|
||||
|
||||
public static ItemStack icon(String category) {
|
||||
if (category.equals("rivencraft:cave_biome"))
|
||||
return PageItem.createSymbolPage(PortRegistryPages.biomePageId(
|
||||
Identifier.fromNamespaceAndPath("minecraft", "lush_caves")));
|
||||
if (category.equals("rivencraft:block") || category.equals("rivencraft:fluid"))
|
||||
return PageItem.createSymbolPage(PortRegistryPages.blockPageId(
|
||||
Identifier.fromNamespaceAndPath("minecraft",
|
||||
category.equals("rivencraft:fluid") ? "water" : "stone")));
|
||||
return ENTRIES.values().stream().filter(entry -> entry.category().equals(category))
|
||||
.findFirst().map(entry -> PageItem.createSymbolPage(entry.id()))
|
||||
.orElseGet(PageItem::createLinkPanelPage);
|
||||
}
|
||||
|
||||
public static String fallbackName(Identifier id) {
|
||||
if (id.getPath().startsWith("biome/") || id.getPath().startsWith("block/"))
|
||||
return PortRegistryPages.fallbackName(id);
|
||||
String path = id.getPath();
|
||||
String stem = path.substring(path.lastIndexOf('/') + 1).replace('_', ' ');
|
||||
if (stem.isEmpty()) return id.toString();
|
||||
return Character.toUpperCase(stem.charAt(0)) + stem.substring(1);
|
||||
}
|
||||
|
||||
private static Map<Identifier, Entry> load() {
|
||||
String resource = "/assets/rivencraft/creative_symbol_pages.json";
|
||||
try (var stream = PortSymbolPages.class.getResourceAsStream(resource)) {
|
||||
if (stream == null) throw new IllegalStateException("Missing generated symbol index " + resource);
|
||||
var json = JsonParser.parseReader(new InputStreamReader(stream, StandardCharsets.UTF_8))
|
||||
.getAsJsonObject();
|
||||
Map<Identifier, Entry> entries = new LinkedHashMap<>();
|
||||
json.entrySet().forEach(item -> {
|
||||
String[] parts = item.getKey().split(":", 2);
|
||||
if (parts.length != 2 || !parts[0].equals(Rivencraft.MOD_ID))
|
||||
throw new IllegalStateException("Unexpected bundled symbol " + item.getKey());
|
||||
Identifier id = Identifier.fromNamespaceAndPath(parts[0], parts[1]);
|
||||
var definition = item.getValue().getAsJsonObject();
|
||||
String category = definition.get("category").getAsString();
|
||||
if (definition.has("semantics") && definition.getAsJsonObject("semantics")
|
||||
.get("role").getAsString().equals("biome_controller"))
|
||||
category = "rivencraft:biome_generation";
|
||||
entries.put(id, new Entry(id, category, definition.get("name").getAsString(),
|
||||
definition.get("description").getAsString(),
|
||||
definition.has("poem") ? definition.get("poem").getAsString() : ""));
|
||||
});
|
||||
return Collections.unmodifiableMap(entries);
|
||||
} catch (java.io.IOException exception) {
|
||||
throw new IllegalStateException("Could not read bundled symbol index", exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,16 @@
|
||||
"property": "minecraft:has_component",
|
||||
"component": "rivencraft:link_panel_page",
|
||||
"on_true": {"type": "minecraft:model", "model": "rivencraft:item/page_link_panel"},
|
||||
"on_false": {"type": "minecraft:model", "model": "rivencraft:item/page"}
|
||||
"on_false": {
|
||||
"type": "minecraft:condition",
|
||||
"property": "minecraft:has_component",
|
||||
"component": "rivencraft:symbol_page_id",
|
||||
"on_true": {
|
||||
"type": "minecraft:special",
|
||||
"base": "rivencraft:item/page",
|
||||
"model": {"type": "rivencraft:symbol_page"}
|
||||
},
|
||||
"on_false": {"type": "minecraft:model", "model": "rivencraft:item/page"}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"linking.link-fissure": {
|
||||
"sounds": [{"name": "rivencraft:linking/link-fissure"}]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.abandoned_camps",
|
||||
"description": "symbol.rivencraft.feature.abandoned_camps.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:abandoned_camps" },
|
||||
"poem": "Terrain Form Abandoned Camp"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.fallen_poplar_trees",
|
||||
"description": "symbol.rivencraft.feature.fallen_poplar_trees.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:fallen_poplar_trees" },
|
||||
"poem": "Nature Form Poplar Fallen"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.poplar_trees",
|
||||
"description": "symbol.rivencraft.feature.poplar_trees.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:poplar_trees" },
|
||||
"poem": "Nature Form Poplar Growth"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.red_shrubs",
|
||||
"description": "symbol.rivencraft.feature.red_shrubs.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:red_shrubs" },
|
||||
"poem": "Nature Form Red Growth"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.shelf_mushrooms",
|
||||
"description": "symbol.rivencraft.feature.shelf_mushrooms.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:shelf_mushrooms" },
|
||||
"poem": "Nature Form Shelf Fungi"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"minecraft:lush_caves",
|
||||
"minecraft:dripstone_caves",
|
||||
"minecraft:deep_dark"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"feature": "minecraft:monster_room",
|
||||
"placement": [
|
||||
{ "type": "minecraft:count", "count": 8 },
|
||||
{ "type": "minecraft:in_square" },
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:uniform",
|
||||
"min_inclusive": { "above_bottom": 1 },
|
||||
"max_inclusive": { "below_top": 1 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Publish a Rivencraft JAR to Gitea, Modrinth, and/or CurseForge.
|
||||
|
||||
Preview is the default. Tokens live in the Secret Service keyring, never in git.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
MODRINTH_PROJECT = "GH2xfZJ8"
|
||||
CURSEFORGE_PROJECT = "1766326"
|
||||
GITEA_API = "https://git.paddedcell.org/api/v1/repos/dnorberg/rivencraft"
|
||||
USER_AGENT = "RivencraftReleaseTool/1.0 (https://git.paddedcell.org/dnorberg/rivencraft)"
|
||||
SITES = ("gitea", "modrinth", "curseforge")
|
||||
|
||||
|
||||
def token_for(site: str) -> str:
|
||||
environment = os.environ.get(f"RIVENCRAFT_{site.upper()}_TOKEN", "").strip()
|
||||
if environment:
|
||||
return environment
|
||||
result = subprocess.run(
|
||||
["secret-tool", "lookup", "application", "rivencraft-publishing", "site", site],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode:
|
||||
raise RuntimeError(f"Could not read {site} token from the system keyring")
|
||||
token = result.stdout.strip()
|
||||
if not token:
|
||||
raise RuntimeError(f"No {site} token found; run `store-token {site}` first")
|
||||
return token
|
||||
|
||||
|
||||
def store_token(site: str) -> None:
|
||||
token = getpass.getpass(f"Paste the {site} API token (input hidden): ").strip()
|
||||
if not token:
|
||||
raise RuntimeError("Empty token; nothing was stored")
|
||||
subprocess.run(
|
||||
[
|
||||
"secret-tool", "store", "--label", f"Rivencraft {site} publishing token",
|
||||
"application", "rivencraft-publishing", "site", site,
|
||||
],
|
||||
input=token + "\n",
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
print(f"Stored {site} token in the system keyring")
|
||||
|
||||
|
||||
def check_token(site: str) -> None:
|
||||
token = token_for(site)
|
||||
if site == "gitea":
|
||||
account = request_json("https://git.paddedcell.org/api/v1/token", token=f"token {token}")
|
||||
scopes = account.get("scopes", [])
|
||||
if "write:repository" not in scopes and "all" not in scopes:
|
||||
raise RuntimeError("Gitea token is valid but lacks write:repository scope")
|
||||
print(f"Gitea API access works for {account['user']['login']} (write:repository)")
|
||||
elif site == "modrinth":
|
||||
try:
|
||||
account = request_json("https://api.modrinth.com/v2/user", token=token)
|
||||
except RuntimeError as error:
|
||||
raise RuntimeError(
|
||||
"Modrinth check failed. The token needs USER_READ for this "
|
||||
"read-only check, plus VERSION_CREATE for publishing. "
|
||||
f"API response: {error}"
|
||||
) from None
|
||||
print(f"Modrinth API access works for {account['username']}")
|
||||
else:
|
||||
versions = request_json(
|
||||
"https://www.curseforge.com/api/game/versions",
|
||||
token=token, token_header="X-Api-Token",
|
||||
)
|
||||
if not isinstance(versions, list):
|
||||
raise RuntimeError("CurseForge returned an unexpected response")
|
||||
print("CurseForge API token accepted")
|
||||
|
||||
|
||||
def request_json(url: str, *, token: str | None = None, token_header: str = "Authorization",
|
||||
payload: bytes | None = None, content_type: str | None = None,
|
||||
method: str = "GET", not_found_ok: bool = False) -> dict | list | None:
|
||||
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
|
||||
if token:
|
||||
headers[token_header] = token
|
||||
if content_type:
|
||||
headers["Content-Type"] = content_type
|
||||
request = urllib.request.Request(url, data=payload, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=120) as response:
|
||||
return json.load(response)
|
||||
except urllib.error.HTTPError as error:
|
||||
if error.code == 404 and not_found_ok:
|
||||
return None
|
||||
# Error bodies can contain server details, but never echo tokens.
|
||||
detail = error.read(600).decode("utf-8", "replace")
|
||||
raise RuntimeError(f"{url}: HTTP {error.code}: {detail}") from None
|
||||
|
||||
|
||||
def multipart(fields: dict[str, str], jar: Path, *, file_field: str = "file") -> tuple[bytes, str]:
|
||||
boundary = "rivencraft-" + secrets.token_hex(16)
|
||||
chunks = []
|
||||
for name, value in fields.items():
|
||||
chunks.extend((
|
||||
f"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n".encode(),
|
||||
value.encode(), b"\r\n",
|
||||
))
|
||||
filename = jar.name.replace('"', "")
|
||||
mime = mimetypes.guess_type(filename)[0] or "application/java-archive"
|
||||
chunks.extend((
|
||||
f"--{boundary}\r\nContent-Disposition: form-data; name=\"{file_field}\"; filename=\"{filename}\"\r\nContent-Type: {mime}\r\n\r\n".encode(),
|
||||
jar.read_bytes(), b"\r\n", f"--{boundary}--\r\n".encode(),
|
||||
))
|
||||
return b"".join(chunks), f"multipart/form-data; boundary={boundary}"
|
||||
|
||||
|
||||
def release_metadata(args: argparse.Namespace) -> tuple[dict, dict]:
|
||||
changelog = args.changelog.read_text(encoding="utf-8")
|
||||
if not changelog.strip():
|
||||
raise RuntimeError("Changelog is empty")
|
||||
modrinth = {
|
||||
"name": args.name,
|
||||
"version_number": args.version,
|
||||
"changelog": changelog,
|
||||
"dependencies": [],
|
||||
"game_versions": [args.minecraft],
|
||||
"version_type": args.channel,
|
||||
"loaders": ["neoforge"],
|
||||
"featured": False,
|
||||
"status": "listed",
|
||||
"project_id": MODRINTH_PROJECT,
|
||||
"file_parts": ["file"],
|
||||
"primary_file": "file",
|
||||
"environment": "client_and_server",
|
||||
}
|
||||
curseforge = {
|
||||
"changelog": changelog,
|
||||
"changelogType": "markdown",
|
||||
"displayName": args.name,
|
||||
"gameVersionNames": [args.minecraft, "NeoForge", "Client", "Server", f"Java {args.java}"],
|
||||
"releaseType": args.channel,
|
||||
"isMarkedForManualRelease": False,
|
||||
}
|
||||
return modrinth, curseforge
|
||||
|
||||
|
||||
def publish_modrinth(metadata: dict, jar: Path) -> None:
|
||||
token = token_for("modrinth")
|
||||
versions = request_json(
|
||||
f"https://api.modrinth.com/v2/project/{MODRINTH_PROJECT}/version",
|
||||
token=token,
|
||||
)
|
||||
if any(version.get("version_number") == metadata["version_number"] for version in versions):
|
||||
raise RuntimeError(f"Modrinth version {metadata['version_number']} already exists")
|
||||
body, content_type = multipart({"data": json.dumps(metadata)}, jar)
|
||||
result = request_json(
|
||||
"https://api.modrinth.com/v2/version", token=token, payload=body,
|
||||
content_type=content_type, method="POST",
|
||||
)
|
||||
print(f"Modrinth: https://modrinth.com/mod/rivencraft/version/{result['version_number']}")
|
||||
|
||||
|
||||
def publish_curseforge(metadata: dict, jar: Path) -> None:
|
||||
token = token_for("curseforge")
|
||||
body, content_type = multipart({"metadata": json.dumps(metadata)}, jar)
|
||||
result = request_json(
|
||||
f"https://www.curseforge.com/api/projects/{CURSEFORGE_PROJECT}/upload-file",
|
||||
token=token, token_header="X-Api-Token", payload=body,
|
||||
content_type=content_type, method="POST",
|
||||
)
|
||||
print(f"CurseForge: https://www.curseforge.com/minecraft/mc-mods/rivencraft/files/{result['id']} (pending approval)")
|
||||
|
||||
|
||||
def publish_gitea(args: argparse.Namespace) -> None:
|
||||
base_version, suffix = args.version.split("-", 1)
|
||||
if suffix != f"{args.channel}.1":
|
||||
raise RuntimeError("Gitea tag convention requires a version ending in the selected channel and .1")
|
||||
tag = f"V{base_version}-mc{args.minecraft}-{suffix}"
|
||||
# The release must refer to a tag already pushed by the normal Git workflow.
|
||||
remote_tag = subprocess.run(
|
||||
["git", "ls-remote", "origin", f"refs/tags/{tag}"],
|
||||
text=True, capture_output=True, check=True,
|
||||
)
|
||||
if not remote_tag.stdout.strip():
|
||||
raise RuntimeError(f"Push the Gitea tag {tag} before publishing")
|
||||
|
||||
token = f"token {token_for('gitea')}"
|
||||
release_url = f"{GITEA_API}/releases/tags/{tag}"
|
||||
release = request_json(release_url, token=token, not_found_ok=True)
|
||||
notes = args.changelog.read_text(encoding="utf-8")
|
||||
title = f"Rivencraft {args.minecraft} {args.channel} {args.version}"
|
||||
if release is None:
|
||||
metadata = {
|
||||
"tag_name": tag,
|
||||
"target_commitish": "main",
|
||||
"name": title,
|
||||
"body": notes,
|
||||
"draft": False,
|
||||
"prerelease": args.channel != "release",
|
||||
}
|
||||
release = request_json(
|
||||
f"{GITEA_API}/releases", token=token,
|
||||
payload=json.dumps(metadata).encode(), content_type="application/json", method="POST",
|
||||
)
|
||||
elif release.get("name") != title or release.get("body", "").strip() != notes.strip():
|
||||
raise RuntimeError(f"Gitea release {tag} exists with different notes or title; inspect it before retrying")
|
||||
|
||||
assets = release.get("assets", [])
|
||||
if any(asset.get("name") == args.jar.name for asset in assets):
|
||||
raise RuntimeError(f"Gitea release {tag} already has {args.jar.name}")
|
||||
body, content_type = multipart({}, args.jar, file_field="attachment")
|
||||
attachment = request_json(
|
||||
f"{GITEA_API}/releases/{release['id']}/assets", token=token,
|
||||
payload=body, content_type=content_type, method="POST",
|
||||
)
|
||||
if attachment.get("name") != args.jar.name or attachment.get("size") != args.jar.stat().st_size:
|
||||
raise RuntimeError("Gitea returned unexpected attachment metadata; inspect the release")
|
||||
print(f"Gitea: {release['html_url']}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
store = sub.add_parser("store-token", help="Securely save a token in the system keyring")
|
||||
store.add_argument("site", choices=SITES)
|
||||
check = sub.add_parser("check-token", help="Verify stored token with a read-only API call")
|
||||
check.add_argument("site", choices=SITES)
|
||||
publish = sub.add_parser("release", help="Preview or publish a JAR release")
|
||||
publish.add_argument("--jar", type=Path, required=True)
|
||||
publish.add_argument("--changelog", type=Path, required=True)
|
||||
publish.add_argument("--version", required=True)
|
||||
publish.add_argument("--name", required=True)
|
||||
publish.add_argument("--minecraft", required=True)
|
||||
publish.add_argument("--java", required=True)
|
||||
publish.add_argument("--channel", choices=("alpha", "beta", "release"), required=True)
|
||||
publish.add_argument("--site", choices=(*SITES, "both", "all"), default="all")
|
||||
publish.add_argument("--publish", action="store_true", help="Actually upload; omitted by default")
|
||||
args = parser.parse_args()
|
||||
if args.command == "store-token":
|
||||
store_token(args.site)
|
||||
return
|
||||
if args.command == "check-token":
|
||||
check_token(args.site)
|
||||
return
|
||||
if not args.jar.is_file() or args.jar.suffix != ".jar":
|
||||
raise RuntimeError(f"JAR does not exist: {args.jar}")
|
||||
if not args.changelog.is_file():
|
||||
raise RuntimeError(f"Changelog does not exist: {args.changelog}")
|
||||
if args.minecraft != "26.3":
|
||||
raise RuntimeError("Only the 26.3 line is configured; 1.21.1 needs required dependency relations")
|
||||
if args.jar.name != f"rivencraft-{args.minecraft}-{args.version}.jar":
|
||||
raise RuntimeError("JAR filename does not match Minecraft and release versions")
|
||||
modrinth, curseforge = release_metadata(args)
|
||||
sites = SITES if args.site == "all" else (("modrinth", "curseforge") if args.site == "both" else (args.site,))
|
||||
print(f"JAR: {args.jar.resolve()} ({args.jar.stat().st_size} bytes)")
|
||||
print(f"Release: {args.version} | {args.channel} | Minecraft {args.minecraft} | NeoForge | Java {args.java}")
|
||||
print(f"Destinations: {', '.join(sites)}")
|
||||
if not args.publish:
|
||||
print("PREVIEW ONLY: add --publish to upload")
|
||||
return
|
||||
if "gitea" in sites:
|
||||
publish_gitea(args)
|
||||
if "modrinth" in sites:
|
||||
publish_modrinth(modrinth, args.jar)
|
||||
if "curseforge" in sites:
|
||||
publish_curseforge(curseforge, args.jar)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (OSError, ValueError, RuntimeError, subprocess.CalledProcessError) as error:
|
||||
print(f"Publishing failed: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
@@ -69,6 +69,26 @@ start_server first
|
||||
send_and_wait 'rivencraft_port compat' 'Registered biomes by namespace:'
|
||||
send_and_wait 'rivencraft_port age create smoke_alpha 123456789' 'Created rivencraft:age/smoke_alpha with seed 123456789'
|
||||
send_and_wait 'rivencraft_port age create smoke_beta 987654321' 'Created rivencraft:age/smoke_beta with seed 987654321'
|
||||
send_and_wait 'rivencraft_port age create smoke_flat 24680 flat' 'Created rivencraft:age/smoke_flat with seed 24680 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create smoke_void 13579 void' 'Created rivencraft:age/smoke_void with seed 13579 terrain=VOID'
|
||||
send_and_wait 'rivencraft_port age create smoke_cave 112233 cave' 'Created rivencraft:age/smoke_cave with seed 112233 terrain=CAVE'
|
||||
send_and_wait 'rivencraft_port age create_multi smoke_flat_multi 24681 flat' 'Created rivencraft:age/smoke_flat_multi with seed 24681 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_multi smoke_void_multi 13580 void' 'Created rivencraft:age/smoke_void_multi with seed 13580 terrain=VOID'
|
||||
send_and_wait 'rivencraft_port age create_multi smoke_cave_multi 112234 cave' 'Created rivencraft:age/smoke_cave_multi with seed 112234 terrain=CAVE'
|
||||
send_and_wait 'rivencraft_port age create_random smoke_random 889900' 'Random Age plan: terrain='
|
||||
send_and_wait 'rivencraft_port age create_rocks smoke_rocks 445566' 'Created rivencraft:age/smoke_rocks with seed 445566 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_rocks_multi smoke_rocks_multi 445566' 'Created rivencraft:age/smoke_rocks_multi with seed 445566 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_rocks_random smoke_rocks_random 445566' 'Created rivencraft:age/smoke_rocks_random with seed 445566 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_vegetation smoke_vegetation 24680' 'Created rivencraft:age/smoke_vegetation with seed 24680 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_dungeons smoke_dungeons 135791' 'Created rivencraft:age/smoke_dungeons with seed 135791 terrain=STANDARD'
|
||||
send_and_wait 'rivencraft_port age create smoke_lake_control 667788' 'Created rivencraft:age/smoke_lake_control with seed 667788 terrain=STANDARD'
|
||||
send_and_wait 'rivencraft_port age create_lakes smoke_lakes 667788' 'Created rivencraft:age/smoke_lakes with seed 667788 terrain=STANDARD'
|
||||
send_and_wait 'rivencraft_port age create_lakes_random smoke_lakes_random 667788' 'Created rivencraft:age/smoke_lakes_random with seed 667788 terrain=STANDARD'
|
||||
# This fixture includes a rare island candidate near the origin at normal rarity.
|
||||
send_and_wait 'rivencraft_port age create_islands smoke_islands 887767 void' 'Created rivencraft:age/smoke_islands with seed 887767 terrain=VOID'
|
||||
send_and_wait 'rivencraft_port age create_islands smoke_islands_flat 887767 flat' 'Created rivencraft:age/smoke_islands_flat with seed 887767 terrain=FLAT'
|
||||
send_and_wait 'rivencraft_port age create_islands smoke_islands_noise 887767 standard' 'Created rivencraft:age/smoke_islands_noise with seed 887767 terrain=STANDARD'
|
||||
send_and_wait 'rivencraft_port age create_islands_random smoke_islands_random 887767 void' 'Created rivencraft:age/smoke_islands_random with seed 887767 terrain=VOID'
|
||||
# The index must reach disk while the server is still running. A graceful
|
||||
# shutdown would hide the interrupted-save regression this checks.
|
||||
index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/runtime_ages.dat"
|
||||
@@ -78,26 +98,229 @@ index_file="$server_dir/world/dimensions/minecraft/overworld/data/rivencraft/run
|
||||
}
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_alpha'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_beta'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_flat_multi'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_void_multi'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_cave_multi'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_random'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks_multi'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_rocks_random'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_vegetation'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_dungeons'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_lakes'
|
||||
gzip -dc "$index_file" | strings | grep -Fq 'rivencraft:age/smoke_lakes_random'
|
||||
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
|
||||
gzip -dc "$index_file" | strings | grep -Fq "rivencraft:age/$island_age"
|
||||
done
|
||||
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave seed=112233 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_rocks_multi' 'rivencraft:age/smoke_rocks_multi seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_rocks_random' 'rivencraft:age/smoke_rocks_random seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_vegetation' 'rivencraft:age/smoke_vegetation seed=24680 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_dungeons' 'rivencraft:age/smoke_dungeons seed=135791 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age dungeons_probe smoke_dungeons' 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners='
|
||||
dungeons_before=$(grep -F 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners=' "$log_file" | tail -1 | sed 's/.*spawners=//')
|
||||
[[ "$dungeons_before" =~ ^[1-9][0-9]*$ ]] || {
|
||||
printf 'Authored Dungeons placed no monster spawners: %s\n' "$dungeons_before" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lake_control' 'Lakes probe rivencraft:age/smoke_lake_control pages=0'
|
||||
lake_control=$(grep -F 'Lakes probe rivencraft:age/smoke_lake_control pages=0' "$log_file" | tail -1 | sed 's/.* water=//; s/ lava=.*//')
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes' 'Lakes probe rivencraft:age/smoke_lakes pages=24 fluids=[minecraft:water]'
|
||||
lakes_before=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||
lake_water=$(printf '%s' "$lakes_before" | sed 's/.* water=//; s/ lava=.*//')
|
||||
[[ "$lake_water" -gt "$lake_control" ]] || {
|
||||
printf 'Water lake pages did not add water: %s -> %s\n%s\n' "$lake_control" "$lake_water" "$lakes_before" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes_random' 'Lakes probe rivencraft:age/smoke_lakes_random pages=24'
|
||||
lakes_random_before=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes_random pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||
[[ "$lakes_random_before" == *minecraft:water* && "$lakes_random_before" == *minecraft:lava* ]] || {
|
||||
printf 'Unmodified lake pages did not resolve both vanilla fluids: %s\n' "$lakes_random_before" >&2
|
||||
exit 1
|
||||
}
|
||||
declare -A islands_before
|
||||
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
|
||||
send_and_wait "rivencraft_port age islands_probe $island_age" "Islands probe rivencraft:age/$island_age pages=4"
|
||||
island_result=$(grep -F "Islands probe rivencraft:age/$island_age pages=4" "$log_file" | tail -1 | sed 's/.*Islands probe /Islands probe /')
|
||||
island_blocks=$(printf '%s' "$island_result" | sed 's/.* count=//; s/ fingerprint=.*//')
|
||||
[[ "$island_blocks" =~ ^[1-9][0-9]*$ ]] || {
|
||||
printf 'Floating Islands placed no selected material: %s\n' "$island_result" >&2
|
||||
exit 1
|
||||
}
|
||||
islands_before[$island_age]=$island_result
|
||||
done
|
||||
island_materials=${islands_before[smoke_islands_random]#*materials=[}
|
||||
island_materials=${island_materials%%]*}
|
||||
IFS=',' read -r -a island_material_list <<< "$island_materials"
|
||||
[[ ${#island_material_list[@]} -eq 4 ]] || {
|
||||
printf 'Repeated unmodified islands did not choose independent materials: %s\n' "$island_materials" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'execute in rivencraft:age/smoke_islands if block 0 0 0 minecraft:air run say Island Void remains empty below' 'Island Void remains empty below'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_islands_flat if block 0 0 0 minecraft:bedrock run say Island Flat retains its floor' 'Island Flat retains its floor'
|
||||
send_and_wait 'rivencraft_port age biome smoke_islands 128 128' 'rivencraft:age/smoke_islands biome at 128,128=minecraft:forest'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_flat run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_void run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_flat_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_flat_multi to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_void_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_void_multi to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_cave run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_cave_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_cave_multi to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_rocks run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_rocks_multi run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks_multi to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_rocks_random run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_rocks_random to be force loaded'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_vegetation run forceload add 0 0' 'Marked chunk [0, 0] in rivencraft:age/smoke_vegetation to be force loaded'
|
||||
send_and_wait 'rivencraft_port age vegetation_probe smoke_flat' 'Vegetation probe rivencraft:age/smoke_flat flowers='
|
||||
flat_vegetation=$(grep -F 'Vegetation probe rivencraft:age/smoke_flat flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
|
||||
send_and_wait 'rivencraft_port age vegetation_probe smoke_vegetation' 'Vegetation probe rivencraft:age/smoke_vegetation flowers='
|
||||
authored_vegetation=$(grep -F 'Vegetation probe rivencraft:age/smoke_vegetation flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
|
||||
flat_flowers=${flat_vegetation%% logs=*}
|
||||
flat_logs=${flat_vegetation##* logs=}
|
||||
authored_flowers=${authored_vegetation%% logs=*}
|
||||
authored_logs=${authored_vegetation##* logs=}
|
||||
[[ "$authored_flowers" -gt "$flat_flowers" && "$authored_logs" -gt "$flat_logs" ]] || {
|
||||
printf 'Authored vegetation did not increase flowers and logs: %s -> %s\n' \
|
||||
"$flat_vegetation" "$authored_vegetation" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count='
|
||||
rocks_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||
[[ "$rocks_before" =~ ^[1-9][0-9]*$ ]] || {
|
||||
printf 'Authored Rocks placed no gold blocks: %s\n' "$rocks_before" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_multi' 'Rocks probe rivencraft:age/smoke_rocks_multi materials=[minecraft:gold_block, minecraft:gold_block, minecraft:gold_block, minecraft:gold_block] count='
|
||||
rocks_multi_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_multi materials=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||
[[ "$rocks_multi_before" =~ ^[1-9][0-9]*$ && "$rocks_multi_before" -gt "$rocks_before" ]] || {
|
||||
printf 'Repeated Rocks pages did not increase placement: %s -> %s\n' "$rocks_before" "$rocks_multi_before" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_random' 'Rocks probe rivencraft:age/smoke_rocks_random materials=['
|
||||
rocks_random_before=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_random materials=' "$log_file" | tail -1 | sed 's/.*Rocks probe /Rocks probe /')
|
||||
[[ "$rocks_random_before" =~ count=[1-9][0-9]*$ ]] || {
|
||||
printf 'Unmodified Rocks page placed no material: %s\n' "$rocks_random_before" >&2
|
||||
exit 1
|
||||
}
|
||||
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 0 0 minecraft:bedrock run say Flat bedrock starts at Y0' 'Flat bedrock starts at Y0'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_flat if block 0 3 0 minecraft:grass_block run say Flat surface is Y3' 'Flat surface is Y3'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_void if block 0 0 0 minecraft:air run say Void remains empty' 'Void remains empty'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_flat_multi if block 0 0 0 minecraft:bedrock run say Multi Flat bedrock starts at Y0' 'Multi Flat bedrock starts at Y0'
|
||||
send_and_wait 'execute in rivencraft:age/smoke_void_multi if block 0 0 0 minecraft:air run say Multi Void remains empty' 'Multi Void remains empty'
|
||||
# Check region interiors after loading the sampled chunks.
|
||||
send_and_wait 'rivencraft_port age biome smoke_flat_multi 128 128' 'rivencraft:age/smoke_flat_multi biome at 128,128=minecraft:plains'
|
||||
send_and_wait 'rivencraft_port age biome smoke_flat_multi 384 128' 'rivencraft:age/smoke_flat_multi biome at 384,128=minecraft:forest'
|
||||
send_and_wait 'rivencraft_port age biome smoke_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
|
||||
send_and_wait 'rivencraft_port age biome smoke_cave_multi 128 128' 'rivencraft:age/smoke_cave_multi biome at 128,128=minecraft:lush_caves'
|
||||
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
|
||||
alpha_before=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
beta_before=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
flat_before=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
void_before=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
random_before=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
if [[ "$alpha_before" == "$beta_before" ]]; then
|
||||
printf 'Different Age seeds produced the same terrain probe: %s\n' "$alpha_before" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ "$flat_before" == "$void_before" ]]; then
|
||||
printf 'Flat and Void Ages produced the same height probe: %s\n' "$flat_before" >&2
|
||||
exit 1
|
||||
fi
|
||||
stop_server
|
||||
|
||||
start_server restart
|
||||
send_and_wait 'rivencraft_port age list' 'smoke_alpha'
|
||||
send_and_wait 'rivencraft_port age sample smoke_alpha' 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_beta' 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_flat' 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_void' 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_cave' 'rivencraft:age/smoke_cave seed=112233 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_flat_multi' 'rivencraft:age/smoke_flat_multi seed=24681 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_void_multi' 'rivencraft:age/smoke_void_multi seed=13580 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_cave_multi' 'rivencraft:age/smoke_cave_multi seed=112234 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_random' 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_rocks' 'rivencraft:age/smoke_rocks seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_rocks_multi' 'rivencraft:age/smoke_rocks_multi seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_rocks_random' 'rivencraft:age/smoke_rocks_random seed=445566 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_vegetation' 'rivencraft:age/smoke_vegetation seed=24680 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age sample smoke_dungeons' 'rivencraft:age/smoke_dungeons seed=135791 chunkOffset=0 terrain='
|
||||
send_and_wait 'rivencraft_port age dungeons_probe smoke_dungeons' 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners='
|
||||
dungeons_after=$(grep -F 'Dungeons probe rivencraft:age/smoke_dungeons pages=8 spawners=' "$log_file" | tail -1 | sed 's/.*spawners=//')
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes' 'Lakes probe rivencraft:age/smoke_lakes pages=24 fluids=[minecraft:water]'
|
||||
lakes_after=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||
send_and_wait 'rivencraft_port age lakes_probe smoke_lakes_random' 'Lakes probe rivencraft:age/smoke_lakes_random pages=24'
|
||||
lakes_random_after=$(grep -F 'Lakes probe rivencraft:age/smoke_lakes_random pages=24' "$log_file" | tail -1 | sed 's/.*Lakes probe /Lakes probe /')
|
||||
for island_age in smoke_islands smoke_islands_flat smoke_islands_noise smoke_islands_random; do
|
||||
send_and_wait "rivencraft_port age islands_probe $island_age" "Islands probe rivencraft:age/$island_age pages=4"
|
||||
island_result=$(grep -F "Islands probe rivencraft:age/$island_age pages=4" "$log_file" | tail -1 | sed 's/.*Islands probe /Islands probe /')
|
||||
[[ "${islands_before[$island_age]}" == "$island_result" ]] || {
|
||||
printf 'Floating Islands changed across restart: %s\n%s\n' "${islands_before[$island_age]}" "$island_result" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks' 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count='
|
||||
rocks_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks materials=[minecraft:gold_block] count=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_multi' 'Rocks probe rivencraft:age/smoke_rocks_multi materials=['
|
||||
rocks_multi_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_multi materials=' "$log_file" | tail -1 | sed 's/.*count=//')
|
||||
send_and_wait 'rivencraft_port age rocks_probe smoke_rocks_random' 'Rocks probe rivencraft:age/smoke_rocks_random materials=['
|
||||
rocks_random_after=$(grep -F 'Rocks probe rivencraft:age/smoke_rocks_random materials=' "$log_file" | tail -1 | sed 's/.*Rocks probe /Rocks probe /')
|
||||
send_and_wait 'rivencraft_port age vegetation_probe smoke_vegetation' 'Vegetation probe rivencraft:age/smoke_vegetation flowers='
|
||||
authored_vegetation_after=$(grep -F 'Vegetation probe rivencraft:age/smoke_vegetation flowers=' "$log_file" | tail -1 | sed 's/.*flowers=//')
|
||||
send_and_wait 'rivencraft_port age biome smoke_flat_multi 384 128' 'rivencraft:age/smoke_flat_multi biome at 384,128=minecraft:forest'
|
||||
send_and_wait 'rivencraft_port age biome smoke_void_multi 384 128' 'rivencraft:age/smoke_void_multi biome at 384,128=minecraft:forest'
|
||||
send_and_wait 'rivencraft_port age biome smoke_cave_multi 384 128' 'rivencraft:age/smoke_cave_multi biome at 384,128=minecraft:dripstone_caves'
|
||||
alpha_after=$(grep -F 'rivencraft:age/smoke_alpha seed=123456789 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
beta_after=$(grep -F 'rivencraft:age/smoke_beta seed=987654321 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
flat_after=$(grep -F 'rivencraft:age/smoke_flat seed=24680 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
void_after=$(grep -F 'rivencraft:age/smoke_void seed=13579 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
random_after=$(grep -F 'rivencraft:age/smoke_random seed=889900 chunkOffset=0 terrain=' "$log_file" | tail -1 | sed 's/.*terrain=//')
|
||||
[[ "$alpha_before" == "$alpha_after" && "$beta_before" == "$beta_after" ]] || {
|
||||
printf 'Age terrain changed across restart: %s/%s -> %s/%s\n' \
|
||||
"$alpha_before" "$beta_before" "$alpha_after" "$beta_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$flat_before" == "$flat_after" && "$void_before" == "$void_after" ]] || {
|
||||
printf 'Flat/Void terrain changed across restart: %s/%s -> %s/%s\n' \
|
||||
"$flat_before" "$void_before" "$flat_after" "$void_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$random_before" == "$random_after" ]] || {
|
||||
printf 'Random Age terrain changed across restart: %s -> %s\n' \
|
||||
"$random_before" "$random_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$rocks_before" == "$rocks_after" ]] || {
|
||||
printf 'Authored Rocks changed across restart: %s -> %s\n' \
|
||||
"$rocks_before" "$rocks_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$rocks_multi_before" == "$rocks_multi_after" && "$rocks_random_before" == "$rocks_random_after" ]] || {
|
||||
printf 'Repeated or unmodified Rocks changed across restart\n' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$authored_vegetation" == "$authored_vegetation_after" ]] || {
|
||||
printf 'Authored vegetation changed across restart: %s -> %s\n' \
|
||||
"$authored_vegetation" "$authored_vegetation_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$dungeons_before" == "$dungeons_after" ]] || {
|
||||
printf 'Authored Dungeons changed across restart: %s -> %s\n' \
|
||||
"$dungeons_before" "$dungeons_after" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ "$lakes_before" == "$lakes_after" && "$lakes_random_before" == "$lakes_random_after" ]] || {
|
||||
printf 'Explicit or random lake plans/blocks changed across restart\n' >&2
|
||||
exit 1
|
||||
}
|
||||
stop_server
|
||||
printf '26.3 dedicated-server smoke passed: two distinct Ages persisted across restart.\n'
|
||||
printf '26.3 dedicated-server smoke passed: terrain, biomes, Rocks, vegetation, Dungeons, lakes, and Floating Islands persisted across restart.\n'
|
||||