Public Access
Compare commits
60
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38e40ddba3 | ||
|
|
a7a73d3c17 | ||
|
|
48fbae9be9 | ||
|
|
c223f3fdca | ||
|
|
d75154fa9b | ||
|
|
9ccec18c18 | ||
|
|
d9464fad76 | ||
|
|
d6e9a58a24 | ||
|
|
fa294be685 | ||
|
|
cb31156584 | ||
|
|
c23b057f51 | ||
|
|
9ab8d3de4a | ||
|
|
56005ac512 | ||
|
|
9ff931afa9 | ||
|
|
eda19de43c | ||
|
|
0fb2f7c1bb | ||
|
|
4f07f675a4 | ||
|
|
b8be99606f | ||
|
|
98595c83ea | ||
|
|
e7e2cf7e5a | ||
|
|
77f15f8599 | ||
|
|
2b2605b277 | ||
|
|
2f1e6bd6f2 | ||
|
|
9d5ff2a2cb | ||
|
|
3ac4c499ab | ||
|
|
9044835ec5 | ||
|
|
7ae41da23e | ||
|
|
9cb63e6994 | ||
|
|
6337b5aa10 | ||
|
|
b9faa28dd5 | ||
|
|
09d2b0800c | ||
|
|
ee905cab68 | ||
|
|
5c08f391a2 | ||
|
|
b8d26a9036 | ||
|
|
d0af75239a | ||
|
|
2e5d6e0427 | ||
|
|
54c10cca6c | ||
|
|
c39ebd78ba | ||
|
|
1ff62e9aaa | ||
|
|
b154455563 | ||
|
|
4a87c9fae6 | ||
|
|
350d137d9e | ||
|
|
59bd1c7f6c | ||
|
|
644d242bba | ||
|
|
3423dd25bf | ||
|
|
c560e328de | ||
|
|
34ffd317aa | ||
|
|
c2c2d7a6f8 | ||
|
|
44205f80e0 | ||
|
|
4f464dbe60 | ||
|
|
783a33456d | ||
|
|
7b5597dd47 | ||
|
|
78a0f7ee51 | ||
|
|
dd219bb58a | ||
|
|
b48cc8a6ce | ||
|
|
779699d7a9 | ||
|
|
33b9bde302 | ||
|
|
a9a8b3cdd5 | ||
|
|
066ef2b758 | ||
|
|
8b32735c2e |
No files matched your search
@@ -32,6 +32,10 @@ bin/
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
### Python tools ###
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
### Minecraft Modding ###
|
||||
run/
|
||||
logs/
|
||||
|
||||
@@ -19,6 +19,10 @@ under the same license; desk wood and inlay textures are cropped from desk.png.
|
||||
The Ink Mixer GUI atlas and block textures are copied from Legacy's
|
||||
assets/mystcraft/gui/inkmixer.png and textures/blocks/inkmixer_*.png under
|
||||
the same license. Rivencraft's block model is original and uses those textures.
|
||||
The bundled mystcraft_library.nbt structure template is a block-layout adaptation
|
||||
of Mystcraft Legacy's ComponentScatteredFeatureSmallLibrary.java at commit
|
||||
9bc8ddc061845df0cd5ea47f4fdef773cff786ae, under LGPL-3.0. The conversion
|
||||
script at scripts/generate_legacy_library_template.py is included to rebuild it.
|
||||
|
||||
The initial build layout is derived from the official NeoForge 1.21.1
|
||||
ModDevGradle MDK: https://github.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle
|
||||
@@ -1,249 +1,129 @@
|
||||
# Rivencraft
|
||||
|
||||
Rivencraft is a clean, incremental port of the ideas and gameplay of
|
||||
[Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to Minecraft
|
||||
1.21.1 on NeoForge.
|
||||
Write a book. Name an Age. Step into a world that did not exist before.
|
||||
|
||||
The bootstrap, books/linking, Ages, writing-system, and Age-grammar milestones are complete.
|
||||
The writing system uses a data-pack symbol catalog and persistent
|
||||
ordered-writing model. Linking Books and Descriptive Books create persistent
|
||||
Ages from either authored supported symbols or seeded random completion.
|
||||
Instability and the remaining unsupported Legacy symbol effects have not been
|
||||
ported yet. The [Milestone 5 acceptance record](docs/MILESTONE_5_ACCEPTANCE.md)
|
||||
lists the supported grammar, optional integrations, and limits.
|
||||
Rivencraft brings the books, writing stations, symbols, and unpredictable Ages
|
||||
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 independently developed project, not a byte-for-byte copy of
|
||||
the 1.12.2 mod.
|
||||
|
||||
Milestone 4 now includes stackable blank Pages plus server-created symbol,
|
||||
unresolved-symbol, and baseline link-panel states. Page visuals follow the
|
||||
legacy runtime-composition rules. Craft an Ink Vial from two black dye, a glass
|
||||
bottle, and a water bucket; the bucket is returned. Hold vanilla paper in one
|
||||
hand and use the Ink Vial in the other to prepare a baseline Link Panel Page.
|
||||
Each vial holds 16 ink units, and the default link-panel cost is two units.
|
||||
The spent vial returns a glass bottle. Server owners can adjust the symbol-copy
|
||||
and link-panel costs in the `writing` section of `rivencraft-server.toml`.
|
||||
The Ink Mixer now offers a Legacy-style station for producing Link Panels:
|
||||
insert paper and Ink Vials, then take a Link Panel from the result slot. Each
|
||||
vial fills its 16-unit basin and returns a glass bottle; taking one Link Panel
|
||||
uses the full vial. No server tick is needed. The earlier paper-in-one-hand
|
||||
method still uses the configurable link-panel ink cost. Ink Mixer
|
||||
additives can roll Disarm, Maintain Momentum, and Intra Linking onto a Link
|
||||
Panel. Descriptive Books retain Disarm and Momentum; Intra Linking is only
|
||||
meaningful for same-dimension travel with a Linking Book.
|
||||
TerraBlender 1.21.1 integration is optional: registered modded biomes receive
|
||||
Biome pages, with cave and Nether biomes in a separate Cave Biome creative tab.
|
||||
TerraBlender Overworld/Nether regions extend compatible Age
|
||||
biome pools. See [the compatibility matrix](docs/TERRABLENDER_COMPATIBILITY.md)
|
||||
for supported versions and in-game verification.
|
||||
Serene Seasons 1.21.1 can maintain separate season progress in an Age dimension.
|
||||
Rivencraft automatically whitelists loaded Ages without a restart when Serene is installed;
|
||||
season pages can instead opt an Age out or hold it in one season.
|
||||
See [the optional compatibility guide](docs/SERENE_SEASONS_COMPATIBILITY.md)
|
||||
for the supported manual configuration and current limits; Rivencraft does not
|
||||
require Serene Seasons or GlitchCore.
|
||||
The Writing Desk can copy a selected written page from its collection tabs.
|
||||
The Writing Desk is now craftable from planks, a glass bottle, and a feather.
|
||||
It occupies two adjacent blocks, with one shared inventory; loaded paper and
|
||||
the selected page appear on its Legacy-inspired model.
|
||||
Desks placed before 0.3.6.1 keep their old one-block footprint until broken
|
||||
and placed again; leave an adjacent space when replacing one.
|
||||
Its 88 collection tabs, paper input, ink vial input, empty glass-bottle return,
|
||||
16-unit ink meter, and inventory persist
|
||||
through world saves; breaking it drops stored contents. The desk screen is a
|
||||
functional foundation using Legacy's original desk and scrollbar textures.
|
||||
Four tabs are visible at once; the controls above and below them move by four
|
||||
(1–4, 5–8, through 85–88). Each viewer can browse a different group. Existing
|
||||
four-tab desks retain their collections and supplies when upgraded.
|
||||
With a notebook selected, pick up a Page from your inventory and click an empty
|
||||
position on the left surface to store one page there. Clear search/filter/sort
|
||||
and turn off `ALL` to place pages at specific positions; occupied positions are
|
||||
never overwritten. A notebook tab exposes all 27 notebook positions (scroll to
|
||||
reach the rest); a loose-page tab exposes only its one occupied position.
|
||||
Empty tabs have no page storage until a notebook or page is put in the tab slot.
|
||||
Click a page on the desk surface to select it for copying. Drag a page from one
|
||||
notebook position to an empty position to move it, or onto another page to swap
|
||||
their positions. This also works between notebooks shown in the `ALL` view.
|
||||
Drag a page out of the panel to turn it back into an item. Drop it over a player
|
||||
inventory slot or desk tab to place it there; a tab with a notebook adds it to
|
||||
the first open position. Dropping elsewhere leaves the page on the cursor for
|
||||
normal inventory placement. The source notebook or tab loses the page only after
|
||||
a valid drop.
|
||||
Put vanilla paper in the
|
||||
upper-left slot and Ink Vials in the upper-right; a copy appears in the
|
||||
lower-left result slot when the selected page is valid. Taking it consumes one
|
||||
paper and, by default, one ink. An available vial refills an empty ink meter
|
||||
when inserted or when copying uses its last ink, and returns a vanilla glass
|
||||
bottle to the lower-right slot. Removing a bottle also retries a blocked refill.
|
||||
Refilling waits if that slot is full or contains another item. The `AZ` control sorts
|
||||
visible pages by name; `ALL` shows pages held across every desk tab. Search and
|
||||
category filtering narrow the view without granting new pages, and cross-tab
|
||||
copy selection is checked by the server.
|
||||
Selecting a stored page on the desk surface shows its Legacy-proportioned
|
||||
portrait preview and name above the player inventory.
|
||||
The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
|
||||
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
|
||||
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
|
||||
supports authored terrain/biomes, structure and vegetation pages, independent
|
||||
Age weather/lighting, seeded and colorable Overworld/Nether/End/Static skies,
|
||||
Normal Time length/phase/direction/bearing and sun-size controls,
|
||||
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.78-beta.1.md).
|
||||
Keep the builds and their saves separate, and back up worlds before updating.
|
||||
|
||||
## Book Binder
|
||||
## Getting started
|
||||
|
||||
For a new survival Age: craft an Ink Vial, an Ink Mixer, a Writing Desk, and a Book Binder.
|
||||
Put vanilla paper and an Ink Vial in the Ink Mixer to make a Link Panel
|
||||
Page. Place leather (or an empty Notebook) and the Link Panel in the Book Binder,
|
||||
enter an Age name, and take the finished Descriptive Book. Written symbol Pages
|
||||
are optional; without them, the book creates a random Age. To duplicate a Page
|
||||
you already own, store it in a Writing Desk tab, select it, supply paper and ink,
|
||||
and take the copied Page from the output. A Notebook can hold and transfer a
|
||||
larger ordered collection of Pages between desks and the binder.
|
||||
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
|
||||
Craft an Ink Mixer, Writing Desk, and Book Binder.
|
||||
2. Put paper and an Ink Vial in the Ink Mixer and take a Link Panel Page. Add
|
||||
ingredients to the basin first if you want a chance of link modifiers.
|
||||
3. Put leather or an empty Notebook and the Link Panel in the Book Binder.
|
||||
Name the Age and take the Descriptive Book. A book with only a Link Panel
|
||||
creates a seeded random Age; add Symbol Pages to author its features.
|
||||
4. Open the book to create its Age, then click the destination preview to
|
||||
travel. Bring a bound Linking Book for the return trip: automatic Return
|
||||
Links are off by default.
|
||||
|
||||
With JEI installed, Rivencraft displays the Ink Mixer operation in its own
|
||||
category and the hand-use, copying, binding, and Linking Book crafting flows in a Writing category.
|
||||
The Link Panel and individual symbol Pages, including registered vanilla biome
|
||||
pages, appear as separately searchable JEI ingredients.
|
||||
Creative inventory separates Link Pages and each symbol category into their own
|
||||
tabs, including distinct Biome and Biome Generation tabs. Blank Pages are hidden
|
||||
from creative inventory and JEI.
|
||||
JEI is optional; vanilla crafting
|
||||
recipes and all writing interactions work without it. For a development client
|
||||
with JEI, run `./gradlew runClient -PwithJei=true`.
|
||||
Find new Symbol Pages in Age libraries, or copy pages you already own with
|
||||
paper and ink at the Writing Desk. The Notebook stores ordered pages, including
|
||||
intentional gaps. Written pages can control terrain, biome distribution,
|
||||
weather, lighting, seasons, sky color, suns and moons, decorations, structures,
|
||||
and more. Incorrect or excessive writing can make an Age unstable; instability
|
||||
may bring decay, lightning, explosions, meteors, burning, erosion, and other
|
||||
hazards. Some combinations are still unsupported and the Binder reports them
|
||||
before consuming ingredients.
|
||||
|
||||
Craft a Book Binder from three iron ingots and five planks. Put leather or an
|
||||
empty Notebook in the left cover slot. Enter a title, then place a Link Panel
|
||||
Page first in the horizontal page strip. Click a page in the strip with a
|
||||
written Page on the cursor to insert it before that position; click it with an
|
||||
empty cursor to take it out. The arrows or mouse wheel scroll through up to
|
||||
27 written Pages. Clicking the strip with a filled Notebook imports its Pages
|
||||
at that position and leaves the emptied Notebook on the cursor. The right slot
|
||||
outputs a Descriptive Book once the title, cover, and Link Panel are present.
|
||||
Inputs are consumed only when the result is taken.
|
||||
Place a Rivencraft book or page on a vanilla Lectern to convert it to a Book
|
||||
Stand. Crystal formations yield shards for Crystal Blocks. A connected crystal
|
||||
frame with a Book Receptacle and a bound book forms a portal; frames can stand
|
||||
upright or lie flat. Libraries, Book Stands, and portals use Legacy-inspired
|
||||
assets and sounds. Handheld travel drops the used book at the departure point,
|
||||
so plan how you will retrieve it.
|
||||
|
||||
The completed book receives its Age identity when assembled. With only a Link
|
||||
Panel, it describes a randomly generated Age. Authored writing is compiled once
|
||||
into a versioned, server-owned Age plan. The plan freezes the Age seed, resolved
|
||||
symbol meanings, page order, and runtime profile, so later data-pack changes do
|
||||
not reinterpret an existing Age. An authored terrain page selects its runtime
|
||||
profile. Built-in pages cover Standard, Amplified, Cave, Island, Flat, and Void;
|
||||
authored biome, environment, and supported feature pages also affect new Ages.
|
||||
The
|
||||
writing is also stored on the book; the Descriptive Book screen's turned pages
|
||||
show ordered symbols and unresolved IDs. The Descriptive Book screen follows
|
||||
Legacy's book spread: the title and Age details are on the left, the destination
|
||||
preview is on the right, and clicks in the bottom corners turn pages. Ordered
|
||||
symbol pages appear before the final blank spread with their runtime-rendered
|
||||
pictograms. Link-panel-only books use Rivencraft glyphs for their generated
|
||||
Age description.
|
||||
## Requirements and optional integrations
|
||||
|
||||
## Linking Book
|
||||
Install the same gameplay mods on the client and server:
|
||||
|
||||
Craft an unwritten Linking Book from one Link Panel Page and one leather in any
|
||||
two crafting-grid positions. An unbound book opens a screen where it can be named and bound to the
|
||||
current location. A bound book opens its destination screen and travels when
|
||||
its preview panel is clicked. The destination is stored on the item and
|
||||
survives inventory and world saves. Travel only succeeds if the target
|
||||
dimension is loaded and a non-fluid, passable landing position can be found
|
||||
nearby.
|
||||
The Linking Book uses the same Legacy page layout and bottom-corner page turns
|
||||
as the Descriptive Book, but without the Descriptive Book's gold cover edges.
|
||||
Both screens play Minecraft's book-page-turn sound when a page is turned.
|
||||
- Minecraft **1.21.1**, NeoForge **21.1.264 or newer in the 21.1 line**, and
|
||||
Java 21.
|
||||
- **Dynamic Dimensions AR Edition 0.9.1** and its **Bad Packets 0.8.2**
|
||||
dependency. These are required for Age dimensions.
|
||||
|
||||
Successful book travel plays Mystcraft Legacy's original linking sound at both
|
||||
the departure and arrival locations.
|
||||
Rivencraft works without any of these optional integrations:
|
||||
|
||||
Right-click an empty vanilla lectern with a Rivencraft book to convert it into
|
||||
a Book Stand and place the book on it. Use a Linking Book on the stand by
|
||||
right-clicking with an empty hand. Sneak-right-click with an empty hand to take
|
||||
the book back and restore the vanilla lectern. Breaking a Book Stand drops both
|
||||
the stored book and a vanilla lectern.
|
||||
| Mod | What it adds |
|
||||
| --- | --- |
|
||||
| JEI | Searchable Pages and guides for the Ink Mixer, Writing Desk, Book Binder, and Linking Book crafting. |
|
||||
| Create (bundles Ponder) | Ponder book snapshots and a crystal-portal construction guide. |
|
||||
| Immersive Portals | Live destination views in book previews and crystal portals. In `AUTO` preview mode it takes priority over Ponder. |
|
||||
| TerraBlender | Eligible biomes from mods using its Overworld/Nether regions become Age biome candidates and Pages. Biomes O' Plenty is one example; it is not hard-coded or required. |
|
||||
| Serene Seasons (with GlitchCore) | Independent per-Age seasons, including normal seasons, no seasons, and four eternal-season modes. |
|
||||
| Ruins | Authorable Ruins-template Pages and Ruins structures in Ages. |
|
||||
|
||||
Bound Linking Books show a rotating Ponder snapshot when Create is installed.
|
||||
Without Create, they use a built-in animated linking panel tinted by the
|
||||
destination sky. The server independently validates every link.
|
||||
The optional builds used in the latest development checks are JEI
|
||||
19.69.0.457, Create 6.0.10, Immersive Portals 6.0.7, TerraBlender 4.1.0.8,
|
||||
Serene Seasons 10.1.0.9, and Ruins 1.21.1.2NF. See the
|
||||
[compatibility review](docs/MILESTONE_7_COMPATIBILITY_REVIEW.md) for the
|
||||
evidence and limits of those combinations.
|
||||
|
||||
## Descriptive Book
|
||||
Without Create or Immersive Portals, book previews use Rivencraft's built-in
|
||||
Legacy-style animation. Ecliptic Seasons is **not supported**: its 1.21.1 API
|
||||
does not offer the live dimension/season control Rivencraft needs.
|
||||
|
||||
Open a page-free Descriptive Book, name its Age, and press Create. Rivencraft
|
||||
assigns the book a permanent Age UUID, selects a deterministic base terrain,
|
||||
creates a dedicated runtime dimension, and prepares a five-by-five cobblestone
|
||||
arrival platform at the
|
||||
highest suitable nearby terrain. Solid blocks and tree leaves are valid
|
||||
footing. Candidate columns are scanned from the top down so cave floors are not
|
||||
selected, and fluid-covered columns are excluded. If no suitable surface is
|
||||
available, the platform is generated at Y=63. Reopen the book and click its preview to travel.
|
||||
The book cannot reopen while its Age is being generated, whether held or on a
|
||||
Book Stand. Once creation finishes, reopening shows the written book.
|
||||
Copies of the written book continue to resolve to the same persistent Age
|
||||
across server restarts.
|
||||
## Saves, settings, and current limits
|
||||
|
||||
New Age dimension folders use a Minecraft-safe lowercase form of the assigned
|
||||
name. Conflicts receive incrementing suffixes such as `_2` and `_3`; existing
|
||||
Age folders are never renamed.
|
||||
Ages and bound books use persistent UUIDs. Back up the **entire world folder**
|
||||
before changing mod versions, data packs, or optional biome mods: the Age
|
||||
registry and dimension data must be restored together. Rivencraft preserves
|
||||
known older save formats where possible, but pre-public-release worlds are not
|
||||
promised universal compatibility. Missing custom symbols retain their IDs in
|
||||
books; missing registry entries or dimension providers can still prevent an
|
||||
Age from loading. See [current implementation](docs/CURRENT_IMPLEMENTATION.md)
|
||||
for supported behavior and deliberate limits.
|
||||
|
||||
Page-free Ages deterministically select one of six base terrains inspired by
|
||||
Mystcraft Legacy: Standard, Amplified, Island, Cave, Flat, or Void. The chosen
|
||||
profile is saved with the Age and does not change when the server restarts.
|
||||
Server settings include `descriptive_books.return_link_mode` (default
|
||||
`DISABLED`), `libraries.loot_mode` (`LEGACY` or `AGE_ONLY`), writing ink costs,
|
||||
season enrollment, and instability controls. The client setting
|
||||
`book_preview.mode` chooses `AUTO`, `IMMERSIVE_PORTALS`, `PONDER`, or `LEGACY`.
|
||||
An operator can inspect Ages and instability with `/rivencraft age` and find
|
||||
libraries with `/rivencraft library nearest`. The creative-only Link Modifier
|
||||
can edit link properties and retire an Age; use it carefully and keep backups.
|
||||
Binder status messages are off by default. An OP can run
|
||||
`/rivencraft binder messages enable` or `/rivencraft binder messages disable`
|
||||
to control them; only OPs see the diagnostics even when enabled.
|
||||
|
||||
Linking creates a bound Return Link to the exact source position and orientation
|
||||
according to the configured Return Link policy.
|
||||
## Project and development
|
||||
|
||||
Server owners can set `descriptive_books.return_link_mode` in Rivencraft's
|
||||
server config to `ON_CREATION` (default), `ALWAYS`, or `DISABLED`.
|
||||
Age creation, persistence, lifecycle commands, configuration, backups, and
|
||||
administrator recovery are documented in [docs/AGES.md](docs/AGES.md).
|
||||
Data-pack runtime profiles are documented in
|
||||
[docs/AGE_PROFILES.md](docs/AGE_PROFILES.md).
|
||||
This repository is the development source. See the [current implementation](docs/CURRENT_IMPLEMENTATION.md),
|
||||
[milestone summaries](docs/MILESTONES.md), [technical document index](docs/README.md),
|
||||
and [issues](https://git.paddedcell.org/dnorberg/rivencraft/issues).
|
||||
Help build the [Rivencraft wiki](https://git.paddedcell.org/dnorberg/rivencraft/wiki)
|
||||
with guides for Ages, pages, and crafting, or open an issue with documentation
|
||||
suggestions and corrections.
|
||||
Milestone 7 is complete in 1.0.0.2. Its [acceptance record](docs/MILESTONE_7_ACCEPTANCE.md)
|
||||
and [regression matrix](docs/MILESTONE_7_REGRESSION.md) distinguish automated
|
||||
checks from manual evidence and known limitations.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A Java 21 JDK
|
||||
- Minecraft 1.21.1
|
||||
- NeoForge 21.1.251 or newer in the 21.1 line
|
||||
- Dynamic Dimensions AR Edition 0.9.1 and Bad Packets 0.8.2 (client and server)
|
||||
|
||||
Create is optional. When installed, Rivencraft uses its Ponder renderer for
|
||||
book previews when Immersive Portals is absent. Immersive Portals is also
|
||||
optional and takes priority in the default `AUTO` mode, providing a live
|
||||
rotating destination view. Its Explore button enables WASD and jump/sneak
|
||||
camera movement plus left-drag camera look; right-click returns to the rotating
|
||||
view. Without either integration, Rivencraft uses its built-in animated linking
|
||||
panel. Clients can set `book_preview.mode` in
|
||||
`rivencraft-client.toml` to `AUTO`, `IMMERSIVE_PORTALS`, `PONDER`, or `LEGACY`.
|
||||
Unavailable optional renderers safely fall back to `LEGACY`.
|
||||
The complete backend matrix, supported combinations, cleanup behavior, and
|
||||
known upstream limits are documented in
|
||||
[docs/PREVIEW_COMPATIBILITY.md](docs/PREVIEW_COMPATIBILITY.md).
|
||||
|
||||
## Build and run
|
||||
|
||||
```shell
|
||||
```sh
|
||||
./gradlew build
|
||||
./gradlew runClient
|
||||
./gradlew runGameTestServer
|
||||
```
|
||||
|
||||
The built mod is written to `build/libs/`.
|
||||
The default dedicated-server GameTest run is deliberately small. Use
|
||||
`./gradlew runGameTestServer -PwithArchivedGameTests=true` for the complete
|
||||
regression suite before a release or when investigating a related issue. See
|
||||
[docs/GAMETESTS.md](docs/GAMETESTS.md).
|
||||
The JAR is written to `build/libs/`. Development runs need the required mods;
|
||||
see [GameTest guidance](docs/GAMETESTS.md) for focused archived suites.
|
||||
Versions use `x.y.z.b`: major release, completed milestone, release slice,
|
||||
then fixes/additional work within that slice.
|
||||
|
||||
## Versioning
|
||||
|
||||
Rivencraft versions use `x.y.z.b`:
|
||||
|
||||
- `x` changes for a major public release.
|
||||
- `y` changes when a development milestone is completed.
|
||||
- `z` changes for each release slice within the current milestone.
|
||||
- `b` changes for bug fixes and additional work within that release slice.
|
||||
|
||||
The build component resets to zero when a new release slice begins. Completing
|
||||
a milestone increments `y` and resets both `z` and `b` to zero. For example,
|
||||
milestone 2 development can progress from `0.1.1.0` to `0.1.1.1`, then
|
||||
`0.1.2.0`; completing the milestone produces `0.2.0.0`.
|
||||
|
||||
## Porting approach
|
||||
|
||||
Mystcraft Legacy targets Forge for Minecraft 1.12.2. Rivencraft therefore
|
||||
ports one subsystem at a time onto current registries, codecs, data packs, and
|
||||
NeoForge APIs instead of importing the legacy source tree wholesale.
|
||||
|
||||
See [docs/PORTING.md](docs/PORTING.md) for the roadmap, live Gitea tracking
|
||||
links, architectural direction, and provenance rules.
|
||||
|
||||
## License
|
||||
|
||||
Rivencraft is licensed under LGPL-3.0-or-later. Mystcraft Legacy is also
|
||||
LGPL-3.0; copied or adapted upstream code must retain its copyright and license
|
||||
notices. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
||||
Rivencraft is licensed under LGPL-3.0-or-later. Legacy-derived material is
|
||||
credited in [NOTICE](NOTICE) and [asset provenance](docs/ASSET_PROVENANCE.md).
|
||||
+8
-1
@@ -17,7 +17,7 @@ tasks.named('wrapper', Wrapper).configure {
|
||||
version = mod_version
|
||||
group = mod_group_id
|
||||
|
||||
List<String> archivedGameTestSuites = ['linking', 'environment', 'writing', 'grammar', 'generation', 'desk', 'binder', 'mixer', 'recipe']
|
||||
List<String> archivedGameTestSuites = ['linking', 'environment', 'writing', 'grammar', 'generation', 'desk', 'binder', 'mixer', 'recipe', 'instability']
|
||||
List<String> selectedArchivedGameTestSuites = providers.gradleProperty('withArchivedGameTests')
|
||||
.map { it.toBoolean() }.orElse(false).get() ? archivedGameTestSuites
|
||||
: providers.gradleProperty('archivedGameTestSuites').orElse('').get()
|
||||
@@ -132,6 +132,8 @@ neoForge {
|
||||
|
||||
server {
|
||||
server()
|
||||
// Keep dedicated compatibility runs away from the normal dev world.
|
||||
gameDirectory = project.file(project.findProperty('serverRunDir') ?: 'run')
|
||||
programArgument '--nogui'
|
||||
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
|
||||
}
|
||||
@@ -243,6 +245,11 @@ dependencies {
|
||||
// Keep them on the compile classpath without forcing either mod at runtime.
|
||||
compileOnly "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
|
||||
compileOnly "maven.modrinth:immersive-portals-neoforge:${immersive_portals_version}"
|
||||
// Ruins is optional; its pre-spawn event is only registered when Ruins is loaded.
|
||||
compileOnly 'curse.maven:ruins-227873:6391825'
|
||||
if (providers.gradleProperty('withRuins').map { it.toBoolean() }.orElse(false).get()) {
|
||||
localRuntime 'curse.maven:ruins-227873:6391825'
|
||||
}
|
||||
// TerraBlender's region API is only used by the guarded compat adapter.
|
||||
compileOnly "com.github.glitchfiend:TerraBlender-neoforge:${minecraft_version}-${terrablender_version}"
|
||||
if (providers.gradleProperty('withTerraBlender').map { it.toBoolean() }.orElse(false).get()) {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 238 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 510 KiB |
+13
-13
@@ -11,7 +11,7 @@ suffixes such as `_2` and `_3`.
|
||||
|
||||
The selected terrain profile, biome distribution, lighting, weather, celestial
|
||||
period, celestial angle, and phase are deterministic and become part of the
|
||||
saved definition. A five-by-five cobblestone platform with clear headroom is
|
||||
saved definition. A five-by-five non-dropping Crystal Platform with clear headroom is
|
||||
prepared at a safe arrival point. The completed book and every copy carrying
|
||||
the same UUID resolve to that definition.
|
||||
|
||||
@@ -42,10 +42,11 @@ on demand when an administrator previously unloaded it. Unloading retains the
|
||||
definition and world folder, so existing books continue to reference the same
|
||||
Age and later reload the same terrain.
|
||||
|
||||
Rivencraft does not automatically delete Ages. Safe permanent deletion needs a
|
||||
complete reference index for books in inventories, containers, Book Stands,
|
||||
and unloaded chunks. Until that exists, retaining the definition prevents an
|
||||
administrator action from silently orphaning books.
|
||||
Rivencraft does not automatically delete Ages. The operator-only creative Link
|
||||
Modifier can deliberately retire an unoccupied Age: it unloads the dimension,
|
||||
moves its data to a sibling `<world>-retired-ages/<uuid>` archive, and records
|
||||
the UUID as dead so existing books cannot silently regenerate it. Back up the
|
||||
whole world first.
|
||||
|
||||
Destination-preview chunk loaders are temporary. Rivencraft releases them when
|
||||
the book closes, the player travels, the player disconnects, the destination
|
||||
@@ -69,10 +70,9 @@ definition.
|
||||
Rivencraft writes gameplay settings to the world/server configuration managed
|
||||
by NeoForge. `descriptive_books.return_link_mode` accepts:
|
||||
|
||||
- `ON_CREATION` (default): create one Return Link the first time a player
|
||||
enters each Age.
|
||||
- `DISABLED` (default): never create Return Links automatically.
|
||||
- `ON_CREATION`: create one Return Link the first time a player enters each Age.
|
||||
- `ALWAYS`: create a Return Link after every successful Descriptive Book trip.
|
||||
- `DISABLED`: never create Return Links automatically.
|
||||
|
||||
Each client has a separate `rivencraft-client.toml`. Its `book_preview.mode`
|
||||
setting accepts `AUTO`, `IMMERSIVE_PORTALS`, `PONDER`, or `LEGACY`. This changes
|
||||
@@ -102,8 +102,8 @@ Use this sequence when a book cannot load an existing Age:
|
||||
matching dimension data and authoritative world saved data from the same
|
||||
backup generation. Do not combine them from different backups.
|
||||
|
||||
There is intentionally no delete command in Milestone 3. Do not manually
|
||||
delete an Age definition or rename its dimension folder: books may exist in
|
||||
inventories, containers, Book Stands, or unloaded chunks, and would be
|
||||
orphaned. `/rivencraft age unload` is the supported reversible maintenance
|
||||
operation.
|
||||
Do not manually delete an Age definition or rename its dimension folder:
|
||||
books may exist in inventories, containers, Book Stands, or unloaded chunks.
|
||||
`/rivencraft age unload` remains the reversible maintenance operation. The
|
||||
Link Modifier's retirement control is not reversible through a book UI;
|
||||
recover its archived data and registry together from a full-world backup.
|
||||
@@ -0,0 +1,7 @@
|
||||
# Age arrival platform
|
||||
|
||||
Newly created Ages place a 5×5 solid platform instead of cobblestone at their saved spawn. The block has full collision, renders as an opaque full block, breaks as easily as glass (0.3 hardness), has no loot table, and cannot be obtained with pick-block. Existing Ages retain their already-placed platform blocks, which adopt the new appearance and hardness without altering saved terrain.
|
||||
|
||||
The model reuses `assets/rivencraft/textures/block/crystal_block.png`, the 16×16 Legacy crystal tile. The former generated cloud texture is removed. The internal block ID remains `rivencraft:cloud_platform` to preserve saved worlds.
|
||||
|
||||
The separate [decay provider](INSTABILITY_DECAY.md) protects the saved spawn area by position, so its protection remains in force regardless of the platform's material.
|
||||
+22
-1
@@ -40,6 +40,27 @@ profiles, not claims that Legacy had identically named terrain symbols.
|
||||
Every family retains a safe cobblestone arrival platform. An omitted terrain
|
||||
page still uses the deterministic seed-selected profile.
|
||||
|
||||
### Cave terrain and Legacy
|
||||
|
||||
Legacy's closest terrain generator is its Nether terrain symbol: it owns an
|
||||
Age-seeded octave-noise generator, accepts terrain/sea materials, and sets a
|
||||
sea level of 32. Legacy's separate Caves symbol instead registers a terrain
|
||||
alteration that carves additional caves; it is not a terrain generator.
|
||||
Minecraft 1.21.1 no longer has Legacy's 1.12.2 height and generation pipeline,
|
||||
so Rivencraft Cave terrain uses `minecraft:caves` noise and the modern Cave,
|
||||
Nether, and tagged modded Nether biome decorations. It cannot reproduce the
|
||||
exact Legacy noise or its older vertical layout. The Caves feature page
|
||||
remains a separate additive carver for other suitable terrain.
|
||||
|
||||
Cave Ages search for safe spawn footing from Y=64 downward rather than using
|
||||
the cave roof; if necessary, the usual cloud platform is placed at Y=63.
|
||||
Weather is clear and Serene Seasons is disabled in Cave terrain. Authored
|
||||
Weather, Season, and Celestial pages (including time modifiers) are retained
|
||||
in the book but have no effect on the Cave Age; each adds 100 base instability.
|
||||
The same policy applies to randomly selected Cave terrain and persists through
|
||||
save/reload. Cave biome selections remain limited to Cave and Nether biomes;
|
||||
surface-biome completion is not used for Cave terrain.
|
||||
|
||||
The separate **Floating Islands** feature page adds rare high-altitude island
|
||||
clusters above Standard, Amplified, Sky Islands, Flat, or Void terrain; it does
|
||||
not replace the terrain page. The feature is seeded by the Age and saved with the written
|
||||
@@ -199,7 +220,7 @@ eligible biomes for Tiled/Grid and Tiny–Huge. The eligible pool comes from the
|
||||
terrain's biome preset, or `#rivencraft:cave_biomes` for Cave terrain, so future
|
||||
biome integrations can expand it without changing the count rule. Single uses
|
||||
one; Native uses its preset biome pool. This random-age maximum does not truncate
|
||||
authored pages: the Binder's 27 symbol-page positions bound an authored book.
|
||||
authored pages: the Binder's 252 symbol-page positions bound an authored book. Legacy's Binder itself had no explicit page-count check; this remains a fixed-slot storage ceiling.
|
||||
Native cannot be paired with explicit biome pages. Legacy filled Tiny–Huge to
|
||||
three; Rivencraft deliberately uses two so a pair of written biome pages stays
|
||||
exactly as written.
|
||||
|
||||
@@ -19,15 +19,21 @@ project's GNU LGPL v3 license. Rivencraft itself uses the same license.
|
||||
| `assets/rivencraft/textures/block/book_binder_press.png` | `assets/mystcraft/textures/entity/bookbinder/press_part.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Book Binder model uses this press texture |
|
||||
| `assets/rivencraft/textures/gui/book_binder_legacy.png` | `assets/mystcraft/gui/pagebinder.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Book Binder GUI uses the original 176×181 panel |
|
||||
| `assets/rivencraft/textures/gui/ink_mixer_legacy.png` | `assets/mystcraft/gui/inkmixer.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Ink Mixer GUI uses the original 176×181 panel |
|
||||
| `assets/rivencraft/textures/block/ink_mixer_top.png`, `ink_mixer_side.png`, `ink_mixer_bottom.png` | `assets/mystcraft/textures/blocks/inkmixer_top.png`, `inkmixer_side.png`, `inkmixer_bottom.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only; Rivencraft supplies its own block model |
|
||||
| `assets/rivencraft/textures/block/inkwell_base.png`, `inkwell_cup.png`, `ink.png`, `ink_glass.png` | `assets/mystcraft/textures/entity/inkwell/` counterparts | Legacy 0.13.7.06 local extraction | Byte-identical copies used by the ported Ink Mixer geometry; the paper overlay uses vanilla white concrete instead of Legacy's black surface |
|
||||
| `assets/rivencraft/textures/item/linking_book_unwritten.png` | `assets/mystcraft/textures/items/unlinked.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Namespace/path rename only |
|
||||
| `assets/rivencraft/textures/item/page_link_panel.png` | `page_background.png` and `PageBuilder.BasicPageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Reproduces the legacy 160 px scale, black panel at `(25,30)`–`(135,75)`, and 128 px resample |
|
||||
| `assets/rivencraft/textures/gui/symbolcomponents.png` | `assets/mystcraft/textures/symbolcomponents.png` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Byte-identical shared 512×512 component sheet for client-side page composition |
|
||||
| Runtime-composed symbol pages (`SymbolPageTextures`) | `page_background.png`, `textures/symbolcomponents.png`, `WordData`, `ModSymbols`, and `PageBuilder.PageSprite` | `9bc8ddc061845df0cd5ea47f4fdef773cff786ae` | Replaces the six pre-generated symbol PNGs; layers four poem words on the shared page background and caches bounded client textures |
|
||||
| `assets/rivencraft/textures/block/crystal_block.png` | `assets/mystcraft/textures/blocks/crystal.png` | Legacy 0.13.7.06 local extraction | Exact 16×16 blue tile shared by Crystal Block and Budding Crystal models after removing a byte-identical duplicate |
|
||||
| `assets/rivencraft/textures/block/crystal_block.png` | `assets/mystcraft/textures/blocks/crystal.png` | Legacy 0.13.7.06 local extraction | Exact 16×16 blue tile shared by Crystal Block, Budding Crystal, and the non-dropping Age arrival platform; its former cloud sprite is removed |
|
||||
| `assets/rivencraft/textures/block/{small,medium,large}_crystal_bud.png`, `crystal_cluster.png` | Minecraft 1.21.1 `textures/block/{small,medium,large}_amethyst_bud.png`, `amethyst_cluster.png` | Minecraft 1.21.1 | Original 16×16 alpha masks and silhouettes preserved; hue shifted toward Legacy blue, with brighter saturation; exported as 8-bit RGBA (not indexed PNG) |
|
||||
| `assets/rivencraft/textures/item/crystal_shard.png` | Original sprite generated with OpenAI ImageGen from the Legacy crystal palette | 2026-10-05 | Cropped and nearest-neighbor scaled to 16×16 |
|
||||
| `assets/rivencraft/sounds/linking/link-fissure.ogg` | `assets/mystcraft/sounds/linking/link-fissure.ogg` | Legacy 0.13.7.06 local extraction | Namespace/path rename only; plays when a Star Fissure returns an entity home |
|
||||
| `assets/rivencraft/sounds/entity/meteor/roar.ogg` | `assets/mystcraft/sounds/entity/meteor/roar.ogg` | Legacy 0.13.7.06 local extraction | Byte-identical copy under the Rivencraft namespace; plays during the bounded meteor descent |
|
||||
| `assets/rivencraft/textures/block/book_receptacle.png` | `assets/mystcraft/textures/blocks/book_receptacle.png` | Legacy 0.13.7.06 local extraction | Byte-identical 16×16 face tile; modern block model recreates the Legacy thin slab |
|
||||
| `assets/rivencraft/textures/block/crystal_portal.png` and `.mcmeta` | `assets/mystcraft/textures/blocks/portal.png` and `.mcmeta` | Legacy 0.13.7.06 local extraction | Byte-identical texture and line-ending-normalized animation metadata, used when Immersive Portals is absent |
|
||||
| `assets/rivencraft/sounds/linking/link-portal.ogg` | `assets/mystcraft/sounds/linking/link-portal.ogg` | Legacy 0.13.7.06 local extraction | Byte-identical portal-link sound |
|
||||
| `assets/rivencraft/sounds/linking/link-disarm.ogg` | `assets/mystcraft/sounds/linking/link-disarm.ogg` | Legacy 0.13.7.06 local extraction | Byte-identical disarm-link sound; takes precedence over the portal sound as in Legacy |
|
||||
| `assets/rivencraft/sounds/linking/link-intra.ogg` | `assets/mystcraft/sounds/linking/link-intra.ogg` | Legacy 0.13.7.06 local extraction | Byte-identical intra-link sound for Linking Books |
|
||||
|
||||
Mystcraft Legacy identifies XCompWiz as its project developer and includes the
|
||||
full LGPL v3 text in its root `LICENSE` file. Rivencraft's root `LICENSE`
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# Current implementation (Milestone 7 development)
|
||||
|
||||
This is the maintained technical overview of Rivencraft as of the 1.0.0.2
|
||||
slice. It describes implemented behavior, not every behavior in Mystcraft
|
||||
Legacy. [The README](../README.md) is the player introduction;
|
||||
[milestone summaries](MILESTONES.md) explain how the implementation arrived
|
||||
here. The linked deep dives are retained as historical audits and should be
|
||||
read with their version dates in mind.
|
||||
|
||||
## Books, linking, and previews
|
||||
|
||||
- Linking Books bind a named position and direction. Same-dimension use needs
|
||||
Intra Linking. Descriptive Books refer to a persistent Age UUID and keep
|
||||
their authored page order. Copies keep that identity.
|
||||
- Book screens use Legacy-inspired page spreads and destination panels. `AUTO`
|
||||
selects optional Immersive Portals first, then Create/Ponder, then the built-in
|
||||
animation. Travel is validated on the server regardless of renderer.
|
||||
- Handheld travel drops the active book at its departure point as a visible
|
||||
book entity. Book Stands and crystal portals keep the book in place. Disarm
|
||||
drops carried items; Maintain Momentum rotates and carries velocity.
|
||||
- A vanilla Lectern converts to a Book Stand when given a Rivencraft book or
|
||||
page. A Book Receptacle in a connected Crystal Block frame activates a
|
||||
vertical or horizontal portal. Immersive Portals can display a live view;
|
||||
otherwise the Legacy-style animated surface remains. Portal arrivals use
|
||||
existing safe footing; a Descriptive portal does not generate a platform.
|
||||
- Automatic Return Links default to `DISABLED`, with `ON_CREATION` and `ALWAYS`
|
||||
server options. Linking Books do not generate platforms by default; a
|
||||
creative Link Modifier can explicitly enable that property.
|
||||
|
||||
See [preview modes](PREVIEW_COMPATIBILITY.md), [Legacy M7 audit](LEGACY_M7_AUDIT.md),
|
||||
and [Age administration](AGES.md).
|
||||
|
||||
## Ages, writing, and structures
|
||||
|
||||
- Dynamic Dimensions AR Edition creates Age worlds. Each Age has a saved UUID,
|
||||
safe dimension ID, seed, spawn, traits, versioned generation plan, and
|
||||
optional season mode. Page-free or Link-Panel-only books get seeded random
|
||||
choices. Authored supported pages override the appropriate choices.
|
||||
- Terrain choices are Standard, Amplified, Island, Cave, Flat, and Void. Cave
|
||||
terrain uses cave/Nether biomes and an appropriate spawn search; normal
|
||||
terrain starts from exposed surfaces. If no safe arrival site exists, the
|
||||
Age-creation path can construct a non-dropping Crystal Platform at Y=63.
|
||||
- The server catalog accepts typed data-pack symbols. The Binder preserves
|
||||
written order and diagnoses conflicts. Its status readout is off by default;
|
||||
`/rivencraft binder messages enable` makes it visible only to OPs. Symbol
|
||||
Pages are composed at runtime from Legacy glyph assets. Number pages 0–24
|
||||
set color strength or, immediately before a Length, Angle, Phase, or Size
|
||||
modifier, its numeric value.
|
||||
Celestial pages can create multiple suns/moons with independent color, size,
|
||||
angle, period, phase, and direction; authored eclipses affect light.
|
||||
- The Writing Desk stores tabs and copies known Symbol Pages with paper and
|
||||
ink. Notebooks hold ordered pages; the Binder consumes a Link Panel and cover
|
||||
to make a finished Descriptive Book. The Ink Mixer makes Link Panels and
|
||||
rolls Disarm, Intra Linking, and Maintain Momentum from additives. Blank
|
||||
Pages are internal bases, not a craftable or JEI-listed output.
|
||||
- Features include terrain additions, decorations, configurable block/fluid
|
||||
materials, ores, vanilla structures including newer 1.21 structures,
|
||||
Legacy-style libraries, crystals, obelisks, and star fissures. Optional Ruins
|
||||
templates gain authorable pages. Some biome/terrain combinations or feature
|
||||
placements are deliberately unsupported and are diagnosed during binding.
|
||||
|
||||
See [symbol semantics](SYMBOL_SEMANTICS.md), [writing data](WRITING_DATA.md),
|
||||
[generation profiles](AGE_PROFILES.md), and [library/Ruins rules](RUINS_COMPATIBILITY.md).
|
||||
|
||||
## Instability and optional mods
|
||||
|
||||
Invalid or excessive writing and valuable generated blocks can add
|
||||
instability. Block abundance is profiled in loaded chunks, so its contribution
|
||||
and selected effects can change as the Age is explored; the authored plan is
|
||||
not rewritten. Effects are bounded to loaded Ages and include status effects,
|
||||
colored decay, lightning, explosions, meteors, burning, and erosion. Operator
|
||||
commands expose the score and providers; the book GUI does not. See
|
||||
[scoring](INSTABILITY_SCORE.md) and [runtime policy](INSTABILITY_RUNTIME.md).
|
||||
|
||||
JEI, Create/Ponder, Immersive Portals, TerraBlender, Serene Seasons with
|
||||
GlitchCore, and Ruins are optional. TerraBlender can supply biomes from any mod
|
||||
using its eligible regions; Biomes O' Plenty is a test example. Serene Seasons
|
||||
has per-Age normal/no/eternal modes and live Age enrollment. Ecliptic Seasons
|
||||
is not integrated because its 1.21.1 API lacks supported live registration and
|
||||
per-Age calendar control. See the [optional-mod matrix](MILESTONE_7_REGRESSION.md).
|
||||
|
||||
## Persistence and boundaries
|
||||
|
||||
An Age registry definition and its dimension folder belong to the **same
|
||||
whole-world backup**. Known older book/writing, plan, workstation, and Age
|
||||
formats have migration or stable-default handling. Unknown symbol IDs remain
|
||||
in books; missing registry objects or removed dimension mods can still prevent
|
||||
world loading. The OP-only Link Modifier can retire an unoccupied Age. That
|
||||
operation marks its UUID dead and moves its dimension data to a sibling
|
||||
`<world>-retired-ages/<uuid>` folder; it is not ordinary book maintenance.
|
||||
Do not use it without a backup.
|
||||
|
||||
This is not an exact port of every Legacy generator, page, hazard, or client
|
||||
renderer. Modern safety budgets and NeoForge's dimension/biome APIs require
|
||||
adaptations. The [Milestone 7 regression matrix](MILESTONE_7_REGRESSION.md)
|
||||
separates tested behavior from unverified multiplayer/restart/optional-mod
|
||||
combinations. The [Milestone 7 acceptance record](MILESTONE_7_ACCEPTANCE.md)
|
||||
records release evidence and remaining validation limits.
|
||||
+10
-4
@@ -1,5 +1,9 @@
|
||||
# GameTest suites
|
||||
|
||||
The current Milestone 7 results and manual handoffs are in the
|
||||
[regression matrix](MILESTONE_7_REGRESSION.md). This file documents the test
|
||||
mechanics and older scenario-specific handoffs.
|
||||
|
||||
The default dedicated-server run is a small smoke suite. Its Rivencraft tests
|
||||
cover core linking, deterministic Age definition, writing persistence, legacy
|
||||
data migration, workstation transactions, and forged/replayed menu actions.
|
||||
@@ -18,12 +22,13 @@ core tests when investigating an issue:
|
||||
./gradlew runGameTestServer -ParchivedGameTestSuites=desk
|
||||
./gradlew runGameTestServer -ParchivedGameTestSuites=grammar,binder
|
||||
./gradlew runGameTestServer -ParchivedGameTestSuites=generation
|
||||
./gradlew runGameTestServer -ParchivedGameTestSuites=instability
|
||||
```
|
||||
|
||||
Available suite names are `linking`, `environment`, `writing`, `grammar`,
|
||||
`generation`, `desk`, `binder`, `mixer`, and `recipe`. The `generation` archive
|
||||
`generation`, `desk`, `binder`, `mixer`, `recipe`, and `instability`. The `generation` archive
|
||||
keeps the detailed feature, structure, ore, crystal, and fissure regressions
|
||||
out of the default smoke run. Three checks formerly registered twice in the
|
||||
out of the default smoke run. The `instability` archive adds multi-player loaded-chunk selection, policy gates, and protected-block checks without rerunning the default scoring and migration tests. Three checks formerly registered twice in the
|
||||
core and their archives now run only through the core wrapper. Run all
|
||||
Rivencraft tests before a
|
||||
release or after broad cross-system changes:
|
||||
@@ -84,8 +89,9 @@ With optional JEI installed, search for the Ink Mixer category. It should show
|
||||
the base paper/ink/vial operation and separate entries for Gunpowder, Mushroom
|
||||
Stew, Experience Bottle, Ender Pearl, Feather, and Fire Charge,
|
||||
with their property chances. Black Dye and Clay Ball should not appear: the
|
||||
former had no working Legacy effect, while Generate Platform is redundant
|
||||
with default Age arrival. Without JEI, Rivencraft should still
|
||||
former had no working Legacy effect, while Clay Ball remains unsupported by
|
||||
the mixer. The creative Link Modifier can enable a single arrival platform for
|
||||
Linking Books; they do not create one by default. Without JEI, Rivencraft should still
|
||||
start and the mixer should work normally.
|
||||
|
||||
Put paper and an Ink Vial in the mixer. Hold Gunpowder on the cursor and
|
||||
|
||||
@@ -27,4 +27,4 @@ Implementation requirements: keep the mixer server-authoritative; persist mixtur
|
||||
|
||||
The built-in vanilla-item rows above are supported except black dye and clay balls. Legacy ignores black dye's empty effect key, so Rivencraft does not consume or advertise it in JEI. Clay Balls are omitted because Age arrival already builds a safe platform by default and a separate Generate Platform link modifier would be redundant. Right-clicking the Ink Mixer basin with a carried item adds one; left-clicking adds the carried stack. The mixture is persistent and its next panel is deterministic. The visual basin tooltip shows current probabilities. Only taking the virtual Link Panel consumes paper/ink and clears the mixture. The Link Panel's property component is copied to a Linking Book by the leather recipe or a Descriptive Book by the Binder. JEI's optional Ink Mixer category lists the base panel and each supported additive interaction with its per-property chance; Rivencraft does not require JEI to run.
|
||||
|
||||
Disarm and Maintain Momentum are applied by the server after confirming a safe destination. Disarm spawns each inventory stack as an item entity at the departure point, including for Descriptive Books. Intra Linking is required only for a Linking Book whose destination is in the player's current dimension; ordinary cross-dimensional Linking Books still work. The Binder treats an Intra Linking panel as an ordinary Link Panel and omits that flag from the resulting Descriptive Book, while retaining Disarm and Maintain Momentum. Automatic Return Links remain enabled under the existing configuration, as requested. The old Generate Platform bit remains readable on pre-0.4.7.1 test items but is neither rolled nor applied. Optional dust-tag mappings remain outside this built-in subset until their cross-mod tags and gameplay meaning are agreed.
|
||||
Disarm and Maintain Momentum are applied by the server after confirming a safe destination. Disarm spawns each inventory stack as an item entity at the departure point, including for Descriptive Books. Intra Linking is required only for a Linking Book whose destination is in the player's current dimension; ordinary cross-dimensional Linking Books still work. The Binder treats an Intra Linking panel as an ordinary Link Panel and omits that flag from the resulting Descriptive Book, while retaining Disarm and Maintain Momentum. Automatic Return Links remain enabled under the existing configuration, as requested. The Ink Mixer still does not roll Generate Platform, but since 0.6.7.1 the creative Link Modifier can explicitly enable it for Linking Books; their default remains off. Optional dust-tag mappings remain outside this built-in subset until their cross-mod tags and gameplay meaning are agreed.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Colored decay (Milestone 6, issue #68)
|
||||
|
||||
Legacy defines seven decay block-state colors and has a texture/model for every one: black, red, green, blue, purple, yellow, and white. That does not mean all seven are active instability hazards. Its active provider deck registers blue, purple, red, and white. Black has a decay handler and provider class, but its provider registration is commented out; green and yellow have visual states without registered spread handlers. Rivencraft currently implements the four active provider colors. Their 16×16 textures are exact copies of the local Mystcraft 1.12.2-0.13.7.06 assets at `assets/mystcraft/textures/blocks/decay_{blue,purple,red,white}.png`. Legacy's `blockdecay.json` and `blockdecay_*.json` map these to cube-all models; Rivencraft uses the same mapping.
|
||||
|
||||
The local Legacy bytecode (`EffectDecayBasic`, `DecayHandlerSpreading`, and the four color handlers) seeds decay in a chunk, then spreads to six neighbors. Blue prefers softer blocks, red uses explosion resistance, purple is slower, and white converts almost anything and damages entities on contact. Rivencraft preserves the four colors, six-neighbor growth, relative conversion tendencies, and white contact damage. It adapts the exact difficulty formulas to modern block hardness and safety checks; fluids are not replaced, and provider attempts are deterministic from the Age seed and persisted attempt counter.
|
||||
|
||||
Decay requires both `instability.world_mutation=true` and `instability_decay.enabled=true` in the server config. Both now default to true, matching Legacy's enabled-by-default Age instability and provider registration. Existing config files retain their saved values, including `false`; change them explicitly to opt in after updating an existing installation. At most `instability_decay.max_attempts_per_call` neighbors (default four) are examined, each within radius one, and at most one block can be replaced per provider run. The shared Age action budget may lower that further. Only chunks already loaded and supplied by the scheduler are touched; no cross-dimension or force-loading path exists. A per-Age `rivencraft_decay` saved data file stores a bounded frontier (1024 positions per color) and attempt counter. Disabling decay stops seeding, spreading, and white contact damage; existing blocks remain so re-enabling can resume.
|
||||
|
||||
Decay rejects the saved Age spawn and its nearby 5×5 arrival platform (with a two-block margin), block entities, fluids, unbreakable blocks, vanilla portal/command/structure infrastructure, blocks in `#minecraft:portals`, all non-decay Rivencraft blocks, and blocks in `#rivencraft:decay_protected`. Data packs can append other mods' protected blocks to that tag. A fake-player break event is posted before replacement, and a placement event after it; a cancellation prevents or restores the change. As with other fake-player automation, protection mods need to handle NeoForge block events for this safeguard to apply.
|
||||
|
||||
GameTests cover persistence, frontier deduplication, selected-chunk limits, and Overworld isolation. A real-client check should enable both settings in a disposable Age, observe growth, leave/reload that Age, then disable decay and verify growth stops. Also test a claim-protected block and a block entity if a protection mod is installed.
|
||||
|
||||
For current test builds, an operator standing in an Age can use `/rivencraft age decay pulse white 32` (or `blue`, `purple`, `red`). This performs at most 32 budgeted provider calls in the player's already-loaded chunk and reports how many blocks converted. It does not edit the saved Age plan or instability score. Repeat the command to observe spread, leave/reload the Age to check persistence, then disable decay and confirm the command refuses to act. This test hook remains useful because the provisional authored-page scoring does not yet provide a reliable route to the Legacy eating deck's 15,000-point gate.
|
||||
@@ -0,0 +1,11 @@
|
||||
# Instability controls and diagnostics (Milestone 6, issue #72)
|
||||
|
||||
The world-specific NeoForge server config (`serverconfig/rivencraft-server.toml`) is the global policy. `instability.enabled` stops all scheduled effects without changing a saved Age. `instability_providers.<id>` switches each Legacy card independently; these switches do not reroll frozen provider levels. The existing `instability_status_effects`, `instability_decay`, `instability_hazards`, `instability_meteor`, `instability_scorching`, and `instability_erosion` groups provide category policy and action limits. `instability.world_mutation`, hazard/meteor `block_damage`, `player_damage`, and `fire` settings control destructive behavior. `mobGriefing` and `doFireTick` still apply. Defaults retain the Legacy-active hazards; the scheduler, spawn protection, loaded-chunk checks, and per-effect caps remain Rivencraft safety adaptations. The dormant-in-Legacy erosion effect remains enabled for project testing but can be switched off.
|
||||
|
||||
Configuration values are read during server operation, but editing the TOML on disk does not guarantee NeoForge will reload it. Restart the server or world after changing the file; `/reload` reloads data packs, not this server config. Per-Age commands below take effect on the next scheduler pass without a restart. A stopped provider does not refund or change the Age's score or saved plan. Existing owned status-effect leases are revoked on their next 20-tick check after global, category, provider, or Age disablement. In-flight meteors are cancelled when their Age or meteor provider is disabled. Existing block changes are not rolled back.
|
||||
|
||||
Operators can inspect `/rivencraft age instability [dimension]`. The argument accepts an Age name (quote names containing spaces), a short path such as `fort`, or a full ID such as `rivencraft:age/fort`. Duplicate names require the full ID shown by `/rivencraft age list`. The command reports the frozen score and each saved contribution, the frozen provider IDs/levels and their *current* runtime eligibility, config limits, and whether this Age is enabled. "Eligible" does not promise an effect on the next tick: chance, cooldowns, loaded chunks, player presence, and protection still apply. An unavailable handler is reported as such instead of being mistaken for an active provider. Pre-scoring Ages remain neutral, and missing provider plans from older scored saves are migrated once as described in [runtime policy](INSTABILITY_RUNTIME.md). `/rivencraft age instability disable <dimension>` and `enable <dimension>` are operator-only and persist for that Age alone. Unknown dimensions are rejected. Neither command mutates a book, score, or provider plan.
|
||||
|
||||
Newly bound Descriptive Books carry only a server-written numerical potential-instability summary for their tooltip and cover. The details and live enablement stay server-side in the operator command. Older written books receive the summary when opened through the server-approved book screen; until then, an absent tooltip score means "not yet synchronized," not zero. The score does not indicate whether effects are currently enabled.
|
||||
|
||||
Manual check: create two scored Ages, disable one with the command, and inspect both. Only the selected Age should show "Age disabled"; its saved breakdown and plan should remain identical. Re-enable it, then switch an individual provider in the world config and restart. Its plan should remain identical while diagnostics show "provider config off." An old neutral Age should report score zero and no selected providers. A non-operator should be unable to invoke the mutation commands. Test tooltips both on a newly bound book and an older book after opening it once.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Environmental instability (#70)
|
||||
|
||||
The local Mystcraft Legacy 0.13.7.06 bytecode was audited for `EffectMeteor`, `EntityMeteor`, `EffectScorched`, `EffectErosion`, and their providers. The active deck registers `meteors` in destructive and `burning` (scorching) in harsh. Erosion's provider is present in bytecode but not registered in the active deck. Its old chunk-population lake routines are not run at Age creation in Rivencraft; ordinary authored lake features already own generation. The old erosion tick spread a neighboring water/lava or Forge fluid block into a random non-liquid position, excluding downward neighbors and block containers.
|
||||
|
||||
Rivencraft keeps the active `meteors` and `burning` provider IDs in frozen Age plans. Erosion does not become a deck card. For this test slice, `instability_erosion.enabled` defaults **on** as an explicit adaptation requested by the user. It runs only in Ages whose frozen instability score reaches the destructive deck threshold (10,000), and may be disabled independently. Existing Age plans are not rerolled.
|
||||
|
||||
Legacy's meteor spawned at Y=500, fell as a custom entity, broke blocks along its path, and made multiple large explosions on impact. Modern Ages can have different build heights and dynamic dimensions. Rivencraft instead records one bounded active descent per Age (16 globally), starting no more than 48 blocks above the loaded surface. Server particles show the trajectory and the byte-identical Legacy `entity/meteor/roar.ogg` plays near the impact. A single strength-3 impact reuses the capped explosion/protection hooks from #69. There is no persistent projectile entity or custom model; stopping/unloading a dimension cancels a descent rather than replaying it on restart. The saved per-Age attempt sequence and cooldown prevent immediate repeated rolls. The adaptation is intentionally less destructive than Legacy's chain of explosions.
|
||||
|
||||
Scorching checks at most eight loaded chunks and eight living-entity candidates per chunk per scheduler pass, igniting at most the configured target count. Targets must be open to sky; survival/adventure players are included by default as in Legacy, while creative/spectator players and spawn safety positions are excluded. Fire duration is four seconds per provider level, capped at twelve seconds. This affects entities, not blocks, and does not request distant chunks.
|
||||
|
||||
Erosion inspects at most eight already-loaded candidate positions per pass and can replace at most one safe block with an adjacent source fluid. It excludes block entities, protected/critical Rivencraft blocks, portals, spawn safety positions, and unloaded neighboring chunks. Break and place events with a fake player permit claim/protection hooks to veto a change; rejected placement is rolled back. World mutation and `mobGriefing` must also be enabled. The modern Y range is taken from the Age level rather than Legacy's fixed 0–127 sample.
|
||||
|
||||
The server config has independent meteor enable/block damage/player damage/fire/caps, scorching enable/player targeting/cap, and erosion enable switches. Meteor frequency preserves Legacy's 1/50,000 chance per chunk-tick across nearby loaded chunks, capped to one active descent per Age. Visual/audio effects still run when meteor block damage is disabled. No client-only class is loaded on a dedicated server. The reused roar sound is recorded in [asset provenance](ASSET_PROVENANCE.md); Legacy has no separate meteor texture in the supplied asset tree.
|
||||
|
||||
Operator checks in a loaded, sufficiently unstable Age: `/rivencraft age environment pulse meteor` starts one protected descent (200-tick cooldown), `/rivencraft age environment pulse burning` ignites the issuing survival player only when open to sky and outside spawn protection, and `/rivencraft age environment pulse erosion` converts the block being looked at if a source fluid touches its side or top. The burning test reports when rain or water would immediately extinguish the fire. Place water or lava next to a plain stone block and aim at the stone to test erosion. These commands refuse to run in the Overworld and still honor enable switches, protection hooks, and world-mutation policy. Natural meteor occurrence is intentionally rare, as in Legacy.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Instability provider runtime (Milestone 6, issue #66)
|
||||
|
||||
Each new Age stores an `InstabilityPlan` beside its frozen `Instability` score. The version-1 plan contains provider IDs and levels. Selection uses the saved score rounded down to a 500-point step and the Age seed. It independently walks Legacy's basic, harsh, destructive, eating, and death decks with their audited entry costs (0, 2,500, 10,000, 15,000, 20,000), shuffling each deck deterministically before paying its weighted card costs. The source card weights and default provider costs are copied from the user's local Mystcraft 0.13.7.06 `InstabilityData` class. This preserves Legacy's multiple-deck behavior, but does not claim byte-for-byte reproduction of its historical Java RNG state, difficulty multiplier, config overrides, or 400-chunk block profiling delay.
|
||||
|
||||
The saved provider IDs are independent of installed handlers. A missing optional handler leaves its ID and level intact and is skipped at runtime. A syntactically invalid saved ID or unsupported plan version fails loading explicitly; an unknown but valid ID is preserved and logged once when encountered. Disabling instability or world mutation never rewrites a saved score or provider plan. Saves from #64 with a frozen score but no provider plan are selected once from that score and Age seed, then marked dirty for persistence. Older pre-score Ages remain neutral and inert.
|
||||
|
||||
From 0.5.8.11, [live block profiling](LIVE_BLOCK_INSTABILITY.md) adds a separate
|
||||
effective score and provider plan once 400 Age and Overworld chunks have been
|
||||
sampled. The authored score/plan described above remain saved as the base;
|
||||
the effective provider selection can change as block abundance is observed.
|
||||
|
||||
The server scheduler runs only for loaded `rivencraft:age/*` levels with a player present and an Age definition. It uses only chunks already loaded near those players, never requests a chunk, and rotates among eligible Ages when the per-tick cap is reached. Configuration bounds the cooldown (minimum 20 ticks), active Ages, handlers and actions per Age, effective provider level, and examined loaded chunks. A handler must claim a shared action permit before each effect. A throwing handler is logged and quarantined temporarily without stopping the server. World-mutating handlers are enabled by default to match Legacy; the setting can still disable them. These limits are guards for upcoming providers, not proof that an arbitrary third-party handler cannot misuse world APIs.
|
||||
|
||||
Issue #67 registers the non-world-mutating potion handlers; see [status-effect policy](INSTABILITY_STATUS_EFFECTS.md). Issue #68 adds configurable [colored decay](INSTABILITY_DECAY.md), enabled by default for newly generated configs. Issue #69 adds lightning and explosions; issue #70 adds [meteor, scorching, and erosion](INSTABILITY_ENVIRONMENTAL.md). The runtime rejects Overworld and preview dimension IDs; its player requirement excludes unattended snapshot chunk loaders. GameTests cover deterministic selection, save round-trips, score-only migration, unknown IDs, scope checks, fair admission, and action caps.
|
||||
|
||||
Issue #72 adds [server config, per-Age controls, diagnostics, and book feedback](INSTABILITY_DIAGNOSTICS.md). Its operator controls never rewrite a frozen score or provider plan.
|
||||
|
||||
The Milestone 6 [regression and migration record](MILESTONE_6_REGRESSION.md)
|
||||
maps the automated acceptance checks and lists the in-game restart, optional-mod,
|
||||
and hazard-visual scenarios still requiring verification.
|
||||
|
||||
# Lightning and explosions (#69)
|
||||
|
||||
The `lightning` and `explosions` cards now run only in loaded Rivencraft Ages with players. Legacy rolled once per chunk per tick: explosions at 1/1000 and lightning at 1/5000 during thunder or 1/100000 otherwise. Each selected hazard card gets an independent roll every scheduler pass, rather than waiting for the rotating two-provider sample. The roll uses up to 81 already-loaded chunks near Age players and the exact probability of at least one Legacy opportunity over that many chunk-ticks. It permits at most one event per hazard type per pass. A per-Age saved sequence and last-event time prevent the same roll/event from replaying after a restart. Lightning also skips an existing bolt at the target.
|
||||
|
||||
Legacy used a real lightning bolt and a flaming, block-damaging strength-3 explosion. Both remain enabled by default. Unlike Legacy, the bolt is visual-only and its damage/fire are applied through separate bounded paths. The explosion uses NeoForge's normal explosion hooks plus break-event checks, with a calculator that restricts destruction to one already-loaded chunk, protects spawn, block entities, portals, decay-protected blocks and Rivencraft infrastructure, and caps affected blocks and entity damage. The visual/audio explosion still occurs when griefing is disabled. `mobGriefing` and `doFireTick` gate block changes and ignition; survival/adventure player damage is separately configurable. These are deliberate modern server-safety differences from Legacy.
|
||||
|
||||
Server config `instability_hazards` controls enablement, block damage, player damage, fire, interval, maximum nearby loaded chunks, maximum destroyed blocks, damaged entities, damage per entity, and explosion radius. `instability.world_mutation=false` also suppresses block damage and fire without suppressing visuals or entity damage. No hazard is dispatched into preview, Overworld, or another Age.
|
||||
|
||||
For an operator smoke test inside an Age, `/rivencraft age hazard pulse lightning` and `/rivencraft age hazard pulse explosion` force one event in a safe loaded chunk. They still honor the Age scope, enable switch, cooldown, spawn exclusion, game rules and damage policies. Repeat after `minimum_gap_ticks` (default 20 ticks); run the same command in the Overworld to verify that it refuses to fire. The earlier 200-tick `min_interval_ticks` test key is retired; existing configs receive the new default when NeoForge corrects them.
|
||||
@@ -1,9 +1,21 @@
|
||||
# Frozen Age instability score (Milestone 6, issue #64)
|
||||
|
||||
The original issue #64 notes below describe the frozen authored score. Later
|
||||
Milestone 6 slices added a separate live block-profile score. For new
|
||||
version-4 plans, ordered period/phase modifiers combine until consumed by a
|
||||
celestial page; each modifier kind left dangling at the end adds +100. Saved
|
||||
version-3 plans keep their earlier stage-sorted meaning and writing cost.
|
||||
|
||||
Every newly created Age now gets a version-1 `Instability` compound inside its server-owned `rivencraft_ages` saved definition. It contains the total `Score` and an ordered `Breakdown` of reason, original page index (zero-based, or -1 for an Age-wide cause), points, and explanation. `AgeRegistrySavedData.instability(UUID)` is the read-only server query. No client tooltip or live data-pack lookup is needed to read a saved score.
|
||||
|
||||
The baseline is explicitly zero for page-free and Link-Panel-only random Ages, and for authored Ages. Incomplete writing receives seed-based completion and a zero-point explanatory entry. Supported writing defects use the provisional Milestone 6 grammar costs: duplicate/conflicting single-choice pages +500, out-of-order or misplaced pages +100. Legacy's audited bright-lighting symbol adds +500 whether authored or seed-selected. Other balance costs are not guessed for symbols that Rivencraft has not implemented. The score is not yet an active hazard or effect selection.
|
||||
The baseline is explicitly zero for page-free and Link-Panel-only random Ages, and for authored Ages. Incomplete writing receives seed-based completion and a zero-point explanatory entry. Supported writing defects use the provisional Milestone 6 grammar costs: duplicate/conflicting single-choice pages +500, out-of-order or misplaced pages +100. Legacy's audited Bright Lighting symbol adds +500 for every occurrence, including a seed-generated controller. Mineshafts, Nether Fortresses, Strongholds, and Villages add +100 for the fourth and every later occurrence. By project balance decision, Bastions, Trial Chambers, Ancient Cities, Shipwrecks, Ocean Ruins, Pillager Outposts, Ruined Portals, Nether Fossils, Buried Treasure, and Trail Ruins follow that same rule. The first three occurrences and all other audited Legacy feature symbols have no direct page cost. An authored structure may override its vanilla biome eligibility; every structure-page occurrence adds +100 if **any** selected biome is unsupported, even when another selected biome is compatible. Seed completion still prefers compatible biomes and therefore avoids that cost when it can. Other modern-only features remain neutral. The frozen score selects a deterministic, persisted provider plan through the Milestone 6 runtime. Registered handlers apply that plan only inside loaded Rivencraft Ages and remain subject to the documented scheduler and effect-specific safety bounds.
|
||||
|
||||
The score is calculated after the Age plan and effective traits are chosen, then saved with the Age identity before its dimension is generated. The same UUID returns the existing definition, including the same score, on repeated use. Spawn updates preserve it. Reload only deserializes the frozen breakdown; it never reruns the current grammar or catalog. An older Age definition without `Instability` is migrated to a persisted version-1 neutral score of zero, rather than retroactively scoring its writing. A future scoring-rule change must introduce an explicit new schema/migration policy, not silently reinterpret existing Ages.
|
||||
The score is calculated after the Age plan and effective traits are chosen, then saved with the Age identity before its dimension is generated. The same UUID returns the existing definition, including the same score, on repeated use. Spawn updates preserve it. Reload only deserializes the frozen breakdown; it never reruns the current grammar or catalog. An older Age definition without `Instability` is migrated to a persisted version-1 neutral score of zero, rather than retroactively scoring its writing. A future scoring-rule change must introduce an explicit new schema/migration policy, not silently reinterpret existing Ages. Issue #66 added the frozen provider selection; issues #67–#70 added the current handlers. See the [runtime policy](INSTABILITY_RUNTIME.md).
|
||||
|
||||
This is now the **authored base** score. [Live block profiling](LIVE_BLOCK_INSTABILITY.md)
|
||||
keeps its sample counts separately and may add a changing effective block score
|
||||
without rewriting the authored breakdown or its saved base provider plan.
|
||||
|
||||
Unknown or unsupported pages remain invalid for *new* Ages. Their absence from a later registry does not change an already saved score. Focused GameTests cover stable/defective writing, random baseline and bright lighting, repeated identity, save round-trip, and missing-symbol/old-save behavior.
|
||||
|
||||
`/rivencraft age instability` inspects the Age containing the command source; an optional dimension ID inspects an unloaded or different registered Age. It reports immutable authored base score, stability bonus, runtime penalty, profiled-block contribution, and difficulty-scaled effective score. The pinned Legacy tree defines player-death and player-presence bonus classes but does not register them as default providers, so Rivencraft leaves player-driven modifiers disabled: bonus and penalty are zero. The live block contribution may still change as chunks are profiled. There is no mutable player-bonus save to migrate in this release. No death, PvP event, player identity, or dimension transition can transfer stability between Ages. Any future opt-in player mechanic must persist separately from `Instability`, define caps and reset behavior, and must not rewrite the generation plan or frozen provider plan.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Age status effects (Milestone 6, issue #67)
|
||||
|
||||
This slice activates the Legacy potion cards selected and saved by issue #66. The user-provided Mystcraft 0.13.7.06 `EffectPotion` chooses a random living entity in a chunk section. A local card requires sky visibility; a global card does not. `EffectPotionEnemy` excludes players. Its amplifier is selected level minus one. The defaults in `InstabilityData` give blindness and nausea 60 ticks, mining fatigue/hunger/poison/slowness/weakness 80 ticks, wither 30 ticks, and enemy regeneration/resistance 200 ticks. Rivencraft preserves those durations, sky rules, and level-to-amplifier mapping, subject to the server's lower duration/level caps.
|
||||
|
||||
General debuffs can affect survival/adventure players and other living entities, as Legacy did. The two enemy buffs affect hostile mobs only; Legacy affected any non-player living entity, so excluding passive animals is a deliberate modern adaptation. Creative and spectator players are exempt per issue #67, although the inspected Legacy potion class did not have that exemption. Existing effects from vanilla or another mod are never overwritten.
|
||||
|
||||
The issue #66 scheduler chooses an already-loaded chunk near a player, at most the configured number of provider calls and action claims per Age run. This provider queries at most `max_candidates_per_attempt` living entities in that chunk, picks one deterministically from the bounded candidates, and claims an action before applying. It holds at most `max_active_per_age` short-lived, owned-effect leases. It neither force-loads chunks nor scans all entities or chunks. Exact target RNG therefore intentionally differs from Legacy's random chunk-section walk; the target types, local/global distinction, duration, and strength are retained.
|
||||
|
||||
The server config can disable all status effects, player targets, general non-player targets, or hostile buffs, and can lower active leases, candidates, and duration. Leaving an Age, logging out, disabling effects, or stopping the server removes only an effect instance this provider actually installed; it will not remove a replacement from another mod. Leases also expire automatically after their short duration. The runtime never runs in the Overworld or a preview dimension. Automated tests cover mappings, target eligibility, creative/spectator exemption, and Overworld isolation. In-game multiplayer, Age-to-Age travel, and restart checks remain useful because GameTests do not create a second dynamic Age for this slice.
|
||||
@@ -1,10 +1,17 @@
|
||||
# Mystcraft Legacy instability audit (Milestone 6, issue #63)
|
||||
|
||||
This is a behavior contract for Milestone 6, **not a completed runtime instability system**. It compares the user-provided 0.13.7.06 class files at `/home/dnorberg/Documents/mystcraft-src/mystcraft-1.12.2-0.13.7.06/` with [Mystcraft Legacy source at commit `9bc8ddc`](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae). Values below are source defaults, not a claim that every historical config or server used them. Rivencraft 0.5.1.0 saves a provisional writing-instability score and repaired active-page order, but has no hazard loop yet.
|
||||
This is the behavior contract used to implement Milestone 6. It is pinned to [Mystcraft Legacy source at commit `9bc8ddc`](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae). Values below are source defaults, not a claim that every historical config or server used them. Rivencraft now persists frozen writing-instability scores and provider plans and runs the implemented providers through a bounded, Age-scoped scheduler. The audit remains authoritative for Legacy behavior; the linked runtime documents identify modern adaptations and remaining gaps.
|
||||
|
||||
Current new Age plans (schema 4) also implement Legacy's ordered period/phase
|
||||
consumption and +100 for each modifier kind still pending after the last
|
||||
celestial page. Repeated same-kind pages combine before consumption rather
|
||||
than charging a replacement penalty. Older plans keep their saved score and
|
||||
earlier stage-sorted time interpretation. The older #65 implementation notes
|
||||
below are historical where they mention a generic misplaced-modifier cost.
|
||||
|
||||
## Score, grammar, and selection in Legacy
|
||||
|
||||
Legacy's [`AgeController`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/AgeController.java) calculates `debug + symbol + block + book/base + bonus`, then applies difficulty 0/1/2/3 multipliers of 0.25/0.5/1/1.75. Global instability is enabled by default; an Age also has its own enable flag. The unmodified score is not simply “number of bad pages.” Block profiling may delay a usable score: `blockinstability` is null until **more than** 400 Age chunks are profiled, and `getInstabilityScore()` returns zero until then. It is recalculated after additional profiling. [`ChunkProfiler`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/profiling/ChunkProfiler.java) compares watched block abundance with a baseline, subtracting only positive excess above each baseline. The baseline is profiled or read from balance config; [`InstabilityDataCalculator`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/profiling/InstabilityDataCalculator.java) sets its default tolerance to 1.05, and default `useconfigs=false`. We should not copy its 400-chunk profiling delay or dynamic block scan into 1.21.1 without a separate performance case.
|
||||
Legacy's [`AgeController`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/AgeController.java) calculates `debug + symbol + block + book/base + bonus`, then applies difficulty 0/1/2/3 multipliers of 0.25/0.5/1/1.75. Global instability is enabled by default; an Age also has its own enable flag. The unmodified score is not simply “number of bad pages.” Block profiling may delay a usable score: `blockinstability` is null until **more than** 400 Age chunks are profiled, and `getInstabilityScore()` returns zero until then. It is recalculated after additional profiling. [`ChunkProfiler`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/profiling/ChunkProfiler.java) compares watched block abundance with a baseline, subtracting only positive excess above each baseline. The baseline is profiled or read from balance config; [`InstabilityDataCalculator`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/profiling/InstabilityDataCalculator.java) sets its default tolerance to 1.05, and default `useconfigs=false`. Rivencraft's later [bounded implementation](LIVE_BLOCK_INSTABILITY.md) retains the threshold and tolerance, but not Legacy's spatial accessibility formula or unbounded profiling.
|
||||
|
||||
| Input | Verified Legacy behavior | Rivencraft M6 interpretation |
|
||||
| --- | --- | --- |
|
||||
@@ -15,6 +22,10 @@ Legacy's [`AgeController`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9b
|
||||
| Unsupported/unmatched page | Legacy logs unmatched symbols during reconstruction and skips their logic; its `AgeData.getBaseInstability()` still has a TODO for calculating instability from pages. This is not a sound compatibility contract. | Remains fatal for *new* books until semantic and runtime support exists; never silently erase the page or pretend its effect applied. Existing saved books retain their IDs. |
|
||||
| Other penalties/bonuses | `AgeController.addInstability(int)` is available to symbol logic. The base/book score is persisted as a `short`; Legacy's [`InstabilityBonusManager`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/instability/bonus/InstabilityBonusManager.java) also has dynamic player death/behavior components. | Persist a versioned score breakdown for Age writing and environmental contributions. Do not infer death/troll mechanics from the mere existence of Legacy classes. |
|
||||
|
||||
The complete Legacy `instabilityModifier` audit found direct feature-page costs only for Mineshafts, Nether Fortresses, Strongholds, and Villages. Their first three occurrences cost zero; the fourth and every later occurrence adds 100. Every other Legacy feature class inherited `SymbolBase`'s zero modifier. Rivencraft applies that exact rule to the four matching pages. By project balance decision, Bastions, Trial Chambers, Ancient Cities, Shipwrecks, Ocean Ruins, Pillager Outposts, Ruined Portals, Nether Fossils, Buried Treasure, and Trail Ruins use the same repeat rule. Matching Legacy features such as Caves, Crystal Formations, Dense Ores, Dungeons, Floating Islands, Huge Trees, Deep/Surface Lakes, Obelisks, Ravines, Skylands, Spheres, Spikes, Star Fissures, and Tendrils have no direct page cost. Legacy could still derive instability later from excess profiled blocks, especially ores. From 0.5.8.11, Rivencraft has a [bounded live block-profile adaptation](LIVE_BLOCK_INSTABILITY.md) using the 400-chunk threshold, including diamond blocks as a deliberate non-Legacy extension. Other modern-only Rivencraft features remain neutral unless they increase watched-block abundance.
|
||||
|
||||
Bright Lighting adds 500 for every authored occurrence. A generated Bright Lighting controller also adds 500. This is independent of the 500-point extra-controller cost, so two authored Bright Lighting pages produce 1,500 total: 500 for each symbol and 500 for the repeated controller.
|
||||
|
||||
Source for controller and modifier defaults: [`InstabilityData`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/instability/InstabilityData.java), [`AgeController`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/AgeController.java). The local 0.13.7.06 `Modifier.class` confirms `dangling_default=100`. Legacy grammar **ranks** weight which missing symbols are selected; they are not a direct instability score or deck tier. See [grammar audit](LEGACY_AGE_GRAMMAR_AUDIT.md) and [random weights](RANDOM_AGE_WEIGHTS.md).
|
||||
|
||||
[`InstabilityController`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/instability/InstabilityController.java) rounds its working score toward zero to the current smallest registered positive provider cost (500 by default), then rebuilds active effects when that rounded score changes. Each seeded deck preserves its saved card order; new cards are shuffled with the Age seed. For each deck it subtracts the deck entry cost, then walks cards in order, paying each provider activation cost until the next card cannot be afforded. A provider's occurrences become its level. **Deck costs are gates, not mutually exclusive severity bands**: at high score multiple decks can contribute effects. Provider `.enabled` config can remove cards. The actual effect set cannot be inferred from a score alone without the saved deck orders and enabled providers ([`Deck`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/instability/Deck.java), [`InstabilityManager`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/instability/InstabilityManager.java)).
|
||||
@@ -35,25 +46,27 @@ These entry costs are from [`InstabilityData.initialize()`](https://github.com/M
|
||||
|
||||
| Family | Legacy behavior | M6 disposition |
|
||||
| --- | --- | --- |
|
||||
| Player and hostile-mob potions | Local/global blindness, fatigue, hunger, nausea, poison, slowness, weakness, wither; hostile regeneration/resistance ([`EffectPotion`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectPotion.java)). | Implement in #67 with server-only, Age-scoped entity targeting and rate limits. No effects in preview/snapshot worlds. |
|
||||
| Colored decay | Blue, purple, red, white providers place advancing decay and extra-tick it; black provider is commented out. Decay handlers differ in spread/consumption ([providers](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/instability/providers), [handlers](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/instability/decay)). | Implement bounded, reversible-by-admin world mutation in #68; exact 1.12 spread/update rates require adaptation and tests. Exclude protected spawn/portal areas. Black/green/yellow are **not** proven active Legacy providers. |
|
||||
| Lightning and explosions | Random per-chunk strike/explosion attempts ([`EffectLightning`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectLightning.java), [`EffectExplosions`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectExplosions.java)). | Implement in #69 with explicit block-damage and fire policy; no effect outside the Age. |
|
||||
| Meteor, scorching | Meteor entity spawn and sky-exposed entity ignition ([`EffectMeteor`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectMeteor.java), [`EffectScorched`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectScorched.java)). | #70: modern meteor entity/impact needs an explicit damage budget; scorching can use modern fire/damage checks. Legacy `meteor/roar.ogg` is available. |
|
||||
| Crumble and erosion | Crumble maps blocks to weaker states; erosion spreads adjacent fluids and has extra lake-generation methods ([`EffectCrumble`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectCrumble.java), [`EffectErosion`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectErosion.java)). | #70: bounded block palette and protected-area checks. Erosion's provider is commented out in Legacy registration: treat it as deliberate opt-in adaptation, not baseline parity. |
|
||||
| Player and hostile-mob potions | Local/global blindness, fatigue, hunger, nausea, poison, slowness, weakness, wither; hostile regeneration/resistance ([`EffectPotion`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectPotion.java)). | Implemented in #67 with server-only, Age-scoped entity targeting and rate limits. No effects run in preview/snapshot worlds. |
|
||||
| Colored decay | Blue, purple, red, white providers place advancing decay and extra-tick it; black provider is commented out. Decay handlers differ in spread/consumption ([providers](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/instability/providers), [handlers](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/instability/decay)). | Implemented in #68 as bounded world mutation with protected spawn/portal areas and configurable shutdown. Black/green/yellow remain excluded because they are **not** proven active Legacy providers. |
|
||||
| Lightning and explosions | Random per-chunk strike/explosion attempts ([`EffectLightning`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectLightning.java), [`EffectExplosions`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectExplosions.java)). | Implemented in #69 with separate damage/fire/griefing policies and loaded-Age bounds; see `INSTABILITY_RUNTIME.md`. |
|
||||
| Meteor, scorching | Meteor entity spawn and sky-exposed entity ignition ([`EffectMeteor`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectMeteor.java), [`EffectScorched`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectScorched.java)). | #70 implements a bounded meteor descent/impact and the active `burning` card; see `INSTABILITY_ENVIRONMENTAL.md`. Legacy `meteor/roar.ogg` is reused. |
|
||||
| Crumble and erosion | Crumble maps blocks to weaker states; erosion spreads adjacent fluids and has extra lake-generation methods ([`EffectCrumble`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectCrumble.java), [`EffectErosion`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectErosion.java)). | #70 implements guarded fluid spread. Erosion's provider is absent from the active Legacy deck; it is deliberately enabled by default in this test slice without adding a saved deck card, per user request. |
|
||||
| Extra ticks | Registered alongside blue/purple/red/white decay, targeting decay blocks ([`EffectExtraTicks`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/effects/EffectExtraTicks.java)). | Implement only as a bounded decay helper in #68; do not accelerate arbitrary blocks/block entities. |
|
||||
| Other dormant code | Black decay, global burning, bedrock crumble, erosion provider are commented out. | Defer unless a later issue explicitly requests them. An authored symbol requiring one must fail clearly until supported. |
|
||||
| Other dormant code | Black decay, global burning, bedrock crumble, erosion provider are commented out; ordinary `burning` (scorching) remains active. | Erosion has been explicitly requested for #70 testing. The other dormant variants remain deferred; an authored symbol requiring one must fail clearly until supported. |
|
||||
|
||||
Assets in the supplied local tree: `assets/mystcraft/textures/blocks/decay_{black,blue,green,purple,red,white,yellow}.png`, corresponding decay block/item model JSON, and `assets/mystcraft/sounds/entity/meteor/roar.ogg`. Before importing, review attribution/license and fit modern 16×16 assets. Texture availability does not make an effect active. Use current Minecraft effects/sounds where adaptation is cleaner; no new artwork is needed for this audit.
|
||||
|
||||
## Rivencraft boundary and proposed safety contract
|
||||
## Rivencraft boundary and current safety contract
|
||||
|
||||
[`WritingGrammar`](../src/main/java/net/dnorberg/rivencraft/writing/WritingGrammar.java) classifies `INCOMPLETE` as seeded completion and supported writing defects as accepted instability. `AgeGenerationPlan.compile()` freezes the original writing, repaired active-page indexes, and a provisional writing-instability score before world generation. #64 now persists a versioned score breakdown; saved provider/deck choice remains future work. The plan must not mutate an already-created Age when a data pack changes. Server-side settings may disable execution, but should not rewrite the saved authored score.
|
||||
[`WritingGrammar`](../src/main/java/net/dnorberg/rivencraft/writing/WritingGrammar.java) classifies `INCOMPLETE` as seeded completion and supported writing defects as accepted instability. `AgeGenerationPlan.compile()` freezes the original writing, repaired active-page indexes, and a provisional writing-instability score before world generation. #64 persists the versioned score breakdown, and #66 derives and persists the provider/deck choice from that frozen score. The plan must not mutate an already-created Age when a data pack changes. Server-side settings may disable execution, but do not rewrite the saved authored score or provider plan.
|
||||
|
||||
For #65, **eligible-to-become-unstable** means a known, supported page whose semantics and resulting world are still deterministic: repeated/conflicting single-choice pages and otherwise valid out-of-order or misplaced pages. The implementation retains every original page, uses the last single-choice page, sorts active categories, and saves 500 per extra/conflicting choice or 100 per misplaced page. These are provisional Rivencraft values, not a full port of every `IAgeSymbol.instabilityModifier`. Missing branches are explicitly classified as zero-cost seeded completion; legal repeated feature/material pages are not automatically errors. **Remain fatal**: missing/duplicate/misordered Link Panel, over-capacity book, unresolved or schema-v1 display-only symbol, unsupported weather/angle/runtime profile/biome/feature, unavailable required optional mod, and any combination the compiler cannot represent safely. Binder and unbound-book previews distinguish blocked writing, seeded completion, and accepted instability before binding; the server revalidates on output. This intentionally differs from Legacy's silent unmatched-symbol skip, universal controller cost assumptions, and modifier-specific dangling costs. A full symbol-specific Legacy penalty port remains a separate balance task, not a claim of parity in this slice.
|
||||
For #65, **eligible-to-become-unstable** means a known, supported page whose semantics and resulting world are still deterministic: repeated/conflicting single-choice pages and otherwise valid out-of-order or misplaced pages. The implementation retains every original page, uses the last single-choice page, sorts active categories, and saves 500 per extra/conflicting choice or 100 per misplaced page. Issue #71 adds the audited Bright Lighting and repeat-structure modifiers described above. Missing branches are explicitly classified as zero-cost seeded completion; legal repeated feature/material pages are not automatically errors, though a fourth matching Legacy structure now receives its source-defined cost. **Remain fatal**: missing/duplicate/misordered Link Panel, over-capacity book, unresolved or schema-v1 display-only symbol, unsupported weather/angle/runtime profile/biome/feature, unavailable required optional mod, and any combination the compiler cannot represent safely. Binder and unbound-book previews distinguish blocked writing, seeded completion, and accepted instability before binding; the server revalidates on output. This intentionally differs from Legacy's silent unmatched-symbol skip, universal controller cost assumptions, and modifier-specific dangling costs.
|
||||
|
||||
Legacy includes `PlayerKilledBonus` and `PlayerTrollPenalty` classes, but the pinned source contains no default provider registration for either. Rivencraft does not activate death, PvP, player-presence, or cross-dimension credit from dormant classes. Consequently #71 has no mutable per-Age bonus state to migrate, cap, reset, or persist: the stability bonus and runtime penalty are both zero, and the effective score equals the frozen base score. `/rivencraft age instability [dimension]` reports those four values and the disabled policy explicitly. This prevents unrelated players, deaths in other dimensions, and repeated deaths from changing an Age. A future opt-in policy must use separate versioned saved data and define its cap/reset rules before it can alter runtime selection.
|
||||
|
||||
Automated #65 checks cover classification, preserved input slots on rejection, and an accepted unstable preview. The live output/one-time Age creation check is assigned to in-game testing because the GameTest storage phase stalled when that transaction ran; the test world is not retained.
|
||||
|
||||
Proposed server defaults for #66–#72 (to confirm during implementation): instability scoring on for newly created Ages; passive entity effects on; world-mutating hazards **off by default** until an operator opts in; explicit per-Age toggle and separate per-family switches. Enforce a central work budget rather than letting every loaded chunk and provider roll independently: default at most one hazard attempt per Age per 20 ticks, one block mutation per successful attempt, and no forced chunk loads. Budgets should be configurable with hard upper bounds, including active Ages, target radius, tick interval, block changes/explosions/meteor impacts, entity effect attempts, and decay spread. Never target another dimension, unloaded chunk, book preview/snapshot, protected spawn/link platform, or portal. Stop safely when the Age unloads or settings change. These are **Rivencraft proposals**, not Legacy defaults or code that exists today; implementation issues must test/adjust them, especially multiple concurrent Ages. Expose score breakdown, selected effects, enablement, and budget suppression in server diagnostics (#72).
|
||||
Issue #66 established a server-only scheduler with bounded per-Age actions, provider calls, levels, cooldowns, loaded-chunk candidates, and rotating active-Age admission. Issues #67–#70 registered the current status, decay, lightning, explosion, meteor, burning, and erosion handlers with protected spawn/link platforms, portal exclusions, and provider-specific damage or spread limits. Instability and world-mutating handlers are enabled by default for newly generated configs to match Legacy's active default; operators can disable them without changing a saved Age. The bounds and protection hooks are **Rivencraft safety adaptations**, not Legacy defaults. Score breakdown, selected effects, enablement, and budget suppression remain part of diagnostics issue #72. See the [runtime policy](INSTABILITY_RUNTIME.md).
|
||||
|
||||
M6 owns scoring, selection, effects, controls, and regression. M7 still owns portal/transport and broader polish; M6 must not depend on new M7 structures. An illegal biome/terrain combination that is presently unrepresentable remains a generation failure until a separate, safe compiler path exists; instability must not be used to disguise absent worldgen support. Existing pre-release saves need explicit migration behavior in #73 rather than a silent rescore.
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Milestone 7 Legacy audit
|
||||
|
||||
Baseline: [Mystcraft Legacy source at `9bc8ddc` (0.13.7.06)](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae), checked against the local 0.13.7.06 asset/class extraction. Source and reused assets are LGPL-3.0; see [asset provenance](ASSET_PROVENANCE.md). This records behavior targets, not byte-for-byte 1.12.2 world generation.
|
||||
|
||||
## Already implemented
|
||||
|
||||
Rivencraft has books, Ages, symbol writing, book stands, a desk, binder, Ink Mixer, link modifiers, safe travel, and optional previews. Crystal Formations, the blue Crystal Block, budding crystals, shards, and Star Fissures were completed in Milestone 5; #76 and #77 must not reimplement them. Vanilla and modern structures are separately page-gated. The Mystcraft library below is neither one of those structures nor a player-authorable page.
|
||||
|
||||
## Libraries: target for #76
|
||||
|
||||
- [`ChunkProviderMyst`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/ChunkProviderMyst.java#L46-L47) installs a scattered-library generator in every Age. [`MapGenScatteredFeatureMyst`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/gen/structure/MapGenScatteredFeatureMyst.java#L34-L103) picks one candidate per 32×32-chunk region with 0–23 chunk offsets and salt `14357617`. Its biome restriction is commented out. There is no Library page and no nearest-structure implementation. Keep automatic sparse placement, modernized for current structure spacing and height APIs, without forcing distant chunks.
|
||||
- [`ComponentScatteredFeatureSmallLibrary`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/gen/structure/ComponentScatteredFeatureSmallLibrary.java#L19-L70) makes one randomly rotated 11×10×11 building at average ground height, with downward cobblestone supports. Its palette is cobblestone, stone stairs/slabs, cobble walls, planks, bookshelves, webs, and air. Preserve its recognizable shape and interior; define safe placement for Cave/Void/Sky Island terrain rather than fabricating ground.
|
||||
- [Loot placement](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/gen/structure/ComponentScatteredFeatureSmallLibrary.java#L762-L791) puts one chest at local `(4,1,2)` and up to five book-bearing lecterns at `(6,2,2)`, `(8,2,4..6)`, `(6,2,8)`. The [treasure table](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/resources/assets/mystcraft/loot_tables/mystcraft_treasure.json) rolls 4–8 plus 1–2 bonus rolls: dynamically registered symbol pages and static vial/booster/leather/paper. [`LootTableHandler`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/treasure/LootTableHandler.java#L69-L94) rerolls lectern contents up to 100 times and requires symbol rank ≥3. Use data-driven server loot, document substitutes for unavailable items, and prevent loot duplication after restart.
|
||||
- The attempted hook into desert pyramid, jungle temple, stronghold library, and dungeon loot uses [`TreasureGenWrapper`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/treasure/TreasureGenWrapper.java), whose `addLoot` is empty in this revision. It is not evidence that those vanilla chests actually yield pages.
|
||||
|
||||
## Crystal portals: targets for #77–#78
|
||||
|
||||
Issue #77 now has a craftable Book Receptacle (eight Crystal Blocks around an empty center), a bounded loaded-chunk crystal-network validator, persistent book holder/source pointers, animated Legacy portal cells, and portal collision routed through the existing Linking/Descriptive Book travel services. A book is only taken after a nonempty frame plan and destination dimension are validated. Removing the book or breaking its support removes the portal; portal cells validate on neighboring block changes, like vanilla portals, rather than self-destructing on recurring ticks during chunk loading. The Descriptive Book path preserves the configured automatic Return Link policy and link modifiers. Crystal-portal arrival does not generate a landing platform, matching Legacy's external travel semantics. A normal Linking Book still needs Intra Linking for same-dimension travel.
|
||||
|
||||
When Immersive Portals is present, render-only, non-teleporting IP portal entities show the book's destination through both faces of each coplanar opening; individual crystal cells are combined into one shaped surface so the destination is not repeated tile by tile. The view's central camera is aligned to the same safe arrival position and yaw/pitch used by book travel (at standing eye height). Perspective still changes naturally as the observer moves in front of a live portal. Each live view is recessed inside the opening; the animated fallback texture stays visible until the live view is established, and crystal blocks render after the IP view so the frame appears on top. Without IP, the animated Legacy portal texture remains visible. Rivencraft portal-block contact remains the travel authority. This is an optional compile-only integration. The first placement of a Descriptive Book can generate/load its Age to establish a view destination. If the optional view is absent after restart, the holder's maintenance pass recreates it. Portal contact uses IP's instant transfer only if the destination's 3×3 chunk area was already loaded; otherwise it uses vanilla Minecraft's dimension transfer.
|
||||
|
||||
Issue #78 hardening: placing each new portal cell fires a NeoForge entity-place protection event attributed to the player who inserted the book. If any cell is rejected, portal placement rolls back and the book remains with the player. Cells are placed without intermediate neighbor notifications so the frame validator cannot remove the first cells before the whole opening exists. Unattended maintenance never generates missing cells or tears down a saved portal during restart: block-change validation handles local frame damage, while the book remains retrievable. Portal owner positions are NBT integer arrays (not compound tags); the reader now matches that format, live-view state changes preserve the block entity's owner, and an intact saved surface with ownerless cells can recover its pointers from the adjacent book receptacle without placing new blocks. A missing neighboring chunk is treated as unknown, not as a broken frame. Portal cooldowns are per-player and session-only, never saved against a server tick counter that resets on restart. A Descriptive Book portal now searches for existing safe footing without creating a platform; Linking Books retain their intentionally floor-optional destination rule. Missing destination dimensions keep the book retrievable and remove any stale Immersive Portals view. Portal blocks and crystal frames are explicitly decay-protected. Book-GUI preview chunk loaders close on GUI exit, travel, logout, and server stop, with a five-minute fail-safe if a release packet is lost; crystal portals themselves add no Rivencraft chunk tickets.
|
||||
|
||||
Manual #78 matrix (use two players where noted): restart with an active portal and confirm the book, surface, and destination view return once its chunk loads; walk through it immediately and after 10 seconds without reinserting the book. Then unload/reload the Age and repeat. With destination chunks loaded, contact should transfer instantly; with them unloaded, it should use Minecraft's normal dimension transfer while the animated fallback remains visible until the live view is ready. Have both players enter the same portal, return through separately bound books, and confirm each cooldown is independent and neither loops immediately. Test a Descriptive Book destination with unsafe feet/head space and with no existing footing: it must choose another footed spot or refuse travel, never generate a platform. Deny a portal cell with a block-protection mod and confirm activation consumes no book and leaves no partial surface. Break one cell and confirm it does not silently reappear. Remove or unload the destination dimension and confirm no crash or stale live view, while the book can still be retrieved. Open a book preview with Immersive Portals, close it, and confirm its temporary chunk loader is released; an abandoned preview should expire within five minutes. Repeat with Immersive Portals absent to confirm the fallback texture and book travel.
|
||||
|
||||
Manual #77 check: build a 3×3 vertical Crystal Block ring, put the Receptacle on one frame crystal's outward face, then insert a bound Linking Book. The center should animate and link on contact; the book stays in the Receptacle. Confirm unbound books and incomplete frames do not consume items; empty-hand right-click retrieves the book and clears the surface. Repeat with a Descriptive Book and with Immersive Portals installed: the latter should show the destination live. Restart with the portal active and confirm the surface/view returns; break the support crystal and confirm book/portal cleanup.
|
||||
|
||||
## Library implementation (#76)
|
||||
|
||||
The operator-only test controls are `/rivencraft library test-spacing 8` (roughly 16× normal candidate density in newly generated chunks), `/rivencraft library test-spacing` (show the current setting), and `/rivencraft library test-spacing reset` (return to Legacy spacing). The override affects only the current Age, lasts only until server restart, and never retroactively places libraries in existing chunks. Normal sites remain eligible. A spacing of `1` attempts a candidate in every new chunk and may be expensive. `/rivencraft library nearest` searches nearby test sites while the override is active. `/rivencraft library test-place` is a separate OP-only manual test: it places a library about 13 blocks ahead of the player in the current Age if an 11×11×11 volume is clear, building a one-block cobblestone platform below it. The command reports the chest coordinates; it does not alter natural spawning or make the manual library a locate candidate. Move to open ground if the command rejects the site.
|
||||
|
||||
Libraries now use Legacy's 32×32-chunk region and 0–23 offset with salt `14357617`, one candidate per region. A candidate builds only on a dry, sturdy, nearly level 11×11 surface with clear headroom; its entire rotated footprint stays in that one chunk. Cave terrain is skipped, Void naturally fails the ground check, and Sky Islands can host a library only on a suitable island top. Rejected candidate sites are not retried elsewhere in the region. This is intentionally safer and sparser than cutting through a modern village, tree, ocean, or cliff. The building now places `data/rivencraft/structure/mystcraft_library.nbt`, generated from all 694 ordered Legacy block placements. The old two-block lectern locations are emptied in the template and replaced at runtime by one-block Rivencraft Book Stands with page loot. The chest is also populated at runtime. Regenerate the template with `python scripts/generate_legacy_library_template.py`; the script pins and checksums the Legacy source.
|
||||
|
||||
The cobblestone supports, stone stairs/slabs, planks, shelves, webs, one chest, and five book stands now follow Legacy's block layout via the packaged template. Players can also place a page on an empty vanilla Lectern, converting it to a Rivencraft Book Stand; sneak-use retrieves the page. The stand renders the page item and its symbol name rather than a book model. `libraries.loot_mode` is a server config with `LEGACY` (default) and `AGE_ONLY` choices. In `LEGACY`, the chest uses `data/rivencraft/loot_table/chests/mystcraft_library.json`, with 4–8 main and 1–2 bonus rolls; Rivencraft Ink Vials replace Legacy vials, and ordinary books/experience bottles replace its unavailable booster item. Stands use random catalog symbol pages, excluding generated block/biome entries. In `AGE_ONLY`, the chest and stands contain only pages from the Age's frozen authored active symbols, or its selected random terrain/features/biome pool; there are no supplies. If an Age has no eligible symbols, the stand falls back to an ordinary book. The Legacy rank ≥3 gate cannot be carried over because Rivencraft symbols have no rank field. A stand's page and Age-only chest pages are saved in block entities; Legacy chests keep their assigned loot table/seed. Neither rerolls on player interaction or restart. Config changes affect only future libraries; existing generated chunks are never retroactively populated.
|
||||
|
||||
For a manual check, create a fresh surface Age and run `/rivencraft library nearest` as an operator. It examines up to 128 deterministic candidates in a six-region radius, one candidate chunk per server tick; this can generate those chunks. It teleports only to a marked intact library with clear floor/headroom, or reports that none was found. Confirm one chest and up to five page-bearing stands, take a page and chest contents, leave/restart, and confirm they do not reappear. Right-click an empty vanilla Lectern with a Symbol Page; it should become a stand displaying that page and symbol name. Sneak-right-click with an empty hand to retrieve it and restore the vanilla Lectern. Switch `libraries.loot_mode` to `AGE_ONLY`, create another fresh Age, and confirm its new library contains only pages that describe that Age (no paper, books, ink or bottles); switching modes must not refill an existing library. Repeat with a second player and with Flat, Cave, Void, and Sky Island Ages: only suitable exposed surfaces should host a library. The archived generation GameTest checks deterministic spacing, negative-coordinate regions, the single-chunk footprint, Age-only page filtering, stand storage, and locator marker detection.
|
||||
|
||||
- A [Book Receptacle](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/block/BlockBookReceptacle.java#L58-L173) attaches to a Crystal Block on any face except downward and holds a Linking or Descriptive Book. The Legacy recipe is eight Crystal Blocks around an empty center. Removing the book or its crystal support deactivates the portal; breaking the receptacle drops the book. [`TileEntityBookReceptacle`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/tileentity/TileEntityBookReceptacle.java#L23-L40) activates/shuts down on server-side book changes.
|
||||
- [`PortalUtils`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/portal/PortalUtils.java#L23-L104) grows portal cells through a connected crystal network. Its tension test requires opposing crystal/portal neighbors on at least two axes; this is more flexible than a single fixed rectangular frame. Active cells trace a path to one receptacle. Modernize the unbounded flood/path walks with loaded-chunk and volume bounds, server validation, and persistent block-entity state.
|
||||
- [`BlockLinkPortal`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/block/BlockLinkPortal.java#L208-L256) is translucent, non-solid, lit, validated on neighbor changes/random ticks, tinted by the book destination, and routes collisions to the receptacle's book. [`ItemLinking.onPortalCollision`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/item/ItemLinking.java#L106-L119) marks travel external, maintains momentum, disables platform generation, and uses the portal sound. The book remains in the receptacle. #77 owns core behavior; #78 owns restart recovery, protection, multiplayer, cooldowns, chunk tickets, and deleted destinations. A buildable portal is distinct from the existing Star Fissure.
|
||||
- [Legacy portal assets](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/resources/assets/mystcraft) include `textures/blocks/book_receptacle.png`, `crystal.png`, animated `portal.png` with its `.mcmeta`, receptacle/crystal/portal block models and blockstates, `textures/entity/linkbook.png`, and `sounds/linking/link-portal.ogg`. Check these before creating art for #80, and add each reused file to [provenance](ASSET_PROVENANCE.md). Legacy's rendering pipeline itself cannot be copied unchanged to 1.21.1.
|
||||
|
||||
## Handheld books and Return Links
|
||||
|
||||
[`ItemLinking.activate`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/item/ItemLinking.java#L79-L103) calls `onLink` before travel. If the player uses the exact book in the selected main-hand slot and the Following flag is absent, it removes the book and spawns an [`EntityLinkbook`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/entity/EntityLinkbook.java#L48-L65) in the **origin** world. Following books stay in hand. The entity can be opened, picked up by sneaking with an empty hand, damaged, and collected by a hopper minecart.
|
||||
|
||||
Issue #75's comment calls this a despawning drop. The code differs subtly: [`EntityLinkbook`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/entity/EntityLinkbook.java#L190-L224) explicitly cannot despawn, but takes one starvation damage every 10,000 ticks and damage while wet. It can eventually be destroyed if left unattended; a vanilla item entity's fixed five-minute timeout is not parity. A modern equivalent should retain the visible book and avoid silently deleting its Age identity at that timeout.
|
||||
|
||||
Legacy's travel path creates no automatic Return Link. Rivencraft now defaults to `DISABLED` in new server configs while retaining `ON_CREATION` and `ALWAYS` as opt-ins. Existing saved server configs keep their explicit value. Issue #85 adds the active-book drop only to hand-held Linking and Descriptive Book travel. On a successful link, the exact active stack becomes a dedicated `DroppedBookEntity` at the departure point, rendered with the same open `BookModel` and textures as the book stand; a rejected link restores it to the original hand. Stand and portal use leave their books in place. The entity has unlimited vanilla lifetime and loses one of its five item-health points every 10,000 loaded ticks, plus one per wet tick, matching Legacy's slow/wet decay. Unlike Legacy's custom entity, the modern book is picked up by ordinary contact, not sneaking with an empty hand, and remains subject to ordinary item hazards. Following is not yet a Rivencraft modifier, so no Following exception is exposed or silently simulated. Both hands can activate a book, whereas Legacy's explicit stack removal tested only its selected main-hand slot.
|
||||
|
||||
## Crafting, sounds, and progression
|
||||
|
||||
| Area | Legacy evidence and modern boundary |
|
||||
| --- | --- |
|
||||
| Crafting | [Legacy recipes](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/resources/assets/mystcraft/recipes) specify the Receptacle, Binder, Mixer, Folder, Desk, and other writing blocks. [`RecipeLinkingbook`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/RecipeLinkingbook.java#L28-L60) combines one Link Panel and leather anywhere in the grid. Rivencraft has Binder, Mixer, Ink Vial, Notebook, Desk, Crystal Block, Linking Book, and Receptacle recipes. #79 keeps these data-driven and adds recipe-book unlocks for Crystal Block, Receptacle, and Ink Vial. The Legacy potion-based ink variant is omitted: in 1.21.1 a simple potion ingredient does not return its glass bottle, while the existing water-bucket recipe returns its bucket. The Legacy Folder/Portfolio use Rivencraft's Notebook; the old two-part Desk is one item; the old Lectern/Book Stand recipes use a vanilla Lectern converted in-world by adding a Rivencraft book or page. Blank Pages remain noncraftable and Link Panels remain outside the Desk. JEI automatically displays the JSON crafting recipes; its existing optional categories cover Mixer, Desk, and Binder processes. The lectern interaction intentionally has no JEI guide. With Create installed, an optional Ponder scene attached to the Receptacle item demonstrates frame construction and book insertion; the core mod still runs without Create. |
|
||||
| Sounds | [`Sounds`](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/Sounds.java#L8-L26) registers pop, link, disarm, following, intra-link, fissure-link, portal-link, and meteor roar. The matching OGGs are in the local extraction. Rivencraft already carries link, fissure-link, and meteor roar. #80 should reuse and credit available Legacy files before creating alternatives. |
|
||||
| Advancements | [Legacy advancement JSONs](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/resources/assets/mystcraft/advancements) have inventory milestones for a symbol page, unlinked Linking Book, and Age Book; a Writing Desk trigger; a safe Age-travel trigger; and a hidden Quinn trigger. #81 should use modern server-owned criteria. Quinn has no established Rivencraft equivalent and should be documented or deliberately omitted, not awarded on preview. |
|
||||
|
||||
## Test boundaries and modern adaptations
|
||||
|
||||
- #76: deterministic sparse, automatic libraries with the 11×10×11 shape, chest and up to five book stands; test restart and nonstandard terrain without forcing distant chunks.
|
||||
- #77: receptacle/crystal activation, connected-frame validation, sound/color, travel through the existing book/Age service, and rejection of invalid books or frames.
|
||||
- #78: full-restart recovery, multiplayer, protection events, loop prevention, destination deletion, and bounded chunk activity.
|
||||
- #79–#81: data-driven recipes/optional JEI, then assets/audio, then advancements. #82–#83 verify NeoForge 1.21.1 compatibility and a focused manual/automated matrix.
|
||||
- #85: drop active non-Following books at origin, disable Return Links for **new** configs, preserve existing config choices, and separately test held, stand, and portal travel.
|
||||
|
||||
The 1.21.1 world height, biome/structure APIs, dynamic Age lifecycle, NeoForge protection events, and dedicated-server rendering boundaries preclude direct transplantation of Legacy's 1.12 generator, metadata, and fixed-function portal renderer. Keep those adaptations explicit in follow-up tests and documentation.
|
||||
@@ -0,0 +1,94 @@
|
||||
# Live block-instability profiling (0.5.8.11–0.5.8.17)
|
||||
|
||||
Mystcraft Legacy measures the abundance of watched blocks in generated chunks,
|
||||
compares that abundance with a baseline, and adds only positive excess to the
|
||||
Age's instability. Its `InstabilityBlockManager` and `ChunkProfiler` are
|
||||
available in the user's local 0.13.7.06 archive. Legacy's Age-side threshold
|
||||
is more than 400 profiled chunks; its default tolerance is 1.05. The Legacy watched
|
||||
blocks include coal, lapis, iron, emerald, redstone, gold and diamond ores,
|
||||
quartz ore, glowstone, and crystal blocks. Diamond **blocks** are not on that
|
||||
default list, but are included here at the player's request.
|
||||
|
||||
Rivencraft samples up to 1,024 distinct loaded chunks per dimension,
|
||||
at most four per server tick. While a player is in an Age, it also gradually
|
||||
generates unsampled chunks around that Age's spawn and the Overworld spawn,
|
||||
at most one chunk per second across both dimensions, until their minimum
|
||||
sample counts are reached.
|
||||
The Overworld is sampled as the baseline; an Age needs 401 samples and the
|
||||
baseline needs 500 before the live block component contributes points. Counts and sampled chunk
|
||||
positions persist per dimension, preventing reloads from double-counting.
|
||||
For each watched block, Rivencraft compares average blocks per sampled chunk
|
||||
and charges `max(0, Age average − 1.05 × Overworld average) × weight`, rounded
|
||||
to the nearest point. The block component is capped at 100,000 points.
|
||||
|
||||
Legacy weights are used for its watched blocks: coal/lapis 5, iron 60, emerald
|
||||
200, redstone 250, gold 750, diamond 4,000, quartz/crystal 20, and glowstone
|
||||
50. Deepslate variants inherit their ore's weight. Modern/high-value extensions
|
||||
include diamond, emerald, gold, iron, raw gold, raw iron and netherite storage
|
||||
blocks, plus ancient debris. Netherite blocks cost 8,000 per excess block per
|
||||
sampled chunk; ancient debris costs 4,000.
|
||||
|
||||
Other mods remain optional. When an installed mod registers common-tagged ores
|
||||
or storage blocks, recognizable `*_ore` blocks, or source-fluid blocks,
|
||||
Rivencraft automatically includes those blocks in live profiling. The source
|
||||
fluid weight is 1.25; common mineral names such as tin,
|
||||
lead, zinc, silver, uranium, platinum, and iridium have explicit tiers, while
|
||||
other tagged ores use 60 and other tagged storage blocks use 120. The optional
|
||||
`instability_modded_materials.block_weight_overrides` list accepts a full-block
|
||||
`modid:block_id=points` entry to cover a block without common tags or replace
|
||||
its inferred weight; zero excludes it. Block entities and non-source fluid
|
||||
plants are excluded. Rivencraft never requires any particular content mod.
|
||||
|
||||
Profiles written before 0.5.8.17 did not retain all modded material
|
||||
counts, so their block sample data resets and is collected again after upgrade.
|
||||
The saved Age definition and authored score are not changed. Later config
|
||||
changes can rescore materials already captured in the profile; a newly added
|
||||
override for a block that was not previously recognized needs fresh samples.
|
||||
|
||||
The authored score and provider plan remain saved as the Age's immutable base.
|
||||
Only Ages created with instability schema v2 participate; existing v1 Ages
|
||||
retain their previously frozen scores/plans, including neutral pre-scoring
|
||||
Ages. This avoids silently rescoring existing worlds.
|
||||
The live block component creates a deterministic **effective** score and
|
||||
provider plan from that base plus the persisted profile; new cards can become
|
||||
active as sampled abundance rises. The command `/rivencraft age instability`
|
||||
shows both the saved base and the currently profiled block component. An
|
||||
unloaded Age has no active profile view until its dimension is loaded again.
|
||||
Turning an effect off does not delete counts or reroll the same effective plan.
|
||||
|
||||
This is not byte-for-byte Legacy profiling. Legacy has a spatial accessibility
|
||||
map and two factors per block, actively builds a dummy-world baseline, and can
|
||||
recompute after subsequent chunk profiling. Rivencraft uses a simpler
|
||||
per-chunk abundance metric, a real-world Overworld baseline, a sample/work cap,
|
||||
and a saved authored base so modern servers remain bounded and restart-safe.
|
||||
The greater-than-400-chunk activation delay is retained. Already-explored
|
||||
chunks count when reloaded, but the bounded spawn-area pass can generate
|
||||
additional terrain. A mod that mutates blocks after the chunk-load event may
|
||||
not be reflected in the first sample.
|
||||
|
||||
## Manual check handed to the player
|
||||
|
||||
The first focused GameTest run after adding the chunk-load listener stalled in
|
||||
its initial 50-test batch and was stopped. It is **not** a passing test result.
|
||||
Do not retry that stalled scenario automatically. In a disposable world:
|
||||
|
||||
1. Create one Age with an authored Diamond Block + Rocks (or Floating Islands)
|
||||
combination and a comparison Age using Stone for the same feature. Explore
|
||||
enough distinct chunks in each Age for `/rivencraft age instability` to show
|
||||
a profiled-block contribution. Explore the Overworld too if its baseline has
|
||||
not reached 400 chunks. The diamond Age should score higher and can gain
|
||||
provider cards; the stone Age should not get a diamond-block contribution.
|
||||
2. Leave and restart the world. Re-enter both Ages and compare their scores and
|
||||
provider IDs/levels. Reloading old chunks must not increase the sample
|
||||
count or score by itself. Generating more diamond-rich chunks may change the
|
||||
effective score and provider set; disabling instability must suppress their
|
||||
effects without erasing the profile.
|
||||
3. Check a naturally generated diamond-ore Age without authored diamond-block
|
||||
features. If its ore abundance is near the Overworld baseline, it should
|
||||
receive little or no ore contribution. Check that profiling does not make
|
||||
chunk loading noticeably stall, and report any exception or server hang.
|
||||
|
||||
Do not use a valued save for this check. The player reported an in-game pass
|
||||
for the 0.5.8.18 candidate on 2026-10-12. The three detailed scenarios above
|
||||
remain useful for future regression runs; the report does not establish that
|
||||
each was exercised individually.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Milestones at a glance
|
||||
|
||||
This is the condensed milestone history. The [current implementation](CURRENT_IMPLEMENTATION.md)
|
||||
is authoritative for what the latest development build does; older acceptance
|
||||
and audit files remain available as dated evidence, not competing current
|
||||
instructions. [Gitea milestones](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/milestones)
|
||||
hold the live issue status.
|
||||
|
||||
## 1 — Bootstrap (complete)
|
||||
|
||||
Established the NeoForge 1.21.1/Java 21 project, mod identity, registries,
|
||||
assets, build, and first automated checks. The port deliberately shares
|
||||
Mystcraft's concepts while maintaining its own modern package and data model.
|
||||
See [porting principles](PORTING.md) and [asset provenance](ASSET_PROVENANCE.md).
|
||||
|
||||
## 2 — Books and linking (complete)
|
||||
|
||||
Introduced persistent Linking and Descriptive Books, conversion of vanilla
|
||||
Lecterns into Book Stands, named books and Ages, safe travel, destination
|
||||
previews, and a configurable Return Link policy. Later milestones refined
|
||||
renderer selection and portal travel. See [preview compatibility](PREVIEW_COMPATIBILITY.md).
|
||||
|
||||
## 3 — Ages (complete)
|
||||
|
||||
Added persistent runtime dimensions, deterministic random Age traits,
|
||||
name-derived unique dimension folders, terrain/biome choices, spawn recovery,
|
||||
and Age lifecycle commands. See [Age administration](AGES.md),
|
||||
[runtime profiles](AGE_PROFILES.md), and [random weights](RANDOM_AGE_WEIGHTS.md).
|
||||
|
||||
## 4 — Writing system (complete)
|
||||
|
||||
Introduced Symbol Pages, Link Panels, Notebooks, the Writing Desk, Book Binder,
|
||||
and Ink Mixer. Writing order and unknown symbol references persist instead of
|
||||
being inferred from UI text. See [writing data](WRITING_DATA.md),
|
||||
[descriptive-book presentation](DESCRIPTIVE_BOOK_PRESENTATION.md), and
|
||||
[Ink Mixer audit](INK_MIXER_MODIFIER_AUDIT.md).
|
||||
|
||||
## 5 — Age grammar (complete for the supported subset)
|
||||
|
||||
Compiled supported pages into frozen Age plans: terrain, biome distribution,
|
||||
environment, features, block/fluid materials, structures, crystals, and
|
||||
fissures. Added optional TerraBlender and Serene Seasons adapters. This was
|
||||
not a claim of complete Legacy grammar parity. See [M5 acceptance](MILESTONE_5_ACCEPTANCE.md),
|
||||
[Legacy grammar audit](LEGACY_AGE_GRAMMAR_AUDIT.md),
|
||||
[symbol schema](SYMBOL_SEMANTICS.md), [TerraBlender](TERRABLENDER_COMPATIBILITY.md),
|
||||
and [Serene Seasons](SERENE_SEASONS_COMPATIBILITY.md).
|
||||
|
||||
## 6 — Instability (complete)
|
||||
|
||||
Added authored and live block-abundance scoring, frozen provider plans,
|
||||
operator diagnostics, bounded status effects, decay, lightning, explosions,
|
||||
meteors, burning, and erosion. Safety budgets are deliberate modern
|
||||
adaptations. See [M6 acceptance](MILESTONE_6_ACCEPTANCE.md),
|
||||
[Legacy instability audit](LEGACY_INSTABILITY_AUDIT.md), and
|
||||
[runtime policy](INSTABILITY_RUNTIME.md).
|
||||
|
||||
## 7 — Structures and polish (complete in 1.0.0.2)
|
||||
|
||||
Added Legacy-layout Age libraries, page-bearing stands, crystal portals and
|
||||
receptacles, the handheld dropped-book entity, data-driven recipes,
|
||||
advancements, asset/audio polish, optional Ruins, compatibility updates, and
|
||||
expanded sky/glyph pages. Issue #83 covered regression, migration, and document
|
||||
consolidation; issue #84 records the final milestone acceptance. See the
|
||||
[M7 acceptance](MILESTONE_7_ACCEPTANCE.md), [M7 Legacy audit](LEGACY_M7_AUDIT.md),
|
||||
[M7 regression matrix](MILESTONE_7_REGRESSION.md), and
|
||||
[M7 compatibility review](MILESTONE_7_COMPATIBILITY_REVIEW.md).
|
||||
@@ -1,6 +1,6 @@
|
||||
# Milestone 5 acceptance: Age grammar
|
||||
|
||||
This is the completion record for [Gitea issue #42](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/issues/42) and release 0.5.0.0. Issues #28–#41, #45–#59, #61, and #62 are closed. #39 was closed with an explicit Ecliptic Seasons limitation, not with a working Ecliptic adapter. #42 is the final gate. Issues #43, #44, and #60 are separate open follow-ups, not hidden Milestone 5 completion claims.
|
||||
This is the completion record for [Gitea issue #42](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/issues/42) and release 0.5.0.0. Issues #28–#41, #45–#59, #61, and #62 are closed. #39 was closed with an explicit Ecliptic Seasons limitation, not with a working Ecliptic adapter. #42 is the final gate. Issues #43, #44, and #60 are separate open follow-ups, not hidden Milestone 5 completion claims.
|
||||
|
||||
## Authoring and generation contract
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Milestone 6 acceptance review (issue #74)
|
||||
|
||||
Milestone 6 covers authored and live block instability, deterministic provider
|
||||
selection, bounded Age effects, diagnostics, and migration. Its required issues
|
||||
#63–#73 are closed. This document records acceptance of **0.6.0.0**, not a
|
||||
claim of exact Mystcraft 1.12.2 parity or a claim that every optional-mod
|
||||
combination was tested. The player reported an in-game pass for the 0.6.0.0
|
||||
candidate on 2026-10-12; the broader manual matrix remains a future checklist.
|
||||
|
||||
## Scoring and writing
|
||||
|
||||
The [pinned Legacy audit](LEGACY_INSTABILITY_AUDIT.md) records source defaults
|
||||
and the Rivencraft mapping. Authored score/breakdown and provider plan are
|
||||
saved per Age; seed-completed missing branches cost zero. Extra single-choice
|
||||
controllers cost +500, and out-of-order non-modifier pages cost +100.
|
||||
Version-4 Age plans consume period/phase modifiers in written order; repeated
|
||||
modifiers combine, while each kind left unconsumed after the last celestial
|
||||
page costs +100. Bright Lighting costs +500 per occurrence. The fourth and
|
||||
later occurrences of each of the four Legacy structure pages cost +100; the
|
||||
same rule applies to named modern structure pages as a project balance
|
||||
extension. Forcing a structure into an unsupported selected biome costs +100
|
||||
per occurrence. Accelerated and Corrupted pages retain their audited costs.
|
||||
Unknown or unrepresentable new writing remains a clear binding error, rather
|
||||
than a silent no-op. See [score details](INSTABILITY_SCORE.md).
|
||||
|
||||
The [live block profile](LIVE_BLOCK_INSTABILITY.md) samples generated chunks
|
||||
gradually and contributes only after the Age and Overworld-baseline thresholds.
|
||||
It can change the effective score and active provider selection as new chunks
|
||||
are profiled, without rewriting authored score or writing. Diamond storage
|
||||
blocks are an intentional addition to Legacy's watched blocks. Ores, mineral
|
||||
storage blocks, and source fluids supplied by installed mods are recognized
|
||||
automatically through common tags and material names; those mods are **not**
|
||||
Rivencraft dependencies. Exact weight overrides are optional configuration.
|
||||
|
||||
## Effects, safety, and diagnostics
|
||||
|
||||
The [runtime policy](INSTABILITY_RUNTIME.md) documents saved deck order,
|
||||
provider costs/levels, per-Age cooldown, rotating Age admission, loaded-chunk
|
||||
selection, and server-side action budgets. Defaults allow four Ages per tick,
|
||||
two provider calls and one effect action per Age run, up to 16 loaded chunk
|
||||
candidates, and a 20-tick per-Age cooldown. These caps and protected
|
||||
spawn/portal/platform areas are modern safety adaptations, not Legacy's
|
||||
unrestricted execution. Global, per-Age, provider, and world-mutation controls
|
||||
suppress execution without rerolling a saved authored plan.
|
||||
|
||||
Implemented effect families include bounded status effects, four active
|
||||
colored decay variants, lightning, explosions, meteors, ordinary burning,
|
||||
and guarded erosion. Erosion is deliberately enabled by default for testing
|
||||
although Legacy did not register its provider in the active deck. Dormant
|
||||
Legacy black/green/yellow decay variants, global burning, and bedrock crumble
|
||||
are **not** claimed as working. Potion targeting excludes Creative/Spectator
|
||||
players; hostile buffs target hostile mobs. The [effect audit](LEGACY_INSTABILITY_AUDIT.md)
|
||||
records those differences. [Operator diagnostics](INSTABILITY_DIAGNOSTICS.md)
|
||||
report authored and live block contributions, selected providers, enabled
|
||||
state, and budget suppression; raw score is not shown in book UI.
|
||||
|
||||
## Migration and validation
|
||||
|
||||
Pre-scoring Ages migrate to neutral instability. Score-only Ages receive a
|
||||
deterministic saved provider plan. Existing plan-v3 Ages keep their old
|
||||
stage-sorted celestial behavior and writing cost; new plans use schema 4.
|
||||
Block profile schema 2 resets old samples once to capture modded materials,
|
||||
and preserves unknown material IDs if an optional mod is later removed.
|
||||
Missing symbol definitions do not reinterpret a saved authored score, but
|
||||
removing required biome/worldgen registry entries may still prevent world
|
||||
loading. General old-save compatibility is tracked separately in issue #60.
|
||||
|
||||
The [Milestone 6 regression record](MILESTONE_6_REGRESSION.md) distinguishes
|
||||
the passing 0.5.8.10 focused archive, the later storage-stalled archive, the
|
||||
0.5.8.18 build, and the player's reported in-game pass. Its expanded manual
|
||||
matrix is a repeatable future checklist, **not** an individually signed-off
|
||||
pass for every optional mod, multiplayer, restart, and hazard combination.
|
||||
For this completion candidate, verify build, diff check, jar checksum, and a
|
||||
fresh in-game pass before publication. Do not rerun the known stalling archive
|
||||
as a release gate; hand any newly stalled GameTest scenario to the player.
|
||||
The completion review also replaced repeated linear scans of saved active
|
||||
symbol indices with one lookup map per call, and shares one effective provider
|
||||
plan across the scheduler, hazard, and environmental lanes of an admitted Age
|
||||
tick. Live-profile scoring within that plan also uses one sample evaluation
|
||||
instead of two. These are
|
||||
behavior-preserving cleanups; no provider rates or budgets were raised.
|
||||
|
||||
## NeoForge decision
|
||||
|
||||
The project currently develops against NeoForge **21.1.251** for Minecraft
|
||||
1.21.1. The [official 21.1.259 changelog](https://maven.neoforged.net/releases/net/neoforged/neoforge/21.1.259/neoforge-21.1.259-changelog.txt)
|
||||
lists mostly loader/Mixin dependency updates, a GUI slot-index API change,
|
||||
and targeted fixes since 21.1.251. None addresses a known Rivencraft defect.
|
||||
Upgrading the development and minimum runtime version during this acceptance
|
||||
gate would force an additional optional-integration test pass for little clear
|
||||
benefit. Keep 21.1.251 for 0.6.0.0; evaluate 21.1.259 in a separate later
|
||||
slice with Create/Ponder, Immersive Portals, Dynamic Dimensions, JEI, and the
|
||||
season/biome integrations. This is a release-risk judgment, not a claim that
|
||||
21.1.259 is incompatible.
|
||||
|
||||
## Gate status
|
||||
|
||||
- Required implementation issues #63–#73: closed.
|
||||
- Known unsupported effects and broader save compatibility: explicitly scoped
|
||||
above, not counted as working Milestone 6 behavior.
|
||||
- Candidate `./gradlew build --offline --no-daemon --quiet`: passed; the
|
||||
compiler reported only existing GameTest helper deprecation warnings.
|
||||
Local JAR SHA-256: `6eed828156dd1e2679b013c3c5bf8e692a20d9ab80ae27417b3cafe1c26422c7`.
|
||||
- Player in-game acceptance of completion candidate: reported pass on
|
||||
2026-10-12; individual optional-mod and multiplayer matrix rows were not
|
||||
separately signed off.
|
||||
- [Gitea V0.6.0.0 release](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/releases/tag/V0.6.0.0):
|
||||
published with changelog and jar; downloaded asset SHA-256 matched the local
|
||||
build. Issue #74 and Milestone 6 were closed with all 12 milestone issues
|
||||
closed and zero remaining open.
|
||||
@@ -0,0 +1,158 @@
|
||||
# Milestone 6 instability regression and migration check (#73)
|
||||
|
||||
This is the release-slice acceptance record for instability, not a promise of
|
||||
general old-save compatibility. The latter remains issue #60. Run the focused
|
||||
archive with `./gradlew build runGameTestServer -ParchivedGameTestSuites=instability`;
|
||||
it includes the default smoke suite.
|
||||
For 0.5.8.10 on 2026-10-10, that dedicated GameTest server passed all 55 required tests
|
||||
with no optional content/client integrations installed (the required Dynamic
|
||||
Dimensions and Bad Packets dependencies were present). The full archive was not run;
|
||||
it has a previously documented storage stall, so do not count it as passing.
|
||||
The 0.5.8.11 run with live block profiling stalled in its initial batch and
|
||||
was stopped; it is **not** a passing result. The live profiling scenario is
|
||||
handed to the player in [the block-profile checklist](LIVE_BLOCK_INSTABILITY.md).
|
||||
|
||||
## Automated coverage
|
||||
|
||||
| Requirement | Existing default test or focused archive |
|
||||
| --- | --- |
|
||||
| Stable writing, defective writing, score explanations, repeated use of the same book | `AgePersistenceGameTests.instabilitySeparatesStableAndDefectiveWriting` |
|
||||
| Frozen score, age-scoped writing cost, random baseline | `AgePersistenceGameTests.randomBaselineAndBrightBalanceAreFrozen`, `auditedSymbolCostsAreCountedAndAgeScoped` |
|
||||
| Complete Age definition, score and provider-plan save/load | `AgePersistenceGameTests.authoredPlanRetainsResolvedMeaningAndAgeIdentity`; `AgeInstabilityRuntimeGameTests.selectionIsDeterministicAndFrozen` |
|
||||
| Old pre-scoring Ages, score-only saves, removed symbols, unavailable handlers | `AgePersistenceGameTests.missingRegistryEntryDoesNotReinterpretAnOldAge`, `instabilitySeparatesStableAndDefectiveWriting`; `AgeInstabilityRuntimeGameTests.savedPlanSurvivesAgeUpdatesAndMissingHandlers` |
|
||||
| Per-Age controls and changed config without score/plan reroll | `AgeInstabilityRuntimeGameTests.perAgeControlsPersistWithoutChangingScoresOrOtherAges`; `Milestone6RegressionGameTests.disablingEffectsCannotRerollFrozenPlan` |
|
||||
| Hazard cooldown and attempt sequence across saved-state reload | `AgeInstabilityRuntimeGameTests.hazardCooldownAndAttemptSequenceSurviveReload` |
|
||||
| Active-Age rotation, action/tick budget | `AgeInstabilityRuntimeGameTests.ageScopeRotationAndActionBudgetAreBounded` |
|
||||
| Multiplayer loaded-chunk-only candidate selection, deduplication and cap | `Milestone6RegressionGameTests.multiplayerCandidatesStayLoadedDeduplicatedAndCapped` |
|
||||
| Decay loaded-chunk target and spawn protection | `AgeDecayGameTests.targetMustRemainInChosenLoadedChunk`, `savedSpawnAndPlatformAreProtected` |
|
||||
| Explosion policy gates and protected blocks | `Milestone6RegressionGameTests.disabledGriefAndProtectedBlocksStayUntouched` |
|
||||
| No Overworld status or decay provider | `AgeStatusEffectGameTests.overworldCannotRunAnAgeStatusProvider`; `AgeDecayGameTests.overworldCannotRunDecayProvider` |
|
||||
| Optional-mod material weights, block overrides, and old profile migration | `Milestone6RegressionGameTests.commonModdedMaterialWeightsStayBounded`, `blockProfileSamplesPersistWithoutDoubleCounting` |
|
||||
| Ordered celestial modifier consumption, repeated modifiers, and dangling +100 per kind | `WritingGrammarGameTests.orderedGrammarAcceptsPartialWritingAndDiagnosesInvalidSequences`; `EnvironmentWritingGameTests.celestialModifiersFollowWrittenOrder` |
|
||||
|
||||
These tests exercise saved-data serialization and deterministic policies. They
|
||||
do **not** simulate a complete process restart, an actual multiplayer server,
|
||||
optional-mod installation, or real terrain and hazard visuals. The three new
|
||||
checks live only in the `instability` archive to avoid duplicating default tests.
|
||||
|
||||
## Migration outcome and known gaps
|
||||
|
||||
- Pre-scoring Milestone 5 Ages load with a neutral score and no effects; they
|
||||
are not retroactively rescored under current writing rules.
|
||||
- Score-only Ages receive a deterministic provider plan once from their saved
|
||||
score and Age seed. An already-saved score and plan retain their meaning when
|
||||
pages, handlers, or runtime config change. Unknown but syntactically valid
|
||||
provider IDs are preserved and skipped if their handler is unavailable.
|
||||
- Missing symbol definitions do not reinterpret a frozen instability score.
|
||||
That check is narrower than arbitrary missing biome, block, or worldgen
|
||||
registry entries: loading a world with removed data packs is not guaranteed.
|
||||
Broad save compatibility and migration remain tracked in #60.
|
||||
- Version-3 plans keep their previous stage-sorted celestial interpretation and
|
||||
frozen writing cost. Only new version-4 plans consume period/phase modifiers
|
||||
in authored order and charge a trailing modifier once per pending kind.
|
||||
- In-process roundtrips cannot prove that Age dimensions reload correctly
|
||||
after a full stop/start. Check that in a real client/server world. Do not add
|
||||
another GameTest for it if it hits the documented storage stall; hand the
|
||||
exact scenario to the player instead.
|
||||
- The dedicated GameTest run establishes classloading with no optional runtime
|
||||
flags. It does not establish classloading with every optional integration
|
||||
installed, nor the reverse of removing one from an existing save.
|
||||
|
||||
## Manual in-game matrix
|
||||
|
||||
Use a fresh test world, then keep one save for the restart and data-pack checks.
|
||||
Record the seed, mod list, age name, and `/rivencraft age instability` output
|
||||
before and after each change. Do not deliberately remove a required registry
|
||||
entry from a valued world; copy the test save first.
|
||||
|
||||
1. Bind a low-instability book and a deliberately unstable book. Reuse each
|
||||
book and verify the Age name, dimension ID, score, selected providers and
|
||||
provider levels remain unchanged. Exit the Ages, stop Minecraft completely,
|
||||
restart and re-enter; compare the same diagnostic values.
|
||||
2. With two survival players in one unstable Age and a third player in the
|
||||
Overworld, verify effects stay in the Age, follow the Age players, and do
|
||||
not appear in the Overworld. Move both Age players away, let old chunks
|
||||
unload, and confirm hazards do not keep remote chunks loaded or operate
|
||||
there. Confirm controls on one Age do not alter a second Age.
|
||||
3. Disable global instability, then world mutation, then one provider. Verify
|
||||
the suppressed effect stops and the frozen score/plan do not change after
|
||||
re-enabling or restarting. With block destruction disabled, trigger an
|
||||
explosion in an expendable Age; check that arrival platforms, portals,
|
||||
block entities and protected blocks survive. Check lightning, explosion,
|
||||
decay, status-effect, burning, meteor and erosion visuals where selected.
|
||||
4. With Biomes O' Plenty or another TerraBlender biome mod, confirm recognized
|
||||
biome pages still author an Age and the instability diagnostics remain
|
||||
stable after restart. Repeat with Serene Seasons installed, then absent in
|
||||
a disposable copy; confirm its Age season behavior and that Rivencraft
|
||||
itself still loads. JEI should display the relevant pages and recipes when
|
||||
installed, while the mod works without it.
|
||||
Separately, install a mod with common-tagged ores/storage blocks or source
|
||||
fluids. Compare a material-rich Age with an ordinary Age after the profile
|
||||
thresholds are reached; the installed mod's recognized blocks should affect
|
||||
the result without a Rivencraft allowlist. Then remove that optional mod in
|
||||
a disposable copy and confirm Rivencraft still starts. Test a single
|
||||
`block_weight_overrides` entry and a zero-weight exclusion. This remains a
|
||||
player-run scenario, not an automated pass.
|
||||
5. Check the book preview separately with its fallback animation, Ponder/Create,
|
||||
and Immersive Portals when available. Opening a book should not change
|
||||
player location, instability diagnostics, or ambient audio in the actual
|
||||
Age. Do not install both season mods together for this matrix.
|
||||
6. On a copied save, remove or alter a **nonessential** data pack providing
|
||||
a writing symbol or optional handler, then reload. Existing saved scores
|
||||
and provider plans must not reroll. If a referenced worldgen registry entry
|
||||
prevents load, record it against issue #60 rather than treating #73's
|
||||
frozen-instability guarantee as general missing-registry support.
|
||||
|
||||
The matrix is guidance for a player-run pass; none of these six scenarios is
|
||||
claimed as completed by the automated result above.
|
||||
The player reported an in-game pass for the 0.5.8.18 candidate on 2026-10-12.
|
||||
That is a release-slice acceptance report, not a claim that every expanded
|
||||
matrix row below was individually exercised.
|
||||
|
||||
### Regression run record
|
||||
|
||||
For each row, record pass/fail, world seed, Age name, Rivencraft version,
|
||||
optional-mod versions, and a short observation. "Not run" is intentional
|
||||
until a player completes the in-game check; a successful build is not a pass.
|
||||
|
||||
| Area | Specific manual observation | Status |
|
||||
| --- | --- | --- |
|
||||
| New vs reused book | Dimension ID, score, and providers do not reroll | Not run |
|
||||
| Full process restart | Re-enter two Ages; saved traits, score, plan, and cooldowns persist | Not run |
|
||||
| Multiplayer isolation | Two Age players receive effects; Overworld player does not | Not run |
|
||||
| Loaded-chunk scope | Departing players permit unloading; hazards do not reach old chunks | Not run |
|
||||
| Controls | Global, per-Age, mutation, and provider toggles suppress effects without reroll | Not run |
|
||||
| Protected blocks | Explosion/decay/erosion spare arrival platforms, portals, and block entities | Not run |
|
||||
| Hazard visuals | Lightning, explosions, decay, status, burning, meteor, and erosion each behave when selected | Not run |
|
||||
| Third-party biomes | TerraBlender biome pages and cave classification survive Age creation/restart | Not run |
|
||||
| Optional integrations absent | Mod launches without JEI, Create/Ponder, Immersive Portals, Serene Seasons, or biome mods | Not run |
|
||||
| Optional integrations present | JEI pages/recipes, preview backends, seasonal behavior, and ambient sound work | Not run |
|
||||
| Optional-mod materials | Tagged ore/storage/source-fluid samples change live profile automatically | Not run |
|
||||
| Material overrides | Positive and zero `block_weight_overrides` values affect only named blocks | Not run |
|
||||
| Removed optional mod | Disposable copy launches without the mod; unknown sampled IDs stay serialized | Not run |
|
||||
| Removed symbol/handler | Frozen score and provider plan survive when nonessential data is removed | Not run |
|
||||
| Ordered modifiers | Five book-order cases below produce expected time and dangling costs | Not run |
|
||||
|
||||
### Focused ordered-writing and optional-material checks for 0.5.8.18
|
||||
|
||||
Use `/rivencraft age instability` in each newly bound Age; old Ages retain
|
||||
their original plan and score. Record each row as pass/fail with the Age name,
|
||||
world seed, installed optional mods, and the command output. The ordered
|
||||
modifier assertions are archived, not a claim that the stalled GameTest suite ran.
|
||||
|
||||
| Book pages after Link Panel, in exact order | Expected result | Result |
|
||||
| --- | --- | --- |
|
||||
| Half Length, Double Length, Normal Time | Length combines to 1.25×, no dangling cost | Not run in game |
|
||||
| Normal Time, Zero Length | Sun keeps normal motion; one dangling +100 | Not run in game |
|
||||
| Rising, Normal Time, Setting | Rising start phase; Setting dangles +100 | Not run in game |
|
||||
| Half Length, Double Length, no celestial page | One combined pending length dangles +100; seeded clock remains | Not run in game |
|
||||
| Normal Time, Zero Length, Normal Time | Last sun consumes Zero Length; duplicate celestial cost remains separate | Not run in game |
|
||||
|
||||
With a common-tagged ore/storage-block or source-fluid mod installed, bind
|
||||
comparable material-rich and ordinary Ages and wait for profiling thresholds.
|
||||
Verify the modded material contributes automatically, then restart and confirm
|
||||
the score/profile remains consistent. On a disposable copy, remove the mod and
|
||||
confirm Rivencraft launches without it; restore the mod before judging the
|
||||
world's registry-dependent terrain. Also check a zero-weight override and a
|
||||
positive override. These are optional-mod compatibility checks, not a new
|
||||
required dependency.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Milestone 7 acceptance — 1.0.0.2
|
||||
|
||||
Milestone 7 is complete for the supported NeoForge 1.21.1 feature set. This is
|
||||
not a claim of total Mystcraft Legacy parity or exhaustive optional-mod
|
||||
certification. The [current implementation](CURRENT_IMPLEMENTATION.md) is the
|
||||
up-to-date behavior summary; the [regression matrix](MILESTONE_7_REGRESSION.md)
|
||||
separates automated evidence from manual scenarios.
|
||||
|
||||
## Scope and issue gate
|
||||
|
||||
The Gitea audit for issue #84 found no other open issues. Required existing
|
||||
#14 (cave terrain), #43 (glyph numbers), #44 (sky), and #60 (old-save policy)
|
||||
were closed. M7 implementation and review issues #75–#83, #85–#90, and
|
||||
later additions #91–#94 were closed before this acceptance. No required
|
||||
issue was silently deferred.
|
||||
|
||||
| Legacy concept | Rivencraft implementation or deliberate difference |
|
||||
| --- | --- |
|
||||
| Cave terrain and writing | Cave terrain uses its cave/Nether biome set and a modern Y=64-start spawn search. Existing non-cave terrain still supports caves as features. See [Age profiles](AGE_PROFILES.md) and [grammar](WRITING_GRAMMAR.md). |
|
||||
| Libraries | The recognizable Legacy small-library layout and sparse candidate spacing are ported as a structure template with a chest and up to five page-bearing Book Stands. It generates only in Ages and only on suitable surfaces; player-selectable Legacy or Age-only loot is supported. See [Legacy M7 audit](LEGACY_M7_AUDIT.md). |
|
||||
| Crystal portal and Receptacle | Connected Crystal Blocks support vertical and horizontal portals. The held book stays in the Receptacle; removing it removes the surface. Rivencraft validates loaded bounds and block protection, saves owner pointers, and uses book-travel rules. Optional Immersive Portals supplies a live destination view, with an animated fallback while unavailable. |
|
||||
| Handheld Linking/Descriptive Books | The used book remains behind as a rendered book entity at the origin. The exact Legacy entity pickup rule is not copied: modern contact pickup is used. Return Links default to disabled but remain configurable. |
|
||||
| Recipes and discovery | JSON crafting recipes and recipe-book unlocks cover the supported writing/crystal progression. Optional JEI covers the Binder, Writing Desk, and Ink Mixer; a Lectern conversion has no JEI guide. Blank Pages are not craftable. Optional Create/Ponder teaches portal construction. See [JEI review](MILESTONE_7_JEI_REVIEW.md). |
|
||||
| Models, sounds, and advancements | Legacy-sourced assets are credited in [asset provenance](ASSET_PROVENANCE.md); 1.21.1-compatible models and renderer adaptations are documented in the [visual audit](MILESTONE_7_VISUAL_AUDIT.md). Server-owned advancements cover book, writing, Age, and portal progress; the unimplemented Quinn equivalent is explicitly omitted in [advancement notes](MILESTONE_7_ADVANCEMENTS.md). |
|
||||
| Numbers, sky, and extra items | Base-25 number glyphs, celestial modifiers and eclipses, Symbol Portfolios, the OP-only Link Modifier, and optional Ruins pages were follow-up additions. Legacy page semantics are adapted to the modern authoring plan rather than a binary copy of 1.12.2 worldgen. |
|
||||
|
||||
## Compatibility and migration
|
||||
|
||||
NeoForge **21.1.264** is the minimum 21.1 build. A player passed the
|
||||
singleplayer client with the optional stack, then a local dedicated server
|
||||
including Immersive Portals, before the minimum was raised. See the
|
||||
[compatibility decision](MILESTONE_7_COMPATIBILITY_REVIEW.md). JEI
|
||||
19.69.0.457 and Serene Seasons 10.1.0.9 received their own player checks;
|
||||
Create/Ponder, Immersive Portals, TerraBlender/biome add-ons, Serene Seasons,
|
||||
JEI, and Ruins remain optional. Ecliptic Seasons is deliberately not
|
||||
integrated: its 1.21.1 public API does not support the required live Age
|
||||
registration and per-Age calendar control. Missing optional mods must not
|
||||
prevent the core mod from loading.
|
||||
|
||||
Known older writing/book, Age-plan, and workstation formats have explicit
|
||||
migration or stable-default handling. An Age registry and its dimension
|
||||
folders must be backed up together. Removed mods, missing custom registry
|
||||
objects, and arbitrary prerelease saves are **not** guaranteed to load.
|
||||
Retiring an Age through the creative Link Modifier archives its data and
|
||||
invalidates its UUID; that intentional destructive operation is not a
|
||||
migration. See the [old-save boundary](MILESTONE_7_REGRESSION.md#old-save-boundary-60).
|
||||
|
||||
## Release validation and limits
|
||||
|
||||
The 0.6.11.0 regression candidate passed `./gradlew --offline build` and
|
||||
fresh-world GameTest runs: core **91/91**, generation **129/129**, and
|
||||
environment **103/103**. The player reported an in-game pass for that
|
||||
candidate. The 1.0.0.1 release candidate additionally allows Number Pages to
|
||||
set Celestial Size. The 1.0.0.2 release makes Binder status diagnostics an
|
||||
OP-only, default-off world setting. Its offline build and core GameTests
|
||||
passed (93/93), and the player reported an in-game pass. The
|
||||
preceding 1.0.0.0 candidate passed core and generation GameTests. Its
|
||||
environment archive stalled in the first 50-test batch, repeatedly printing
|
||||
its field without completing; it was stopped and is **not** counted as a
|
||||
1.0.0.0–1.0.0.2 pass. The previous 0.6.11.0 run of that same suite passed 103/103.
|
||||
The focused sky, season, and number-page in-game checks in the regression
|
||||
matrix remain useful regression checks, not claimed full-matrix passes. The
|
||||
Gitea release changelog records the tested JAR and verified SHA-256.
|
||||
|
||||
These results do not prove every two-player race, full restart, migration
|
||||
from every development world, or optional-mod combination. The specific
|
||||
manual matrix remains in [M7 regression](MILESTONE_7_REGRESSION.md); those
|
||||
broader combinations are known validation limits, not claimed passes. Modern
|
||||
chunk and tick budgets, safer portal validation, 1.21.1 world-height and
|
||||
biome APIs, and the dynamic-dimension backend are deliberate deviations from
|
||||
Legacy. A complete Legacy worldgen or link-property port is outside this
|
||||
milestone.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Milestone 7 advancements and discovery (#81)
|
||||
|
||||
Rivencraft's advancement tab follows the visible progression in Mystcraft
|
||||
Legacy's `assets/mystcraft/advancements/`: Page, unwritten Linking Book,
|
||||
Writing Desk writing, Age Book, and safe Age travel. Titles such as *The Ending
|
||||
Has Not Yet Been Written*, *D'ni Ink*, and *The Art of Writing* reference Myst
|
||||
without requiring any assets from that game. The Legacy hidden Quinn trigger
|
||||
has no established Rivencraft action and is deliberately omitted.
|
||||
|
||||
`root`, `page`, and `linking_book` use vanilla server-side tick/inventory
|
||||
criteria. `link_panel`, `copy_page`, `bind_age`, `open_portal`, and `enter_age`
|
||||
use an impossible criterion awarded only after the server commits the
|
||||
corresponding action. The Age-travel award requires a successful book or portal
|
||||
link ending in a registered Rivencraft Age. Opening a book GUI, rendering a
|
||||
destination preview, viewing a recipe, or merely loading an Age cannot award
|
||||
it. Progress is stored by Minecraft per player and survives world restarts.
|
||||
|
||||
Vanilla recipe-book unlock advancements now cover the Writing Desk, Ink Mixer,
|
||||
Book Binder, and Notebook, in addition to the existing Ink Vial, Crystal
|
||||
Block, and Book Receptacle unlocks. The component-sensitive Linking Book
|
||||
crafting recipe remains a special recipe: its Link Panel requirement and the
|
||||
Writing Desk/Ink Mixer/Book Binder processes are explained by Rivencraft's
|
||||
optional JEI integration. No recipe or advancement requires JEI, Create, or
|
||||
Immersive Portals to be installed.
|
||||
|
||||
Manual Survival check: collect a Page and Linking Book; use the Ink Mixer to
|
||||
take a Link Panel; copy a Page at the Writing Desk; bind a Descriptive Book;
|
||||
enter its Age; activate a Crystal Portal. Confirm each advancement appears
|
||||
only after the completed action, not from an output preview or GUI opening.
|
||||
Repeat the actions with two players to confirm only the actor receives each
|
||||
award. Restart an existing save and confirm completed progress remains, then
|
||||
complete one previously unearned milestone. With JEI absent, the vanilla
|
||||
recipe book should still reveal standard crafting recipes as their ingredients
|
||||
are discovered; with JEI installed, the special writing guides should appear.
|
||||
|
||||
The automated Book Binder output-award scenario stalls in the GameTest storage
|
||||
runner, so it is intentionally omitted under the project test policy. For its
|
||||
manual check, open a ready Binder, observe that merely previewing the finished
|
||||
book does not award *A Book of Ages*, then take the book and confirm the award.
|
||||
Repeat by shift-clicking the result in a fresh Binder.
|
||||
@@ -0,0 +1,53 @@
|
||||
# Milestone 7 compatibility review (#82)
|
||||
|
||||
Reviewed 2026-10-19 for Minecraft 1.21.1. The project now compiles against
|
||||
NeoForge 21.1.264 and advertises that as its minimum. The official
|
||||
[NeoForge Maven metadata](https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml)
|
||||
lists 21.1.264 as the newest 21.1 release. Its
|
||||
[changelog](https://maven.neoforged.net/releases/net/neoforged/neoforge/21.1.264/neoforge-21.1.264-changelog.txt)
|
||||
includes received split-packet size checks (21.1.260), network setup payload
|
||||
direction/decoding limits (21.1.261), ad-hoc channel identifier limits
|
||||
(21.1.262), and an initial attachment-sync fix (21.1.264). Those are concrete
|
||||
reasons to evaluate an upgrade, but do not by themselves prove Rivencraft and
|
||||
its optional integrations work on that loader.
|
||||
|
||||
**Decision: adopt 21.1.264 beginning with 0.6.6.2.** An override build
|
||||
succeeded, but its GameTest server repeated its storage-progress display
|
||||
without producing test results. That run was stopped; it is neither a pass nor
|
||||
evidence of a NeoForge defect. The player subsequently passed an in-game
|
||||
singleplayer test on 21.1.264 with the optional stack, then passed a dedicated
|
||||
server test including Immersive Portals travel to a Rivencraft Age. The
|
||||
dedicated server also reached the ready state with only the required mods
|
||||
before the full optional stack was installed. Both modes used Rivencraft
|
||||
0.6.6.1. The earlier 21.1.251 baseline passed 70 required GameTests for
|
||||
release V0.6.6.0.
|
||||
|
||||
| Dependency | Project baseline | Newest checked 1.21.1 release | Follow-up |
|
||||
| --- | --- | --- | --- |
|
||||
| Dynamic Dimensions AR Edition (required) | 0.9.1 | 0.9.1 | No newer release found on [CurseForge](https://www.curseforge.com/minecraft/mc-mods/dynamicdimension-ar-edition/files/all). |
|
||||
| Bad Packets (required) | neo-0.8.2 | neo-0.8.2 | No newer matching [Modrinth](https://modrinth.com/mod/badpackets/versions) release found. |
|
||||
| JEI (optional) | 19.57.0.449 at #82 review | 19.69.0.457 beta | Updated in #86 after client and dedicated GameTest passes; see [JEI review](MILESTONE_7_JEI_REVIEW.md). |
|
||||
| Create (optional) | 6.0.10 | 6.0.10 | No newer matching [Modrinth](https://modrinth.com/mod/create/versions) release found. Its jar bundles Ponder 1.0.82. |
|
||||
| Ponder (bundled with Create) | 1.0.82 | 1.0.87 library artifact | [Maven](https://maven.createmod.net/net/createmod/ponder/ponder-neoforge/maven-metadata.xml) has a newer artifact, but Create 6.0.10 still bundles 1.0.82. Do not treat this as a separately installed Create update. |
|
||||
| Immersive Portals (optional) | 6.0.7 | 6.0.7 | No newer matching [Modrinth](https://modrinth.com/mod/immersive-portals-neoforge/versions) release found. |
|
||||
| TerraBlender (optional) | 4.1.0.8 | 4.1.0.8 | No newer matching [Modrinth](https://modrinth.com/mod/terrablender/versions) release found. |
|
||||
| Serene Seasons (optional) | 10.1.0.8 at #82 review | 10.1.0.9 | Updated in #88 after the player's full seasonal matrix passed; see [Serene review](MILESTONE_7_SERENE_REVIEW.md). |
|
||||
| GlitchCore (Serene dependency) | 2.1.0.2 | 2.1.0.2 | No newer matching [Modrinth](https://modrinth.com/mod/glitchcore/versions) release found. |
|
||||
| Biomes O' Plenty (TerraBlender test addon) | 21.1.0.14 in user's Prism instance | 21.1.0.14 | No newer matching [Modrinth](https://modrinth.com/mod/biomes-o-plenty/versions) release found. |
|
||||
|
||||
The Modrinth version comparisons above were filtered to **NeoForge and
|
||||
Minecraft 1.21.1** on 2026-10-19. Create's bundled Ponder version was read
|
||||
from `META-INF/jarjar/metadata.json` in `create-1.21.1-6.0.10.jar`; the
|
||||
separate library-artifact comparison came from Create's Maven. Dynamic
|
||||
Dimensions was checked on CurseForge. Versions for other Minecraft lines and
|
||||
similarly named mods were excluded.
|
||||
|
||||
The full dedicated test stack contained Create/Ponder, Immersive Portals, JEI,
|
||||
TerraBlender with Biomes O' Plenty, and Serene Seasons with GlitchCore. The
|
||||
server loaded a new `rivencraft:age/test`, and its shutdown log confirms that
|
||||
all Overworld, Nether, End, and Age chunks were saved. The player explicitly
|
||||
approved 21.1.264 after that test. This does not independently certify every
|
||||
optional combination, the LEGACY preview fallback on a client without Create
|
||||
and Immersive Portals, or a full restart/rejoin cycle; keep those in the
|
||||
regression checklist. Serene's GameTest mock-player network limitation still
|
||||
requires real-client checks for seasonal behavior.
|
||||
@@ -0,0 +1,41 @@
|
||||
# JEI compatibility review (#86)
|
||||
|
||||
Reviewed 2026-10-20 for Minecraft 1.21.1 and NeoForge 21.1.264. Rivencraft
|
||||
now targets JEI 19.69.0.457 after the client's in-game acceptance check. JEI
|
||||
remains a `compileOnly` API and an opt-in
|
||||
`localRuntime` dependency for development; the published mod has no required
|
||||
JEI dependency.
|
||||
|
||||
The [official 1.21.1 NeoForge files](https://www.curseforge.com/minecraft/mc-mods/jei/files/all?page=1&pageSize=20&version=1.21.1)
|
||||
list 19.69.0.457 (beta, 2026-10-20) after 19.64.0.455 (release, 2026-10-16).
|
||||
The [19.69.0.457 Modrinth changelog](https://modrinth.com/mod/jei/versions?g=1.21.1&l=neoforge)
|
||||
has a published-jar bytecode compatibility check. Its preceding 19.69.0.456
|
||||
build changes ingredient search to Lucene and adds a runtime ingredient-search
|
||||
refresh API. This makes page search a required client check, even though the
|
||||
current Rivencraft plugin still compiles against the 457 API. The new Maven
|
||||
runtime artifact is much larger (about 7 MB versus 2.3 MB for 455); a successful
|
||||
compile alone is not evidence that its runtime plugin works.
|
||||
|
||||
Checks performed before changing the pinned dependency:
|
||||
|
||||
| Configuration | Result |
|
||||
| --- | --- |
|
||||
| `./gradlew -Pjei_version=19.69.0.457 -PwithJei=true build` | Passed; existing JEI API uses compile with four deprecation warnings and no errors. |
|
||||
| `./gradlew -Pjei_version=19.69.0.457 -PwithJei=true runGameTestServer` | Passed; dedicated GameTest server loaded JEI 19.69.0.457 and all 74 required tests passed. |
|
||||
| `./gradlew -Pjei_version=19.69.0.457 runGameTestServer` | Passed without JEI; all 74 required tests passed. |
|
||||
| Prism client with JEI 19.69.0.457 and Rivencraft 0.6.6.6 | Client joined an integrated world; JEI registered Rivencraft extra ingredients and started without a JEI-plugin exception. The player reported an in-game pass for guide display, page search, and recipe visibility. |
|
||||
|
||||
On first 457 launch, JEI migrated its old `lowMemorySlowSearchEnabled` config
|
||||
key and saved a backup of the original `jei-client.ini`; that warning was
|
||||
handled by JEI and did not block startup. No Rivencraft config was changed.
|
||||
|
||||
The in-game acceptance report covers the visible JEI integration. A dedicated
|
||||
GameTest server proves no server-side startup failure but does not exercise the
|
||||
JEI GUI; keep page search, recipes, blank-Page hiding, and the Ink Mixer,
|
||||
Writing Desk, and Book Binder guides in future upgrade checks. JEI is not
|
||||
bundled into or required by the published Rivencraft jar.
|
||||
|
||||
The final pinned `0.6.6.7` build passed. A repeat GameTest run from the pinned
|
||||
configuration stalled during test-world preparation before running any cases;
|
||||
it was stopped. The two successful pre-pin runs above used the same JEI version
|
||||
and code, with the dependency supplied by a Gradle property override.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Milestone 7 regression and migration matrix (issue #83)
|
||||
|
||||
This is the dated regression record for the 0.6.11.0 candidate. The
|
||||
[Milestone 7 acceptance](MILESTONE_7_ACCEPTANCE.md) owns the final sign-off. A passing
|
||||
GameTest is server-side evidence; it is not proof of client rendering,
|
||||
multiplayer timing, or every optional-mod combination.
|
||||
|
||||
## Automated evidence
|
||||
|
||||
| Area | Focused check | Candidate result |
|
||||
| --- | --- | --- |
|
||||
| Dedicated server, core linking, book/Age identity, old writing and workstation formats | `./gradlew --offline runGameTestServer`; includes `CoreGameTests`, `AgePersistenceGameTests`, `CrystalPortalGameTests`, M7 crafting/advancement checks | 91/91 required GameTests passed on NeoForge 21.1.264; clean shutdown and fresh-world removal. |
|
||||
| Deterministic structures and terrain | `./gradlew --offline runGameTestServer -ParchivedGameTestSuites=generation`; includes library template/spacing/loot/stand checks, cave profiles, vanilla structures, crystal formations, fissures | 129/129 passed after correcting one village test to assert its own contribution rather than a seed-dependent total; fresh-world removal. |
|
||||
| Sky, glyph numbers, seasons, creative/JEI page feed | `./gradlew --offline runGameTestServer -ParchivedGameTestSuites=environment` | 103/103 passed on the 0.6.11.0 candidate, including issue #94's number-page check; fresh-world removal. |
|
||||
| Packaged models and data | `Milestone7ResourceGameTests.coreModelsResolveBundledTextures` in the core suite; parses critical block models, checks their Rivencraft texture references and the library NBT resource | Passed in the 91-test core run. This checks resource presence, not visual orientation or transparency. |
|
||||
| Recipe behavior | `Milestone7CraftingGameTests` and `LinkingBookRecipeGameTests` through the loaded Recipe Manager | Passed in the core run; confirms crystal and receptacle quantities, ink bucket return, no Blank Page crafting, and Linking Book ingredients. |
|
||||
| Portal state/save-load | `CrystalPortalGameTests` plus `AgePersistenceGameTests` in the core suite | Passed; covers activation/release, fallback orientation, horizontal/underside placement, uninterrupted surface, cooldown, retired Age and seed/plan identity. Full server restart remains manual. |
|
||||
|
||||
The generation archive initially failed
|
||||
`AgeStructureGameTests.mixedEndAndTaigaVillagesChargeForEndPortion` because it
|
||||
compared the total instability score (1200) with the village-only expectation
|
||||
(200). A seed-completed sky legitimately supplied an extra sun (+1000). The
|
||||
production scoring was unchanged; the check now isolates the
|
||||
`forced_biome_villages` contribution. The rerun passed 129/129. This is a
|
||||
repaired assertion, not a hidden feature failure.
|
||||
|
||||
Every default GameTest run creates a new world with Minecraft's fixed seed 0
|
||||
and deletes it after pass or fail. See [GameTest guidance](GAMETESTS.md).
|
||||
Do not retry a storage-stalled GameTest: stop it, remove that automated
|
||||
scenario, and give its exact steps to the player. No run in this issue has
|
||||
stalled so far. The older full-archive storage stall is historical and is not
|
||||
counted as a current pass. On the subsequent **1.0.0.0 release candidate**,
|
||||
the environment archive stalled during its first 50-test batch and was
|
||||
stopped. The 103/103 result above applies to 0.6.11.0 only, not to 1.0.0.0
|
||||
or the later 1.0.0.1–1.0.0.2 candidates. For the 1.0.0.2 manual handoff, create an Age
|
||||
with Number Pages immediately before Celestial Angle, Phase, and Size pages;
|
||||
confirm each affects the intended body rather than becoming a stray page. Also verify a
|
||||
fixed-season Age remains fixed across a restart while the Overworld remains
|
||||
independent. Do not rerun the stalled suite merely to obtain a green count.
|
||||
|
||||
## Old-save boundary (#60)
|
||||
|
||||
Issue #60 established a policy of preserving old saves where practical. Tests
|
||||
cover bare-list `OrderedWriting`, page-free books, unknown symbol IDs, older
|
||||
Age definitions with stable defaults, plan-v3/plan-v4 interpretation, and
|
||||
workstation inventory migration. A bound book's UUID and its Age's registered
|
||||
dimension remain stable through ordinary load. This does **not** imply every
|
||||
pre-public-release world, removed biome mod, missing dimension provider, or
|
||||
changed datapack can be opened. Back up the full world; the Age registry and
|
||||
dimension folders are a unit. The creative Link Modifier can intentionally
|
||||
retire an Age, archiving its data and marking the UUID dead. That is an explicit
|
||||
administrative boundary, not a silent migration.
|
||||
|
||||
## Manual matrix for the player
|
||||
|
||||
Use a backup or disposable save. Record the exact mod list and jar version.
|
||||
These checks are **pending for 0.6.11.0** unless a later note records an actual
|
||||
pass. Historical passes on earlier slices should not be silently promoted to
|
||||
this candidate.
|
||||
|
||||
| Scenario | What to verify |
|
||||
| --- | --- |
|
||||
| Full restart | Create a random and an authored Age, a library with a looted chest/page stand, an active portal, and a bound book. Stop and restart the server; UUID, terrain, loot removal, stand contents, portal activation, and return travel must persist. |
|
||||
| Two players | Cross the same portal at nearly the same time, return independently, and use separate book previews. Cooldowns, destination positions, dropped-book ownership/retrieval, and preview chunk loaders must not interfere. |
|
||||
| Cross-dimension travel | Compare the same Descriptive Book by hand, stand, and portal. Check footing/platform rules, arrival coordinates and facing, Disarm, Maintain Momentum, Intra Linking, and Return Link mode. |
|
||||
| Client visuals/audio | Compare book spreads, glyphs, D'ni numbers, page/desk/binder/mixer models, 16×16 crystal buds and translucent elements, portal frame/surface order, skies/eclipses, page turns, and Legacy link sounds. A server test cannot observe pixels or audio. |
|
||||
| Optional renderers | With neither Create nor Immersive Portals, confirm Legacy fallback. With Create alone, confirm Ponder snapshots and portal guide. With Immersive Portals, confirm live view, Explore exit, sky/sound isolation, portal travel, and fallback while chunks load. With both, `AUTO` should choose Immersive Portals. |
|
||||
| Optional world/recipe mods | Test JEI guides/search, TerraBlender with a biome-producing addon (for example Biomes O' Plenty), Serene's six season modes plus Overworld independence/restart, and Ruins template selection/spawn weighting. Repeat startup without optional mods. |
|
||||
| Migration | Copy a backed-up older development world. Verify books and Notebooks retain their Age/page IDs, a desk and stand keep contents, old Ages load or provide a clear error, and removing a custom symbol retains its unresolved ID. Do not test by modifying the only copy. |
|
||||
| Binder diagnostics | As a non-OP, verify the Binder still creates books but shows no instability/seeded/status messages. As an OP, run `/rivencraft binder messages enable`, reopen the Binder, and verify those messages appear only for the OP. Disable them and restart the world; the setting should remain disabled. |
|
||||
|
||||
Historical evidence: the player approved NeoForge 21.1.264 on singleplayer
|
||||
and a dedicated server with Immersive Portals; JEI 19.69.0.457 guides, Serene
|
||||
Seasons 10.1.0.9's full per-Age matrix, Ruins generation, and issue #94's
|
||||
numbered celestials each received an in-game pass on their respective earlier
|
||||
release slices. See [compatibility review](MILESTONE_7_COMPATIBILITY_REVIEW.md),
|
||||
[JEI review](MILESTONE_7_JEI_REVIEW.md), [Serene review](MILESTONE_7_SERENE_REVIEW.md),
|
||||
and [Ruins notes](RUINS_COMPATIBILITY.md). The current candidate's broad
|
||||
multiplayer/restart/optional-mod matrix remains unverified until tested.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Serene Seasons 10.1.0.9 compatibility review (#88)
|
||||
|
||||
Reviewed 2026-10-20 for Minecraft 1.21.1 and NeoForge 21.1.264. Rivencraft
|
||||
now compiles against [Serene Seasons 10.1.0.9](https://modrinth.com/mod/serene-seasons/version/pHEgQQUE)
|
||||
with [GlitchCore 2.1.0.2](https://modrinth.com/mod/glitchcore/version/S2TfWrZR).
|
||||
Both remain optional. The Serene update fixes a precipitation-mixin recursion;
|
||||
it does not introduce a new dynamic-dimension API.
|
||||
|
||||
The 10.1.0.8 and 10.1.0.9 bytecode for Rivencraft's version-sensitive
|
||||
`SeasonsConfig`, `SeasonHandler`, `SeasonSavedData`, and `SeasonTime` integration
|
||||
classes is identical. In particular, the dimension whitelist field, per-level
|
||||
saved season clock, and season update method have unchanged signatures and
|
||||
implementations. The adapter still writes the whitelist through GlitchCore's
|
||||
config API and sends Rivencraft's whitelist update to already connected clients.
|
||||
|
||||
Verification:
|
||||
|
||||
| Check | Result |
|
||||
| --- | --- |
|
||||
| Build against 10.1.0.9 and GlitchCore 2.1.0.2 | Passed. |
|
||||
| Real Prism client with Serene 10.1.0.9, GlitchCore 2.1.0.2, and Rivencraft 0.6.6.7 | Both mods loaded and the player joined an Age. |
|
||||
| No Seasons, normal Seasons, and all four Eternal modes in new and saved Ages | Player reported the full in-game matrix passed. |
|
||||
| Reconnect, restart, and Overworld independence | Player reported the full in-game matrix passed. |
|
||||
| Isolated dedicated-server startup without Serene/GlitchCore | Reached `Done` on port 25568 with only required runtime mods, then saved and stopped. |
|
||||
| Isolated dedicated-server startup with Serene/GlitchCore | Reached `Done` on port 25569 with 10.1.0.9/2.1.0.2, then saved and stopped. |
|
||||
|
||||
Serene's sync packet is incompatible with GameTest mock players, so its
|
||||
in-game behavior must be checked with a real client and server. The seasonal
|
||||
test path in [SERENE_SEASONS_COMPATIBILITY.md](SERENE_SEASONS_COMPATIBILITY.md)
|
||||
remains the regression checklist for future upgrades. Rivencraft does not
|
||||
bundle or require Serene Seasons or GlitchCore.
|
||||
@@ -0,0 +1,63 @@
|
||||
# Milestone 7 visual and audio polish (#80)
|
||||
|
||||
The source is the local Mystcraft 0.13.7.06 extraction under
|
||||
`/home/dnorberg/Documents/mystcraft-src/mystcraft-1.12.2-0.13.7.06/`.
|
||||
Reused textures and OGG files are recorded in [asset provenance](ASSET_PROVENANCE.md).
|
||||
The `dev images/book binder.png` and `dev images/ink mixer.png` screenshots
|
||||
are the Legacy model references used for this slice.
|
||||
|
||||
## Legacy comparison and modern boundary
|
||||
|
||||
- The Crystal Block, Book Receptacle, and 16×512 portal animation already use
|
||||
Legacy sprites. The Receptacle's 16×16×6 model already matches the Legacy
|
||||
slab. The fallback portal now uses Legacy's four-pixel-deep, double-faced
|
||||
`linkportal_ns.json` geometry, with the original animation; it declares a
|
||||
translucent render layer and portal particle texture. The block remains
|
||||
non-occluding. When Immersive Portals supplies a live view, the fallback
|
||||
model is hidden, while the crystal frame is drawn over the view.
|
||||
- The Age arrival platform now reuses the same Legacy crystal tile as Crystal
|
||||
Block. It is an opaque, occluding full block, while retaining its empty loot
|
||||
table and unobtainable pick-block behavior. It has glass-like 0.3 hardness.
|
||||
The old cloud sprite is unused and removed.
|
||||
- The Book Binder now includes the two inner braces and central press/book
|
||||
piece omitted by the earlier model, using the existing byte-identical
|
||||
Legacy textures. The Ink Mixer uses Legacy's `inkwell_model.json` geometry
|
||||
and four original inkwell textures. A thin white paper surface replaces the
|
||||
black paper appearance from the reference; the old simplified mixer tiles
|
||||
are no longer used. The half-alpha glass atlas is intentionally rendered on
|
||||
a cutout layer; its more opaque look passed the player's in-game check.
|
||||
- Legacy's link listener selects Disarm before a custom portal sound, then
|
||||
Following, Intra Linking, and ordinary Link. Rivencraft has no Following
|
||||
modifier yet, so it selects Disarm, portal, Intra Linking, or ordinary Link
|
||||
in that order. The Disarm and Intra OGGs are byte-identical Legacy copies.
|
||||
- Legacy sends 50 small neutral link particles in roughly a one-by-two-block
|
||||
region at both departure and arrival. Its fixed-function particle class is
|
||||
not portable to 1.21.1. Rivencraft uses a one-shot burst of 50 vanilla Cloud
|
||||
particles at both ends, emitted only after successful server-authoritative
|
||||
travel. There is no continuous portal-particle loop in the inspected Legacy
|
||||
`BlockLinkPortal` class, so the portal surface itself adds none.
|
||||
- The Legacy `textures/entity/linkbook.png` and `linking/pop.ogg` are inspected
|
||||
but not copied in this slice. The custom dropped Linkbook entity is part of
|
||||
#85; adding its assets before the entity exists would leave unused resources.
|
||||
Library walls, shelves, chest, and webs use vanilla blocks in the template,
|
||||
so there are no separate library art files to port.
|
||||
|
||||
## Manual visual/audio check
|
||||
|
||||
Use the same bound book in a held use and a Crystal Portal. Confirm the link
|
||||
particles and audio play at both ends, and the portal landing matches the book.
|
||||
Repeat with Disarm and Intra Linking panels to check the distinct sounds.
|
||||
Compare the fallback portal, crystal frame, and Receptacle with Immersive
|
||||
Portals absent; verify the animated surface is visible from both sides and
|
||||
does not expose caves or other geometry through solid blocks. With Immersive
|
||||
Portals installed, confirm the live view replaces only the fallback surface,
|
||||
the frame draws in front, and no fallback texture covers the live view.
|
||||
Create/Ponder is optional and must not affect either rendering path.
|
||||
Check a freshly generated arrival platform from above and below: the crystal
|
||||
tile should be opaque, break about as quickly as glass, and drop no block item.
|
||||
Compare the Book Binder press and Ink Mixer cup/glass against the `dev images`
|
||||
references; the mixer paper should appear white, and neither block should
|
||||
show missing-texture checkerboards or see-through-world artifacts.
|
||||
|
||||
The player reported that the in-game visual and audio checks pass on 2026-10-19,
|
||||
including acceptance of the cutout glass appearance and current block sounds.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Rivencraft 1.0.1.2-alpha.1 — Minecraft 26.3
|
||||
|
||||
Experimental **NeoForge 26.3** port preview. This is a separate artifact from
|
||||
the supported Minecraft 1.21.1 release, and it does **not** have feature
|
||||
parity. Use a disposable test world or make a backup first. Do not migrate a
|
||||
1.21.1 Rivencraft world to this alpha.
|
||||
|
||||
## Working in this alpha
|
||||
|
||||
- Rivencraft loads on Minecraft 26.3 / NeoForge 26.3.0.99 with Java 25.
|
||||
- The first-party runtime Age backend creates separately seeded, persistent
|
||||
prototype dimensions. The operator-only `/rivencraft_port age` commands can
|
||||
create and inspect them.
|
||||
- Runtime Age catalogs are synchronized and acknowledged before cross-dimension
|
||||
travel. A missing or unacknowledged destination is refused.
|
||||
- Operator-created Descriptive Books can enter prototype Ages.
|
||||
- Handheld Linking Books can be named and bound to a location, then used from
|
||||
their book GUI to return there. The destination data survives a restart.
|
||||
- The Crystal Block and Crystal Shard are registered with reused art.
|
||||
|
||||
## Not yet ported
|
||||
|
||||
Normal authored Age generation, book crafting and workstations, Page symbols,
|
||||
most blocks and items, portal systems, Link modifiers, the rotating destination
|
||||
preview, instability, and optional mod integrations. The Linking Book GUI shows
|
||||
a placeholder destination panel rather than a live scene. Generated Ages use
|
||||
prototype vanilla terrain, not the player's authored Pages.
|
||||
|
||||
The [26.3 port status](MINECRAFT_26_3_PORT.md) and [open port issues](https://git.paddedcell.org/dnorberg/rivencraft/issues)
|
||||
track the remaining work. The stable 1.21.1 release remains available
|
||||
separately. Please file alpha bugs with the exact Minecraft, NeoForge and
|
||||
Rivencraft versions, plus the relevant client/server log.
|
||||
|
||||
## Verification
|
||||
|
||||
- Clean 26.3 Gradle build passed.
|
||||
- Disposable dedicated-server smoke test passed: two distinct Age seeds and
|
||||
their terrain persisted across a restart.
|
||||
- The user verified the handheld Linking Book GUI click and travel in-game on
|
||||
2026-10-27.
|
||||
|
||||
These checks are **not** the full port acceptance matrix or a stability claim.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Rivencraft 1.0.1.3-alpha.1 — Minecraft 26.3
|
||||
|
||||
Experimental **NeoForge 26.3** port preview. This is separate from the
|
||||
supported Minecraft 1.21.1 release and does **not** have feature parity. Use
|
||||
a disposable test world or make a backup first. Do not migrate a 1.21.1
|
||||
Rivencraft world to this alpha.
|
||||
|
||||
## Changes since 1.0.1.2-alpha.1
|
||||
|
||||
- An unbound Descriptive Book now opens a naming screen. Binding it creates a
|
||||
distinct prototype Age with a random seed; duplicate names receive a
|
||||
numbered suffix.
|
||||
- A bound Descriptive Book opens a book screen whose destination panel can be
|
||||
clicked to enter its Age. The server validates the held book and waits for
|
||||
the client's Age catalog acknowledgement before travel.
|
||||
- Retired Age IDs are excluded when assigning a new book's Age name, so an
|
||||
archived world cannot be silently reused.
|
||||
|
||||
## Still experimental
|
||||
|
||||
These Ages use prototype vanilla terrain. Page-authored generation, crafting
|
||||
and workstations, most items and blocks, portals, Link modifiers, instability,
|
||||
live destination previews, and optional mod integrations are not ported. The
|
||||
book panel is a placeholder, not a rendered destination view. The
|
||||
[port status](MINECRAFT_26_3_PORT.md) and [open port issues](https://git.paddedcell.org/dnorberg/rivencraft/issues)
|
||||
track the remaining work. The stable 1.21.1 release remains available
|
||||
separately.
|
||||
|
||||
## Verification
|
||||
|
||||
- Clean 26.3 Gradle build passed.
|
||||
- Disposable dedicated-server smoke test passed: two distinct Age seeds and
|
||||
their terrain persisted across restart.
|
||||
- The player verified Descriptive Book binding and travel in game on
|
||||
2026-10-29.
|
||||
|
||||
These checks are **not** the full port acceptance matrix or a stability claim.
|
||||
File diff suppressed because it is too large.
Load diff
+31
-9
@@ -1,21 +1,41 @@
|
||||
# Porting roadmap
|
||||
|
||||
For a concise current status, start with [current implementation](CURRENT_IMPLEMENTATION.md)
|
||||
and [milestone summaries](MILESTONES.md). This file retains the original
|
||||
roadmap and architectural rationale.
|
||||
|
||||
Rivencraft ports Mystcraft Legacy from Minecraft 1.12.2 to NeoForge 1.21.1.
|
||||
The world-generation, dimension, registry, networking, and rendering APIs have
|
||||
changed substantially, so the work is divided into independently testable
|
||||
milestones.
|
||||
|
||||
## Porting principles
|
||||
|
||||
The primary goal is to reproduce verified Mystcraft Legacy behavior as closely
|
||||
as practical. Legacy source and the supplied 0.13.7.06 build are the behavioral
|
||||
reference for grammar, probabilities, ordering, defaults, visuals, and gameplay
|
||||
effects. A difference should be deliberate and documented rather than inferred
|
||||
from modern implementation convenience.
|
||||
|
||||
Minecraft 1.21.1 has different world heights, registries, threading, networking,
|
||||
data-pack APIs, and server protection expectations. Where an exact port would be
|
||||
unsafe, technically impossible, or incompatible with current Minecraft,
|
||||
Rivencraft preserves the player-visible intent while using bounded modern
|
||||
mechanics. New features may extend Legacy when they fit Age writing and linking,
|
||||
but extensions must be identified separately from compatibility behavior and
|
||||
must not silently change existing saved Ages.
|
||||
|
||||
## Project tracking
|
||||
|
||||
Gitea is the source of truth for project status:
|
||||
|
||||
- [Issues](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/issues) contain
|
||||
- [Issues](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/issues) contain
|
||||
scoped implementation tasks, bugs, acceptance criteria, and technical
|
||||
discussion.
|
||||
- [Milestones](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestones)
|
||||
- [Milestones](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/milestones)
|
||||
group issues into the porting phases below and show their live completion
|
||||
state.
|
||||
- [Releases](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/releases)
|
||||
- [Releases](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/releases)
|
||||
contain user-facing changelogs for shipped versions.
|
||||
|
||||
New work should have an issue before implementation begins. Commits should
|
||||
@@ -32,21 +52,23 @@ acceptance issue and all required implementation issues are complete.
|
||||
3. **Ages** — persistent generated dimensions, deterministic traits,
|
||||
lifecycle management, data-pack integration, and optional destination
|
||||
renderer compatibility. **Complete.** See
|
||||
[Milestone 3](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestone/8).
|
||||
[Milestone 3](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/milestone/8).
|
||||
4. **Writing system** — pages, notebooks, desks, book assembly, and the baseline
|
||||
Ink Mixer for Link Panels. Symbol effects were deferred to Milestone 5.
|
||||
**Complete.** See [Milestone 4](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestone/9)
|
||||
**Complete.** See [Milestone 4](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/milestone/9)
|
||||
and [writing-system data and migration](WRITING_DATA.md).
|
||||
5. **Age grammar** — translate supported symbol combinations into modern biome,
|
||||
noise, dimension-type, and world-generation configuration, with optional
|
||||
TerraBlender and Serene Seasons integration. **Complete for the supported
|
||||
subset.** See the [acceptance record](MILESTONE_5_ACCEPTANCE.md) and
|
||||
[Legacy grammar audit](LEGACY_AGE_GRAMMAR_AUDIT.md).
|
||||
6. **Instability** — deterministic validation and server-safe effects. Start with the
|
||||
[Legacy instability audit](LEGACY_INSTABILITY_AUDIT.md), which separates
|
||||
source behavior from proposed 1.21.1 safety limits.
|
||||
6. **Instability** — deterministic validation and server-safe effects.
|
||||
**Complete.** See the [Legacy instability audit](LEGACY_INSTABILITY_AUDIT.md)
|
||||
and [Milestone 6 acceptance](MILESTONE_6_ACCEPTANCE.md).
|
||||
7. **Structures and polish** — libraries, crystals, portals, recipes, models,
|
||||
sounds, advancements, compatibility, and migration tests.
|
||||
sounds, advancements, compatibility, and migration tests. **In progress.**
|
||||
See the [Milestone 7 regression matrix](MILESTONE_7_REGRESSION.md) and
|
||||
[Legacy audit](LEGACY_M7_AUDIT.md).
|
||||
|
||||
## Architectural direction
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ being captured in the preview.
|
||||
|
||||
## Supported combinations and upstream limits
|
||||
|
||||
The compatibility baseline is Minecraft 1.21.1, NeoForge 21.1.251, Create
|
||||
The compatibility baseline is Minecraft 1.21.1, NeoForge 21.1.264, Create
|
||||
6.0.10, and Immersive Portals 6.0.7. Core Age generation and the `LEGACY`
|
||||
backend run without Create or Immersive Portals; the headless GameTest server
|
||||
uses that dependency-free path.
|
||||
@@ -66,8 +66,11 @@ unstable in a particular graphics or mod configuration.
|
||||
|
||||
For preview-related releases:
|
||||
|
||||
1. Run `./gradlew runGameTestServer -PwithArchivedGameTests=true` to verify the complete selection matrix,
|
||||
core Age creation, persistence, and travel without optional renderers.
|
||||
1. Run `./gradlew runGameTestServer` for the core selection matrix, Age
|
||||
creation, persistence, and travel without optional renderers. Add the
|
||||
relevant archives from [GameTest guidance](GAMETESTS.md); if a scenario
|
||||
stalls, stop it and hand that exact check to a real client rather than
|
||||
treating the interrupted run as a pass.
|
||||
2. Open held Linking and Descriptive Books once with each available backend.
|
||||
3. Open both book types from a Book Stand and confirm Explore is disabled.
|
||||
4. With Immersive Portals, enter and leave Explore, travel between dimensions,
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Documentation map
|
||||
|
||||
Start with the player-facing [project README](../README.md). The two maintained
|
||||
technical entry points are [current implementation](CURRENT_IMPLEMENTATION.md)
|
||||
and [milestone summaries](MILESTONES.md). Use [GameTest guidance](GAMETESTS.md)
|
||||
to run checks. The files below retain detailed design, provenance, and
|
||||
version-specific test evidence; an older milestone record may describe a
|
||||
former behavior or loader version.
|
||||
|
||||
| Milestone | Detailed records |
|
||||
| --- | --- |
|
||||
| M1–M3 | [Porting roadmap](PORTING.md), [Age administration](AGES.md), [Age profiles](AGE_PROFILES.md), [preview compatibility](PREVIEW_COMPATIBILITY.md), [random Age weights](RANDOM_AGE_WEIGHTS.md) |
|
||||
| M4 | [Writing data](WRITING_DATA.md), [book presentation](DESCRIPTIVE_BOOK_PRESENTATION.md), [Ink Mixer audit](INK_MIXER_MODIFIER_AUDIT.md) |
|
||||
| M5 | [Acceptance](MILESTONE_5_ACCEPTANCE.md), [Legacy grammar audit](LEGACY_AGE_GRAMMAR_AUDIT.md), [writing grammar](WRITING_GRAMMAR.md), [symbol semantics](SYMBOL_SEMANTICS.md), [TerraBlender](TERRABLENDER_COMPATIBILITY.md), [Serene Seasons](SERENE_SEASONS_COMPATIBILITY.md) |
|
||||
| M6 | [Acceptance](MILESTONE_6_ACCEPTANCE.md), [regression](MILESTONE_6_REGRESSION.md), [Legacy audit](LEGACY_INSTABILITY_AUDIT.md), [score](INSTABILITY_SCORE.md), [live blocks](LIVE_BLOCK_INSTABILITY.md), [runtime](INSTABILITY_RUNTIME.md), [diagnostics](INSTABILITY_DIAGNOSTICS.md), [status effects](INSTABILITY_STATUS_EFFECTS.md), [decay](INSTABILITY_DECAY.md), [environmental hazards](INSTABILITY_ENVIRONMENTAL.md), [cloud platform](AGE_CLOUD_PLATFORM.md) |
|
||||
| M7 | [Acceptance](MILESTONE_7_ACCEPTANCE.md), [regression and manual matrix](MILESTONE_7_REGRESSION.md), [Legacy audit](LEGACY_M7_AUDIT.md), [visual/audio](MILESTONE_7_VISUAL_AUDIT.md), [advancements](MILESTONE_7_ADVANCEMENTS.md), [JEI](MILESTONE_7_JEI_REVIEW.md), [Serene review](MILESTONE_7_SERENE_REVIEW.md), [compatibility](MILESTONE_7_COMPATIBILITY_REVIEW.md), [Ruins](RUINS_COMPATIBILITY.md) |
|
||||
|
||||
[Asset provenance](ASSET_PROVENANCE.md) applies across milestones. Detailed
|
||||
audits are intentionally retained for source attribution and migration
|
||||
history; these three entry points replace the former practice of treating
|
||||
every dated task note as a current installation guide.
|
||||
@@ -0,0 +1,66 @@
|
||||
# Optional Ruins template pages (issue #91)
|
||||
|
||||
Rivencraft supports Ruins 1.21.1.2NF as an optional mod. When it is installed,
|
||||
every `.tml` template in Ruins' active `config/ruins_config/<generic-or-biome>/`
|
||||
folders receives a separate Ruins page in Rivencraft's creative and JEI page
|
||||
feeds. New templates added to Ruins' config appear after a restart; removing a
|
||||
template makes its page unavailable to new books. With Ruins absent, no Ruins
|
||||
pages or structure integration are active.
|
||||
|
||||
An authored Age with Ruins pages is admitted through Ruins' dimension gate.
|
||||
Rivencraft also admits the authored templates through their individual
|
||||
`dimensionsToSpawnIn` gates, which usually list only `overworld` and would
|
||||
otherwise prevent them from loading in an Age. Ruins still owns chunk
|
||||
inspection, spawn chance, surface validation, biome pools, and spacing.
|
||||
Rivencraft filters Ruins' weighted
|
||||
template choice to the exact pages in that Age and vetoes an unauthorized
|
||||
template in Ruins' pre-spawn event. A page therefore selects *which* template
|
||||
may appear; it does not guarantee a structure in every chunk. Manual
|
||||
`/testruin` placements are unaffected.
|
||||
|
||||
Repeating a template page multiplies that template's Ruins selection weight by
|
||||
its page count and adds one spawn attempt per chunk for each duplicate page.
|
||||
The extra attempts are capped at 32 per chunk (or Ruins' configured count if
|
||||
higher). Distinct template pages do not add attempts, and Ruins' spacing and
|
||||
surface checks still apply. As with Rivencraft's standard
|
||||
structure pages, the first three copies of each template are instability-free;
|
||||
each additional copy adds 100 instability. Ruins retains its own biome and
|
||||
surface checks, so the standard structure forced-biome penalty does not apply.
|
||||
|
||||
Ruins' event identifies a template only by its filename. If two active folders
|
||||
contain the same `.tml` basename, Rivencraft cannot distinguish them and
|
||||
disables all Ruins pages for that configuration. Give each template a unique
|
||||
filename to restore support. Ruins must also be able to parse the template and
|
||||
find a suitable surface in the selected biome; a page cannot repair an invalid
|
||||
or biome-incompatible template.
|
||||
|
||||
The adapter and dimension/selection mixin are version-sensitive to Ruins
|
||||
1.21.1.2NF. The published Rivencraft jar does not include Ruins. To include it
|
||||
in a development run, use `-PwithRuins=true`.
|
||||
|
||||
## Manual check
|
||||
|
||||
1. Start a client with Ruins 1.21.1.2NF and Rivencraft 0.6.9.3. Confirm the
|
||||
Ruins Pages tab contains one page per configured `.tml` file, and search a
|
||||
distinctive template name in JEI.
|
||||
2. Bind a new Descriptive Book with a Link Panel, Standard Terrain, a suitable
|
||||
surface biome, and one Ruins page (for example ZombieHut). Enter the Age and
|
||||
explore *unvisited* chunks. Ruins marks inspected chunks and will not retry
|
||||
chunks explored under the earlier broken build. Only the selected Ruins
|
||||
template should appear. Ruins'
|
||||
own chance and distance settings still determine how far exploration takes.
|
||||
3. For duplicate weighting, bind an Age with one copy of one template page and
|
||||
three copies of a second template page valid in the same biome. Across enough
|
||||
new candidate chunks, the second should be selected more often relative to
|
||||
the first, and there should be two additional spawn attempts per inspected
|
||||
chunk. Four copies of one template should add 100 instability, visible
|
||||
through the OP Age instability diagnostic, while three copies add none.
|
||||
4. Make another Age without a Ruins page and confirm Ruins structures do not
|
||||
appear there. Restart the game and confirm the authored Age keeps its page
|
||||
selection. Then remove Ruins and confirm Rivencraft starts normally without
|
||||
a Ruins tab.
|
||||
|
||||
The Ruins-enabled GameTest server passed all 90 required tests and loaded the
|
||||
template-dimension and attempt mixins. Its test world was removed afterward.
|
||||
Use the client check above to verify actual Ruins generation and the increased
|
||||
attempt rate.
|
||||
@@ -20,7 +20,7 @@ Milestone 5 issue #29 adds typed meaning to the existing data-pack symbol catalo
|
||||
|
||||
`poem` is optional, client-safe artwork metadata: exactly four alphabetic words, ordered top, right, bottom, left. The client layers their glyphs from the shared Legacy component sheet at runtime. GUI thumbnails use the full transparent artwork over their page backgrounds; item icons use the same artwork reduced from 16 to 13 pixels across, over the page texture. Unregistered words use a stable generated component combination; when a symbol has no poem, the six original built-in IDs retain their Legacy poems and other IDs produce four stable fallback words. This affects appearance only, never Age generation. Generated images are cached up to 256 entries and discarded on resource reload.
|
||||
|
||||
The Writing Desk's 88 collection tabs use Legacy's number components, chiefly the bottom row of `symbolcomponents.png`, rather than word sigils. Legacy's `ModWords.constructNumber` combines four unit marks (1–4) with four five-step marks (5–20) to form 1–24 and uses a separate zero mark. Rivencraft extends these marks positionally in base 25 for tabs 25–88 (for example, 25 is 1 followed by 0). Hovering still reveals the decimal number for accessibility. The former arbitrary-word glyph generator remains useful for rendering text: on page-free Descriptive Books, the generated Age description now appears as a sequence of layered word glyphs instead of Minecraft's enchantment font. The Age name stays readable.
|
||||
The Writing Desk retains 88 collection tabs, numbered from zero as in Legacy. Legacy's `ModWords.constructNumber` defines 0 as component 1 and 1–24 by overlaying a unit mark (1–4) and a five-step mark (5–20) from the bottom row of `symbolcomponents.png` in the *same square*. Legacy's Writing Desk has 25 tabs (0–24) and scrolls its four-tab window **one tab at a time**. Thus the supplied Legacy screenshots labeled 1–4, 5–8, and so on show overlapping windows, not 88 distinct numerals. Rivencraft retains its previously requested four-at-a-time navigation and 88 tabs. Its additional tab numbers use positional base 25: each place is an overlaid Legacy digit, with the places side by side (25 = 1,0). The two glyphs are sized and positioned to fit the tab. Hovering reveals the decimal tab index for accessibility. The generated-word mechanism renders page-free Descriptive Book text as layered glyphs instead of Minecraft's enchantment font. The Age name stays readable.
|
||||
|
||||
| Role | Value type and allowed values |
|
||||
| --- | --- |
|
||||
@@ -32,9 +32,10 @@ The Writing Desk's 88 collection tabs use Legacy's number components, chiefly th
|
||||
| `weather` | `normal`, `fast`, `slow`, `off`, `always`, `cloudy`, `rain`, `snow`, or `storm`. |
|
||||
| `season` | `no_seasons`, `seasons`, `eternal_winter`, `eternal_summer`, `eternal_fall`, or `eternal_spring`. Seasonal effects other than No Seasons require optional Serene Seasons. |
|
||||
| `celestial` | `normal_time` (the first representable single-cycle behavior). |
|
||||
| `period_modifier` | JSON number in `[0, 2]`; zero is stopped time. |
|
||||
| `angle_modifier` | JSON number in `[0, 360)` degrees. |
|
||||
| `phase_modifier` | JSON number in `[0, 360)` degrees; the compiler will convert to turns. |
|
||||
| `period_modifier` | JSON number in `[0, 2]`; zero is stopped time. An immediately preceding D'ni number page overrides it with digit × 0.08. |
|
||||
| `angle_modifier` | JSON number in `[0, 360)` degrees. An immediately preceding D'ni number page overrides it with digit × 15°, wrapping 360° to 0°. |
|
||||
| `phase_modifier` | JSON number in `[0, 360)` degrees; the compiler converts it to turns. An immediately preceding D'ni number page overrides it with digit × 15°, wrapping 360° to 0°. |
|
||||
| `size_modifier` | JSON scale in `[0.25, 3]`. An immediately preceding D'ni number page overrides it with a linear 0.25×–3× scale: digit 0 is 0.25× and digit 24 is 3×. Without a number, Small/Normal/Large remain 0.5×/1×/2×. |
|
||||
| `block` | Canonical namespaced material ID for a compatible following feature; registered full-collision blocks and source fluids receive generated Pages. |
|
||||
| `feature` | Canonical namespaced feature-effect ID; the compiler must recognize and validate it. This includes supported decorations and structures. |
|
||||
|
||||
|
||||
@@ -87,10 +87,10 @@ placement. The surface draws the native 30×40 portrait page region from
|
||||
Legacy's book UI atlas; symbols are layered at a uniform scale, while the
|
||||
square item models remain unchanged in inventories.
|
||||
|
||||
A Notebook holds 27 ordered positions. Its pages, names, gaps, and unresolved
|
||||
A Notebook holds 252 ordered positions. Its pages, names, gaps, and unresolved
|
||||
IDs are saved on the item, so moving the Notebook between an inventory, Desk,
|
||||
and Binder does not rearrange them. A loose Page in a Desk tab exposes just one
|
||||
position; a Notebook exposes all 27. The Desk has 88 persistent collection
|
||||
position; a Notebook exposes all 252. The Desk has 88 persistent collection
|
||||
tabs, shown four at a time. Use the up/down tab controls to move by four, put
|
||||
paper and Ink Vials in their input slots, select a stored page, and take the
|
||||
copy from the virtual output. Taking it spends one paper and the configured
|
||||
@@ -100,7 +100,7 @@ copying. Moving a page within the Desk or dragging it into the inventory removes
|
||||
the original only when the destination accepts it.
|
||||
|
||||
The Book Binder takes a leather or empty-Notebook cover, an Age title, one Link
|
||||
Panel, and optionally up to 27 ordered written Pages. A filled Notebook can be
|
||||
Panel, and optionally up to 252 ordered written Pages. A filled Notebook can be
|
||||
imported at a chosen strip position; the empty Notebook remains recoverable.
|
||||
The preview result is virtual. Taking the completed Descriptive Book commits the
|
||||
inputs once, creates its Age identity, and saves schema-versioned
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
`WritingGrammar` interprets the saved `OrderedWriting` on the server using only `SymbolCatalog`'s schema-v2 semantics. Client display metadata, page names, and supplied translations never determine an Age's meaning. The parser is pure: it returns immutable parsed entries and indexed diagnostics without changing a book or its pages.
|
||||
|
||||
New Binder writing has at most one Link Panel plus 27 symbol pages. The Link Panel must be first. A Link-Panel-only book, or an older book with no writing component, keeps the existing random Age path. Any authored supported symbol makes the book a partial or complete grammar. From 0.4.3.0, the server compiles it into a persistent Age plan before consuming Binder inputs or generating chunks.
|
||||
New Binder writing has at most one Link Panel plus 252 symbol pages (the current fixed-slot save-format ceiling, not a Legacy gameplay limit). The Link Panel must be first. A Link-Panel-only book, or an older book with no writing component, keeps the existing random Age path. Any authored supported symbol makes the book a partial or complete grammar. From 0.4.3.0, the server compiles it into a persistent Age plan before consuming Binder inputs or generating chunks.
|
||||
|
||||
The supported order follows the representable subset of the [Legacy grammar audit](LEGACY_AGE_GRAMMAR_AUDIT.md): terrain; zero or more biome pages; biome controller; weather; season; lighting; sky effects; zero or more period or phase modifiers; one celestial page; then zero or more features, each optionally preceded by a compatible material page. Biome pages precede the controller that consumes them, and celestial modifiers precede the celestial page. Terrain, controller, weather, season, lighting, sky, and celestial are single-choice categories; identical repeats are duplicates and differing values conflict. Repeated modifiers, biome pages, feature pages, and material pages are intentional and retain their order. An unpaired material page receives a seeded compatible feature during compilation. Angle modifiers remain recognized in the schema but are rejected at binding until a sky renderer can apply sun tilt.
|
||||
|
||||
Missing terrain, controller, weather, lighting, celestial, or the controller's minimum biome count is an **incomplete** warning, not an error: the compiler retains seed-selected traits for missing branches. An omitted terrain page uses a seed-selected profile compatible with authored feature pages, so changing the Age name cannot accidentally select Flat, Void, or Cave terrain for Rocks or Lakes. If no profile can support the writing, binding still rejects it. Native consumes no authored biome pages; Single consumes one; Tiled/Grid and Tiny–Huge require two, with seeded filler when pages are missing. From 0.5.1.0, known misplaced or repeated pages no longer block creation: the book keeps its original order, while the server repairs category order, uses the last single-choice controller, and stores a writing-instability score in the Age plan. A conflicting/repeated choice adds 500; an out-of-order page or misplaced modifier adds 100. Native ignores surplus authored biome pages and Single uses the first, with the conflict recorded as instability. These are Rivencraft's provisional penalties, not a claim of exact Legacy values for every symbol. Incorrectly placed pages are **not** red-highlighted; their warning remains available as a tooltip. Unknown IDs, schema-v1 display-only symbols, unavailable dependencies/profiles, unsupported semantic effects or feature/material pairs, missing/extra Link Panels, and oversized writing still reject a new Age. The Binder disables output for those fatal errors and shows a red page indication where applicable. Rejected inputs remain removable.
|
||||
Missing terrain, controller, weather, lighting, celestial, or the controller's minimum biome count is an **incomplete** warning, not an error: the compiler retains seed-selected traits for missing branches. An omitted terrain page uses a seed-selected profile compatible with authored feature pages, so changing the Age name cannot accidentally select Flat, Void, or Cave terrain for Rocks or Lakes. If no profile can support the writing, binding still rejects it. Native consumes no authored biome pages; Single consumes one; Tiled/Grid and Tiny–Huge require two, with seeded filler when pages are missing. From 0.5.1.0, known misplaced or repeated pages no longer block creation: the book keeps its original order, while the server repairs category order, uses the last single-choice controller, and stores a writing-instability score in the Age plan. A conflicting/repeated choice adds 500; an out-of-order page or misplaced modifier adds 100. Native ignores surplus authored biome pages and Single uses the first, with the conflict recorded as instability. Audited Legacy symbol modifiers also add 500 for each Bright Lighting page and 100 for each Mineshafts, Nether Fortresses, Strongholds, or Villages occurrence after the third. Modern structures configured with the same rule are Bastions, Trial Chambers, Ancient Cities, Shipwrecks, Ocean Ruins, Pillager Outposts, Ruined Portals, Nether Fossils, Buried Treasure, and Trail Ruins. Every repeated structure page increases that structure's placement rate: probability-based structures multiply their chance, grid-based structures use a denser grid, and Strongholds add more positions to each ring and more positions overall. One page retains the vanilla placement rate. Authored structures can generate outside their vanilla biome set; each structure-page occurrence that requires this override adds 100 instability. Seed completion selects compatible biomes where possible. Other feature pages have no direct symbol cost. Incorrectly placed pages are **not** red-highlighted; their warning remains available as a tooltip. Unknown IDs, schema-v1 display-only symbols, unavailable dependencies/profiles, unsupported semantic effects or feature/material pairs, missing/extra Link Panels, and oversized writing still reject a new Age. The Binder disables output for those fatal errors and shows a red page indication where applicable. Rejected inputs remain removable.
|
||||
|
||||
The plan stores the original writing order, the authoritative semantic value of each resolved symbol, the active repaired-page indexes, the writing-instability score, the Age seed, and a copy of the selected runtime profile. An explicit terrain page overrides random terrain selection; an omitted terrain uses a seed-selected compatible profile. The built-in Standard, Amplified, Cave, Island, Flat, and Void terrain pages select their matching generation resources. Age loading uses the frozen runtime profile and does not need the original symbol definition or runtime-profile JSON, though its referenced noise, biome-preset, and dimension-type registry entries must remain available. Existing bound books without plans are not reinterpreted. Page-free and Link-Panel-only books keep the pre-existing random path. Biome and environment pages affect new Ages, and the supported feature subset below is compiled into the same frozen plan. Runtime effects for a nonzero writing-instability score are not implemented yet.
|
||||
The plan stores the original writing order, the authoritative semantic value of each resolved symbol, the active repaired-page indexes, the writing-instability score, the Age seed, and a copy of the selected runtime profile. An explicit terrain page overrides random terrain selection; an omitted terrain uses a seed-selected compatible profile. The built-in Standard, Amplified, Cave, Island, Flat, and Void terrain pages select their matching generation resources. Age loading uses the frozen runtime profile and does not need the original symbol definition or runtime-profile JSON, though its referenced noise, biome-preset, and dimension-type registry entries must remain available. Existing bound books without plans are not reinterpreted. Page-free and Link-Panel-only books keep the pre-existing random path. Biome and environment pages affect new Ages, and the supported feature subset below is compiled into the same frozen plan. Nonzero instability can select a persisted provider plan; status effects, decay, lightning, explosions, meteors, burning, and test-enabled erosion run through the bounded Age-scoped scheduler described in [the runtime policy](INSTABILITY_RUNTIME.md).
|
||||
|
||||
## Supported feature pages (issue #35)
|
||||
|
||||
|
||||
+4
-4
@@ -18,11 +18,11 @@ minecraft_version=1.21.1
|
||||
# as they do not follow standard versioning conventions.
|
||||
minecraft_version_range=[1.21.1]
|
||||
# The Neo version must agree with the Minecraft version to get a valid artifact
|
||||
neo_version=21.1.251
|
||||
neo_version=21.1.264
|
||||
ponder_version=1.0.82
|
||||
dynamic_dimensions_file_id=7707789
|
||||
badpackets_version=0.8.2
|
||||
jei_version=19.57.0.449
|
||||
jei_version=19.69.0.457
|
||||
# The loader version range can only use the major version of FML as bounds
|
||||
loader_version_range=[1,)
|
||||
|
||||
@@ -36,10 +36,10 @@ mod_name=Rivencraft
|
||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||
mod_license=LGPL-3.0-or-later
|
||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||
mod_version=0.5.3.0
|
||||
mod_version=1.0.1.1
|
||||
immersive_portals_version=6.0.7
|
||||
terrablender_version=4.1.0.8
|
||||
serene_seasons_version_id=B6nTjh4b
|
||||
serene_seasons_version_id=pHEgQQUE
|
||||
glitchcore_version_id=S2TfWrZR
|
||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||
# This should match the base package used for the mod sources.
|
||||
|
||||
@@ -0,0 +1,970 @@
|
||||
# Minecraft 26.3 beta port workspace
|
||||
|
||||
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, selected material/vegetation/structure features, and
|
||||
independent Age weather and lighting. Celestial systems, instability, crystal
|
||||
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
|
||||
sprite, and four Crystal Shards craft into a Crystal Block; both were also
|
||||
verified in game. The current local iteration adds Budding Crystal and all
|
||||
four blue crystal growth stages with vanilla-style harvesting. Natural crystal
|
||||
formation generation is not yet ported; use Creative or commands to obtain a
|
||||
Budding Crystal for this test. The player verified the growth chain in game.
|
||||
The next local slice adds the Link Panel Page as a separate creative entry,
|
||||
using the existing item texture. Blank Pages remain unavailable in Creative;
|
||||
the player verified the Link Panel's appearance in game. Ink Vials now use
|
||||
the existing crafting recipe and animated sprite. With paper in the other
|
||||
hand, an Ink Vial prepares a Link Panel for two of its sixteen ink uses, as
|
||||
in the current 1.21.1 default. The player verified the Ink Vial interaction
|
||||
in game. Symbol writing remains unported.
|
||||
A prepared Link Panel can now be combined shapelessly with leather to craft
|
||||
an unwritten Linking Book, as in 1.21.1. This custom recipe rejects a blank
|
||||
Page and extra ingredients. The first Book Binder slice now assembles random-Age
|
||||
Descriptive Books from leather and a Link Panel. Authored pages remain later.
|
||||
|
||||
The next local `1.0.1.10-alpha.1` slice exposes all eight combinations of
|
||||
plain, Disarm, Maintain Momentum, and Intra-Linking Link Panels in the Link
|
||||
Pages creative tab. Leather crafting preserves those flags on the Linking
|
||||
Book. Intra-Linking permits same-dimension travel; Maintain Momentum rotates
|
||||
horizontal speed to the destination yaw; Disarm leaves the player's inventory
|
||||
at the departure point after a successful teleport. These are development
|
||||
variants until the Ink Mixer is ported; other Link Panel modifiers are not
|
||||
implemented in 26.3. The player passed this modifier slice in game.
|
||||
|
||||
The local `1.0.1.11-alpha.1` slice adds a craftable Book Binder with persistent
|
||||
leather, Link Panel, and output slots. Name an Age in its GUI and press Bind;
|
||||
the server creates a random prototype Age and places its completed Descriptive
|
||||
Book in the output. Disarm and Maintain Momentum carry from the panel into the
|
||||
book; Intra-Linking has no effect on Age creation. Symbol-page authoring and
|
||||
the Ink Mixer remain unported. This slice passes the build and dedicated-server
|
||||
smoke test. The player passed crafting and travel in game, but found that E
|
||||
closed the focused title field and the provisional layout did not match the
|
||||
existing 1.21.1 Binder GUI.
|
||||
|
||||
The local `1.0.1.12-alpha.1` follow-up reuses the 1.21.1 Binder layout: title
|
||||
bar, cover and output at opposite sides, and a central Link Panel page strip.
|
||||
The title persists in the block, the missing-panel cue pulses red, and a
|
||||
focused title field consumes inventory/hotbar keys. Click the output once to
|
||||
bind, then click again to take the completed book. Authored symbol strips and
|
||||
the full 1.21.1 Binder grammar remain unported. This same slice also ports the
|
||||
1.21.1 Ink Mixer block, menu, animation, modifier additives, 16-ink-per-panel
|
||||
cost, and automatic vial exchange. The creative tab includes it and its
|
||||
vanilla-shaped recipe is available. This slice needs an in-game Binder visual/
|
||||
keyboard check and Ink Mixer interaction check.
|
||||
|
||||
The local `1.0.1.13-alpha.1` iteration makes Binder assembly place the new
|
||||
Descriptive Book directly on the cursor with one click. It also renders the
|
||||
five-page strip from the 1.21.1 GUI and refreshes the Mixer's virtual result
|
||||
in the same server click that changes paper or ink. The Mixer now consumes
|
||||
one ink unit and one paper per Link Panel (16 panels per full vial); additive
|
||||
chances reset when a panel is taken. In-game retest of the first-update
|
||||
behavior and Binder visual/click flow is pending.
|
||||
|
||||
The local `1.0.1.14-alpha.1` follow-up compares the current 26.3 Binder against
|
||||
the new dev-image sequence. The Binder no longer paints five permanently empty
|
||||
pages: the first strip page appears when a Link Panel is inserted or carried,
|
||||
and the missing-panel cue remains above it. Clicking that page inserts/removes
|
||||
the Link Panel using the same server-owned cursor flow as 1.21.1. Its tooltip
|
||||
also shows the stored panel. Multi-page symbol writing and strip scrolling
|
||||
cannot be exercised yet because symbol pages are still unported to 26.3.
|
||||
|
||||
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.
|
||||
|
||||
The local `1.0.1.55-beta.1` slice adds authored Villages, Mineshafts, and
|
||||
Strongholds. Repeated pages (up to 32 per type) scale the registered placement
|
||||
using the 1.21.1 frequency/spacing and ring-count rules. Copies are Age-local:
|
||||
the Overworld's placements and biome source are not modified. Authored
|
||||
structures can bypass their usual biome eligibility without changing the
|
||||
terrain biome; their instability cost remains pending with the instability port.
|
||||
|
||||
Villages and Strongholds allow surface noise terrain and Flat, but not Cave
|
||||
or Void. Mineshafts allow noise terrain including Cave, but not Flat or Void.
|
||||
These pages do not accept Block/Fluid modifiers. Their ordered counts persist
|
||||
in the feature plan. In books using these structure pages, unselected sets
|
||||
from these three families are disabled; other structure families and random
|
||||
feature selection remain later port work. Earlier books without this new
|
||||
structure plan retain their existing native structure behavior.
|
||||
|
||||
For manual checks, bind a new Standard/Plains Age with Villages, Mineshafts,
|
||||
and Strongholds, then use `/locate structure minecraft:village_plains`,
|
||||
`/locate structure minecraft:mineshaft`, and
|
||||
`/locate structure minecraft:stronghold` to inspect generated structures.
|
||||
Compare one page with several copies in a different Age, and restart/revisit.
|
||||
Flat/Plains with Villages and an otherwise incompatible surface biome with
|
||||
Villages provide separate terrain and biome-override checks.
|
||||
|
||||
The local `1.0.1.56-beta.1` follow-up fixes authored structures outside their
|
||||
normal biomes. Build 55 only bypassed the final biome check; 26.3's jigsaw
|
||||
placement can reject a whole biome column earlier. The override now supplies
|
||||
the generation context's biome predicate before either check, only for the
|
||||
explicitly authored structures in that Age. Terrain/height/geometry rules and
|
||||
unrelated structures keep their normal checks.
|
||||
|
||||
Test with a new Standard or Flat Age using a normally unsupported village
|
||||
biome, such as The End, plus a Villages page. Multiple copies increase density.
|
||||
Use `/locate structure #minecraft:village` rather than selecting only the
|
||||
Plains variant: an override Age can choose other village styles. Old chunks
|
||||
that already rejected their structure attempt are not regenerated; existing
|
||||
build-55 Ages can also test the fix in previously unexplored chunks.
|
||||
|
||||
The local `1.0.1.57-beta.1` slice adds the remaining 1.21.1 authored structure
|
||||
families: Nether Fortresses, Bastions, Trial Chambers, Buried Treasure,
|
||||
Shipwrecks, Ocean Ruins, Pillager Outposts, Ruined Portals, Nether Fossils,
|
||||
Ancient Cities, and Trail Ruins. It also enables the existing 26.3-only
|
||||
Abandoned Camps page using Minecraft's registered camp set and variants.
|
||||
|
||||
All 15 supported structure-set pages share saved counts, biome overrides,
|
||||
and repeated-page density scaling (up to 32 per type). Fortress and Bastion
|
||||
sets reuse the separate 1.21.1 placement definitions; the combined vanilla
|
||||
Nether set is disabled for authored structure plans so the pages stay separate.
|
||||
Unselected supported families are gated in these plans. Already generated
|
||||
chunks and Ages without an authored structure plan keep their existing path.
|
||||
|
||||
Only Villages/Strongholds support Flat; none support Void. Fortress, Bastion,
|
||||
and Nether Fossils require Cave terrain. Villages/Strongholds, Buried Treasure,
|
||||
Shipwrecks, Ocean Ruins, Outposts, Trail Ruins, and Abandoned Camps require
|
||||
surface terrain. Mineshafts, Trial Chambers, Ancient Cities, and Ruined Portals
|
||||
allow surface noise or Cave terrain. Physical terrain/height checks still apply
|
||||
even when the authored page overrides biome eligibility. Instability penalties
|
||||
and random selection of these features are not part of this slice.
|
||||
|
||||
For a quick in-game check, author a new Standard Age with Trial Chambers,
|
||||
Trail Ruins, and Abandoned Camps. Use `/locate structure minecraft:trial_chambers`,
|
||||
`/locate structure minecraft:trail_ruins`, and
|
||||
`/locate structure minecraft:abandoned_camp_forest` to inspect actual generation.
|
||||
Separately, test a Cave/Nether Wastes Age with Fortress and Bastion pages
|
||||
(`/locate structure minecraft:fortress` and
|
||||
`/locate structure minecraft:bastion_remnant`). Repeated pages and restart/
|
||||
new-chunk generation remain useful checks. Structures can still fail when
|
||||
their geometry or required terrain is unavailable.
|
||||
|
||||
The local `1.0.1.58-beta.1` slice enables Caves and Ravines feature pages.
|
||||
They add carving to the chosen noise terrain (Standard, Amplified, Island,
|
||||
or Cave); they are not aliases for the Cave terrain page. Flat/Void and
|
||||
Block/Fluid modifiers remain unsupported, matching the 1.21.1 feature rules.
|
||||
|
||||
Authored paths retain 1.21.1's source range and seeded salts, using 26.3's
|
||||
registered cave/Nether-cave/canyon carvers. Nether-biome sources select the
|
||||
Nether cave carver and skip Ravines. The paths merge into the native carving
|
||||
mask before Minecraft applies aquifer, replacement, and surface rules; no
|
||||
second approximation carves raw air after terrain generation. Biomes are not
|
||||
replaced. The page list is saved, and old plans default to no extra carving.
|
||||
|
||||
Duplicate pages are accepted up to 32 per type, but repeat the same seeded
|
||||
pattern in 1.21.1; this port skips those redundant passes rather than multiplying
|
||||
carving density. Random feature selection and instability remain pending.
|
||||
New chunks only. For manual testing, author a Standard/Plains Age with one
|
||||
Caves page and one Ravines page and inspect underground in Spectator mode.
|
||||
Restart and explore more chunks. A separate Cave/Nether Wastes Age with Caves
|
||||
checks the Nether variant without altering its biome distribution.
|
||||
|
||||
The local `1.0.1.59-beta.1` slice audits page presentation for the current port.
|
||||
Each Symbol Page now reports whether its effect is implemented. Unported pages
|
||||
have red-tinted item artwork and a red Binder page background, plus a red status
|
||||
line. Supported pages retain their normal artwork. This indicates port
|
||||
availability only: supported pages in an invalid order, terrain combination,
|
||||
or excessive quantity are not individually tinted red. Link Panels are unchanged.
|
||||
|
||||
The stale Standard/one-Biome message is removed. Structure descriptions now
|
||||
describe authored biome overrides rather than falsely requiring vanilla biome
|
||||
eligibility. Material pages explain the immediate-before-feature requirement;
|
||||
Flat/Caves/Ravines/Floating Islands descriptions reflect the current rules.
|
||||
These overrides are specific to the 26.3 language file and do not change 1.21.1.
|
||||
No new generation effect is introduced in this UI slice.
|
||||
|
||||
For manual validation, compare a working Villages or Caves page with an
|
||||
unported celestial or lighting page in Creative inventory
|
||||
and the Binder. Only the latter should be red; supported Block/Fluid modifiers
|
||||
and biome controllers should stay normal even though they need other pages.
|
||||
|
||||
The local `1.0.1.60-beta.1` slice implements Poplar Trees, Fallen Poplar Trees,
|
||||
Red Shrubs, and Shelf Mushrooms. They use the existing saved vegetation plan,
|
||||
ordered attempts, surface-biome rules, and combined 32-vegetation-page limit.
|
||||
They accept Standard, Amplified, Island, and Flat terrain, but not Cave/Void
|
||||
or Block/Fluid modifiers. The four pages now show as supported, without a red tint.
|
||||
|
||||
Poplar Trees uses the vanilla red/orange/yellow leaf-litter tree variants with
|
||||
their relative Dappled Forest weights and six attempts per page per chunk;
|
||||
it does not also select spruce or fallen trees. Fallen Poplar Trees makes one
|
||||
surface attempt per page, using the vanilla fallen-tree feature, decorators,
|
||||
and sapling survival rules. Red Shrubs uses the vanilla patch placement.
|
||||
All still require suitable ground and space; authoring does not bypass those
|
||||
requirements, and existing chunks are not redecorated.
|
||||
|
||||
Shelf Mushrooms applies Minecraft's standing/fallen-log decorator to existing
|
||||
log groups, not a new tree feature. It can decorate natural trees or logs from
|
||||
earlier authored tree pages; no suitable logs means no mushrooms. The vanilla
|
||||
side/age/spacing and water-avoidance logic is retained. Writes stay within the
|
||||
current chunk and require valid support. Consecutive mushroom pages reuse the
|
||||
host scan; adding trees invalidates the cached host list for subsequent pages.
|
||||
|
||||
For manual testing, bind a new Flat/Plains Age with Poplar Trees, Fallen Poplar
|
||||
Trees, Red Shrubs, then Shelf Mushrooms. Check leaf colours, fallen trunks,
|
||||
shrubs, and mushrooms on logs. The four pages should no longer be red. Restart,
|
||||
revisit existing terrain, and explore new chunks to check persistence.
|
||||
|
||||
The local `1.0.1.61-beta.1` follow-up removes the inappropriate surface-terrain
|
||||
and surface-biome gates from Shelf Mushrooms only. No tree page is required:
|
||||
natural logs or generated log Tendrils/Spheres/other features are hosts on
|
||||
any terrain, including Void and Cave, and in any biome. Other plants keep their
|
||||
existing terrain/biome restrictions. For example, Void + Oak Log + Tendrils +
|
||||
Shelf Mushrooms is valid authoring; there must still be exposed log faces.
|
||||
|
||||
The mushroom scan now considers log faces throughout the chunk height instead
|
||||
of treating a connected tendril as a standing tree with a small base-height
|
||||
decoration band. Placement retains native replaceability/water checks, valid
|
||||
support, random age, a per-side chance, and spacing checks. It does not grow
|
||||
hosts or continuously decorate player-built logs after chunk generation.
|
||||
Test a new Void Age with the example pages and inspect tendril surfaces.
|
||||
|
||||
The local `1.0.1.62-beta.1` sound slice reuses the complete 1.21.1 sound manifest
|
||||
and all six OGG files directly in the build. Successful Linking/Descriptive Book
|
||||
travel now plays the normal, Intra-Linking, or Disarm sound at departure and
|
||||
arrival, with the same modifier precedence and cloud bursts as 1.21.1. Failed
|
||||
travel stays silent. Star Fissures also play their return sound after arrival,
|
||||
instead of relying only on audio sent immediately before a dimension switch.
|
||||
Portal and meteor sound events are bundled for their later gameplay ports;
|
||||
this does not implement those systems. Binding still uses vanilla page audio.
|
||||
Manual checks: normal book travel, Intra-Linking, Disarm, rejected travel, and
|
||||
a Star Fissure return. Existing books/Ages do not need to be recreated.
|
||||
|
||||
The local `1.0.1.63-beta.1` slice ports the six active weather pages: Normal,
|
||||
Fast, Slow, Off, Always, and Storm. One weather page is accepted per book and
|
||||
its policy is saved in the Age's own data directory. Missing weather pages
|
||||
currently use Normal; random weather selection is still part of the pending
|
||||
random-trait port. Cave terrain forces Off, matching 1.21.1. Precipitation
|
||||
still follows biome temperature and precipitation rules; a desert does not
|
||||
start showing rain merely because Always or Storm is selected.
|
||||
|
||||
26.3 normally shares server WeatherData. Runtime Ages now use an isolated
|
||||
native WeatherData instance with its own persisted rain/thunder timers. The
|
||||
weather controller runs before the native cycle, allowing vanilla rendering,
|
||||
lightning, snow and client dimension-specific packets to handle the result.
|
||||
Fixed weather is refreshed before its timers expire; Fast and Slow use the
|
||||
1.21.1 duration ranges and preserve saved timers on reload. Overworld weather
|
||||
and other Ages are not changed. The vanilla `/weather` command still targets
|
||||
the server's global weather, not the independent Age policy.
|
||||
|
||||
Operator diagnostics: `/rivencraft_port age create_weather <name> <seed> <mode>`
|
||||
creates a Flat test Age; `/rivencraft_port age weather_probe <name>` reports
|
||||
its policy, rain/thunder state and timers, data independence, and global state.
|
||||
For manual testing, bind separate Storm/Always/Off books with Plains, compare
|
||||
the Overworld, revisit and restart, and check that Cave + Storm remains dry.
|
||||
The six weather pages should no longer be tinted red. Existing Ages get a
|
||||
Normal independent cycle; author a new book to select another policy.
|
||||
|
||||
The local `1.0.1.64-beta.1` fix corrects the Binder's biome compiler: it now
|
||||
positively selects biome-category pages instead of treating every page not
|
||||
in an exclusion list as a biome. Weather pages no longer trigger an unknown
|
||||
biome lookup. The weather fixture command now exercises the same page compiler
|
||||
before creating an Age, covering the weather-only symbol list used by a Link
|
||||
Panel + Storm book. Existing Binder contents can be reused after updating.
|
||||
|
||||
The local `1.0.1.65-beta.1` spawn correction removes the Flat-layer shortcut
|
||||
and heightmap-only choice for non-Cave Descriptive Book arrivals. The search
|
||||
starts at the actual maximum build height and traces each nearby column down
|
||||
to its first colliding block, leaves, or fluid. A column without safe dry
|
||||
footing/headroom is skipped instead of descending through its roof or water
|
||||
into an underground pocket. Leaves are valid footing; fluids are not.
|
||||
|
||||
This also revalidates a stored Star Fissure arrival from above on non-Cave
|
||||
terrain. Cave terrain retains its Y=64-downward search, and bound Linking
|
||||
Books retain their exact coordinates. If no natural surface is safe, the
|
||||
safety platform is placed above the actual highest obstruction (or floor
|
||||
Y=63 in an empty Void), never buried below it. Plain Flat terrain still lands
|
||||
on its ground, not an artificially elevated platform. No new Age is required.
|
||||
Operator-only `/rivencraft_port age surface_probe <name> <x> <z>` reports the
|
||||
actual highest obstruction and a safe nearby surface without creating a
|
||||
platform or teleporting. It can generate chunks while inspecting the region.
|
||||
|
||||
The local `1.0.1.66-beta.1` slice ports Normal, Dark, and Bright Lighting.
|
||||
One lighting page is accepted per book and its choice is saved in the Age
|
||||
index before catalog publication. Missing lighting currently defaults to
|
||||
Normal; random lighting selection and Bright's instability cost await those
|
||||
systems' later ports. Lighting pages should no longer be tinted red.
|
||||
|
||||
Normal surface Ages retain the Overworld type. Dark removes skylight with
|
||||
ambient light 0.0; Bright uses ambient light 0.25 and retains surface skylight,
|
||||
matching 1.21.1. **Cave + Bright is supported:** it has ambient light 0.25
|
||||
without skylight. Cave Normal/Dark use no skylight and ambient light 0.0.
|
||||
Placed light sources continue working; ambient light is not a placed light
|
||||
level or a guarantee that monsters cannot spawn.
|
||||
|
||||
The build derives three registered dimension-type variants from the current
|
||||
vanilla Overworld JSON, changing only has_skylight and ambient_light. This
|
||||
preserves native attributes/timelines and synchronizes types through the
|
||||
normal login registry exchange; no dynamic client registry mutation is used.
|
||||
These variants currently use the vanilla type, not custom runtime changes to
|
||||
the host Overworld dimension type made by another data pack.
|
||||
Saved lighting survives reload, reseed, restart and retirement rollback.
|
||||
Existing non-Cave Ages without a choice remain Normal; old Cave Ages now use
|
||||
the no-skylight baseline. Use new Ages for clear visual comparisons and avoid
|
||||
confusing previously lit chunks with the current dimension settings.
|
||||
|
||||
Manual check: create Normal/Dark/Bright surface books and Cave + Bright;
|
||||
compare an unlit enclosed room, check placed torches, revisit/restart and
|
||||
confirm the Overworld stays unchanged. Operator diagnostics:
|
||||
`/rivencraft_port age create_lighting <name> <seed> <mode> <terrain>` and
|
||||
`/rivencraft_port age lighting_probe <name>` report the registered type,
|
||||
skylight and ambient light. Test JAR: `1.0.1.66-beta.1`.
|
||||
|
||||
The local `1.0.1.67-beta.1` correction supersedes slice 66's Bright rendering:
|
||||
26.3's lightmap uses `minecraft:visual/ambient_light_color`, not the old
|
||||
dimension `ambient_light` field. Bright surface/Cave types now set that native
|
||||
attribute to white and ambient_light to 1.0. Cave still has no skylight.
|
||||
Bright Ages reject natural/chunk-generation hostile spawns independently of
|
||||
stored light levels. Existing mobs, passive mobs, spawners, eggs, commands
|
||||
and special-event spawns are not deleted or globally disabled. Actual block
|
||||
light propagation and plant light requirements are unchanged. Existing Bright
|
||||
Ages receive the corrected registered types after restart; no new Age needed.
|
||||
|
||||
Planned color-page target: **ambient light color**, alongside sky/celestial
|
||||
colors. Reuse the native 26.3 attribute for authored tints; Bright defaults to
|
||||
neutral white until the color-page port. This does not enable color pages yet.
|
||||
|
||||
The local `1.0.1.68-beta.1` replaces slice 67's special spawn veto with actual
|
||||
effective ambient brightness. The spawn event handler is removed. Bright's
|
||||
combined light-engine reading now has a minimum of 15 on server and client,
|
||||
including after sky darkening. Its white rendering attribute is retained.
|
||||
Vanilla darkness checks therefore reject light-sensitive monsters naturally;
|
||||
light-ignoring mobs, spawners and other spawn sources use their native rules,
|
||||
not Rivencraft exemptions or bans. This does not remove existing mobs.
|
||||
|
||||
Ambient light is not stored as emitted block light or fabricated skylight.
|
||||
Consumers of combined brightness, including plant light checks, now see the
|
||||
ambient contribution. Normal/Dark and non-Rivencraft dimensions are unchanged.
|
||||
Cave Bright remains without skylight. Existing Bright Ages update on restart.
|
||||
This supersedes slice 67's statement that crop light checks are unchanged.
|
||||
|
||||
The local `1.0.1.69-beta.1` slice seed-completes missing weather and lighting
|
||||
pages for newly bound books, including Link-Panel-only random Ages. Explicit
|
||||
pages take precedence. Cave weather remains Off, while Cave lighting can
|
||||
still choose Bright. Old Ages retain their saved choices; they are not rerolled.
|
||||
|
||||
The supported pool uses the 1.21.1 Legacy-rank rule: Normal is rank 1, other
|
||||
choices rank 2. Lighting weights are 3:1:1 for Normal/Dark/Bright; weather
|
||||
weights are 6:1:1:1:1:1 for Normal/Fast/Slow/Off/Always/Storm. The common weight
|
||||
is derived from the supported pool size. Unported Cloudy/Rain/Snow modes are
|
||||
not selected. Independent seed salts keep a missing lighting roll unchanged
|
||||
when weather is explicitly written, and vice versa. This matches the rank
|
||||
policy, not the exact 1.21.1 seed stream/pool. No random feature list, sky or
|
||||
celestial choices are added by this slice.
|
||||
|
||||
`/rivencraft_port age create_random <name> <seed>` uses the same environment
|
||||
resolver and logs its frozen plan. `/rivencraft_port age environment_probe
|
||||
<seed> <terrain>` inspects deterministic defaults and explicit Storm/Bright
|
||||
precedence without creating an Age. Other explicit operator fixtures keep
|
||||
their Normal defaults to isolate the feature under test.
|
||||
|
||||
For manual checking, create several Link-Panel-only books and verify weather/
|
||||
lighting varies across Ages and persists on revisit/restart. Then author an
|
||||
explicit Normal Weather + Normal Lighting book, and a Cave + Bright book.
|
||||
Include explicit Normal pages when comparing terrain without random lighting
|
||||
or weather changes. Random blocklist policy and instability remain pending.
|
||||
|
||||
The local `1.0.1.70-beta.1` slice ports the Overworld, Nether, and End Sky
|
||||
pages. One sky style is accepted per book and persisted independently of
|
||||
terrain, weather and lighting. Missing sky pages still use Overworld; random
|
||||
sky selection is not part of this slice. Static Sky remains unsupported/red
|
||||
until its Legacy animated renderer is ported, rather than being replaced by
|
||||
a flat gray backdrop. Color and individual celestial pages are also pending.
|
||||
|
||||
Nether Sky uses the native no-sky backdrop and reddish fog; End Sky uses the
|
||||
native End backdrop. Neither changes dimension height, coordinate scale,
|
||||
terrain, Nether water/bed rules, or enables an End dragon fight. All lighting
|
||||
choices work with these styles, including Cave + Bright. Bright's render
|
||||
attribute and effective ambient level 15 are retained on each variant.
|
||||
|
||||
The registered variants derive the vanilla Overworld type and preserve its
|
||||
gameplay attributes. Non-Overworld skies use a derived day timeline without
|
||||
the three sky/fog/cloud tint tracks, retaining native gameplay schedules and
|
||||
illumination while avoiding Overworld day tint on those backdrops. Their
|
||||
cloud color is transparent. Biome/weather fog modifiers still apply. Types
|
||||
sync through normal registry login; no new rendering dependency is required.
|
||||
Sky choices survive reload/reseed/restart and retirement rollback. Old Ages
|
||||
without a sky choice retain Overworld style.
|
||||
|
||||
For visual checks, author Plains + Normal Lighting + No Weather with each
|
||||
sky page; compare the sky/fog and verify the same terrain and normal bed/water
|
||||
behavior. Then try Cave + Bright + End Sky and revisit after restarting.
|
||||
Static Sky should remain red. Operator fixtures:
|
||||
`/rivencraft_port age create_sky <name> <seed> <sky> <lighting> <terrain>` and
|
||||
`/rivencraft_port age sky_probe <name>`. Use new books to author a sky choice;
|
||||
this does not rewrite the sky choice of already-bound books.
|
||||
|
||||
The local `1.0.1.71-beta.1` slice adds the previously pending Static Sky.
|
||||
Its client renderer adapts the 1.21.1 Legacy-style background: six independent
|
||||
48×48 noise faces, default gray tint `#929292`, and noise intensity 65–224.
|
||||
The noise updates every two world ticks. A single small dynamic texture and
|
||||
cached cube replace thousands of per-cell draw vertices; resources are closed
|
||||
with the sky renderer. No artwork or rendering dependency is added.
|
||||
|
||||
Static Sky is now supported rather than red, and persists through the same
|
||||
Age index as other sky styles. Four registered lighting variants retain the
|
||||
existing terrain/gameplay rules. Their native skybox is NONE (no End flashes);
|
||||
only the extracted client state schedules the custom background sky pass.
|
||||
Animation state resets between frames and does not consult a different live
|
||||
world while rendering. Color pages, celestial pages, and random sky selection
|
||||
remain pending.
|
||||
|
||||
Build and disposable dedicated-server creation/restart checks passed for
|
||||
Static + Normal + Standard and Static + Bright + Cave. These do not validate
|
||||
client rendering. For manual testing, bind a new Static Sky + Normal Lighting
|
||||
+ No Weather book, look around/above/below for animated gray static, then
|
||||
visit an Overworld/End Sky Age and return to check there is no sky-state leak.
|
||||
Revisit after restarting. Also try Static Sky + Cave + Bright (look outside
|
||||
terrain or in spectator mode). The fixture accepts `static` as its sky value.
|
||||
The dedicated test server was stopped and its temporary world deleted.
|
||||
Changes remain local; no commit/push/publication was requested.
|
||||
|
||||
The local `1.0.1.72-beta.1` slice completes seeded default sky selection for
|
||||
new Ages. Missing sky pages now choose Overworld, Nether, End, or Static
|
||||
instead of always Overworld. This applies both to Link-Panel-only books and
|
||||
authored books without a sky controller. An explicit sky page always wins.
|
||||
|
||||
The selection uses the first random draw and seed salt from 1.21.1's
|
||||
`AgeSkyProfile.random`. Its equally likely Inherited and Overworld slots both
|
||||
use the native Overworld presentation in this port, giving effective weights
|
||||
2:1:1:1. Sky uses an independent random stream, so adding sky/weather/lighting
|
||||
controllers does not reroll the other choices, terrain, or biomes. Colors,
|
||||
individual celestial bodies, and random feature completion remain pending.
|
||||
|
||||
The chosen sky is saved at Age creation and is not recalculated on travel,
|
||||
reload, restart, or reseed. Existing Ages (including ones with no saved sky
|
||||
entry) keep their previous presentation. Explicit diagnostic fixtures retain
|
||||
their controlled defaults; `create_random` uses the same complete environment
|
||||
resolver as player books. `environment_probe` now also checks every explicit
|
||||
sky override and cross-controller independence.
|
||||
|
||||
For manual checking, create several new Link-Panel-only books and compare
|
||||
their sky styles. Also author a terrain/biome book without a sky page, and one
|
||||
with an explicit Overworld Sky page. Revisit/restart to check persistence.
|
||||
For repeatable operator fixtures, `create_random <name> 0` selects Nether,
|
||||
seed 2 selects Overworld, seed 6 Static, and seed 8 End. These seeds only test
|
||||
the current port selection, not complete 1.21.1 world-generation equivalence.
|
||||
No commit/push/publication is performed until explicitly requested.
|
||||
|
||||
The local `1.0.1.73-beta.1` slice ports shared Red/Green/Blue/White/Black
|
||||
colors and their Number modifiers using the 1.21.1 additive mixing rules.
|
||||
Numbers 0–24 directly before a color contribute `number × 10`; White adds to
|
||||
all three channels and Black subtracts from them. Missing channels begin at
|
||||
zero, groups can be written in any channel order, and the completed result is
|
||||
clamped to 0–255 per channel. A color without a number gets a deterministic
|
||||
seeded amount. Numbers currently require a following color: celestial numeric
|
||||
targets and Static Sky resolution (#109) are not implemented by this slice.
|
||||
|
||||
Color groups preceding a Sky page tint that sky; a trailing color group also
|
||||
tints the sky, matching the existing 1.21.1 behavior. The new **Ambient Light
|
||||
Color** page (Lighting creative category) consumes the preceding color group
|
||||
for ambient illumination instead. Author Bright separately when full effective
|
||||
light level 15 is wanted; this color target does not change stored block light
|
||||
or the selected lighting controller. A target without a color group leaves
|
||||
that color native. For example:
|
||||
|
||||
`Number 24 → Blue → Overworld Sky → Number 24 → White → Ambient Light Color → Bright`
|
||||
|
||||
This gives a blue sky/fog with white ambient illumination. Do not use an
|
||||
unmodified Blue page when an exact amount is desired: its amount is seeded.
|
||||
Explicit colors are saved once; they survive reload/reseed/restart. Existing
|
||||
Ages without a color profile remain native. There are no random color groups
|
||||
for books that do not contain color pages yet.
|
||||
|
||||
Sky colors override the native sky/fog color attributes, tint Static Sky's
|
||||
animated cells, and multiply the native End background texture. End texture
|
||||
detail/brightness remains native; a tint does not make it a flat solid color.
|
||||
Ambient color uses 26.3's native ambient-light attribute. Both profiles sync in
|
||||
the Age catalog before travel, not through unbounded RGB dimension types.
|
||||
Client state is replaced with each catalog and cleared on disconnect. Use the
|
||||
same beta JAR on server and client: the catalog protocol has changed.
|
||||
|
||||
Build and dedicated-server checks cover compiled color groups, native fallback,
|
||||
all four sky types, catalog encoding/decoding, reload and reseed. Client color
|
||||
rendering and page availability still need manual checking. Test the example
|
||||
above, a tinted Static/End Sky, return to an uncolored Age/Overworld, and revisit
|
||||
after restarting. `create_colors <name> <seed> <sky>` creates a repeatable Flat,
|
||||
Bright fixture with sky `#643296` and ambient `#6496c8`; `color_probe <name>`
|
||||
checks saved colors against sampled attributes and round-trips the catalog.
|
||||
No commit/push/publication is performed until explicitly requested.
|
||||
|
||||
The local `1.0.1.74-beta.1` slice adds authored day-cycle lengths through the
|
||||
existing **Normal Time** page. Put one **Half Length**, **Full Length**,
|
||||
**Double Length**, or **Zero Length** before Normal Time. These use the
|
||||
1.21.1 period values 0.5, 1, 2, and 0 normal days respectively. Half runs twice
|
||||
as fast; Double runs half as fast. A Number immediately before a Length page
|
||||
overrides it with `number × 0.08` days (0–24), including Number 0 for stopped
|
||||
time. Normal Time without a Length uses one normal day.
|
||||
|
||||
Zero Length freezes the native day timelines at a deterministic, saved random
|
||||
time. It does not snap back to noon on reload/reseed, and contributes no
|
||||
fractional clock interpolation. A book with no Normal Time page retains the
|
||||
existing native cycle; missing periods are not randomly completed yet.
|
||||
Until the full grammar port, a Length requires one following Normal Time page;
|
||||
repeated Length/Normal Time pages are unsupported.
|
||||
|
||||
The implementation wraps only this Age's native Overworld-clock timeline
|
||||
sampling, on server and client. It does not change the global clock or tick
|
||||
rate. Native sun/moon angles, sky/fog/day shading, moon phase, and gameplay
|
||||
attributes supplied by those timelines use the sampled time. Weather layers
|
||||
and authored colors remain intact. Explicit RGB sky/fog overrides remain
|
||||
constant rather than acquiring day tint. Global `/time` changes still shift
|
||||
moving Ages' underlying clock; Zero remains fixed. `/time query` reports the
|
||||
global clock, not the scaled sample; use `cycle_probe` to inspect the latter.
|
||||
|
||||
This controls the existing native pair only. Separate Sun/Moon pages, custom
|
||||
angles/phases/directions/sizes, per-body colors, eclipses, and random celestial
|
||||
completion remain pending. Normal Time does not add visible bodies to End,
|
||||
Nether or Static skies; include Overworld Sky for the visual cycle test.
|
||||
Client/server must use the same beta: the catalog protocol is now 3.
|
||||
|
||||
For manual testing, bind **Link Panel → Half Length → Normal Time → Overworld
|
||||
Sky** (optionally add Normal Lighting and No Weather), compare with Full and
|
||||
Double, then try Zero Length and check that the sky stays still without
|
||||
bouncing. Revisit after restarting. For numeric authoring, put Number 12 before
|
||||
Full Length for a 0.96-day cycle. Existing Ages are not rewritten. Operator
|
||||
fixtures: `create_cycle <name> <seed> <half|full|double|zero> [number]` and
|
||||
`cycle_probe <name>` under `/rivencraft_port age`.
|
||||
|
||||
Port build and disposable-server probes passed for all four lengths and
|
||||
numeric overrides, native fallback, real sun-angle/light differences, catalog
|
||||
round trips, unload/reload, and reseed preservation. Rendering/travel/reconnect
|
||||
checks still require manual validation. Changes remain local until explicitly
|
||||
requested for commit/push/publication.
|
||||
|
||||
The local `1.0.1.75-beta.1` slice adds **Rising**, **Zenith**, **Setting**, and
|
||||
**Nadir** Phase pages and **Clockwise/Counterclockwise** Direction pages to the
|
||||
native Normal Time cycle. Put these before Normal Time, alongside an optional
|
||||
Length. This does not implement separate Sun/Moon bodies or rename Normal
|
||||
Time/Full Length; the naming cleanup is tracked separately in #110.
|
||||
|
||||
The phase conversion matches 1.21.1's `applyInitialCelestialPhase`: written
|
||||
angles start at midnight, while native day time zero is sunrise. Rising,
|
||||
Zenith, Setting, and Nadir select native times 0, 6000, 12000, and 18000.
|
||||
A Number immediately before a Phase overrides it with `number × 15°` from
|
||||
midnight; Number 6 means sunrise, 12 noon, 18 sunset, and 0 or 24 midnight.
|
||||
These are native day-time positions, not uneased geometric sun angles.
|
||||
|
||||
Written phase is anchored when the Age is created, independently of the
|
||||
current Overworld time and Length. Moving cycles then progress from that
|
||||
saved point. Counterclockwise runs the cycle backward; Clockwise is the
|
||||
default. This reverses the Age's day timelines, including their moon-phase
|
||||
and gameplay attributes, not just a visual disc. The global clock is untouched.
|
||||
As before, global `/time` changes can shift moving samples.
|
||||
|
||||
With Zero Length, written Phase replaces the seeded random stopping time:
|
||||
Zero + Zenith stays at noon, Zero + Nadir at midnight. Direction has no motion
|
||||
to reverse in a stopped cycle. Without a Phase, old behavior and saved Ages
|
||||
are preserved. Origins, phase offsets, and direction survive reload, restart,
|
||||
and reseed. Old profile fields default to origin/offset zero and forward
|
||||
motion. The catalog protocol is now 4; server and client need the same beta.
|
||||
|
||||
Interim grammar permits at most one each of Length, Phase and Direction before
|
||||
one Normal Time target. Angle/bearing, size, multiple bodies and eclipses remain
|
||||
unsupported. No random phase/direction completion was added. Example visual
|
||||
checks (include Link Panel, Overworld Sky, Normal Lighting and No Weather):
|
||||
|
||||
- `Zero Length → Zenith → Normal Time`: frozen noon, including after restart.
|
||||
- `Zero Length → Nadir → Normal Time`: frozen midnight.
|
||||
- `Half Length → Zenith → Counterclockwise → Normal Time`: starts at noon and
|
||||
travels backward, twice as fast. Compare with Clockwise.
|
||||
- `Zero Length → Number 6 → Nadir → Normal Time`: Number overrides Nadir to sunrise.
|
||||
|
||||
Operator fixture: `/rivencraft_port age create_phase <name> <seed> <length>
|
||||
<rising|zenith|setting|nadir> <clockwise|counterclockwise> [phaseNumber]`.
|
||||
`cycle_probe` reports both the saved origin and current native/scaled samples.
|
||||
Build and disposable-server checks cover named/numeric phases, opposite motion,
|
||||
fixed day/night light levels, older profile defaults, catalog round trips,
|
||||
reload and reseed. Client rendering/reconnect checks remain manual. Changes
|
||||
remain local until explicitly requested for commit/push/publication.
|
||||
|
||||
The local `1.0.1.76-beta.1` slice ports **North/East/South/West Angle** pages
|
||||
for the native Normal Time cycle. Their bearings match the 1.21.1 symbol
|
||||
values: 0°, 90°, 180°, and 270°. Put at most one Angle before Normal Time.
|
||||
A Number immediately before it overrides the bearing with `number × 15°`,
|
||||
wrapping at 360°: Number 3 is 45°, Number 12 is 180°, and Number 24 returns
|
||||
to 0°. Angle is the orbit's horizontal bearing, distinct from Phase (position
|
||||
along that orbit) and Direction (which way time progresses).
|
||||
|
||||
The client uses the extracted saved bearing to rotate the native sun/moon
|
||||
path, star field, and sunrise/sunset glow together. The native buffers,
|
||||
textures, phase, and lighting tracks are retained. Bearing does not change
|
||||
altitude, day length, light levels, or clock direction. Without an Angle page,
|
||||
the existing native orientation is preserved; old profiles use the same
|
||||
fallback. No seeded random bearing completion was added. This does not add
|
||||
bodies to Nether/End/Static skies or port custom Sun/Moon counts, sizes,
|
||||
per-body colors, or eclipses. #110 naming work remains separate.
|
||||
|
||||
Bearing persists in the existing cycle profile and catalog; the catalog
|
||||
protocol is now 5, requiring the same beta on server and client. Extracted
|
||||
bearing resets with sky state and is replaced at each render, preventing an
|
||||
Age's orientation from leaking into the next world. Sunrise/sunset glow is
|
||||
rotated relative to the native -90° body-path orientation, not around a
|
||||
different axis. No new textures or dimension variants are required.
|
||||
|
||||
Visual check: bind **Link Panel → Zero Length → Rising Phase → North Angle →
|
||||
Normal Time → Overworld Sky** (Normal Lighting/No Weather help comparison).
|
||||
Compare with East/South/West Angle, then put Number 3 before North Angle for
|
||||
a 45° intermediate bearing. Use Rising/Setting rather than Zenith when
|
||||
checking bearing: a body directly overhead stays overhead under a horizontal
|
||||
rotation. Check the glow stays aligned, then travel to an unmodified Age or
|
||||
Overworld and revisit after restart.
|
||||
|
||||
Operator fixture: `/rivencraft_port age create_angle <name> <seed>
|
||||
<north|east|south|west> [number]` creates a frozen sunrise for comparison.
|
||||
The fixture validates all 25 numeric overrides, cardinal values, timing
|
||||
independence, older profile defaults and catalog codec round trips. Dedicated
|
||||
checks cover persistence and unchanged sampled day lighting; actual rendered
|
||||
orientation remains a manual check. No commit/push/publication is performed
|
||||
until explicitly requested.
|
||||
|
||||
The local `1.0.1.77-beta.1` slice fixes automatic sky selection for authored
|
||||
celestials. **Normal Time without a Sky page now selects Overworld Sky**,
|
||||
the current port's celestial-capable style, instead of sometimes selecting
|
||||
Nether/End/Static and hiding the native bodies. Explicit Sky pages retain
|
||||
precedence. Books without celestial pages retain the existing seeded sky
|
||||
distribution, and lighting/weather choices are unchanged. The resolver also
|
||||
recognizes Sun/Moon page intentions for their eventual port; this does not
|
||||
make those still-unported body pages bindable.
|
||||
|
||||
This matches the visible outcome of the current bundled 1.21.1 Age path:
|
||||
its native fallback is Overworld, and its custom sky renderer draws authored
|
||||
suns/moons over its backgrounds. No 1.21.1 change or follow-up issue is needed
|
||||
for those bundled paths. The fix applies at creation of new 26.3 Ages; it
|
||||
does not rewrite existing saved sky choices. Recreate an older affected book
|
||||
to test the corrected automatic choice. Weather and terrain can still obscure
|
||||
celestials naturally; this is a compatible-sky rule, not a guarantee of a clear
|
||||
view from underground or during storms.
|
||||
|
||||
Test **Link Panel → Normal Time**, optionally with No Weather and Normal
|
||||
Lighting, but without any Sky page. The celestial view should be present.
|
||||
Also test Length/Phase/Angle/Direction + Normal Time without a Sky page and
|
||||
confirm that an explicit Sky page still wins. `create_cycle` now exercises
|
||||
this missing-Sky path rather than forcing an explicit sky in its fixture.
|
||||
`environment_probe` verifies all three celestial intentions, explicit Sky
|
||||
precedence in either order, and unchanged lighting/weather selection.
|
||||
No commit/push/publication is performed until explicitly requested.
|
||||
|
||||
The local `1.0.1.78-beta.1` slice ports **Small/Normal/Large Celestial**
|
||||
Size modifiers for Normal Time: 0.5×, 1× and 2× respectively. A Number page
|
||||
immediately before Size overrides that preset with the existing 1.21.1
|
||||
0.25×–3× range (Number 0 = 0.25×, 12 = 1.625×, 24 = 3×).
|
||||
As in 1.21.1, Size before Normal Time sizes its sun; the paired moon stays
|
||||
normal-sized. Standalone Sun/Moon and independent body sizing remain unported.
|
||||
Size changes neither day length nor illumination, phase or bearing. Older
|
||||
profiles default to 1×; new profiles retain size through reload/restart/reseed.
|
||||
The catalog protocol is now 6, so clients and servers must use matching builds.
|
||||
Rendering retains the native sun texture/pipeline without new GPU buffers.
|
||||
|
||||
Test **Link Panel → Zero Length → Zenith Phase → Large Celestial → Normal
|
||||
Time**, optionally with No Weather and Normal Lighting. Compare Small and
|
||||
Normal, then Number 0/12/24 immediately before Size. No Sky page is needed.
|
||||
The sun should change size, while the moon and Overworld remain unchanged.
|
||||
Reopen the world to check persistence. `create_size <name> <seed>
|
||||
<small|normal|large> [number]` creates a frozen-noon fixture without explicit
|
||||
Sky; `cycle_probe <name>` reports its saved size and catalog round trip.
|
||||
Build and disposable-server checks passed, including all numeric sizes,
|
||||
reload, reseed, restart and unchanged sampled lighting/time. Actual rendered
|
||||
size remains a manual check. The test server stopped and its world was deleted.
|
||||
No commit/push/publication is performed until explicitly requested.
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
./gradlew -p ports/mc263 build
|
||||
```
|
||||
|
||||
This standalone Gradle build uses Minecraft 26.3, NeoForge 26.3.0.99,
|
||||
ModDevGradle 2.0.148, and Java 25. The repository root remains the independent
|
||||
Minecraft 1.21.1/Java 21 build:
|
||||
|
||||
```sh
|
||||
./gradlew build
|
||||
```
|
||||
|
||||
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
|
||||
and properties. Set a nonconflicting `server-port` there before running if
|
||||
another Minecraft server is active. The prototype restores all indexed Ages
|
||||
during the Overworld load, before initial chunk preparation completes.
|
||||
@@ -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).
|
||||
@@ -0,0 +1,72 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.65-beta.1
|
||||
|
||||
This beta collects the player-tested port slices since 1.0.1.54-beta.1.
|
||||
It is not yet feature-complete with the Minecraft 1.21.1 release.
|
||||
|
||||
## Age generation
|
||||
|
||||
- Added authored structure pages: Villages, Mineshafts, Strongholds, Trial
|
||||
Chambers, Buried Treasure, Shipwrecks, Ocean Ruins, Pillager Outposts,
|
||||
Ruined Portals, Nether Fossils, Ancient Cities, Trail Ruins, Nether
|
||||
Fortresses, Bastions, and 26.3's Abandoned Camps. Dungeons remain their
|
||||
separate feature.
|
||||
- Fixed the early structure-biome check so authored structures can generate
|
||||
outside their native biome restrictions, within the supported terrain rules.
|
||||
Repeated structure pages increase density up to the current authoring cap.
|
||||
- Added Caves and Ravines feature pages through 26.3's native carving stage.
|
||||
These are distinct from Cave terrain; Flat/Void support remains pending.
|
||||
- Added Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf Mushrooms.
|
||||
Shelf Mushrooms find exposed logs at any height, including log Tendrils in
|
||||
Void terrain. No Trees page is required.
|
||||
- Added Normal, Fast, Slow, Off, Always, and Storm weather pages. Each Age
|
||||
has independent saved weather data and timers; its policy does not change
|
||||
the Overworld or other Ages. Cave terrain keeps weather disabled.
|
||||
|
||||
## Fixes and feedback
|
||||
|
||||
- Non-Cave Descriptive Book arrivals trace actual blocks down from maximum
|
||||
build height instead of using a Flat-layer shortcut or only a heightmap.
|
||||
The search stops at roofs/fluids rather than selecting underground pockets.
|
||||
Leaves count as footing; submerged footing is rejected. Fallback platforms
|
||||
sit above obstructions, with floor Y=63 in an empty Void.
|
||||
- Stored surface-Age Star Fissure arrivals are rechecked from above. Cave
|
||||
terrain still searches down from Y=64, and Linking Books retain exact
|
||||
bound destinations.
|
||||
- Restored Legacy linking sounds and successful-link cloud bursts. Disarm
|
||||
and Intra-Linking use their respective sounds. Star Fissure return audio
|
||||
also plays after arrival so a dimension switch does not discard it.
|
||||
- Fixed weather-only authoring, including Link Panel + Storm books: non-biome
|
||||
pages no longer get interpreted as biome IDs.
|
||||
- Unsupported pages now have a red tint and availability tooltip. Supported
|
||||
pages and their descriptions reflect the implemented 26.3 behavior.
|
||||
- Added operator weather and surface diagnostics for testing.
|
||||
|
||||
## Current limits
|
||||
|
||||
- Only the supported page/terrain combinations can be bound. Most repeated
|
||||
features are capped at 32; vegetation uses a combined 32-page limit. The
|
||||
full 1.21.1 grammar recovery and instability systems remain unported.
|
||||
- Pageless books currently randomize terrain and biome distribution, not the
|
||||
complete feature/environment list. Omitted weather currently means Normal.
|
||||
- Base carvers do not carve authored solid volumes added after 26.3's combined
|
||||
terrain operation. Previously generated chunks are not regenerated.
|
||||
- The full writing/discovery loop, celestial/lighting systems, crystal portals,
|
||||
instability, and optional-integration parity remain in progress. Creative
|
||||
availability alone does not mean a page is supported; check its tooltip.
|
||||
- Vanilla `/weather` targets global server weather, not the independent Age
|
||||
policy. Biomes retain their normal precipitation rules.
|
||||
|
||||
The player reported in-game passes for these slices. Build/static checks passed.
|
||||
Targeted disposable dedicated-server checks covered all six weather policies,
|
||||
Overworld isolation, unload/reload and restart persistence, weather-only page
|
||||
compilation, and dry surface/leaf spawn probes. This is not a claim of full
|
||||
dedicated-server or optional-mod regression coverage. Test servers were stopped
|
||||
and their temporary worlds removed.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99** or a compatible 26.3 build,
|
||||
and **Java 25**. This build does not require Dynamic Dimensions or Bad Packets.
|
||||
Keep it separate from the 1.21.1 build and saves. Back up entire worlds before
|
||||
updating; beta-save compatibility is not guaranteed.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -0,0 +1,59 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.71-beta.1
|
||||
|
||||
This beta collects the player-tested lighting and sky port slices since
|
||||
1.0.1.65-beta.1. It is not yet feature-complete with Minecraft 1.21.1.
|
||||
|
||||
## Lighting and seeded environments
|
||||
|
||||
- Added Normal, Dark, and Bright lighting pages with saved per-Age settings.
|
||||
- Bright works in Cave terrain too, without adding skylight or weather.
|
||||
- Bright supplies effective ambient light level 15 to native combined-light
|
||||
checks and the white ambient rendering attribute. Vanilla darkness-based
|
||||
monster checks respond to that lighting; there is no separate mob-spawn
|
||||
veto. Mobs that ignore light retain their normal rules.
|
||||
- Missing weather and lighting controllers now resolve deterministically from
|
||||
the Age seed, using independent choices and Legacy-style Normal weighting.
|
||||
Explicit pages win; Cave terrain still forces weather Off. Existing Ages
|
||||
keep their saved choices rather than rerolling on load.
|
||||
|
||||
## Sky styles
|
||||
|
||||
- Added Overworld, Nether, End, and animated Static Sky pages, independently
|
||||
of terrain, weather, and lighting. Choices persist on reload and restart.
|
||||
- Nether Sky uses a reddish fog backdrop; End Sky uses the native End sky.
|
||||
These are visual styles, not Nether/End terrain or gameplay conversions:
|
||||
dimension geometry, coordinate scale, bed/water rules, and dragon behavior
|
||||
are not changed.
|
||||
- Static Sky adapts the existing Legacy-style renderer: six independent
|
||||
48×48 animated gray noise faces. A cached cube and small dynamic texture
|
||||
avoid rebuilding thousands of per-cell vertices each frame. Client-only
|
||||
rendering resources are released with the sky renderer.
|
||||
- Non-Overworld backgrounds have transparent clouds and a derived day
|
||||
timeline that avoids Overworld sky/fog/cloud color tracks while retaining
|
||||
gameplay and illumination schedules.
|
||||
- Updated supported-page feedback and added operator sky/environment probes.
|
||||
|
||||
## Current limits and testing
|
||||
|
||||
- Missing Sky pages still default to Overworld; random sky selection, color
|
||||
pages and individual celestial bodies remain pending. Number-controlled
|
||||
Static Sky resolution is tracked separately in issue #109 for both versions.
|
||||
- The writing/discovery loop, instability, portals, and optional-integration
|
||||
parity are still incomplete. Creative availability is not a support claim;
|
||||
check page tooltips and the beta guide for supported combinations.
|
||||
- Previously generated chunks are not regenerated. Save compatibility during
|
||||
the beta is not guaranteed; keep backups and keep both version tracks apart.
|
||||
|
||||
The player reported in-game passes for the included slices, including Static
|
||||
Sky. Build checks passed for both version tracks. Targeted disposable-server
|
||||
checks covered environment defaults, explicit choices, registered sky/lighting
|
||||
variants, persistence and restart, including Cave + Bright and Static Sky.
|
||||
This is not a claim of complete dedicated-server or optional-mod coverage.
|
||||
Test servers were stopped and temporary worlds removed.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99** and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required by this build. The 1.21.1 release
|
||||
and its dependencies are unchanged.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -0,0 +1,53 @@
|
||||
# Rivencraft 26.3 beta 1.0.1.78-beta.1
|
||||
|
||||
Collects the player-validated port slices since 1.0.1.71-beta.1. This beta is
|
||||
not yet feature-complete with the Minecraft 1.21.1 version.
|
||||
|
||||
## Sky and color writing
|
||||
|
||||
- Missing Sky pages now select a deterministic seeded style. Books with
|
||||
Normal Time and no explicit Sky select celestial-compatible Overworld Sky.
|
||||
Explicit Sky pages always win; weather and lighting choices stay independent.
|
||||
- Ported Red, Green, Blue, Black and White color pages. Preceding Number pages
|
||||
control component strength; unnumbered colors use seeded values. Colors mix
|
||||
before clamping the completed group. Colors before Sky tint that sky; a
|
||||
trailing color group also targets the sky.
|
||||
- Added an Ambient Color controller for an independent ambient-light tint.
|
||||
Color alone does not change effective illumination or monster-spawn rules.
|
||||
- Native, End and animated Static backgrounds use saved Age color settings.
|
||||
|
||||
## Normal Time celestial controls
|
||||
|
||||
- Length pages control the native day cycle; a preceding Number fine-tunes
|
||||
length. Zero Length freezes the cycle at a seeded time or an authored Phase,
|
||||
without interpolation bounce or changing the shared Overworld clock.
|
||||
- Rising, Zenith, Setting and Nadir Phase pages choose the starting phase.
|
||||
Clockwise and Counterclockwise control direction. North, East, South and
|
||||
West Angle pages choose orbit bearing. Preceding Number pages fine-tune
|
||||
Phase and Angle in 15-degree increments.
|
||||
- Small, Normal and Large Celestial size Normal Time's sun to 0.5×, 1× and 2×.
|
||||
A preceding Number overrides size from 0.25× to 3×. As in 1.21.1, the paired
|
||||
moon remains normal-sized. Size does not alter illumination or day length.
|
||||
- Settings persist through reload/restart and remain stable when reseeding
|
||||
future terrain. Older profiles retain native/default values.
|
||||
- Added operator diagnostics for environment, color and celestial profiles.
|
||||
|
||||
## Compatibility and testing
|
||||
|
||||
- Catalog protocol is now 6: use matching client/server builds.
|
||||
- Player in-game checks passed for the included slices. Build checks and
|
||||
targeted disposable-server diagnostics covered numeric modifiers, catalog
|
||||
round trips, unchanged sampled lighting, reload, reseed and restart.
|
||||
- Standalone Sun/Moon controllers, multiple bodies, per-body colors, eclipses,
|
||||
full writing/discovery, instability, portals and optional-integration parity
|
||||
remain incomplete. Creative availability does not imply full page support.
|
||||
- Number-controlled Static Sky resolution remains tracked in issue #109;
|
||||
time-page naming improvements remain tracked in issue #110.
|
||||
- Existing chunks are not regenerated. Save compatibility is not guaranteed
|
||||
during beta; keep backups and keep the two Minecraft version tracks apart.
|
||||
|
||||
Requires Minecraft **26.3**, NeoForge **26.3.0.99**, and **Java 25**. Dynamic
|
||||
Dimensions and Bad Packets are not required. Minecraft 1.21.1 is unchanged.
|
||||
|
||||
Report issues and help improve the wiki at
|
||||
[BlackSheep Git](https://git.paddedcell.org/dnorberg/rivencraft).
|
||||
@@ -0,0 +1,267 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'net.neoforged.moddev' version '2.0.148'
|
||||
}
|
||||
|
||||
group = 'net.dnorberg.rivencraft'
|
||||
version = mod_version
|
||||
|
||||
base {
|
||||
archivesName = "rivencraft-${minecraft_version}"
|
||||
}
|
||||
|
||||
java.toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||
|
||||
neoForge {
|
||||
version = project.neo_version
|
||||
runs {
|
||||
client {
|
||||
client()
|
||||
if (project.hasProperty('connectAddress')) {
|
||||
programArgument '--quickPlayMultiplayer'
|
||||
programArgument project.property('connectAddress').toString()
|
||||
}
|
||||
if (project.hasProperty('clientName')) {
|
||||
programArgument '--username'
|
||||
programArgument project.property('clientName').toString()
|
||||
gameDirectory = project.layout.buildDirectory.dir('run-client-' + project.property('clientName')).get().asFile
|
||||
}
|
||||
}
|
||||
server {
|
||||
server()
|
||||
gameDirectory = project.hasProperty('serverRunDir')
|
||||
? file(project.property('serverRunDir').toString())
|
||||
: project.layout.buildDirectory.dir('run-server').get().asFile
|
||||
programArgument '--nogui'
|
||||
}
|
||||
}
|
||||
mods {
|
||||
rivencraft {
|
||||
sourceSet(sourceSets.main)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def generateModMetadata = tasks.register('generateModMetadata', ProcessResources) {
|
||||
def replaceProperties = [
|
||||
mod_id: mod_id,
|
||||
mod_name: mod_name,
|
||||
mod_license: mod_license,
|
||||
mod_version: mod_version,
|
||||
neo_version: neo_version,
|
||||
minecraft_version_range: minecraft_version_range,
|
||||
]
|
||||
inputs.properties replaceProperties
|
||||
expand replaceProperties
|
||||
from 'src/main/templates'
|
||||
into 'build/generated/sources/modMetadata'
|
||||
}
|
||||
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
|
||||
|
||||
// Derive registered presentation variants without duplicating vanilla gameplay settings.
|
||||
abstract class GeneratePortDimensionTypes extends DefaultTask {
|
||||
@InputFile abstract RegularFileProperty getMinecraftJar()
|
||||
@OutputDirectory abstract DirectoryProperty getOutputDir()
|
||||
|
||||
@TaskAction void generate() {
|
||||
new java.util.zip.ZipFile(minecraftJar.get().asFile).withCloseable { archive ->
|
||||
def entry = archive.getEntry('data/minecraft/dimension_type/overworld.json')
|
||||
if (entry == null) throw new GradleException('Missing vanilla Overworld dimension type')
|
||||
String template = archive.getInputStream(entry).withCloseable { it.getText('UTF-8') }
|
||||
def dayEntry = archive.getEntry('data/minecraft/timeline/day.json')
|
||||
if (dayEntry == null) throw new GradleException('Missing vanilla day timeline')
|
||||
def day = archive.getInputStream(dayEntry).withCloseable {
|
||||
new groovy.json.JsonSlurper().parseText(it.getText('UTF-8'))
|
||||
}
|
||||
// Keep gameplay schedules and illumination, but do not tint these
|
||||
// native non-Overworld sky backgrounds blue/black with the day cycle.
|
||||
['fog_color', 'sky_color', 'cloud_color'].each { day.tracks.remove("minecraft:visual/${it}".toString()) }
|
||||
// Markers are global per clock and already owned by minecraft:day.
|
||||
day.remove('time_markers')
|
||||
File dayFile = new File(outputDir.get().asFile, 'data/rivencraft/timeline/age_sky_day.json')
|
||||
dayFile.parentFile.mkdirs()
|
||||
dayFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(day)) + '\n'
|
||||
File timelineTag = new File(outputDir.get().asFile, 'data/rivencraft/tags/timeline/age_sky.json')
|
||||
timelineTag.parentFile.mkdirs()
|
||||
timelineTag.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([
|
||||
values: ['#minecraft:universal', 'rivencraft:age_sky_day',
|
||||
'minecraft:moon', 'minecraft:early_game']])) + '\n'
|
||||
['overworld', 'nether', 'end', 'static'].each { sky ->
|
||||
['age_normal', 'age_dark', 'age_bright', 'age_cave_bright'].each { lighting ->
|
||||
if (sky == 'overworld' && lighting == 'age_normal') return
|
||||
String name = lighting + (sky == 'overworld' ? '' : "_sky_${sky}")
|
||||
def definition = new groovy.json.JsonSlurper().parseText(template)
|
||||
definition.has_skylight = lighting in ['age_normal', 'age_bright']
|
||||
definition.ambient_light = lighting in ['age_bright', 'age_cave_bright'] ? 1.0 : 0.0
|
||||
// 26.3's lightmap reads this attribute, not ambient_light.
|
||||
if (definition.ambient_light == 1.0)
|
||||
definition.attributes['minecraft:visual/ambient_light_color'] = '#ffffff'
|
||||
if (sky != 'overworld') {
|
||||
definition.skybox = sky == 'end' ? 'end' : 'none'
|
||||
definition.attributes['minecraft:visual/fog_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#181318' : '#6b2725'
|
||||
definition.attributes['minecraft:visual/sky_color'] = sky == 'static' ? '#929292' : sky == 'end' ? '#000000' : '#6b2725'
|
||||
definition.attributes['minecraft:visual/cloud_color'] = '#00000000'
|
||||
if (sky == 'nether') {
|
||||
definition.attributes['minecraft:visual/fog_start_distance'] = 10.0
|
||||
definition.attributes['minecraft:visual/fog_end_distance'] = 96.0
|
||||
}
|
||||
definition.timelines = '#rivencraft:age_sky'
|
||||
}
|
||||
File result = new File(outputDir.get().asFile, "data/rivencraft/dimension_type/${name}.json")
|
||||
result.parentFile.mkdirs()
|
||||
result.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(definition)) + '\n'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def generatePortDimensionTypes = tasks.register('generatePortDimensionTypes', GeneratePortDimensionTypes) {
|
||||
dependsOn 'createMinecraftArtifacts'
|
||||
minecraftJar.set(layout.buildDirectory.file("moddev/artifacts/minecraft-patched-${neo_version}.jar"))
|
||||
outputDir.set(layout.buildDirectory.dir('generated/resources/dimensionTypes'))
|
||||
}
|
||||
sourceSets.main.resources.srcDir generatePortDimensionTypes
|
||||
|
||||
tasks.named('processResources', ProcessResources).configure {
|
||||
// Reuse the existing sprite and base model without copying binary assets.
|
||||
from('../../src/main/resources') {
|
||||
include 'data/rivencraft/worldgen/structure_set/fortresses.json'
|
||||
include 'data/rivencraft/worldgen/structure_set/bastions.json'
|
||||
include 'assets/rivencraft/models/block/star_fissure.json'
|
||||
include 'assets/rivencraft/blockstates/star_fissure.json'
|
||||
include 'assets/rivencraft/sounds.json'
|
||||
include 'assets/rivencraft/sounds/**/*.ogg'
|
||||
include 'assets/rivencraft/textures/item/crystal_shard.png'
|
||||
include 'assets/rivencraft/textures/item/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'
|
||||
include 'assets/rivencraft/models/item/ink_vial.json'
|
||||
include 'assets/rivencraft/models/item/crystal_shard.json'
|
||||
include 'assets/rivencraft/textures/item/descriptive_book.png'
|
||||
include 'assets/rivencraft/models/item/descriptive_book.json'
|
||||
include 'assets/rivencraft/textures/item/linking_book.png'
|
||||
include 'assets/rivencraft/textures/item/linking_book_unwritten.png'
|
||||
include 'assets/rivencraft/models/item/linking_book_bound.json'
|
||||
include 'assets/rivencraft/textures/gui/bookui_cover.png'
|
||||
include 'assets/rivencraft/textures/gui/notebook_pages.png'
|
||||
include 'assets/rivencraft/textures/gui/bookui_pager.png'
|
||||
include 'assets/rivencraft/textures/block/crystal_block.png'
|
||||
include 'assets/rivencraft/textures/block/book_binder.png'
|
||||
include 'assets/rivencraft/textures/block/book_binder_press.png'
|
||||
include 'assets/rivencraft/textures/gui/book_binder_legacy.png'
|
||||
include 'assets/rivencraft/textures/gui/ink_mixer_legacy.png'
|
||||
include 'assets/rivencraft/textures/block/inkwell_base.png'
|
||||
include 'assets/rivencraft/textures/block/inkwell_cup.png'
|
||||
include 'assets/rivencraft/textures/block/ink.png'
|
||||
include 'assets/rivencraft/textures/block/ink_glass.png'
|
||||
include 'assets/rivencraft/models/block/ink_mixer.json'
|
||||
include 'assets/rivencraft/models/item/ink_mixer.json'
|
||||
include 'assets/rivencraft/blockstates/ink_mixer.json'
|
||||
include 'data/rivencraft/loot_table/blocks/ink_mixer.json'
|
||||
include 'assets/rivencraft/models/block/book_binder.json'
|
||||
include 'assets/rivencraft/models/item/book_binder.json'
|
||||
include 'assets/rivencraft/blockstates/book_binder.json'
|
||||
include 'data/rivencraft/loot_table/blocks/book_binder.json'
|
||||
include 'assets/rivencraft/models/block/crystal_block.json'
|
||||
include 'assets/rivencraft/blockstates/crystal_block.json'
|
||||
include 'assets/rivencraft/blockstates/budding_crystal.json'
|
||||
include 'assets/rivencraft/blockstates/small_crystal_bud.json'
|
||||
include 'assets/rivencraft/blockstates/medium_crystal_bud.json'
|
||||
include 'assets/rivencraft/blockstates/large_crystal_bud.json'
|
||||
include 'assets/rivencraft/blockstates/crystal_cluster.json'
|
||||
include 'assets/rivencraft/models/block/budding_crystal.json'
|
||||
include 'assets/rivencraft/models/block/small_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/block/medium_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/block/large_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/block/crystal_cluster.json'
|
||||
include 'assets/rivencraft/models/item/budding_crystal.json'
|
||||
include 'assets/rivencraft/models/item/small_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/item/medium_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/item/large_crystal_bud.json'
|
||||
include 'assets/rivencraft/models/item/crystal_cluster.json'
|
||||
include 'assets/rivencraft/textures/block/small_crystal_bud.png'
|
||||
include 'assets/rivencraft/textures/block/medium_crystal_bud.png'
|
||||
include 'assets/rivencraft/textures/block/large_crystal_bud.png'
|
||||
include 'assets/rivencraft/textures/block/crystal_cluster.png'
|
||||
include 'data/rivencraft/loot_table/blocks/crystal_block.json'
|
||||
include 'data/minecraft/tags/block/mineable/pickaxe.json'
|
||||
include 'data/minecraft/tags/item/cluster_max_harvestables.json'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
tasks.named('runServer').configure {
|
||||
standardInput = System.in
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
org.gradle.jvmargs=-Xmx2G
|
||||
org.gradle.caching=true
|
||||
|
||||
minecraft_version=26.3
|
||||
minecraft_version_range=[26.3]
|
||||
neo_version=26.3.0.99
|
||||
mod_id=rivencraft
|
||||
mod_name=Rivencraft
|
||||
mod_license=LGPL-3.0-or-later
|
||||
mod_version=1.0.1.78-beta.1
|
||||
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"item.rivencraft.crystal_shard": "Crystal Shard",
|
||||
"item.rivencraft.descriptive_book": "Descriptive Book",
|
||||
"item.rivencraft.linking_book": "Linking Book",
|
||||
"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.implemented": "Implemented in 26.3; book and terrain requirements still apply.",
|
||||
"symbol.rivencraft.weather.normal.description": "Use an independent normal weather cycle. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.sky.overworld.description": "Use the native Overworld sky. Terrain and lighting remain independently authored.",
|
||||
"symbol.rivencraft.sky.nether.description": "Use a Nether-style sky and reddish fog, without Nether terrain or gameplay rules.",
|
||||
"symbol.rivencraft.sky.end.description": "Use the native End sky backdrop, without End terrain or a dragon fight.",
|
||||
"symbol.rivencraft.sky.static.description": "Animated static surrounds the Age. Preceding color pages tint it; otherwise it is gray.",
|
||||
"symbol.rivencraft.lighting.ambient_color": "Ambient Light Color",
|
||||
"symbol.rivencraft.lighting.ambient_color.description": "Apply preceding shared colors to ambient light. Use Bright separately for full effective ambient lighting.",
|
||||
"symbol.rivencraft.number.description": "Before Color: value × 10 per channel. Before Length: value × 0.08 days. Before Phase/Angle: value × 15 degrees. Before Size: 0.25× to 3× (0-24).",
|
||||
"symbol.rivencraft.size.small.description": "Before Normal Time: render the sun at 0.5× size. Its paired moon stays normal-sized, matching 1.21.1. A preceding Number overrides the size.",
|
||||
"symbol.rivencraft.size.normal.description": "Before Normal Time: render the sun at 1× size. A preceding Number selects 0.25× through 3× (0-24). Its paired moon is unchanged.",
|
||||
"symbol.rivencraft.size.large.description": "Before Normal Time: render the sun at 2× size. A preceding Number selects 0.25× through 3× (0-24). Its paired moon is unchanged.",
|
||||
"symbol.rivencraft.angle.north.description": "Before Normal Time: set the native celestial orbit bearing to 0 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.angle.east.description": "Before Normal Time: set the native celestial orbit bearing to 90 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.angle.south.description": "Before Normal Time: set the native celestial orbit bearing to 180 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.angle.west.description": "Before Normal Time: set the native celestial orbit bearing to 270 degrees. A preceding Number overrides Angle in 15-degree steps.",
|
||||
"symbol.rivencraft.phase.rising.description": "Before Normal Time: start this Age's native cycle at sunrise. A preceding Number overrides Phase in 15-degree steps from midnight.",
|
||||
"symbol.rivencraft.phase.zenith.description": "Before Normal Time: start this Age's native cycle at noon. With Zero Length it remains noon.",
|
||||
"symbol.rivencraft.phase.setting.description": "Before Normal Time: start this Age's native cycle at sunset. A preceding Number overrides Phase in 15-degree steps from midnight.",
|
||||
"symbol.rivencraft.phase.nadir.description": "Before Normal Time: start this Age's native cycle at midnight. With Zero Length it remains midnight.",
|
||||
"symbol.rivencraft.direction.clockwise.description": "Before Normal Time: advance this Age's native day cycle in its usual direction.",
|
||||
"symbol.rivencraft.direction.counterclockwise.description": "Before Normal Time: run this Age's native day cycle in reverse. Does not change the Overworld clock.",
|
||||
"symbol.rivencraft.celestial.normal_time.description": "Use the native sun/moon cycle with Length, Phase, Direction, Angle, and sun Size modifiers. Without a Length, use one normal day.",
|
||||
"symbol.rivencraft.period.zero.description": "Before Normal Time: freeze the Age's day timelines at the written Phase, or a saved seeded time if Phase is omitted. A preceding Number overrides the Length.",
|
||||
"symbol.rivencraft.period.half.description": "Before Normal Time: a half-length day (twice as fast). A preceding Number sets Length to value × 0.08 days.",
|
||||
"symbol.rivencraft.period.full.description": "Before Normal Time: a normal-length day. A preceding Number sets Length to value × 0.08 days.",
|
||||
"symbol.rivencraft.period.double.description": "Before Normal Time: a double-length day (half as fast). A preceding Number sets Length to value × 0.08 days.",
|
||||
"symbol.rivencraft.color.red.description": "Add red to the next sky or Ambient Light Color target; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.green.description": "Add green to the next sky or Ambient Light Color target; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.blue.description": "Add blue to the next sky or Ambient Light Color target; trailing colors tint the sky. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.white.description": "Add equally to all RGB channels. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.color.black.description": "Subtract equally from all RGB channels. Precede with a Number, or use a seeded amount.",
|
||||
"symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness. Cave terrain remains without skylight.",
|
||||
"symbol.rivencraft.lighting.dark.description": "Remove skylight from this Age; placed light sources still work.",
|
||||
"symbol.rivencraft.lighting.bright.description": "Provide full ambient brightness and effective light level 15. Vanilla light-based spawn rules apply, including in Cave terrain without skylight.",
|
||||
"symbol.rivencraft.weather.fast.description": "Use the faster Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.slow.description": "Use the slower Legacy weather cycle in this Age only. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.off.description": "Keep this Age clear without changing Overworld weather.",
|
||||
"symbol.rivencraft.weather.always.description": "Keep this Age rainy, or snowy in cold biomes, without thunder. Cave terrain keeps weather off.",
|
||||
"symbol.rivencraft.weather.storm.description": "Keep this Age rainy and thundering. Precipitation still depends on the biome; Cave terrain keeps weather off.",
|
||||
"item.rivencraft.page.symbol.unimplemented": "Not yet implemented in 26.3; cannot be bound into an Age.",
|
||||
"item.rivencraft.link_property.disarm": "Disarm",
|
||||
"item.rivencraft.link_property.maintain_momentum": "Maintain Momentum",
|
||||
"item.rivencraft.link_property.intra_linking": "Intra-Linking",
|
||||
"item.rivencraft.ink_vial": "Ink Vial",
|
||||
"item.rivencraft.ink_vial.remaining": "Ink: %s/%s",
|
||||
"item.rivencraft.ink_vial.usage": "Use with paper in the other hand, or place in an Ink Mixer.",
|
||||
"item.rivencraft.page.blank.port_status": "Internal symbol-page base; not an authoring ingredient.",
|
||||
"item.rivencraft.page.link_panel.port_status": "Use with leather in a Book Binder to make a Descriptive Book.",
|
||||
"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": "Unsupported page or page combination",
|
||||
"block.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"container.rivencraft.ink_mixer": "Ink Mixer",
|
||||
"screen.rivencraft.ink_mixer.ink": "Ink: %s/%s",
|
||||
"screen.rivencraft.ink_mixer.additive_hint": "Click with an additive to mix it in",
|
||||
"screen.rivencraft.ink_mixer.chance": "%s: %s%%",
|
||||
"block.rivencraft.crystal_block": "Crystal Block",
|
||||
"block.rivencraft.budding_crystal": "Budding Crystal",
|
||||
"block.rivencraft.small_crystal_bud": "Small Crystal Bud",
|
||||
"block.rivencraft.medium_crystal_bud": "Medium Crystal Bud",
|
||||
"block.rivencraft.large_crystal_bud": "Large Crystal Bud",
|
||||
"block.rivencraft.crystal_cluster": "Crystal Cluster",
|
||||
"itemGroup.rivencraft": "Rivencraft",
|
||||
"itemGroup.rivencraft.pages.link": "Rivencraft Link Pages",
|
||||
"symbol.rivencraft.feature.poplar_trees": "Poplar Trees",
|
||||
"symbol.rivencraft.feature.poplar_trees.description": "Add red, orange, and yellow poplar trees to surface terrain with suitable soil.",
|
||||
"symbol.rivencraft.feature.fallen_poplar_trees": "Fallen Poplar Trees",
|
||||
"symbol.rivencraft.feature.fallen_poplar_trees.description": "Add fallen poplar logs and stumps to surface terrain with suitable soil.",
|
||||
"symbol.rivencraft.feature.shelf_mushrooms": "Shelf Mushrooms",
|
||||
"symbol.rivencraft.feature.shelf_mushrooms.description": "Add shelf mushrooms to exposed log faces on any terrain, including Void. No tree page is required.",
|
||||
"symbol.rivencraft.feature.red_shrubs": "Red Shrubs",
|
||||
"symbol.rivencraft.feature.red_shrubs.description": "Add red shrub patches to surface terrain with suitable ground.",
|
||||
"symbol.rivencraft.feature.abandoned_camps": "Abandoned Camps",
|
||||
"symbol.rivencraft.feature.abandoned_camps.description": "Allow abandoned camps on surface noise terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.block.generated.description": "Place immediately before a compatible feature; unpaired materials are not yet supported.",
|
||||
"symbol.rivencraft.biome.native.description": "Use the terrain's native biome source; do not include Biome pages.",
|
||||
"symbol.rivencraft.terrain.flat.description": "A level, biome-decorated landscape with its foundation at Y=0.",
|
||||
"symbol.rivencraft.feature.floating_islands.description": "Add high floating islands alongside noise, Flat, or Void terrain; not Cave terrain.",
|
||||
"symbol.rivencraft.feature.caves.description": "Add extra cave carving to noise terrain, including Cave; not Flat or Void.",
|
||||
"symbol.rivencraft.feature.ravines.description": "Add canyon carving to noise terrain; not Flat or Void. Skips Nether biomes.",
|
||||
"symbol.rivencraft.feature.mineshafts.description": "Allow mineshafts in noise terrain, including Cave, regardless of normal biome eligibility.",
|
||||
"symbol.rivencraft.feature.nether_fortresses.description": "Allow Nether fortresses in Cave terrain; the authored page overrides biome eligibility.",
|
||||
"symbol.rivencraft.feature.strongholds.description": "Allow strongholds in surface noise or Flat terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.feature.villages.description": "Allow villages in surface noise or Flat terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.feature.trial_chambers.description": "Allow underground Trial Chambers in noise terrain, including Cave, regardless of biome eligibility.",
|
||||
"symbol.rivencraft.feature.bastions.description": "Allow Bastion Remnants in Cave terrain; the authored page overrides biome eligibility.",
|
||||
"symbol.rivencraft.feature.buried_treasure.description": "Allow buried treasure in surface noise terrain; biome eligibility is overridden, but suitable terrain is required.",
|
||||
"symbol.rivencraft.feature.shipwrecks.description": "Allow shipwrecks in surface noise terrain, including outside their usual ocean and beach biomes.",
|
||||
"symbol.rivencraft.feature.ocean_ruins.description": "Allow ocean ruins in surface noise terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.feature.pillager_outposts.description": "Allow pillager outposts in surface noise terrain, including outside their usual biomes.",
|
||||
"symbol.rivencraft.feature.ruined_portals.description": "Allow ruined portals in noise terrain, including Cave, regardless of normal biome eligibility.",
|
||||
"symbol.rivencraft.feature.nether_fossils.description": "Allow Nether fossils in Cave terrain; the authored page overrides biome eligibility.",
|
||||
"symbol.rivencraft.feature.ancient_cities.description": "Allow underground Ancient Cities in noise terrain, including Cave, regardless of biome eligibility.",
|
||||
"symbol.rivencraft.feature.trail_ruins.description": "Allow Trail Ruins in surface noise terrain, including outside their usual biomes."
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
|
||||
rootProject.name = 'rivencraft-mc263-port'
|
||||
@@ -0,0 +1,190 @@
|
||||
package net.dnorberg.rivencraft;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||
import net.dnorberg.rivencraft.age.PortAgeCommands;
|
||||
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;
|
||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
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;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.crafting.RecipeSerializer;
|
||||
import net.minecraft.world.inventory.MenuType;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.fml.common.Mod;
|
||||
import net.neoforged.neoforge.registries.DeferredBlock;
|
||||
import net.neoforged.neoforge.registries.DeferredItem;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
import net.neoforged.neoforge.common.extensions.IMenuTypeExtension;
|
||||
import java.util.function.Supplier;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/** First 26.3 registry slice; gameplay remains in the 1.21.1 target for now. */
|
||||
@Mod(Rivencraft.MOD_ID)
|
||||
public final class Rivencraft {
|
||||
public static final String MOD_ID = "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 = sound("linking.link");
|
||||
public static final Supplier<SoundEvent> LINK_DISARM = sound("linking.link-disarm");
|
||||
public static final Supplier<SoundEvent> LINK_INTRA = sound("linking.link-intra");
|
||||
public static final Supplier<SoundEvent> LINK_FISSURE = sound("linking.link-fissure");
|
||||
public static final Supplier<SoundEvent> LINK_PORTAL = sound("linking.link-portal");
|
||||
public static final Supplier<SoundEvent> METEOR_ROAR = sound("entity.meteor.roar");
|
||||
|
||||
private static Supplier<SoundEvent> sound(String name) {
|
||||
return SOUNDS.register(name, () -> SoundEvent.createVariableRangeEvent(
|
||||
Identifier.fromNamespaceAndPath(MOD_ID, name)));
|
||||
}
|
||||
private static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
|
||||
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
|
||||
private static final DeferredRegister<MenuType<?>> MENUS =
|
||||
DeferredRegister.create(Registries.MENU, MOD_ID);
|
||||
private static final DeferredRegister<RecipeSerializer<?>> RECIPE_SERIALIZERS =
|
||||
DeferredRegister.create(Registries.RECIPE_SERIALIZER, MOD_ID);
|
||||
private static final DeferredRegister.DataComponents COMPONENTS =
|
||||
DeferredRegister.createDataComponents(Registries.DATA_COMPONENT_TYPE, MOD_ID);
|
||||
|
||||
public static final Supplier<DataComponentType<Identifier>> AGE_BOOK_ID = COMPONENTS.registerComponentType(
|
||||
"age_book_id", builder -> builder.persistent(Identifier.CODEC)
|
||||
.networkSynchronized(Identifier.STREAM_CODEC));
|
||||
public static final Supplier<DataComponentType<LinkBookTarget>> LINK_BOOK_TARGET = COMPONENTS.registerComponentType(
|
||||
"link_book_target", builder -> builder.persistent(LinkBookTarget.CODEC)
|
||||
.networkSynchronized(LinkBookTarget.STREAM_CODEC));
|
||||
/** A present component distinguishes Link Panels from unwritten base Pages. */
|
||||
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));
|
||||
public static final Supplier<DataComponentType<LinkProperties>> LINK_PROPERTIES = COMPONENTS.registerComponentType(
|
||||
"link_properties", builder -> builder.persistent(LinkProperties.CODEC)
|
||||
.networkSynchronized(LinkProperties.STREAM_CODEC));
|
||||
|
||||
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).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).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", () ->
|
||||
new BlockEntityType<>(PortBookBinderBlockEntity::new, BOOK_BINDER.get()));
|
||||
public static final Supplier<MenuType<PortBookBinderMenu>> BOOK_BINDER_MENU =
|
||||
MENUS.register("book_binder", () -> IMenuTypeExtension.create(PortBookBinderMenu::new));
|
||||
public static final Supplier<BlockEntityType<InkMixerBlockEntity>> INK_MIXER_BLOCK_ENTITY =
|
||||
BLOCK_ENTITY_TYPES.register("ink_mixer", () ->
|
||||
new BlockEntityType<>(InkMixerBlockEntity::new, INK_MIXER.get()));
|
||||
public static final Supplier<MenuType<InkMixerMenu>> INK_MIXER_MENU =
|
||||
MENUS.register("ink_mixer", () -> IMenuTypeExtension.create(InkMixerMenu::new));
|
||||
public static final DeferredItem<BlockItem> CRYSTAL_BLOCK_ITEM = ITEMS.registerSimpleBlockItem(CRYSTAL_BLOCK);
|
||||
public static final DeferredBlock<BuddingCrystalBlock> BUDDING_CRYSTAL = BLOCKS.registerBlock(
|
||||
"budding_crystal", BuddingCrystalBlock::new,
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.BUDDING_AMETHYST).mapColor(MapColor.COLOR_BLUE));
|
||||
public static final DeferredItem<BlockItem> BUDDING_CRYSTAL_ITEM = ITEMS.registerSimpleBlockItem(BUDDING_CRYSTAL);
|
||||
public static final DeferredBlock<AmethystClusterBlock> SMALL_CRYSTAL_BUD = BLOCKS.registerBlock(
|
||||
"small_crystal_bud", properties -> new AmethystClusterBlock(3.0F, 4.0F, properties),
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.SMALL_AMETHYST_BUD));
|
||||
public static final DeferredItem<BlockItem> SMALL_CRYSTAL_BUD_ITEM = ITEMS.registerSimpleBlockItem(SMALL_CRYSTAL_BUD);
|
||||
public static final DeferredBlock<AmethystClusterBlock> MEDIUM_CRYSTAL_BUD = BLOCKS.registerBlock(
|
||||
"medium_crystal_bud", properties -> new AmethystClusterBlock(4.0F, 3.0F, properties),
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.MEDIUM_AMETHYST_BUD));
|
||||
public static final DeferredItem<BlockItem> MEDIUM_CRYSTAL_BUD_ITEM = ITEMS.registerSimpleBlockItem(MEDIUM_CRYSTAL_BUD);
|
||||
public static final DeferredBlock<AmethystClusterBlock> LARGE_CRYSTAL_BUD = BLOCKS.registerBlock(
|
||||
"large_crystal_bud", properties -> new AmethystClusterBlock(5.0F, 3.0F, properties),
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.LARGE_AMETHYST_BUD));
|
||||
public static final DeferredItem<BlockItem> LARGE_CRYSTAL_BUD_ITEM = ITEMS.registerSimpleBlockItem(LARGE_CRYSTAL_BUD);
|
||||
public static final DeferredBlock<AmethystClusterBlock> CRYSTAL_CLUSTER = BLOCKS.registerBlock(
|
||||
"crystal_cluster", properties -> new AmethystClusterBlock(7.0F, 3.0F, properties),
|
||||
() -> BlockBehaviour.Properties.ofFullCopy(Blocks.AMETHYST_CLUSTER));
|
||||
public static final DeferredItem<BlockItem> CRYSTAL_CLUSTER_ITEM = ITEMS.registerSimpleBlockItem(CRYSTAL_CLUSTER);
|
||||
public static final DeferredItem<Item> CRYSTAL_SHARD = ITEMS.registerSimpleItem("crystal_shard");
|
||||
public static final DeferredItem<PageItem> PAGE = ITEMS.registerItem("page", PageItem::new);
|
||||
public static final DeferredItem<InkVialItem> INK_VIAL = ITEMS.registerItem(
|
||||
"ink_vial", InkVialItem::new, properties -> properties.stacksTo(16));
|
||||
public static final DeferredItem<DescriptiveBookItem> DESCRIPTIVE_BOOK = ITEMS.registerItem(
|
||||
"descriptive_book", DescriptiveBookItem::new, properties -> properties.stacksTo(1));
|
||||
public static final DeferredItem<LinkingBookItem> LINKING_BOOK = ITEMS.registerItem(
|
||||
"linking_book", LinkingBookItem::new, properties -> properties.stacksTo(1));
|
||||
public static final Supplier<RecipeSerializer<LinkingBookRecipe>> LINKING_BOOK_RECIPE =
|
||||
RECIPE_SERIALIZERS.register("linking_book", () -> LinkingBookRecipe.SERIALIZER);
|
||||
|
||||
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(net.dnorberg.rivencraft.age.PortAgeWeather::onLevelTickPre);
|
||||
NeoForge.EVENT_BUS.addListener(PortAgeCommands::register);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogin);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onLogout);
|
||||
NeoForge.EVENT_BUS.addListener(AgeCatalogSync::onServerStopped);
|
||||
modEventBus.addListener(AgeCatalogSync::registerPayloads);
|
||||
modEventBus.addListener(PortBookNetworking::registerPayloads);
|
||||
LOGGER.info("Initializing Rivencraft 26.3 port slice (not release-ready)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Acknowledges the exact catalog version applied on the client main thread. */
|
||||
public record AgeCatalogAckPayload(int version) implements CustomPacketPayload {
|
||||
public static final Type<AgeCatalogAckPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_catalog_ack"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AgeCatalogAckPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.VAR_INT, AgeCatalogAckPayload::version, AgeCatalogAckPayload::new
|
||||
);
|
||||
|
||||
@Override
|
||||
public Type<AgeCatalogAckPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Full server level catalog, sent on login and after a runtime Age is added. */
|
||||
public record AgeCatalogPayload(int version, List<Identifier> dimensions,
|
||||
Map<Identifier, PortAgeColors> colors,
|
||||
Map<Identifier, PortDayCycle> dayCycles) implements CustomPacketPayload {
|
||||
public static final Type<AgeCatalogPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_catalog"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AgeCatalogPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.VAR_INT, AgeCatalogPayload::version,
|
||||
Identifier.STREAM_CODEC.apply(ByteBufCodecs.list()), AgeCatalogPayload::dimensions,
|
||||
ByteBufCodecs.map(HashMap::new, Identifier.STREAM_CODEC, PortAgeColors.STREAM_CODEC), AgeCatalogPayload::colors,
|
||||
ByteBufCodecs.map(HashMap::new, Identifier.STREAM_CODEC, PortDayCycle.STREAM_CODEC), AgeCatalogPayload::dayCycles,
|
||||
AgeCatalogPayload::new
|
||||
);
|
||||
|
||||
public AgeCatalogPayload {
|
||||
dimensions = List.copyOf(dimensions);
|
||||
colors = Map.copyOf(colors);
|
||||
dayCycles = Map.copyOf(dayCycles);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<AgeCatalogPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
|
||||
import net.neoforged.neoforge.event.server.ServerStoppedEvent;
|
||||
import net.neoforged.neoforge.network.PacketDistributor;
|
||||
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
|
||||
|
||||
/** Refuses runtime-Age travel until the connected client confirms its catalog. */
|
||||
public final class AgeCatalogSync {
|
||||
private static final Map<MinecraftServer, State> STATES = new IdentityHashMap<>();
|
||||
|
||||
private AgeCatalogSync() {
|
||||
}
|
||||
|
||||
public static void registerPayloads(RegisterPayloadHandlersEvent event) {
|
||||
var registrar = event.registrar("6");
|
||||
registrar.playToClient(AgeCatalogPayload.TYPE, AgeCatalogPayload.STREAM_CODEC);
|
||||
registrar.playToServer(AgeCatalogAckPayload.TYPE, AgeCatalogAckPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) acknowledge(player, payload.version());
|
||||
});
|
||||
}
|
||||
|
||||
public static void onLogin(PlayerEvent.PlayerLoggedInEvent event) {
|
||||
if (event.getEntity() instanceof ServerPlayer player) {
|
||||
try {
|
||||
send(player);
|
||||
} catch (RuntimeException exception) {
|
||||
Rivencraft.LOGGER.error("Could not send initial Age catalog to {}; travel remains blocked",
|
||||
player.getName().getString(), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void onLogout(PlayerEvent.PlayerLoggedOutEvent event) {
|
||||
if (event.getEntity() instanceof ServerPlayer player) {
|
||||
State state = STATES.get(player.level().getServer());
|
||||
if (state != null) state.acknowledged.remove(player.getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
public static void onServerStopped(ServerStoppedEvent event) {
|
||||
STATES.remove(event.getServer());
|
||||
}
|
||||
|
||||
public static void onCatalogChanged(MinecraftServer server) {
|
||||
State state = state(server);
|
||||
state.version++;
|
||||
state.acknowledged.clear();
|
||||
for (ServerPlayer player : server.getPlayerList().getPlayers()) {
|
||||
try {
|
||||
send(player);
|
||||
} catch (RuntimeException exception) {
|
||||
Rivencraft.LOGGER.error("Could not send Age catalog to {}; travel remains blocked",
|
||||
player.getName().getString(), exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean ready(ServerPlayer player, Identifier target) {
|
||||
MinecraftServer server = player.level().getServer();
|
||||
State state = state(server);
|
||||
return state.version == state.acknowledged.getOrDefault(player.getUUID(), -1)
|
||||
&& server.levelKeys().stream().anyMatch(key -> key.identifier().equals(target));
|
||||
}
|
||||
|
||||
private static void send(ServerPlayer player) {
|
||||
MinecraftServer server = player.level().getServer();
|
||||
State state = state(server);
|
||||
state.acknowledged.remove(player.getUUID());
|
||||
var dimensions = server.levelKeys().stream().map(key -> key.identifier()).sorted().toList();
|
||||
PacketDistributor.sendToPlayer(player, new AgeCatalogPayload(state.version, dimensions,
|
||||
RuntimeAgeBackend.savedColors(server), RuntimeAgeBackend.savedDayCycles(server)));
|
||||
}
|
||||
|
||||
private static void acknowledge(ServerPlayer player, int version) {
|
||||
State state = state(player.level().getServer());
|
||||
if (version != state.version) {
|
||||
Rivencraft.LOGGER.warn("Ignoring stale Age catalog acknowledgement {} from {} (current {})",
|
||||
version, player.getName().getString(), state.version);
|
||||
return;
|
||||
}
|
||||
state.acknowledged.put(player.getUUID(), version);
|
||||
Rivencraft.LOGGER.info("Age catalog {} acknowledged by {}", version, player.getName().getString());
|
||||
}
|
||||
|
||||
private static State state(MinecraftServer server) {
|
||||
return STATES.computeIfAbsent(server, ignored -> new State());
|
||||
}
|
||||
|
||||
private static final class State {
|
||||
private int version = 1;
|
||||
private final Map<UUID, Integer> acknowledged = new HashMap<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Server-owned seed index for the first-party 26.3 runtime-level prototype. */
|
||||
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)),
|
||||
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),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortLighting.CODEC)
|
||||
.optionalFieldOf("lighting", Map.of()).forGetter(index -> index.lighting),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortSkyStyle.CODEC)
|
||||
.optionalFieldOf("sky", Map.of()).forGetter(index -> index.sky),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortAgeColors.CODEC)
|
||||
.optionalFieldOf("colors", Map.of()).forGetter(index -> index.colors),
|
||||
Codec.unboundedMap(Identifier.CODEC, PortDayCycle.CODEC)
|
||||
.optionalFieldOf("day_cycles", Map.of()).forGetter(index -> index.dayCycles)
|
||||
).apply(instance, AgeLevelIndex::new));
|
||||
|
||||
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "runtime_ages"),
|
||||
AgeLevelIndex::new,
|
||||
CODEC
|
||||
);
|
||||
|
||||
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 final Map<Identifier, PortLighting> lighting;
|
||||
private final Map<Identifier, PortSkyStyle> sky;
|
||||
private final Map<Identifier, PortAgeColors> colors;
|
||||
private final Map<Identifier, PortDayCycle> dayCycles;
|
||||
|
||||
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<>();
|
||||
this.lighting = new LinkedHashMap<>();
|
||||
this.sky = new LinkedHashMap<>();
|
||||
this.colors = new LinkedHashMap<>();
|
||||
this.dayCycles = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
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,
|
||||
Map<Identifier, PortLighting> lighting,
|
||||
Map<Identifier, PortSkyStyle> sky, Map<Identifier, PortAgeColors> colors,
|
||||
Map<Identifier, PortDayCycle> dayCycles) {
|
||||
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);
|
||||
this.lighting = new LinkedHashMap<>(lighting);
|
||||
this.sky = new LinkedHashMap<>(sky);
|
||||
this.colors = new LinkedHashMap<>(colors);
|
||||
this.dayCycles = new LinkedHashMap<>(dayCycles);
|
||||
}
|
||||
|
||||
Map<Identifier, Long> entries() {
|
||||
return Map.copyOf(ages);
|
||||
}
|
||||
|
||||
boolean contains(Identifier id) {
|
||||
return ages.containsKey(id) || retired.contains(id);
|
||||
}
|
||||
|
||||
Long seed(Identifier id) {
|
||||
return ages.get(id);
|
||||
}
|
||||
|
||||
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); }
|
||||
PortLighting lighting(Identifier id) { return lighting.getOrDefault(id, PortLighting.NORMAL); }
|
||||
PortSkyStyle sky(Identifier id) { return sky.getOrDefault(id, PortSkyStyle.OVERWORLD); }
|
||||
PortAgeColors colors(Identifier id) { return colors.getOrDefault(id, PortAgeColors.NATIVE); }
|
||||
Map<Identifier, PortAgeColors> colors() { return Map.copyOf(colors); }
|
||||
PortDayCycle dayCycle(Identifier id) { return dayCycles.getOrDefault(id, PortDayCycle.NATIVE); }
|
||||
Map<Identifier, PortDayCycle> dayCycles() { return Map.copyOf(dayCycles); }
|
||||
|
||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light, PortSkyStyle skyStyle,
|
||||
PortAgeColors colorProfile, PortDayCycle dayCycle) {
|
||||
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);
|
||||
if (light != PortLighting.NORMAL) lighting.put(id, light);
|
||||
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
|
||||
if (!colorProfile.equals(PortAgeColors.NATIVE)) colors.put(id, colorProfile);
|
||||
if (!dayCycle.equals(PortDayCycle.NATIVE)) dayCycles.put(id, dayCycle);
|
||||
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);
|
||||
lighting.remove(id);
|
||||
sky.remove(id);
|
||||
colors.remove(id);
|
||||
dayCycles.remove(id);
|
||||
retired.add(id);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
||||
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
|
||||
PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light,
|
||||
PortSkyStyle skyStyle, PortAgeColors colorProfile, PortDayCycle dayCycle) {
|
||||
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);
|
||||
if (light != PortLighting.NORMAL) lighting.put(id, light);
|
||||
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
|
||||
if (!colorProfile.equals(PortAgeColors.NATIVE)) colors.put(id, colorProfile);
|
||||
if (!dayCycle.equals(PortDayCycle.NATIVE)) dayCycles.put(id, dayCycle);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void reseed(Identifier id, long seed) {
|
||||
if (!ages.containsKey(id)) throw new IllegalArgumentException("Age not indexed: " + id);
|
||||
ages.put(id, seed);
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.joml.Vector3f;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
/** Frozen shared-color writing, adapted from 1.21.1 AgeSkyProfile. -1 means native. */
|
||||
public record PortAgeColors(int sky, int ambient) {
|
||||
public static final PortAgeColors NATIVE = new PortAgeColors(-1, -1);
|
||||
public static final Codec<PortAgeColors> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("sky").forGetter(PortAgeColors::sky),
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("ambient").forGetter(PortAgeColors::ambient)
|
||||
).apply(instance, PortAgeColors::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortAgeColors> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.INT, PortAgeColors::sky, ByteBufCodecs.INT, PortAgeColors::ambient, PortAgeColors::new);
|
||||
public static final Identifier AMBIENT = Identifier.fromNamespaceAndPath("rivencraft", "lighting/ambient_color");
|
||||
private static volatile Map<Identifier, PortAgeColors> clientColors = Map.of();
|
||||
|
||||
public PortAgeColors {
|
||||
if (sky < -1 || sky > 0xffffff || ambient < -1 || ambient > 0xffffff)
|
||||
throw new IllegalArgumentException("Invalid Age color");
|
||||
}
|
||||
|
||||
public static void updateClient(Map<Identifier, PortAgeColors> colors) { clientColors = Map.copyOf(colors); }
|
||||
|
||||
public static PortAgeColors forLevel(Level level) {
|
||||
if (level instanceof PortAgeLevel age) return age.colors();
|
||||
return level.isClientSide() ? clientColors.getOrDefault(level.dimension().identifier(), NATIVE) : NATIVE;
|
||||
}
|
||||
|
||||
public static boolean isColor(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "color/red", "color/green", "color/blue", "color/white", "color/black" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static int number(Identifier page) {
|
||||
if (!page.getNamespace().equals("rivencraft") || !page.getPath().startsWith("number/")) return -1;
|
||||
try {
|
||||
int value = Integer.parseInt(page.getPath().substring(7));
|
||||
return value >= 0 && value <= 24 ? value : -1;
|
||||
} catch (NumberFormatException exception) { return -1; }
|
||||
}
|
||||
|
||||
public static boolean isModifier(Identifier page) { return isColor(page) || number(page) >= 0 || page.equals(AMBIENT); }
|
||||
|
||||
public static boolean validNumbers(List<Identifier> pages) {
|
||||
for (int i = 0; i < pages.size(); i++)
|
||||
if (number(pages.get(i)) >= 0 && (i + 1 == pages.size()
|
||||
|| !isColor(pages.get(i + 1)) && !PortDayCycle.isPeriod(pages.get(i + 1))
|
||||
&& !PortDayCycle.isPhase(pages.get(i + 1)) && !PortDayCycle.isAngle(pages.get(i + 1))
|
||||
&& !PortDayCycle.isSize(pages.get(i + 1)))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PortAgeColors compile(List<Identifier> pages, long seed) {
|
||||
if (!validNumbers(pages)) throw new IllegalArgumentException("Number pages must precede a supported Color, Length, Phase, Angle, or Size modifier");
|
||||
int red = 0, green = 0, blue = 0, sky = -1, ambient = -1;
|
||||
boolean written = false;
|
||||
for (int i = 0; i < pages.size(); i++) {
|
||||
Identifier page = pages.get(i);
|
||||
if (isColor(page)) {
|
||||
int amount = i > 0 ? number(pages.get(i - 1)) : -1;
|
||||
if (amount < 0) amount = new Random(seed ^ (0x9E3779B97F4A7C15L * (i + 1L))).nextInt(25);
|
||||
amount *= 10;
|
||||
written = true;
|
||||
switch (page.getPath()) {
|
||||
case "color/red" -> red += amount;
|
||||
case "color/green" -> green += amount;
|
||||
case "color/blue" -> blue += amount;
|
||||
case "color/white" -> { red += amount; green += amount; blue += amount; }
|
||||
case "color/black" -> { red -= amount; green -= amount; blue -= amount; }
|
||||
}
|
||||
} else if (PortSkyStyle.forPage(page) != null || page.equals(AMBIENT)) {
|
||||
if (written) {
|
||||
int rgb = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
|
||||
if (page.equals(AMBIENT)) ambient = rgb; else sky = rgb;
|
||||
}
|
||||
red = green = blue = 0;
|
||||
written = false;
|
||||
}
|
||||
}
|
||||
if (written) sky = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
|
||||
return new PortAgeColors(sky, ambient);
|
||||
}
|
||||
|
||||
private static int clamp(int value) { return Math.max(0, Math.min(255, value)); }
|
||||
public static Vector3fc vector(int rgb) {
|
||||
return new Vector3f((rgb >> 16 & 255) / 255f, (rgb >> 8 & 255) / 255f, (rgb & 255) / 255f);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,63 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.saveddata.WeatherData;
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
|
||||
/** Runtime Age with independent native weather, rather than 26.3's shared server cycle. */
|
||||
final class PortAgeLevel extends ServerLevel {
|
||||
private final WeatherData ageWeather;
|
||||
private final PortWeatherSettings weatherSettings;
|
||||
private final boolean cave;
|
||||
private PortAgeColors colors = PortAgeColors.NATIVE;
|
||||
private PortDayCycle dayCycle = PortDayCycle.NATIVE;
|
||||
|
||||
PortDayCycle dayCycle() { return dayCycle == null ? PortDayCycle.NATIVE : dayCycle; }
|
||||
void configureDayCycle(PortDayCycle cycle) {
|
||||
this.dayCycle = cycle;
|
||||
environmentAttributes().invalidateTickCache();
|
||||
updateSkyBrightness();
|
||||
}
|
||||
|
||||
PortAgeColors colors() { return colors == null ? PortAgeColors.NATIVE : colors; }
|
||||
void configureColors(PortAgeColors colors) {
|
||||
this.colors = colors;
|
||||
environmentAttributes().invalidateTickCache();
|
||||
}
|
||||
|
||||
PortAgeLevel(MinecraftServer server, ResourceKey<Level> key, LevelStem stem,
|
||||
long biomeZoomSeed, PortAgeTerrain terrain) {
|
||||
super(server, server.executor, server.storageSource,
|
||||
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
|
||||
key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false);
|
||||
ageWeather = getDataStorage().computeIfAbsent(WeatherData.TYPE);
|
||||
weatherSettings = getDataStorage().computeIfAbsent(PortWeatherSettings.TYPE);
|
||||
cave = terrain == PortAgeTerrain.CAVE;
|
||||
PortAgeWeather.apply(this);
|
||||
// The base constructor initializes visual levels from server weather.
|
||||
// Replace them before publishing this Age or sending its join packets.
|
||||
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeatherData getWeatherData() {
|
||||
// The base constructor may query weather before our fields are initialized.
|
||||
return ageWeather == null ? super.getWeatherData() : ageWeather;
|
||||
}
|
||||
|
||||
PortWeatherMode weatherMode() { return cave ? PortWeatherMode.OFF : weatherSettings.mode(); }
|
||||
|
||||
void configureWeather(PortWeatherMode mode) {
|
||||
weatherSettings.setMode(mode);
|
||||
PortAgeWeather.apply(this);
|
||||
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||
getDataStorage().saveAndJoin();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Set;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
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.ServerPlayer;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** Shared, server-authoritative landing for the 26.3 book and operator probe. */
|
||||
public final class PortAgeTravel {
|
||||
private PortAgeTravel() {
|
||||
}
|
||||
|
||||
public static String enter(ServerPlayer player, Identifier id) {
|
||||
ServerLevel target = player.level().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (target == null) return "Age not loaded: " + id;
|
||||
if (!AgeCatalogSync.ready(player, id)) {
|
||||
return "Age catalog not acknowledged by client; remaining in source dimension";
|
||||
}
|
||||
boolean fissure = target.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
|
||||
&& features.terrainFeatures() != null && features.terrainFeatures().starFissures();
|
||||
BlockPos landing = fissure ? PortStarFissureFeature.savedSpawn(target) : null;
|
||||
if (landing != null) landing = isCaveTerrain(target)
|
||||
? findFootedDestination(target, landing).orElse(null)
|
||||
: findSurfaceLanding(target, landing.getX(), landing.getZ());
|
||||
if (landing == null) landing = findLanding(target);
|
||||
if (landing == null) return "No safe test landing available; remaining in source dimension";
|
||||
if (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) {
|
||||
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));
|
||||
}
|
||||
BlockPos surface = findSurfaceLanding(target, 0, 0);
|
||||
if (surface != null) return surface;
|
||||
// Only build above the actual highest obstruction, never in a pocket
|
||||
// beneath it. Void Ages retain the usual platform floor at Y=63.
|
||||
BlockPos obstruction = highestObstruction(target, 0, 0);
|
||||
int y = obstruction == null ? 64 : Math.max(64, obstruction.getY() + 3);
|
||||
return makePlatform(target, new BlockPos(0, y, 0));
|
||||
}
|
||||
|
||||
static BlockPos findSurfaceLanding(ServerLevel target, int originX, int originZ) {
|
||||
for (int radius = 0; radius <= 16; radius++) {
|
||||
for (int x = -radius; x <= radius; x++) {
|
||||
for (int z = -radius; z <= radius; z++) {
|
||||
if (Math.max(Math.abs(x), Math.abs(z)) != radius) continue;
|
||||
BlockPos floor = highestObstruction(target, originX + x, originZ + z);
|
||||
if (floor != null && hasFooting(target, floor.above())) return floor.above();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** First obstructing surface seen from the sky; never continue through a roof or liquid. */
|
||||
static BlockPos highestObstruction(ServerLevel level, int x, int z) {
|
||||
var chunk = level.getChunk(x >> 4, z >> 4);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, level.getMaxY() - 1, z);
|
||||
for (int y = level.getMaxY() - 1; y >= level.getMinY(); y--) {
|
||||
pos.setY(y);
|
||||
var state = chunk.getBlockState(pos);
|
||||
if (!state.isAir() && (!state.getFluidState().isEmpty()
|
||||
|| state.is(net.minecraft.tags.BlockTags.LEAVES)
|
||||
|| !state.getCollisionShape(level, pos).isEmpty())) return pos.immutable();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean isCaveTerrain(ServerLevel target) {
|
||||
var generator = target.getChunkSource().getGenerator();
|
||||
if (generator instanceof PortFeatureChunkGenerator features) generator = features.base();
|
||||
return generator instanceof net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator noise
|
||||
&& noise.generatorSettings().value().noiseSettings().equals(target.registryAccess()
|
||||
.lookupOrThrow(Registries.NOISE_SETTINGS)
|
||||
.getOrThrow(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES)
|
||||
.value().noiseSettings());
|
||||
}
|
||||
|
||||
private static BlockPos makePlatform(ServerLevel target, BlockPos foot) {
|
||||
if (foot.getY() <= target.getMinY() || foot.getY() + 2 >= target.getMaxY()) return null;
|
||||
// A carved pocket inside a flooded cave immediately refills. Refuse
|
||||
// that landing instead of placing the player in submerged spawn space.
|
||||
if (!target.getFluidState(foot).isEmpty() || !target.getFluidState(foot.above()).isEmpty()) return null;
|
||||
for (Direction direction : Direction.Plane.HORIZONTAL) {
|
||||
if (!target.getFluidState(foot.relative(direction)).isEmpty()
|
||||
|| !target.getFluidState(foot.above().relative(direction)).isEmpty()) return null;
|
||||
}
|
||||
for (int x = -1; x <= 1; x++) {
|
||||
for (int z = -1; z <= 1; z++) {
|
||||
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) {
|
||||
if (foot.getY() <= target.getMinY() || foot.getY() + 1 >= target.getMaxY()
|
||||
|| !target.getWorldBorder().isWithinBounds(foot)) return false;
|
||||
BlockPos floor = foot.below();
|
||||
var floorState = target.getBlockState(floor);
|
||||
return (floorState.isFaceSturdy(target, floor, Direction.UP)
|
||||
|| floorState.is(net.minecraft.tags.BlockTags.LEAVES))
|
||||
&& floorState.getFluidState().isEmpty()
|
||||
&& target.getFluidState(foot).isEmpty()
|
||||
&& target.getFluidState(foot.above()).isEmpty()
|
||||
&& passable(target, foot) && passable(target, foot.above());
|
||||
}
|
||||
|
||||
private static boolean passable(ServerLevel level, BlockPos pos) {
|
||||
var state = level.getBlockState(pos);
|
||||
return state.getCollisionShape(level, pos).isEmpty() || state.is(net.minecraft.tags.BlockTags.LEAVES);
|
||||
}
|
||||
|
||||
/** Same local-then-surface safe-return search used by 1.21.1 fissures. */
|
||||
public static java.util.Optional<BlockPos> findFootedDestination(ServerLevel level, BlockPos origin) {
|
||||
var local = BlockPos.betweenClosedStream(origin.offset(-4, -2, -4), origin.offset(4, 2, 4))
|
||||
.map(BlockPos::immutable).sorted(java.util.Comparator.comparingDouble(pos -> pos.distSqr(origin)))
|
||||
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||
if (local.isPresent()) return local;
|
||||
for (int radius = 1; radius <= 64; radius++) {
|
||||
final int ring = radius;
|
||||
var found = BlockPos.betweenClosedStream(origin.offset(-ring, 0, -ring), origin.offset(ring, 0, ring))
|
||||
.filter(pos -> Math.max(Math.abs(pos.getX() - origin.getX()), Math.abs(pos.getZ() - origin.getZ())) == ring)
|
||||
.map(BlockPos::immutable)
|
||||
.flatMap(pos -> {
|
||||
BlockPos surface = new BlockPos(pos.getX(),
|
||||
level.getHeight(Heightmap.Types.MOTION_BLOCKING, pos.getX(), pos.getZ()), pos.getZ());
|
||||
return java.util.stream.Stream.of(surface, surface.above(), surface.below(),
|
||||
surface.above(2), surface.below(2));
|
||||
})
|
||||
.filter(pos -> level.isInWorldBounds(pos) && level.getWorldBorder().isWithinBounds(pos))
|
||||
.filter(pos -> hasFooting(level, pos)).findFirst();
|
||||
if (found.isPresent()) return found;
|
||||
}
|
||||
return java.util.Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
import net.minecraft.world.level.saveddata.WeatherData;
|
||||
import net.neoforged.neoforge.event.tick.LevelTickEvent;
|
||||
|
||||
/** 1.21.1 weather policies, applied before the independent native Age cycle advances. */
|
||||
public final class PortAgeWeather {
|
||||
private static final Map<PortAgeLevel, Integer> STATES = new WeakHashMap<>();
|
||||
|
||||
private PortAgeWeather() {}
|
||||
|
||||
public static void onLevelTickPre(LevelTickEvent.Pre event) {
|
||||
if (event.getLevel() instanceof PortAgeLevel level) apply(level);
|
||||
}
|
||||
|
||||
static void apply(PortAgeLevel level) {
|
||||
PortWeatherMode mode = level.weatherMode();
|
||||
if (mode == PortWeatherMode.NORMAL) {
|
||||
STATES.remove(level);
|
||||
return;
|
||||
}
|
||||
WeatherData weather = level.getWeatherData();
|
||||
if (mode == PortWeatherMode.FAST || mode == PortWeatherMode.SLOW) {
|
||||
int state = (weather.isRaining() ? 1 : 0) | (weather.isThundering() ? 2 : 0);
|
||||
Integer previous = STATES.put(level, state);
|
||||
if (previous != null && previous == state) return;
|
||||
// Keep persisted timers on reload; rescale only when a cycle changes.
|
||||
if (previous == null && weather.getRainTime() > 0) return;
|
||||
int scale = mode == PortWeatherMode.FAST ? 1 : 4;
|
||||
int duration = 6000 * scale + level.getRandom().nextInt(
|
||||
(weather.isRaining() ? 6000 : 84000) * scale);
|
||||
set(weather, duration, weather.isRaining(), weather.isThundering());
|
||||
} else {
|
||||
STATES.remove(level);
|
||||
boolean rain = mode != PortWeatherMode.OFF;
|
||||
boolean thunder = mode == PortWeatherMode.STORM;
|
||||
if (weather.isRaining() != rain || weather.isThundering() != thunder
|
||||
|| weather.getRainTime() < 200 || weather.getThunderTime() < 200
|
||||
|| weather.getClearWeatherTime() != 0) set(weather, 6000, rain, thunder);
|
||||
}
|
||||
}
|
||||
|
||||
private static void set(WeatherData data, int duration, boolean rain, boolean thunder) {
|
||||
data.setClearWeatherTime(0);
|
||||
data.setRainTime(duration);
|
||||
data.setThunderTime(duration);
|
||||
data.setRaining(rain);
|
||||
data.setThundering(thunder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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 java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.QuartPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.CarvingMask;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.WorldGenerationContext;
|
||||
import net.minecraft.world.level.levelgen.WorldgenRandom;
|
||||
import net.minecraft.world.level.levelgen.carver.WorldCarver;
|
||||
|
||||
/** Authored Caves/Ravines, independent of the terrain page, with 1.21.1 source seeds. */
|
||||
public final class PortCarverPages {
|
||||
public static final Identifier CAVES = Identifier.fromNamespaceAndPath("rivencraft", "feature/caves");
|
||||
public static final Identifier RAVINES = Identifier.fromNamespaceAndPath("rivencraft", "feature/ravines");
|
||||
private static final long CAVE_SALT = 0x4341564553464541L;
|
||||
private static final long RAVINE_SALT = 0x524156494E45534CL;
|
||||
|
||||
private PortCarverPages() {}
|
||||
|
||||
public static boolean supports(Identifier page) { return CAVES.equals(page) || RAVINES.equals(page); }
|
||||
|
||||
public static void validateCounts(List<Identifier> pages) {
|
||||
Map<Identifier, Integer> counts = new HashMap<>();
|
||||
for (Identifier page : pages) if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32)
|
||||
throw new IllegalArgumentException("Carver plan requires Caves/Ravines pages, at most 32 per type");
|
||||
}
|
||||
|
||||
public static void validateRegistry(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
if (pages.stream().noneMatch(PortCarverPages::supports)) return;
|
||||
var carvers = registries.lookupOrThrow(Registries.CARVER);
|
||||
for (String name : pages.contains(CAVES) ? List.of("cave", "nether_cave", "canyon") : List.of("canyon")) {
|
||||
if (name.equals("canyon") && !pages.contains(RAVINES)) continue;
|
||||
carvers.get(ResourceKey.create(Registries.CARVER, Identifier.withDefaultNamespace(name)))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing authored carver minecraft:" + name));
|
||||
}
|
||||
}
|
||||
|
||||
public static void carve(ChunkGenerator generator, ChunkAccess chunk, RandomState randomState,
|
||||
HolderLookup.Provider registries, List<Identifier> pages, long seed, CarvingMask mask) {
|
||||
if (pages.isEmpty()) return;
|
||||
var registry = registries.lookupOrThrow(Registries.CARVER);
|
||||
var biomes = generator.getBiomeSource().createUncachedResolver(randomState);
|
||||
var context = new WorldGenerationContext(generator, chunk.getHeightAccessorForGeneration());
|
||||
var random = new WorldgenRandom(new LegacyRandomSource(0L));
|
||||
ChunkPos target = chunk.getPos();
|
||||
// 1.21.1 repeats identical seeds for duplicate Caves/Ravines pages.
|
||||
// Their union is identical; avoid replaying the same mask up to 32 times.
|
||||
for (Identifier page : pages.stream().distinct().toList()) {
|
||||
boolean ravine = page.equals(RAVINES);
|
||||
WorldCarver overworld = registry.getOrThrow(ResourceKey.create(Registries.CARVER,
|
||||
Identifier.withDefaultNamespace(ravine ? "canyon" : "cave"))).value();
|
||||
WorldCarver nether = ravine ? null : registry.getOrThrow(ResourceKey.create(Registries.CARVER,
|
||||
Identifier.withDefaultNamespace("nether_cave"))).value();
|
||||
for (int dx = -8; dx <= 8; dx++) {
|
||||
for (int dz = -8; dz <= 8; dz++) {
|
||||
ChunkPos source = new ChunkPos(target.x() + dx, target.z() + dz);
|
||||
boolean isNether = biomes.getNoiseBiome(QuartPos.fromBlock(source.getMinBlockX()), 0,
|
||||
QuartPos.fromBlock(source.getMinBlockZ())).is(BiomeTags.IS_NETHER);
|
||||
if (ravine && isNether) continue;
|
||||
WorldCarver carver = isNether ? nether : overworld;
|
||||
random.setLargeFeatureSeed(seed ^ (ravine ? RAVINE_SALT : CAVE_SALT), source.x(), source.z());
|
||||
if (carver.isStartChunk(random)) carver.carve(context, random, target, source, mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** 26.3 port of the 1.21.1 surface crystal mound with budding hearts and seeded buds. */
|
||||
final class PortCrystalFormationsFeature {
|
||||
private static final long SALT = 0x4352595354414C53L;
|
||||
private static final int CHANCE = 15;
|
||||
|
||||
private PortCrystalFormationsFeature() {}
|
||||
|
||||
record Site(int x, int z, long seed) {}
|
||||
|
||||
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||
long seed = mix(ageSeed ^ SALT ^ (long) chunk.x() * 341873128712L
|
||||
^ (long) chunk.z() * 132897987541L ^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||
RandomSource random = RandomSource.create(seed);
|
||||
if (random.nextInt(CHANCE) != 0) return null;
|
||||
return new Site(chunk.getMinBlockX() + 5 + random.nextInt(6),
|
||||
chunk.getMinBlockZ() + 5 + random.nextInt(6), seed);
|
||||
}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, int occurrence) {
|
||||
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||
if (site == null) return;
|
||||
RandomSource random = RandomSource.create(site.seed() ^ 0x42554444494E474CL);
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, site.x(), site.z());
|
||||
if (y <= level.getMinY() + 1 || y + 4 >= level.getMaxY()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
int x = site.x() + dx;
|
||||
int z = site.z() + dz;
|
||||
if (Math.abs(level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z) - y) > 1) return;
|
||||
BlockState floor = level.getBlockState(pos.set(x, y - 1, z));
|
||||
if (!floor.isFaceSturdy(level, pos, Direction.UP) || !floor.getFluidState().isEmpty()
|
||||
|| floor.hasBlockEntity()) return;
|
||||
for (int h = 0; h < 4; h++) {
|
||||
BlockState existing = level.getBlockState(pos.set(x, y + h, z));
|
||||
if (!existing.isAir() && !existing.canBeReplaced()) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = -1; dx <= 1; dx++) {
|
||||
for (int dz = -1; dz <= 1; dz++) {
|
||||
boolean center = dx == 0 && dz == 0;
|
||||
int height = center ? 3 : random.nextInt(3) == 0 ? 2 : 1;
|
||||
for (int h = 0; h < height; h++) {
|
||||
boolean budding = h == height - 1 && (center || random.nextBoolean());
|
||||
level.setBlock(pos.set(site.x() + dx, y + h, site.z() + dz),
|
||||
budding ? Rivencraft.BUDDING_CRYSTAL.get().defaultBlockState()
|
||||
: Rivencraft.CRYSTAL_BLOCK.get().defaultBlockState(), 2);
|
||||
}
|
||||
if (level.getBlockState(pos.set(site.x() + dx, y + height - 1, site.z() + dz))
|
||||
.is(Rivencraft.BUDDING_CRYSTAL) && random.nextBoolean()) {
|
||||
level.setBlock(pos.set(site.x() + dx, y + height, site.z() + dz),
|
||||
Rivencraft.SMALL_CRYSTAL_BUD.get().defaultBlockState()
|
||||
.setValue(AmethystClusterBlock.FACING, Direction.UP), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.clock.ClockInstance;
|
||||
import net.minecraft.world.clock.ClockManager;
|
||||
import net.minecraft.world.clock.WorldClock;
|
||||
import net.minecraft.world.clock.WorldClocks;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Authored native celestial controls. Never mutates the shared clock. */
|
||||
public record PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize) {
|
||||
public static final PortDayCycle NATIVE = new PortDayCycle(false, 1, 0);
|
||||
public static final Identifier NORMAL_TIME = Identifier.fromNamespaceAndPath("rivencraft", "celestial/normal_time");
|
||||
public static final Codec<PortDayCycle> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.BOOL.fieldOf("authored").forGetter(PortDayCycle::authored),
|
||||
Codec.doubleRange(0, 2).fieldOf("period").forGetter(PortDayCycle::period),
|
||||
Codec.intRange(0, 23999).fieldOf("stopped_time").forGetter(PortDayCycle::stoppedTime),
|
||||
Codec.LONG.optionalFieldOf("origin_tick", 0L).forGetter(PortDayCycle::originTick),
|
||||
Codec.intRange(0, 23999).optionalFieldOf("phase_tick", 0).forGetter(PortDayCycle::phaseTick),
|
||||
Codec.BOOL.optionalFieldOf("clockwise", true).forGetter(PortDayCycle::clockwise),
|
||||
Codec.intRange(-1, 359).optionalFieldOf("bearing", -1).forGetter(PortDayCycle::bearing),
|
||||
Codec.floatRange(0.25F, 3F).optionalFieldOf("sun_size", 1F).forGetter(PortDayCycle::sunSize)
|
||||
).apply(instance, PortDayCycle::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortDayCycle> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.BOOL, PortDayCycle::authored, ByteBufCodecs.DOUBLE, PortDayCycle::period,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::stoppedTime, ByteBufCodecs.VAR_LONG, PortDayCycle::originTick,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::phaseTick, ByteBufCodecs.BOOL, PortDayCycle::clockwise,
|
||||
ByteBufCodecs.INT, PortDayCycle::bearing, ByteBufCodecs.FLOAT, PortDayCycle::sunSize, PortDayCycle::new);
|
||||
private static volatile Map<Identifier, PortDayCycle> clientCycles = Map.of();
|
||||
|
||||
public PortDayCycle {
|
||||
if (!Double.isFinite(period) || period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|
||||
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing >= 360
|
||||
|| !Float.isFinite(sunSize) || sunSize < 0.25F || sunSize > 3F)
|
||||
throw new IllegalArgumentException("Invalid day cycle");
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime) {
|
||||
this(authored, period, stoppedTime, 0, 0, true);
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, -1);
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, 1F);
|
||||
}
|
||||
|
||||
public static boolean isPeriod(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "period/full", "period/half", "period/double", "period/zero" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isPhase(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "phase/rising", "phase/zenith", "phase/setting", "phase/nadir" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isDirection(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && (page.getPath().equals("direction/clockwise")
|
||||
|| page.getPath().equals("direction/counterclockwise"));
|
||||
}
|
||||
|
||||
public static boolean isAngle(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "angle/north", "angle/east", "angle/south", "angle/west" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean supports(Identifier page) {
|
||||
return page.equals(NORMAL_TIME) || isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page);
|
||||
}
|
||||
|
||||
public static boolean isSize(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "size/small", "size/normal", "size/large" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean validWriting(List<Identifier> pages) {
|
||||
int target = pages.indexOf(NORMAL_TIME), periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0;
|
||||
if (target != pages.lastIndexOf(NORMAL_TIME)) return false;
|
||||
for (int i = 0; i < pages.size(); i++) {
|
||||
if (isPeriod(pages.get(i)) && (++periods > 1 || i > target)) return false;
|
||||
if (isPhase(pages.get(i)) && (++phases > 1 || i > target)) return false;
|
||||
if (isDirection(pages.get(i)) && (++directions > 1 || i > target)) return false;
|
||||
if (isAngle(pages.get(i)) && (++angles > 1 || i > target)) return false;
|
||||
if (isSize(pages.get(i)) && (++sizes > 1 || i > target)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static PortDayCycle compile(List<Identifier> pages, long seed) {
|
||||
return compile(pages, seed, 0);
|
||||
}
|
||||
|
||||
public static PortDayCycle compile(List<Identifier> pages, long seed, long nativeTicks) {
|
||||
if (!validWriting(pages)) throw new IllegalArgumentException("Place at most one Length, Phase, Direction, Angle, and Size before Normal Time");
|
||||
if (!pages.contains(NORMAL_TIME)) return NATIVE;
|
||||
double period = 1;
|
||||
Integer phase = null;
|
||||
boolean clockwise = true;
|
||||
int bearing = -1;
|
||||
float size = 1F;
|
||||
for (int i = 0; i < pages.size(); i++) {
|
||||
Identifier page = pages.get(i);
|
||||
int number = i > 0 ? PortAgeColors.number(pages.get(i - 1)) : -1;
|
||||
if (isPhase(page)) {
|
||||
int degrees = number >= 0 ? number * 15 % 360 : switch (page.getPath()) {
|
||||
case "phase/rising" -> 90;
|
||||
case "phase/zenith" -> 180;
|
||||
case "phase/setting" -> 270;
|
||||
default -> 0;
|
||||
};
|
||||
// Same conversion as 1.21.1 AgeDimensionService.applyInitialCelestialPhase:
|
||||
// the written angle starts at Nadir, while native time zero is sunrise.
|
||||
phase = (degrees * 24000 / 360 + 18000) % 24000;
|
||||
} else if (isAngle(page)) {
|
||||
bearing = number >= 0 ? number * 15 % 360 : switch (page.getPath()) {
|
||||
case "angle/east" -> 90;
|
||||
case "angle/south" -> 180;
|
||||
case "angle/west" -> 270;
|
||||
default -> 0;
|
||||
};
|
||||
} else if (isSize(page)) {
|
||||
size = number >= 0 ? (float) (0.25D + number * (2.75D / 24D)) : switch (page.getPath()) {
|
||||
case "size/small" -> 0.5F;
|
||||
case "size/large" -> 2F;
|
||||
default -> 1F;
|
||||
};
|
||||
} else if (isDirection(page)) {
|
||||
clockwise = page.getPath().equals("direction/clockwise");
|
||||
} else if (isPeriod(page)) period = number >= 0 ? number * 0.08 : switch (page.getPath()) {
|
||||
case "period/half" -> 0.5;
|
||||
case "period/double" -> 2;
|
||||
case "period/zero" -> 0;
|
||||
default -> 1;
|
||||
};
|
||||
}
|
||||
int stopped = period == 0 ? phase != null ? phase
|
||||
: new Random(seed ^ 0x4441594359434C45L).nextInt(24000) : 0;
|
||||
return new PortDayCycle(true, period, stopped, phase != null ? nativeTicks : 0,
|
||||
phase == null ? 0 : phase, clockwise, bearing, size);
|
||||
}
|
||||
|
||||
public long ticks(long nativeTicks) {
|
||||
if (!authored) return nativeTicks;
|
||||
return (long) Math.floor(time(nativeTicks));
|
||||
}
|
||||
|
||||
private double time(long nativeTicks) {
|
||||
return !authored ? nativeTicks : period == 0 ? stoppedTime
|
||||
: (clockwise ? 1 : -1) * ((double) nativeTicks - originTick) / period + phaseTick;
|
||||
}
|
||||
|
||||
public static long nativeTicks(Level level) {
|
||||
return level.clockManager().getInstance(level.registryAccess().lookupOrThrow(
|
||||
net.minecraft.core.registries.Registries.WORLD_CLOCK).getOrThrow(WorldClocks.OVERWORLD)).totalTicks();
|
||||
}
|
||||
|
||||
public static void updateClient(Map<Identifier, PortDayCycle> cycles) { clientCycles = Map.copyOf(cycles); }
|
||||
public static PortDayCycle forLevel(Level level) {
|
||||
if (level instanceof PortAgeLevel age) return age.dayCycle();
|
||||
return level.isClientSide() ? clientCycles.getOrDefault(level.dimension().identifier(), NATIVE) : NATIVE;
|
||||
}
|
||||
|
||||
/** Timelines keep this manager; profiles are read lazily after level construction/catalog sync. */
|
||||
public static ClockManager timelines(Level level, ClockManager nativeManager) {
|
||||
Map<Holder<WorldClock>, ClockInstance> wrappers = new HashMap<>();
|
||||
return definition -> {
|
||||
if (!definition.is(WorldClocks.OVERWORLD)) return nativeManager.getInstance(definition);
|
||||
return wrappers.computeIfAbsent(definition, ignored -> new ClockInstance() {
|
||||
private ClockInstance source() { return nativeManager.getInstance(definition); }
|
||||
public long totalTicks() { return forLevel(level).ticks(source().totalTicks()); }
|
||||
public float partialTick() {
|
||||
PortDayCycle cycle = forLevel(level);
|
||||
if (!cycle.authored) return source().partialTick();
|
||||
if (cycle.period == 0) return 0;
|
||||
double time = cycle.time(source().totalTicks());
|
||||
return (float) (time - Math.floor(time)
|
||||
+ (cycle.clockwise ? 1 : -1) * source().partialTick() / cycle.period);
|
||||
}
|
||||
public float rate() {
|
||||
PortDayCycle cycle = forLevel(level);
|
||||
return !cycle.authored ? source().rate() : cycle.period == 0 ? 0
|
||||
: (float) ((cycle.clockwise ? 1 : -1) * source().rate() / cycle.period);
|
||||
}
|
||||
public boolean isPaused() { return source().isPaused() || forLevel(level).authored && forLevel(level).period == 0; }
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,28 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Seed-completes missing environment controllers once, before an Age is saved. */
|
||||
public record PortEnvironmentSelection(PortWeatherMode weather, PortLighting lighting, PortSkyStyle sky) {
|
||||
public static PortEnvironmentSelection resolve(List<Identifier> pages, long seed, PortAgeTerrain terrain) {
|
||||
PortLighting lighting = pages.stream().map(PortLighting::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x4C49474854494E47L), PortLighting.values()));
|
||||
PortWeatherMode weather = terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF
|
||||
: pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x574541544845525FL), PortWeatherMode.values()));
|
||||
return new PortEnvironmentSelection(weather, lighting, PortSkyStyle.resolve(pages, seed));
|
||||
}
|
||||
|
||||
private static <T> T rankedDefault(Random random, T[] choices) {
|
||||
// LegacyRankChoice: the first (Normal) option is rank 1, the rest rank 2.
|
||||
// Its rank-1 weight is the complete rare group plus one. Derive this
|
||||
// from the supported pool, rather than selecting unported weather.
|
||||
int[] weights = new int[choices.length];
|
||||
Arrays.fill(weights, 1);
|
||||
weights[0] = choices.length;
|
||||
return PortRandomAgeSelection.weighted(random, List.of(choices), weights);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.tags.BiomeTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.NoiseColumn;
|
||||
import net.minecraft.world.level.StructureManager;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.densityfunction.SamplerContext;
|
||||
import net.minecraft.world.level.levelgen.blockpredicates.BlockPredicate;
|
||||
import net.minecraft.world.level.levelgen.feature.BlockBlobFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.LakeFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.stateproviders.BlockStateProvider;
|
||||
import net.minecraft.world.level.levelgen.placement.BiomeFilter;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.minecraft.world.level.levelgen.placement.RarityFilter;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
|
||||
/** Delegates terrain generation and applies frozen Age-scoped features. */
|
||||
public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
||||
private static final ResourceKey<PlacedFeature> FOREST_ROCK = ResourceKey.create(
|
||||
Registries.PLACED_FEATURE, Identifier.withDefaultNamespace("forest_rock"));
|
||||
private static final ResourceKey<PlacedFeature> DUNGEONS = ResourceKey.create(
|
||||
Registries.PLACED_FEATURE, Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "dungeons"));
|
||||
private static final TagKey<Block> ROCK_GROUND = TagKey.create(
|
||||
Registries.BLOCK, Identifier.withDefaultNamespace("forest_rock_can_place_on"));
|
||||
public static final MapCodec<PortFeatureChunkGenerator> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
ChunkGenerator.CODEC.fieldOf("base").forGetter(generator -> generator.base),
|
||||
PortRockPlan.CODEC.optionalFieldOf("rocks").forGetter(generator -> Optional.ofNullable(generator.rocks)),
|
||||
PortVegetationPlan.CODEC.optionalFieldOf("vegetation")
|
||||
.forGetter(generator -> Optional.ofNullable(generator.vegetation)),
|
||||
com.mojang.serialization.Codec.intRange(0, 32).optionalFieldOf("dungeons", 0)
|
||||
.forGetter(generator -> generator.dungeons),
|
||||
PortLakePlan.CODEC.optionalFieldOf("lakes")
|
||||
.forGetter(generator -> Optional.ofNullable(generator.lakes)),
|
||||
PortTerrainFeaturePlan.CODEC.optionalFieldOf("terrain_features")
|
||||
.forGetter(generator -> Optional.ofNullable(generator.terrainFeatures)),
|
||||
com.mojang.serialization.Codec.LONG.fieldOf("age_seed").forGetter(generator -> generator.ageSeed)
|
||||
).apply(instance, (base, rocks, vegetation, dungeons, lakes, terrainFeatures, seed) ->
|
||||
new PortFeatureChunkGenerator(base, rocks.orElse(null), vegetation.orElse(null),
|
||||
dungeons, lakes.orElse(null), terrainFeatures.orElse(null), seed)));
|
||||
|
||||
private final ChunkGenerator base;
|
||||
private final PortRockPlan rocks;
|
||||
private final PortVegetationPlan vegetation;
|
||||
private final int dungeons;
|
||||
private final PortLakePlan lakes;
|
||||
private final PortTerrainFeaturePlan terrainFeatures;
|
||||
private final long ageSeed;
|
||||
|
||||
public PortFeatureChunkGenerator(ChunkGenerator base, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, long ageSeed) {
|
||||
super(base.getBiomeSource(), base::getBiomeGenerationSettings);
|
||||
this.base = base;
|
||||
this.rocks = rocks;
|
||||
this.vegetation = vegetation;
|
||||
this.dungeons = dungeons;
|
||||
this.lakes = lakes;
|
||||
this.terrainFeatures = terrainFeatures;
|
||||
this.ageSeed = ageSeed;
|
||||
}
|
||||
|
||||
public ChunkGenerator base() { return base; }
|
||||
public PortRockPlan rocks() { return rocks; }
|
||||
public PortVegetationPlan vegetation() { return vegetation; }
|
||||
public int dungeons() { return dungeons; }
|
||||
public PortLakePlan lakes() { return lakes; }
|
||||
public PortTerrainFeaturePlan terrainFeatures() { return terrainFeatures; }
|
||||
|
||||
@Override protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
|
||||
@Override public void validate() { base.validate(); }
|
||||
@Override public ChunkPos getOrigin(RandomState random) { return base.getOrigin(random); }
|
||||
@Override public java.util.Optional<Identifier> getTypeNameForDataFixer() { return base.getTypeNameForDataFixer(); }
|
||||
@Override public CompletableFuture<ChunkAccess> createBiomes(RandomState random, Blender blender,
|
||||
StructureManager structures, ChunkAccess chunk) {
|
||||
return base.createBiomes(random, blender, structures, chunk);
|
||||
}
|
||||
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
||||
RandomState random, long seed) {
|
||||
if (hasStructurePages()) {
|
||||
var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource());
|
||||
var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures());
|
||||
if (base instanceof PortFlatChunkGenerator || base instanceof net.minecraft.world.level.levelgen.FlatLevelSource)
|
||||
return ChunkGeneratorStructureState.createForFlat(random, seed, getOrigin(random), planning,
|
||||
lookup.listElements().map(holder -> (Holder<StructureSet>) holder));
|
||||
return ChunkGeneratorStructureState.createForNormal(random, seed, getOrigin(random), planning, lookup);
|
||||
}
|
||||
return base.createState(sets, random, seed);
|
||||
}
|
||||
|
||||
private boolean hasStructurePages() {
|
||||
return terrainFeatures != null && !terrainFeatures.structures().isEmpty();
|
||||
}
|
||||
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||
StructureManager structures, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
if (hasStructurePages()) super.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
else base.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
||||
base.createReferences(level, structures, chunk);
|
||||
}
|
||||
@Override public Pair<BlockPos, Holder<Structure>> findNearestMapStructure(ServerLevel level,
|
||||
HolderSet<Structure> targets, BlockPos origin, int radius, boolean skipKnown) {
|
||||
return base.findNearestMapStructure(level, targets, origin, radius, skipKnown);
|
||||
}
|
||||
@Override public void spawnOriginalMobs(WorldGenRegion level) { base.spawnOriginalMobs(level); }
|
||||
@Override public int getSpawnHeight(LevelHeightAccessor height) { return base.getSpawnHeight(height); }
|
||||
@Override public int getGenDepth() { return base.getGenDepth(); }
|
||||
@Override public int getSeaLevel() { return base.getSeaLevel(); }
|
||||
@Override public int getMinY() { return base.getMinY(); }
|
||||
@Override public CompletableFuture<ChunkAccess> buildTerrain(ChunkAccess chunk, Blender blender,
|
||||
RandomState random, StructureManager structures, BiomeManager biomes,
|
||||
WorldGenRegion carverBiomeRegion, Set<Holder<Biome>> possibleBiomes) {
|
||||
CompletableFuture<ChunkAccess> built = base.buildTerrain(
|
||||
chunk, blender, random, structures, biomes, carverBiomeRegion, possibleBiomes);
|
||||
if (terrainFeatures == null || !terrainFeatures.skylands() && terrainFeatures.choices().isEmpty()) return built;
|
||||
return built.thenApply(result -> {
|
||||
// Match 1.21.1: filter the base terrain once, then add the authored
|
||||
// solid volumes, irrespective of the Skylands page's written position.
|
||||
if (terrainFeatures.skylands()) PortSkylandsFeature.filter(result, ageSeed);
|
||||
java.util.Map<Identifier, Integer> occurrences = new java.util.HashMap<>();
|
||||
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||
if (PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||
int occurrence = occurrences.merge(choice.page(), 1, Integer::sum) - 1;
|
||||
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
|
||||
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||
if (choice.page().equals(PortTerrainFeaturePlan.FLOATING_ISLANDS))
|
||||
PortFloatingIslandFeature.generate(result, featureSeed, material);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.TENDRILS))
|
||||
PortTendrilFeature.generate(result, featureSeed, material);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.SPHERES))
|
||||
PortSpheresFeature.generate(result, featureSeed, material);
|
||||
else throw new IllegalStateException("Unsupported terrain feature: " + choice.page());
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
@Override public int getBaseHeight(int x, int z, Heightmap.Types type,
|
||||
LevelHeightAccessor height, RandomState random) {
|
||||
return base.getBaseHeight(x, z, type, height, random);
|
||||
}
|
||||
@Override public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor height, RandomState random) {
|
||||
return base.getBaseColumn(x, z, height, random);
|
||||
}
|
||||
@Override public void addDebugScreenInfo(List<String> info, RandomState random,
|
||||
BlockPos pos, SamplerContext context) {
|
||||
base.addDebugScreenInfo(info, random, pos, context);
|
||||
}
|
||||
@Override public void refreshFeaturesPerStep() {
|
||||
base.refreshFeaturesPerStep();
|
||||
super.refreshFeaturesPerStep();
|
||||
}
|
||||
|
||||
@Override public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk,
|
||||
StructureManager structures) {
|
||||
base.applyBiomeDecoration(level, chunk, structures);
|
||||
if (terrainFeatures != null) {
|
||||
PortDenseOres.place(level, chunk, this, ageSeed, terrainFeatures.denseOres());
|
||||
int spikeOccurrence = 0;
|
||||
int obeliskOccurrence = 0;
|
||||
int crystalOccurrence = 0;
|
||||
for (PortTerrainFeaturePlan.Choice choice : terrainFeatures.choices()) {
|
||||
if (!PortTerrainFeaturePlan.isSurface(choice.page())) continue;
|
||||
BlockState material = BuiltInRegistries.BLOCK.getValue(choice.material()).defaultBlockState();
|
||||
if (choice.page().equals(PortTerrainFeaturePlan.SPIKES))
|
||||
PortSpikesFeature.generate(level, chunk, ageSeed, material, spikeOccurrence++);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.OBELISKS))
|
||||
PortObelisksFeature.generate(level, chunk, ageSeed, material, obeliskOccurrence++);
|
||||
else if (choice.page().equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS))
|
||||
PortCrystalFormationsFeature.generate(level, chunk, ageSeed, crystalOccurrence++);
|
||||
else throw new IllegalStateException("Unsupported surface feature: " + choice.page());
|
||||
}
|
||||
}
|
||||
ChunkPos chunkPos = chunk.getPos();
|
||||
long chunkSeed = ageSeed ^ (long) chunkPos.x() * 341873128712L
|
||||
^ (long) chunkPos.z() * 132897987541L;
|
||||
BlockPos origin = new BlockPos(chunkPos.getMinBlockX(), level.getMinY(), chunkPos.getMinBlockZ());
|
||||
if (rocks != null) {
|
||||
PlacedFeature vanilla = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.getOrThrow(FOREST_ROCK).value();
|
||||
var placement = vanilla.placement().stream()
|
||||
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList();
|
||||
for (int occurrence = 0; occurrence < rocks.materials().size(); occurrence++) {
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
BlockState material = BuiltInRegistries.BLOCK.getValue(
|
||||
rocks.materials().get(occurrence)).defaultBlockState();
|
||||
PlacedFeature authored = new PlacedFeature(Holder.direct(new BlockBlobFeature(
|
||||
material, BlockPredicate.matchesTag(ROCK_GROUND))), placement);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
if (dungeons > 0) {
|
||||
PlacedFeature authored = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.getOrThrow(DUNGEONS).value();
|
||||
for (int occurrence = 0; occurrence < dungeons; occurrence++) {
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ 0x44554E47454F4E53L ^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
if (lakes != null) {
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||
java.util.Map<PortLakePlan.Choice, Integer> occurrences = new java.util.HashMap<>();
|
||||
for (PortLakePlan.Choice choice : lakes.choices()) {
|
||||
PlacedFeature vanilla = registry.getOrThrow(ResourceKey.create(
|
||||
Registries.PLACED_FEATURE, PortLakePlan.placedFeature(choice.page()))).value();
|
||||
if (!(vanilla.feature().value() instanceof LakeFeature lake))
|
||||
throw new IllegalStateException("Lake placement no longer references a LakeFeature: " + choice.page());
|
||||
BlockState fluid = BuiltInRegistries.BLOCK.getValue(choice.fluid()).defaultBlockState();
|
||||
LakeFeature feature = new LakeFeature(BlockStateProvider.holderOf(fluid), lake.barrier(),
|
||||
lake.canPlaceFeature(), lake.canReplaceWithAirOrFluid(), lake.canReplaceWithBarrier());
|
||||
PlacedFeature authored = new PlacedFeature(Holder.direct(feature), vanilla.placement().stream()
|
||||
.filter(modifier -> !(modifier instanceof BiomeFilter))
|
||||
.map(modifier -> !PortLakePlan.isDeep(choice.page()) && modifier instanceof RarityFilter
|
||||
? RarityFilter.onAverageOnceEvery(12) : modifier)
|
||||
.toList());
|
||||
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ (long) choice.hashCode() * 0x9E3779B97F4A7C15L
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
authored.place(level, this, random, origin);
|
||||
}
|
||||
}
|
||||
if (vegetation != null) {
|
||||
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
|
||||
Holder<Biome> biome = level.getBiome(new BlockPos(
|
||||
chunkPos.getMinBlockX() + 8, surface, chunkPos.getMinBlockZ() + 8));
|
||||
boolean surfaceBiome = !biome.is(BiomeTags.IS_END) && !PortRegistryPages.isCaveBiome(biome);
|
||||
var registry = level.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE);
|
||||
List<BlockPos> mushroomHosts = null;
|
||||
for (int occurrence = 0; occurrence < vegetation.pages().size(); occurrence++) {
|
||||
Identifier page = vegetation.pages().get(occurrence);
|
||||
RandomSource random = RandomSource.create(chunkSeed
|
||||
^ (long) page.hashCode() * 0x9E3779B97F4A7C15L
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
if (page.equals(PortVegetationPlan.SHELF_MUSHROOMS)) {
|
||||
if (mushroomHosts == null) mushroomHosts = PortShelfMushrooms.findHosts(chunk);
|
||||
PortShelfMushrooms.decorate(level, chunk, mushroomHosts, random);
|
||||
continue;
|
||||
}
|
||||
if (!surfaceBiome) continue;
|
||||
Identifier placedId = PortVegetationPlan.placedFeature(page);
|
||||
PlacedFeature vanilla = registry.getOrThrow(
|
||||
ResourceKey.create(Registries.PLACED_FEATURE, placedId)).value();
|
||||
PlacedFeature authored = new PlacedFeature(vanilla.feature(), vanilla.placement().stream()
|
||||
.filter(modifier -> !(modifier instanceof BiomeFilter)).toList());
|
||||
authored.place(level, this, random, origin);
|
||||
if (PortVegetationPlan.addsLogs(page)) mushroomHosts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,52 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.Locale;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.dimension.DimensionType;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Authored lighting choices using statically registered, client-synchronized types. */
|
||||
public enum PortLighting {
|
||||
NORMAL, DARK, BRIGHT;
|
||||
|
||||
public static final Codec<PortLighting> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(Locale.ROOT)), mode -> mode.name().toLowerCase(Locale.ROOT));
|
||||
|
||||
public static PortLighting forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "lighting/normal" -> NORMAL;
|
||||
case "lighting/dark" -> DARK;
|
||||
case "lighting/bright" -> BRIGHT;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isBright(Level level) {
|
||||
return level.dimensionTypeRegistration().unwrapKey().map(key -> {
|
||||
Identifier id = key.identifier();
|
||||
return id.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& (id.getPath().equals("age_bright") || id.getPath().equals("age_cave_bright")
|
||||
|| id.getPath().startsWith("age_bright_sky_")
|
||||
|| id.getPath().startsWith("age_cave_bright_sky_"));
|
||||
}).orElse(false);
|
||||
}
|
||||
|
||||
Holder<DimensionType> dimensionType(MinecraftServer server, PortAgeTerrain terrain,
|
||||
Holder<DimensionType> overworldType, PortSkyStyle sky) {
|
||||
boolean cave = terrain == PortAgeTerrain.CAVE;
|
||||
if (this == NORMAL && !cave && sky == PortSkyStyle.OVERWORLD) return overworldType;
|
||||
String variant = this == BRIGHT ? (cave ? "age_cave_bright" : "age_bright")
|
||||
: this == DARK || cave ? "age_dark" : "age_normal";
|
||||
if (sky != PortSkyStyle.OVERWORLD) variant += "_sky_" + sky.name().toLowerCase(Locale.ROOT);
|
||||
return server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(
|
||||
ResourceKey.create(Registries.DIMENSION_TYPE,
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, variant)));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
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,70 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.ShelfMushroomBlock;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.feature.treedecorators.ShelfMushroomDecorator;
|
||||
import net.minecraft.world.level.levelgen.feature.treedecorators.TreeDecorator;
|
||||
|
||||
/** Shelf mushrooms on exposed log faces, regardless of how the logs were generated. */
|
||||
final class PortShelfMushrooms {
|
||||
private PortShelfMushrooms() {}
|
||||
|
||||
static List<BlockPos> findHosts(ChunkAccess chunk) {
|
||||
List<BlockPos> logs = new ArrayList<>();
|
||||
var sections = chunk.getSections();
|
||||
for (int index = 0; index < sections.length; index++) {
|
||||
var section = sections[index];
|
||||
if (!section.maybeHas(state -> state.is(BlockTags.LOGS))) continue;
|
||||
int baseY = chunk.getSectionYFromSectionIndex(index) << 4;
|
||||
for (int y = 0; y < 16; y++) for (int z = 0; z < 16; z++) for (int x = 0; x < 16; x++) {
|
||||
if (section.getBlockState(x, y, z).is(BlockTags.LOGS))
|
||||
logs.add(new BlockPos(chunk.getPos().getMinBlockX() + x, baseY + y,
|
||||
chunk.getPos().getMinBlockZ() + z));
|
||||
}
|
||||
}
|
||||
return logs;
|
||||
}
|
||||
|
||||
static void decorate(WorldGenLevel level, ChunkAccess chunk, List<BlockPos> logs, RandomSource random) {
|
||||
// Reuse vanilla replaceability/water checks, but not its tree-base height
|
||||
// band: a log tendril or sphere has no meaningful tree base.
|
||||
TreeDecorator.Context context = new TreeDecorator.Context(level,
|
||||
(pos, state) -> level.setBlock(pos, state, 2), random, Set.of(), Set.of(), Set.of());
|
||||
for (BlockPos log : logs) {
|
||||
if (!level.getBlockState(log).is(BlockTags.LOGS)) continue;
|
||||
Direction first = Direction.Plane.HORIZONTAL.getRandomDirection(random);
|
||||
for (Direction facing : List.of(first, first.getClockWise())) {
|
||||
if (random.nextFloat() > 0.25F) continue;
|
||||
BlockPos pos = log.relative(facing);
|
||||
if ((pos.getX() >> 4) != chunk.getPos().x() || (pos.getZ() >> 4) != chunk.getPos().z()) continue;
|
||||
if (pos.getY() < level.getMinY() || pos.getY() >= level.getMaxY()) continue;
|
||||
if (!level.getFluidState(pos).isEmpty()
|
||||
|| !ShelfMushroomDecorator.isBlockReplaceableWithShelfMushroom(context, pos)
|
||||
|| level.getBlockState(pos.below()).is(Blocks.SHELF_MUSHROOM)) continue;
|
||||
boolean adjacent = false;
|
||||
for (Direction side : Direction.Plane.HORIZONTAL) {
|
||||
if (level.getBlockState(pos.relative(side)).is(Blocks.SHELF_MUSHROOM)) {
|
||||
adjacent = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (adjacent) continue;
|
||||
var mushroom = Blocks.SHELF_MUSHROOM.defaultBlockState()
|
||||
.setValue(ShelfMushroomBlock.FACING, facing)
|
||||
.setValue(ShelfMushroomBlock.AGE, random.nextInt(2));
|
||||
if (!mushroom.canSurvive(level, pos)) continue;
|
||||
context.setBlock(pos, mushroom);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Persisted sky styles, including the client-rendered Legacy static background. */
|
||||
public enum PortSkyStyle {
|
||||
OVERWORLD, NETHER, END, STATIC;
|
||||
|
||||
public static final Codec<PortSkyStyle> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(Locale.ROOT)), sky -> sky.name().toLowerCase(Locale.ROOT));
|
||||
|
||||
public static PortSkyStyle forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "sky/overworld" -> OVERWORLD;
|
||||
case "sky/nether" -> NETHER;
|
||||
case "sky/end" -> END;
|
||||
case "sky/static" -> STATIC;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static PortSkyStyle resolve(List<Identifier> pages, long seed) {
|
||||
return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElseGet(() -> pages.stream().anyMatch(PortSkyStyle::isCelestial)
|
||||
? OVERWORLD : random(seed));
|
||||
}
|
||||
|
||||
private static boolean isCelestial(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||
return switch (page.getPath()) {
|
||||
// Only Overworld currently draws bodies in this port. Recognize the
|
||||
// Sun/Moon intentions too, without marking those pages implemented.
|
||||
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static PortSkyStyle random(long seed) {
|
||||
// Match the first draw in 1.21.1 AgeSkyProfile.random: its equally
|
||||
// likely INHERITED and OVERWORLD slots currently share the native
|
||||
// Overworld presentation here. Do not add unported colors/bodies.
|
||||
int choice = new Random(seed ^ 0x534B5950524F464CL).nextInt(5);
|
||||
return switch (choice) {
|
||||
case 0, 1 -> OVERWORLD;
|
||||
case 2 -> NETHER;
|
||||
case 3 -> END;
|
||||
case 4 -> STATIC;
|
||||
default -> throw new IllegalStateException("Invalid random sky choice");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 port of Rivencraft's subtractive Skylands filter, adapted from
|
||||
* Mystcraft Legacy's SymbolSkylands (LGPL-3.0-or-later). Keeps upper solid
|
||||
* terrain above a seven-octave cutoff near Y=76 and removes liquids.
|
||||
*/
|
||||
final class PortSkylandsFeature {
|
||||
private static final long SALT = 0x534B594C414E4453L;
|
||||
private static final int BASE_CUTOFF = 76;
|
||||
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||
|
||||
private PortSkylandsFeature() {}
|
||||
|
||||
static void filter(ChunkAccess chunk, long ageSeed) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minY = chunk.getMinY();
|
||||
int maxY = chunk.getMaxY();
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
int cutoff = cutoff(ageSeed, originX + localX, originZ + localZ);
|
||||
for (int y = minY; y < maxY; y++) {
|
||||
pos.set(originX + localX, y, originZ + localZ);
|
||||
BlockState state = chunk.getBlockState(pos);
|
||||
if (!removes(state, y, cutoff)) continue;
|
||||
chunk.setBlockState(pos, AIR);
|
||||
surface.update(localX, y, localZ, AIR);
|
||||
floor.update(localX, y, localZ, AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean removes(BlockState state, int y, int cutoff) {
|
||||
return !state.isAir() && (y <= cutoff || state.liquid());
|
||||
}
|
||||
|
||||
static int cutoff(long ageSeed, int x, int z) {
|
||||
double variation = 0.0D;
|
||||
int cellSize = 128;
|
||||
double amplitude = 12.0D;
|
||||
for (int octave = 0; octave < 7; octave++) {
|
||||
variation += valueNoise(ageSeed ^ SALT ^ (long) octave * 0x9E3779B97F4A7C15L,
|
||||
x, z, cellSize) * amplitude;
|
||||
cellSize /= 2;
|
||||
amplitude /= 2.0D;
|
||||
}
|
||||
return BASE_CUTOFF + (int) Math.round(variation);
|
||||
}
|
||||
|
||||
private static double valueNoise(long seed, int x, int z, int cellSize) {
|
||||
int cellX = Math.floorDiv(x, cellSize);
|
||||
int cellZ = Math.floorDiv(z, cellSize);
|
||||
double u = smooth((x - cellX * cellSize) / (double) cellSize);
|
||||
double v = smooth((z - cellZ * cellSize) / (double) cellSize);
|
||||
double north = lerp(value(seed, cellX, cellZ), value(seed, cellX + 1, cellZ), u);
|
||||
double south = lerp(value(seed, cellX, cellZ + 1), value(seed, cellX + 1, cellZ + 1), u);
|
||||
return lerp(north, south, v);
|
||||
}
|
||||
|
||||
private static double value(long seed, int x, int z) {
|
||||
long hash = mix(seed ^ (long) x * 0xD1B54A32D192ED03L ^ (long) z * 0xABC98388FB8FAC03L);
|
||||
return (hash >>> 11) * 0x1.0p-52 - 1.0D;
|
||||
}
|
||||
|
||||
private static double smooth(double value) { return value * value * (3.0D - 2.0D * value); }
|
||||
|
||||
private static double lerp(double first, double second, double amount) {
|
||||
return first + (second - first) * amount;
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 adaptation of Rivencraft 1.21.1's solid sphere nodes, derived from
|
||||
* Mystcraft Legacy's MapGenSpheresMyst (Mystcraft contributors,
|
||||
* LGPL-3.0-or-later, commit 9bc8ddc). Writes stay within the target chunk.
|
||||
*/
|
||||
final class PortSpheresFeature {
|
||||
private static final int SOURCE_RANGE = 8;
|
||||
private static final long SALT = 0x53504845524553L;
|
||||
|
||||
private PortSpheresFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||
var target = chunk.getPos();
|
||||
Random master = new Random(ageSeed ^ SALT);
|
||||
long xSeed = master.nextLong();
|
||||
long zSeed = master.nextLong();
|
||||
for (int sx = target.x() - SOURCE_RANGE; sx <= target.x() + SOURCE_RANGE; sx++) {
|
||||
for (int sz = target.z() - SOURCE_RANGE; sz <= target.z() + SOURCE_RANGE; sz++) {
|
||||
Random source = new Random((long) sx * xSeed ^ (long) sz * zSeed ^ ageSeed ^ SALT);
|
||||
if (source.nextFloat() > 0.05F) continue;
|
||||
double x = sx * 16 + source.nextInt(16);
|
||||
double y = source.nextInt(source.nextInt(192) + 1) + 32;
|
||||
double z = sz * 16 + source.nextInt(16);
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
1.0F + source.nextFloat() * 4.0F, 0, 0, -1, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||
double x, double y, double z, float radius, float yaw, float pitch,
|
||||
int step, int length) {
|
||||
Random random = new Random(seed);
|
||||
if (length <= 0) {
|
||||
int maximum = SOURCE_RANGE * 16 - 16;
|
||||
length = maximum - random.nextInt(maximum / 4);
|
||||
}
|
||||
boolean singleNode = step == -1;
|
||||
if (singleNode) step = length / 2;
|
||||
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||
boolean slowPitch = random.nextInt(6) == 0;
|
||||
float yawDrift = 0;
|
||||
float pitchDrift = 0;
|
||||
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||
for (; step < length; step++) {
|
||||
double nodeRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||
float horizontal = Mth.cos(pitch);
|
||||
x += Mth.cos(yaw) * horizontal;
|
||||
y += Mth.sin(pitch);
|
||||
z += Mth.sin(yaw) * horizontal;
|
||||
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||
pitch += pitchDrift * 0.1F;
|
||||
yaw += yawDrift * 0.1F;
|
||||
pitchDrift = pitchDrift * 0.9F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||
yawDrift = yawDrift * 0.75F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||
trace(chunk, material, random.nextLong(), x, y, z,
|
||||
random.nextFloat() * 0.5F + 0.5F, yaw - Mth.HALF_PI, pitch / 3, step, length);
|
||||
trace(chunk, material, random.nextLong(), x, y, z,
|
||||
random.nextFloat() * 0.5F + 0.5F, yaw + Mth.HALF_PI, pitch / 3, step, length);
|
||||
return;
|
||||
}
|
||||
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||
double dx = x - middleX;
|
||||
double dz = z - middleZ;
|
||||
double remaining = length - step;
|
||||
double reach = radius + 18;
|
||||
if (dx * dx + dz * dz - remaining * remaining > reach * reach) return;
|
||||
if (Math.abs(dx) <= 16 + nodeRadius * 2 && Math.abs(dz) <= 16 + nodeRadius * 2)
|
||||
fillNode(chunk, material, x, y, z, nodeRadius);
|
||||
if (singleNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillNode(ChunkAccess chunk, BlockState material,
|
||||
double centerX, double centerY, double centerZ, double radius) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minX = Math.max(0, Mth.floor(centerX - radius) - originX - 1);
|
||||
int maxX = Math.min(16, Mth.floor(centerX + radius) - originX + 2);
|
||||
int minZ = Math.max(0, Mth.floor(centerZ - radius) - originZ - 1);
|
||||
int maxZ = Math.min(16, Mth.floor(centerZ + radius) - originZ + 2);
|
||||
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - radius) - 1);
|
||||
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + radius) + 2);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||
double dy = (worldY + 0.5D - centerY) / radius;
|
||||
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||
double dz = (originZ + localZ + 0.5D - centerZ) / radius;
|
||||
for (int localX = minX; localX < maxX; localX++) {
|
||||
double dx = (originX + localX + 0.5D - centerX) / radius;
|
||||
if (!insideSphere(dx, dy, dz)) continue;
|
||||
pos.set(originX + localX, worldY, originZ + localZ);
|
||||
BlockState previous = chunk.getBlockState(pos);
|
||||
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||
chunk.setBlockState(pos, material);
|
||||
surface.update(localX, worldY, localZ, material);
|
||||
floor.update(localX, worldY, localZ, material);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean insideSphere(double dx, double dy, double dz) {
|
||||
return dx * dx + dy * dy + dz * dz < 1.0D;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 port of Rivencraft 1.21.1's rare surface spikes, adapted from
|
||||
* Mystcraft Legacy's WorldGenSpikesAdv (Mystcraft contributors,
|
||||
* LGPL-3.0-or-later, commit 9bc8ddc). The footprint stays within one chunk.
|
||||
*/
|
||||
final class PortSpikesFeature {
|
||||
private static final long SALT = 0x5350494B4553L;
|
||||
|
||||
private PortSpikesFeature() {}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
RandomSource random = RandomSource.create(ageSeed ^ SALT
|
||||
^ (long) originX * 341873128712L ^ (long) originZ * 132897987541L
|
||||
^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||
if (random.nextInt(18) != 0) return;
|
||||
int x = originX + 4 + random.nextInt(8);
|
||||
int z = originZ + 4 + random.nextInt(8);
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||
int height = 6 + random.nextInt(32);
|
||||
int width = 1 + random.nextInt(4);
|
||||
if (y < level.getMinY() + 1 || y + height >= level.getMaxY()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
if (!level.isEmptyBlock(pos.set(x, y, z))) return;
|
||||
for (int px = x - width; px <= x + width; px++) {
|
||||
for (int pz = z - width; pz <= z + width; pz++) {
|
||||
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||
if (!level.getBlockState(pos.set(px, y - 1, pz)).isSolid()) return;
|
||||
}
|
||||
}
|
||||
for (int px = x - width; px <= x + width; px++) {
|
||||
for (int pz = z - width; pz <= z + width; pz++) {
|
||||
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||
int top = y + 1 + random.nextInt(random.nextInt(height) + 1);
|
||||
for (int py = y; py < top; py++) {
|
||||
pos.set(px, py, pz);
|
||||
if (level.isEmptyBlock(pos)) level.setBlock(pos, material, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean insideFootprint(int dx, int dz, int width) {
|
||||
return dx * dx + dz * dz <= width * width + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/**
|
||||
* Ports Rivencraft 1.21.1's Legacy fissure footprint/carving.
|
||||
* Derived from Mystcraft's WorldGenMystStarFissure, LGPL-3.0,
|
||||
* copyright XCompWiz and Mystcraft contributors.
|
||||
*/
|
||||
public final class PortStarFissureFeature {
|
||||
private static final long SALT = 0x5354415246495353L;
|
||||
|
||||
private PortStarFissureFeature() {}
|
||||
|
||||
/** Per-Age data prevents revisits/restarts from carving the same fissure again. */
|
||||
private static final class Arrival extends SavedData {
|
||||
private static final Codec<Arrival> CODEC = BlockPos.CODEC.optionalFieldOf("spawn").codec()
|
||||
.xmap(Arrival::new, data -> Optional.ofNullable(data.spawn));
|
||||
private static final SavedDataType<Arrival> TYPE = new SavedDataType<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "star_fissure_arrival"),
|
||||
() -> new Arrival(Optional.empty()), CODEC);
|
||||
private BlockPos spawn;
|
||||
|
||||
private Arrival(Optional<BlockPos> spawn) { this.spawn = spawn.orElse(null); }
|
||||
}
|
||||
|
||||
public static BlockPos savedSpawn(ServerLevel level) {
|
||||
return level.getDataStorage().computeIfAbsent(Arrival.TYPE).spawn;
|
||||
}
|
||||
|
||||
public static void generateOnce(ServerLevel level, BlockPos spawn, boolean caveTerrain) {
|
||||
Arrival data = level.getDataStorage().computeIfAbsent(Arrival.TYPE);
|
||||
if (data.spawn != null) return;
|
||||
generate(level, level.getSeed(), spawn, caveTerrain);
|
||||
data.spawn = spawn.immutable();
|
||||
data.setDirty();
|
||||
}
|
||||
|
||||
/** Legacy chooses 8–23 blocks after the spawn chunk, then grows along +Z. */
|
||||
public static List<BlockPos> footprint(long ageSeed, BlockPos spawn) {
|
||||
long chunkX = spawn.getX() >> 4;
|
||||
long chunkZ = spawn.getZ() >> 4;
|
||||
Random random = new Random(ageSeed ^ SALT ^ chunkX ^ (chunkZ << 32));
|
||||
int originX = ((int) chunkX << 4) + random.nextInt(16) + 8;
|
||||
int originZ = ((int) chunkZ << 4) + random.nextInt(16) + 8;
|
||||
int length = random.nextInt(8) + 10;
|
||||
int[][] intervals = new int[length][2];
|
||||
List<BlockPos> columns = new ArrayList<>();
|
||||
addRow(columns, originX, originZ, 0, 0, 0);
|
||||
for (int row = 1; row < length; row++) {
|
||||
int range = length + Math.min(row, length - row) + 1;
|
||||
int right = (random.nextInt(range) - (range >> 1) + intervals[row - 1][1]) / 2;
|
||||
int left = (random.nextInt(range) - (range >> 1) + intervals[row - 1][0]) / 2;
|
||||
if (left > right) {
|
||||
int swap = left;
|
||||
left = right;
|
||||
right = swap;
|
||||
}
|
||||
left = Math.min(left, intervals[row - 1][1]);
|
||||
right = Math.max(right, intervals[row - 1][0]);
|
||||
intervals[row][0] = left;
|
||||
intervals[row][1] = right;
|
||||
addRow(columns, originX, originZ, row, left, right);
|
||||
}
|
||||
return List.copyOf(columns);
|
||||
}
|
||||
|
||||
private static void addRow(List<BlockPos> columns, int originX, int originZ, int row, int left, int right) {
|
||||
for (int x = left; x <= right; x++) {
|
||||
// Only X/Z are used; the real floor is the dimension's minimum Y.
|
||||
columns.add(new BlockPos(originX + x, 0, originZ + row * 2));
|
||||
columns.add(new BlockPos(originX + x, 0, originZ + row * 2 + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private static void generate(ServerLevel level, long ageSeed, BlockPos spawn, boolean caveTerrain) {
|
||||
int floorY = level.getMinY();
|
||||
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
|
||||
for (BlockPos column : footprint(ageSeed, spawn)) {
|
||||
int top = level.getHeight(Heightmap.Types.WORLD_SURFACE, column.getX(), column.getZ());
|
||||
if (caveTerrain) {
|
||||
for (int y = Math.max(spawn.getY(), floorY + 1); y < top; y++) {
|
||||
if (level.getBlockState(mutable.set(column.getX(), y, column.getZ())).is(Blocks.BEDROCK)) {
|
||||
top = y;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int y = floorY + 1; y < top; y++)
|
||||
level.setBlock(mutable.set(column.getX(), y, column.getZ()), Blocks.AIR.defaultBlockState(), 2);
|
||||
level.setBlock(mutable.set(column.getX(), floorY, column.getZ()),
|
||||
Rivencraft.STAR_FISSURE.get().defaultBlockState(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import net.dnorberg.rivencraft.mixin.StructurePlacementAccessor;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderOwner;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.RandomSpreadStructurePlacement;
|
||||
|
||||
/** 1.21.1 structure planning/density rules adapted to modern placement classes. */
|
||||
public final class PortStructurePages {
|
||||
private static final Map<Identifier, Identifier> SETS = Map.ofEntries(
|
||||
vanillaSet("villages", "villages"),
|
||||
vanillaSet("mineshafts", "mineshafts"),
|
||||
vanillaSet("strongholds", "strongholds"),
|
||||
vanillaSet("trial_chambers", "trial_chambers"),
|
||||
vanillaSet("buried_treasure", "buried_treasures"),
|
||||
vanillaSet("shipwrecks", "shipwrecks"),
|
||||
vanillaSet("ocean_ruins", "ocean_ruins"),
|
||||
vanillaSet("pillager_outposts", "pillager_outposts"),
|
||||
vanillaSet("ruined_portals", "ruined_portals"),
|
||||
vanillaSet("nether_fossils", "nether_fossils"),
|
||||
vanillaSet("ancient_cities", "ancient_cities"),
|
||||
vanillaSet("trail_ruins", "trail_ruins"),
|
||||
vanillaSet("abandoned_camps", "abandoned_camp"),
|
||||
Map.entry(page("nether_fortresses"), Identifier.fromNamespaceAndPath("rivencraft", "fortresses")),
|
||||
Map.entry(page("bastions"), Identifier.fromNamespaceAndPath("rivencraft", "bastions")));
|
||||
// Suppress vanilla's combined Nether set so an authored Fortress page does
|
||||
// not also introduce Bastions (or vice versa).
|
||||
private static final Set<Identifier> CONTROLLED_SETS = Stream.concat(SETS.values().stream(),
|
||||
Stream.of(Identifier.withDefaultNamespace("nether_complexes")))
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
private static final Set<Identifier> CAVE_ONLY = Set.of(
|
||||
page("nether_fortresses"), page("bastions"), page("nether_fossils"));
|
||||
private static final Set<Identifier> SURFACE_ONLY = Set.of(
|
||||
page("villages"), page("strongholds"), page("buried_treasure"), page("shipwrecks"),
|
||||
page("ocean_ruins"), page("pillager_outposts"), page("trail_ruins"), page("abandoned_camps"));
|
||||
private static final Set<Identifier> FLAT_ALLOWED = Set.of(page("villages"), page("strongholds"));
|
||||
|
||||
private PortStructurePages() {}
|
||||
|
||||
private static Identifier page(String name) {
|
||||
return Identifier.fromNamespaceAndPath("rivencraft", "feature/" + name);
|
||||
}
|
||||
|
||||
private static Map.Entry<Identifier, Identifier> vanillaSet(String page, String set) {
|
||||
return Map.entry(page(page), Identifier.withDefaultNamespace(set));
|
||||
}
|
||||
|
||||
public static boolean supports(Identifier page) { return SETS.containsKey(page); }
|
||||
|
||||
public static void validateCounts(List<Identifier> pages) {
|
||||
Map<Identifier, Integer> counts = new HashMap<>();
|
||||
for (Identifier page : pages) {
|
||||
if (!supports(page) || counts.merge(page, 1, Integer::sum) > 32)
|
||||
throw new IllegalArgumentException("Structure plan requires supported pages, at most 32 per feature");
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean supportsTerrain(Identifier page, PortAgeTerrain terrain) {
|
||||
if (!supports(page) || terrain == PortAgeTerrain.VOID) return false;
|
||||
if (terrain == PortAgeTerrain.FLAT && !FLAT_ALLOWED.contains(page)) return false;
|
||||
if (CAVE_ONLY.contains(page)) return terrain == PortAgeTerrain.CAVE;
|
||||
return terrain != PortAgeTerrain.CAVE || !SURFACE_ONLY.contains(page);
|
||||
}
|
||||
|
||||
public static void validateRegistry(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
|
||||
pages.stream().filter(PortStructurePages::supports).distinct().forEach(page ->
|
||||
sets.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing structure set for " + page)));
|
||||
}
|
||||
|
||||
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
|
||||
Map<Identifier, Integer> result = new HashMap<>();
|
||||
for (Identifier page : pages) result.merge(SETS.get(page), 1, Integer::sum);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean authored(Structure structure, HolderLookup.Provider registries, List<Identifier> pages) {
|
||||
var sets = registries.lookupOrThrow(Registries.STRUCTURE_SET);
|
||||
return enabled(pages).keySet().stream()
|
||||
.map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value())
|
||||
.flatMap(set -> set.structures().stream()).anyMatch(entry -> entry.structure().value() == structure);
|
||||
}
|
||||
|
||||
/** Extra possible biomes affect planning only; terrain still uses its original source. */
|
||||
public static BiomeSource planningBiomes(BiomeSource actual, HolderLookup<StructureSet> sets, List<Identifier> pages) {
|
||||
List<Holder<Biome>> extra = enabled(pages).keySet().stream()
|
||||
.map(id -> sets.getOrThrow(ResourceKey.create(Registries.STRUCTURE_SET, id)).value())
|
||||
.flatMap(set -> set.structures().stream()).flatMap(entry -> entry.structure().value().biomes().stream())
|
||||
.distinct().toList();
|
||||
return new BiomeSource() {
|
||||
@Override protected MapCodec<? extends BiomeSource> codec() {
|
||||
throw new UnsupportedOperationException("Planning biome sources are never serialized");
|
||||
}
|
||||
@Override protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return Stream.concat(actual.possibleBiomes().stream(), extra.stream());
|
||||
}
|
||||
@Override public net.minecraft.world.level.biome.BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
return actual.createResolver(sampler);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static HolderLookup<StructureSet> filtered(HolderLookup<StructureSet> source, List<Identifier> pages,
|
||||
BiomeSource actual) {
|
||||
Map<Identifier, Integer> counts = enabled(pages);
|
||||
Map<ResourceKey<StructureSet>, Holder.Reference<StructureSet>> copies = new HashMap<>();
|
||||
return new HolderLookup<>() {
|
||||
@Override public Optional<Holder.Reference<StructureSet>> get(ResourceKey<StructureSet> key) {
|
||||
if (CONTROLLED_SETS.contains(key.identifier()) && !counts.containsKey(key.identifier())) return Optional.empty();
|
||||
return source.get(key).filter(holder -> counts.containsKey(key.identifier()) || hasBiome(holder.value(), actual))
|
||||
.map(holder -> copies.computeIfAbsent(key, ignored -> {
|
||||
int count = counts.getOrDefault(key.identifier(), 1);
|
||||
return count == 1 ? holder : new LocalReference(key, scaled(holder.value(), count));
|
||||
}));
|
||||
}
|
||||
@Override public Optional<HolderSet.Named<StructureSet>> get(TagKey<StructureSet> key) { return source.get(key); }
|
||||
@Override public Stream<Holder.Reference<StructureSet>> listElements() {
|
||||
return source.listElements().flatMap(holder -> get(holder.key()).stream());
|
||||
}
|
||||
@Override public Stream<HolderSet.Named<StructureSet>> listTags() { return source.listTags(); }
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean hasBiome(StructureSet set, BiomeSource source) {
|
||||
Set<Holder<Biome>> possible = source.possibleBiomes();
|
||||
return set.structures().stream().flatMap(entry -> entry.structure().value().biomes().stream()).anyMatch(possible::contains);
|
||||
}
|
||||
|
||||
private static StructureSet scaled(StructureSet set, int count) {
|
||||
var placement = set.placement();
|
||||
if (!(placement instanceof RandomSpreadStructurePlacement)
|
||||
&& !(placement instanceof ConcentricRingsStructurePlacement)) return set;
|
||||
StructurePlacementAccessor settings = (StructurePlacementAccessor) placement;
|
||||
if (placement instanceof RandomSpreadStructurePlacement random) {
|
||||
float original = settings.rivencraft$frequency();
|
||||
float frequency = Math.min(1.0F, original * count);
|
||||
double frequencyFactor = original > 0 ? frequency / original : 1;
|
||||
int spacing = Math.max(1, (int) Math.round(random.spacing() / Math.sqrt(count / frequencyFactor)));
|
||||
int separation = spacing == 1 ? 0 : Math.min(spacing - 1,
|
||||
(int) Math.round((double) random.separation() * spacing / random.spacing()));
|
||||
return new StructureSet(set.structures(), new RandomSpreadStructurePlacement(placement.locateOffset(),
|
||||
settings.rivencraft$frequencyReductionMethod(), frequency, settings.rivencraft$salt(),
|
||||
settings.rivencraft$exclusionZone(), spacing, separation, random.spreadType()));
|
||||
}
|
||||
if (placement instanceof ConcentricRingsStructurePlacement rings) {
|
||||
int total = (int) Math.min(4095, (long) rings.count() * count);
|
||||
int spread = Math.min(total, (int) Math.min(1023, (long) rings.spread() * count));
|
||||
return new StructureSet(set.structures(), new ConcentricRingsStructurePlacement(placement.locateOffset(),
|
||||
settings.rivencraft$frequencyReductionMethod(), settings.rivencraft$frequency(),
|
||||
settings.rivencraft$salt(), settings.rivencraft$exclusionZone(), rings.distance(), spread,
|
||||
total, rings.preferredBiomes()));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private static final class LocalReference extends Holder.Reference<StructureSet> {
|
||||
private static final HolderOwner<StructureSet> OWNER = new HolderOwner<>() {};
|
||||
private LocalReference(ResourceKey<StructureSet> key, StructureSet value) { super(Type.STAND_ALONE, OWNER, key, value); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.Random;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* 26.3 adaptation of Rivencraft 1.21.1's winding solid volumes, originally
|
||||
* adapted from Mystcraft Legacy's SymbolTendrils and MapGenCavesMyst (LGPL-3.0-or-later).
|
||||
* Each chunk reconstructs the neighboring source paths to preserve continuous borders.
|
||||
*/
|
||||
final class PortTendrilFeature {
|
||||
private static final long SALT = 0x54454E4452494C53L;
|
||||
private static final int SOURCE_RANGE = 8;
|
||||
private static final int SOURCE_RATE = 15;
|
||||
private static final int SOURCE_SIZE = 18;
|
||||
|
||||
private PortTendrilFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, BlockState material) {
|
||||
var target = chunk.getPos();
|
||||
Random master = new Random(ageSeed ^ SALT);
|
||||
long xSeed = master.nextLong();
|
||||
long zSeed = master.nextLong();
|
||||
for (int sourceX = target.x() - SOURCE_RANGE; sourceX <= target.x() + SOURCE_RANGE; sourceX++) {
|
||||
for (int sourceZ = target.z() - SOURCE_RANGE; sourceZ <= target.z() + SOURCE_RANGE; sourceZ++) {
|
||||
Random source = new Random((long) sourceX * xSeed ^ (long) sourceZ * zSeed ^ ageSeed ^ SALT);
|
||||
int nodes = source.nextInt(source.nextInt(source.nextInt(SOURCE_SIZE) + 1) + 1);
|
||||
if (source.nextInt(SOURCE_RATE) != 0) continue;
|
||||
for (int node = 0; node < nodes; node++) {
|
||||
double x = sourceX * 16 + source.nextInt(16);
|
||||
double y = source.nextInt(source.nextInt(120) + 8);
|
||||
double z = sourceZ * 16 + source.nextInt(16);
|
||||
int branches = 1;
|
||||
if (source.nextInt(4) == 0) {
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
1.0F + source.nextFloat() * 6.0F, 0.0F, 0.0F, -1, -1, 0.5D);
|
||||
branches += source.nextInt(4);
|
||||
}
|
||||
for (int branch = 0; branch < branches; branch++) {
|
||||
float yaw = source.nextFloat() * Mth.TWO_PI;
|
||||
float pitch = (source.nextFloat() - 0.5F) / 4.0F;
|
||||
float radius = source.nextFloat() * 2.0F + source.nextFloat();
|
||||
if (source.nextInt(10) == 0)
|
||||
radius *= source.nextFloat() * source.nextFloat() * 3.0F + 1.0F;
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
radius, yaw, pitch, 0, 0, 1.0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||
double x, double y, double z, float radius, float yaw, float pitch,
|
||||
int step, int length, double verticalScale) {
|
||||
Random random = new Random(seed);
|
||||
if (length <= 0) {
|
||||
int maximum = SOURCE_RANGE * 16 - 16;
|
||||
length = maximum - random.nextInt(maximum / 4);
|
||||
}
|
||||
boolean singleNode = step == -1;
|
||||
if (singleNode) step = length / 2;
|
||||
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||
boolean slowPitch = random.nextInt(6) == 0;
|
||||
float yawDrift = 0.0F;
|
||||
float pitchDrift = 0.0F;
|
||||
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||
for (; step < length; step++) {
|
||||
double horizontalRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||
double verticalRadius = horizontalRadius * verticalScale;
|
||||
float horizontalMotion = Mth.cos(pitch);
|
||||
x += Mth.cos(yaw) * horizontalMotion;
|
||||
y += Mth.sin(pitch);
|
||||
z += Mth.sin(yaw) * horizontalMotion;
|
||||
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||
pitch += pitchDrift * 0.1F;
|
||||
yaw += yawDrift * 0.1F;
|
||||
pitchDrift = pitchDrift * 0.9F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||
yawDrift = yawDrift * 0.75F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||
yaw - Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||
yaw + Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||
return;
|
||||
}
|
||||
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||
double distanceX = x - middleX;
|
||||
double distanceZ = z - middleZ;
|
||||
double remaining = length - step;
|
||||
double reach = radius + 18.0D;
|
||||
if (distanceX * distanceX + distanceZ * distanceZ - remaining * remaining > reach * reach) return;
|
||||
if (x >= middleX - 16.0D - horizontalRadius * 2.0D
|
||||
&& x <= middleX + 16.0D + horizontalRadius * 2.0D
|
||||
&& z >= middleZ - 16.0D - horizontalRadius * 2.0D
|
||||
&& z <= middleZ + 16.0D + horizontalRadius * 2.0D) {
|
||||
fillNode(chunk, material, x, y, z, horizontalRadius, verticalRadius);
|
||||
}
|
||||
if (singleNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillNode(ChunkAccess chunk, BlockState material, double centerX, double centerY,
|
||||
double centerZ, double horizontalRadius, double verticalRadius) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minX = Math.max(0, Mth.floor(centerX - horizontalRadius) - originX - 1);
|
||||
int maxX = Math.min(16, Mth.floor(centerX + horizontalRadius) - originX + 2);
|
||||
int minZ = Math.max(0, Mth.floor(centerZ - horizontalRadius) - originZ - 1);
|
||||
int maxZ = Math.min(16, Mth.floor(centerZ + horizontalRadius) - originZ + 2);
|
||||
int minY = Math.max(chunk.getMinY() + 1, Mth.floor(centerY - verticalRadius) - 1);
|
||||
int maxY = Math.min(chunk.getMaxY(), Mth.floor(centerY + verticalRadius) + 2);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||
double dy = (worldY + 0.5D - centerY) / verticalRadius;
|
||||
if (dy <= -0.7D || dy * dy >= 1.0D) continue;
|
||||
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||
double dz = (originZ + localZ + 0.5D - centerZ) / horizontalRadius;
|
||||
for (int localX = minX; localX < maxX; localX++) {
|
||||
double dx = (originX + localX + 0.5D - centerX) / horizontalRadius;
|
||||
if (dx * dx + dy * dy + dz * dz >= 1.0D) continue;
|
||||
pos.set(originX + localX, worldY, originZ + localZ);
|
||||
BlockState previous = chunk.getBlockState(pos);
|
||||
if (previous == material || previous.is(Blocks.BEDROCK)) continue;
|
||||
chunk.setBlockState(pos, material);
|
||||
surface.update(localX, worldY, localZ, material);
|
||||
floor.update(localX, worldY, localZ, material);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.HashMap;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen terrain filters and ordered solid/surface features for an Age. */
|
||||
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures, List<Identifier> carvers) {
|
||||
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/floating_islands");
|
||||
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
|
||||
public static final Identifier SKYLANDS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/skylands");
|
||||
public static final Identifier SPHERES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spheres");
|
||||
public static final Identifier SPIKES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/spikes");
|
||||
public static final Identifier OBELISKS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/obelisks");
|
||||
public static final Identifier CRYSTAL_FORMATIONS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/crystal_formations");
|
||||
public static final Identifier CRYSTAL_MATERIAL = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "crystal_block");
|
||||
public static final Identifier DENSE_ORES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dense_ores");
|
||||
public static final Identifier STAR_FISSURES = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/star_fissures");
|
||||
public static final int MAX_PER_FEATURE = 32;
|
||||
public static final Codec<PortTerrainFeaturePlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Choice.CODEC.listOf().fieldOf("choices").forGetter(PortTerrainFeaturePlan::choices),
|
||||
Codec.BOOL.optionalFieldOf("skylands", false).forGetter(PortTerrainFeaturePlan::skylands),
|
||||
Codec.intRange(0, MAX_PER_FEATURE).optionalFieldOf("dense_ores", 0)
|
||||
.forGetter(PortTerrainFeaturePlan::denseOres),
|
||||
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers)
|
||||
).apply(instance, PortTerrainFeaturePlan::new));
|
||||
|
||||
public record Choice(Identifier page, Identifier material) {
|
||||
public static final Codec<Choice> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("page").forGetter(Choice::page),
|
||||
Identifier.CODEC.fieldOf("material").forGetter(Choice::material)
|
||||
).apply(instance, Choice::new));
|
||||
|
||||
public Choice {
|
||||
if (!isSupported(page) || material == null)
|
||||
throw new IllegalArgumentException("Invalid terrain feature choice");
|
||||
if (CRYSTAL_FORMATIONS.equals(page) && !CRYSTAL_MATERIAL.equals(material))
|
||||
throw new IllegalArgumentException("Crystal formations use Rivencraft crystal, not a Block modifier");
|
||||
}
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan {
|
||||
choices = List.copyOf(choices);
|
||||
structures = List.copyOf(structures);
|
||||
carvers = List.copyOf(carvers);
|
||||
PortStructurePages.validateCounts(structures);
|
||||
PortCarverPages.validateCounts(carvers);
|
||||
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
|
||||
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
|
||||
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty())
|
||||
throw new IllegalArgumentException("Terrain feature plan must not be empty");
|
||||
var counts = new HashMap<Identifier, Integer>();
|
||||
for (Choice choice : choices) {
|
||||
if (counts.merge(choice.page(), 1, Integer::sum) > MAX_PER_FEATURE)
|
||||
throw new IllegalArgumentException("Terrain feature plan permits at most 32 occurrences per feature");
|
||||
}
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures) {
|
||||
this(choices, skylands, denseOres, starFissures, structures, List.of());
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres, boolean starFissures) {
|
||||
this(choices, skylands, denseOres, starFissures, List.of());
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres) {
|
||||
this(choices, skylands, denseOres, false);
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands) { this(choices, skylands, 0, false); }
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices) { this(choices, false, 0, false); }
|
||||
|
||||
public static boolean isSupported(Identifier page) {
|
||||
return FLOATING_ISLANDS.equals(page) || TENDRILS.equals(page) || SPHERES.equals(page) || isSurface(page);
|
||||
}
|
||||
|
||||
public static boolean acceptsMaterial(Identifier page) {
|
||||
return isSupported(page) && !CRYSTAL_FORMATIONS.equals(page);
|
||||
}
|
||||
|
||||
public static boolean isSurface(Identifier page) {
|
||||
return SPIKES.equals(page) || OBELISKS.equals(page) || CRYSTAL_FORMATIONS.equals(page);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Ordered, frozen plants and log decorations; not all require surface terrain. */
|
||||
public record PortVegetationPlan(List<Identifier> pages) {
|
||||
public static final Identifier FLOWERS = page("flowers");
|
||||
public static final Identifier TREES = page("trees");
|
||||
public static final Identifier HUGE_TREES = page("huge_trees");
|
||||
public static final Identifier POPLAR_TREES = page("poplar_trees");
|
||||
public static final Identifier FALLEN_POPLAR_TREES = page("fallen_poplar_trees");
|
||||
public static final Identifier RED_SHRUBS = page("red_shrubs");
|
||||
public static final Identifier SHELF_MUSHROOMS = page("shelf_mushrooms");
|
||||
public static final Codec<PortVegetationPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.listOf().fieldOf("pages").forGetter(PortVegetationPlan::pages)
|
||||
).apply(instance, PortVegetationPlan::new));
|
||||
|
||||
public PortVegetationPlan {
|
||||
pages = List.copyOf(pages);
|
||||
if (pages.isEmpty() || pages.size() > 32 || pages.stream().anyMatch(page -> !isVegetation(page)))
|
||||
throw new IllegalArgumentException("Vegetation plan requires 1–32 supported pages");
|
||||
}
|
||||
|
||||
public static boolean isVegetation(Identifier page) {
|
||||
return page.equals(FLOWERS) || page.equals(TREES) || page.equals(HUGE_TREES)
|
||||
|| page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES)
|
||||
|| page.equals(RED_SHRUBS) || page.equals(SHELF_MUSHROOMS);
|
||||
}
|
||||
|
||||
public static Identifier placedFeature(Identifier page) {
|
||||
if (page.equals(FLOWERS)) return Identifier.withDefaultNamespace("flower_default");
|
||||
if (page.equals(TREES)) return Identifier.withDefaultNamespace("trees_plains");
|
||||
if (page.equals(HUGE_TREES)) return Identifier.withDefaultNamespace("trees_old_growth_spruce_taiga");
|
||||
if (page.equals(POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "poplar_trees");
|
||||
if (page.equals(FALLEN_POPLAR_TREES)) return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "fallen_poplar_trees");
|
||||
if (page.equals(RED_SHRUBS)) return Identifier.withDefaultNamespace("patch_red_shrub");
|
||||
throw new IllegalArgumentException("Unsupported vegetation page " + page);
|
||||
}
|
||||
|
||||
public static boolean addsLogs(Identifier page) {
|
||||
return page.equals(TREES) || page.equals(HUGE_TREES)
|
||||
|| page.equals(POPLAR_TREES) || page.equals(FALLEN_POPLAR_TREES);
|
||||
}
|
||||
|
||||
public static boolean requiresSurface(Identifier page) {
|
||||
return isVegetation(page) && !page.equals(SHELF_MUSHROOMS);
|
||||
}
|
||||
|
||||
private static Identifier page(String name) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** The six active weather pages in the 1.21.1 writing system. */
|
||||
public enum PortWeatherMode {
|
||||
NORMAL, FAST, SLOW, OFF, ALWAYS, STORM;
|
||||
|
||||
public static final Codec<PortWeatherMode> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(java.util.Locale.ROOT)),
|
||||
mode -> mode.name().toLowerCase(java.util.Locale.ROOT));
|
||||
|
||||
public static PortWeatherMode forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "weather/normal" -> NORMAL;
|
||||
case "weather/fast" -> FAST;
|
||||
case "weather/slow" -> SLOW;
|
||||
case "weather/off" -> OFF;
|
||||
case "weather/always" -> ALWAYS;
|
||||
case "weather/storm" -> STORM;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Age-local policy; native WeatherData separately persists its live cycle. */
|
||||
final class PortWeatherSettings extends SavedData {
|
||||
private static final Codec<PortWeatherSettings> CODEC = PortWeatherMode.CODEC
|
||||
.fieldOf("mode").codec().xmap(PortWeatherSettings::new, settings -> settings.mode);
|
||||
static final SavedDataType<PortWeatherSettings> TYPE = new SavedDataType<>(
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "weather_policy"),
|
||||
() -> new PortWeatherSettings(PortWeatherMode.NORMAL), CODEC);
|
||||
|
||||
private PortWeatherMode mode;
|
||||
|
||||
private PortWeatherSettings(PortWeatherMode mode) { this.mode = mode; }
|
||||
PortWeatherMode mode() { return mode; }
|
||||
void setMode(PortWeatherMode mode) {
|
||||
this.mode = mode;
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,513 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
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;
|
||||
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.LevelResource;
|
||||
import net.neoforged.neoforge.common.NeoForge;
|
||||
import net.neoforged.neoforge.common.world.chunk.ForcedChunkManager;
|
||||
import net.neoforged.neoforge.event.level.LevelEvent;
|
||||
|
||||
/**
|
||||
* Server-side runtime-level foundation for 26.3. Authored generators and
|
||||
* dynamic client registry synchronization remain separate migration steps.
|
||||
*/
|
||||
public final class RuntimeAgeBackend {
|
||||
private RuntimeAgeBackend() {
|
||||
}
|
||||
|
||||
public static void onLevelLoad(LevelEvent.Load event) {
|
||||
if (!(event.getLevel() instanceof ServerLevel overworld) || overworld.dimension() != Level.OVERWORLD) {
|
||||
return;
|
||||
}
|
||||
MinecraftServer server = overworld.getServer();
|
||||
AgeLevelIndex index = index(server);
|
||||
for (Map.Entry<Identifier, Long> age : index.entries().entrySet()) {
|
||||
try {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, PortWeatherMode.NORMAL);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, PortLighting.NORMAL);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, PortSkyStyle.OVERWORLD);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting, PortSkyStyle sky) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, sky, PortAgeColors.NATIVE);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting, PortSkyStyle sky, PortAgeColors colors) {
|
||||
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, weather, lighting, sky, colors, PortDayCycle.NATIVE);
|
||||
}
|
||||
|
||||
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
|
||||
PortLighting lighting, PortSkyStyle sky, PortAgeColors colors, PortDayCycle dayCycle) {
|
||||
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, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
((PortAgeLevel) level).configureWeather(weather);
|
||||
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
// 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.
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
return level;
|
||||
}
|
||||
|
||||
/** Unloads an Age only when no player remains; save(true) flushes other entities. */
|
||||
public static void unload(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
requireSeed(server, id);
|
||||
unloadLoaded(server, id);
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
}
|
||||
|
||||
public static ServerLevel reload(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
long seed = requireSeed(server, id);
|
||||
ServerLevel level = load(server, id, seed, index(server).biomePlan(id), index(server).terrain(id),
|
||||
index(server).rocks(id), index(server).vegetation(id), index(server).dungeons(id), index(server).lakes(id),
|
||||
index(server).terrainFeatures(id));
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
return level;
|
||||
}
|
||||
|
||||
/** Existing saved chunks stay in place; the replacement level uses the new seed. */
|
||||
public static void reseed(MinecraftServer server, Identifier id, long seed) {
|
||||
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, 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, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
/** Retires one unloaded Age and moves only its own storage to a recoverable archive. */
|
||||
public static void retire(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
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);
|
||||
PortLighting lighting = index.lighting(id);
|
||||
PortSkyStyle sky = index.sky(id);
|
||||
PortAgeColors colors = index.colors(id);
|
||||
PortDayCycle dayCycle = index.dayCycle(id);
|
||||
if (loaded) unloadLoaded(server, id);
|
||||
// Persist the tombstone before moving files: a crash mid-archive must
|
||||
// not let an old book silently regenerate this identity.
|
||||
index.retire(id);
|
||||
try {
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
if (Files.exists(paths.live(), LinkOption.NOFOLLOW_LINKS)) {
|
||||
Files.createDirectories(paths.archive().getParent());
|
||||
try {
|
||||
Files.move(paths.live(), paths.archive(), StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException exception) {
|
||||
Files.move(paths.live(), paths.archive());
|
||||
}
|
||||
}
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
try {
|
||||
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, lighting, sky, colors, dayCycle);
|
||||
server.overworld().getDataStorage().saveAndJoin();
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
if (loaded) {
|
||||
try {
|
||||
load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
} catch (RuntimeException rollbackFailure) {
|
||||
exception.addSuppressed(rollbackFailure);
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Could not archive Age " + id, exception);
|
||||
}
|
||||
AgeCatalogSync.onCatalogChanged(server);
|
||||
Rivencraft.LOGGER.info("Retired 26.3 prototype Age {} (archived at {})", id, paths.archive());
|
||||
}
|
||||
|
||||
public static Map<Identifier, Long> savedAges(MinecraftServer server) {
|
||||
checkServerThread(server);
|
||||
return index(server).entries();
|
||||
}
|
||||
|
||||
public static Map<Identifier, PortAgeColors> savedColors(MinecraftServer server) {
|
||||
checkServerThread(server);
|
||||
return index(server).colors();
|
||||
}
|
||||
|
||||
public static Map<Identifier, PortDayCycle> savedDayCycles(MinecraftServer server) {
|
||||
checkServerThread(server);
|
||||
return index(server).dayCycles();
|
||||
}
|
||||
|
||||
/** Includes retired IDs, which must never be silently reused by a new book. */
|
||||
public static boolean isNameTaken(MinecraftServer server, Identifier id) {
|
||||
checkServerThread(server);
|
||||
ageName(id);
|
||||
return index(server).contains(id);
|
||||
}
|
||||
|
||||
private static AgeLevelIndex index(MinecraftServer server) {
|
||||
return server.overworld().getDataStorage().computeIfAbsent(AgeLevelIndex.TYPE);
|
||||
}
|
||||
|
||||
private static void checkServerThread(MinecraftServer server) {
|
||||
if (!server.isSameThread()) throw new IllegalStateException("Runtime Age operation outside server thread");
|
||||
}
|
||||
|
||||
private static long requireSeed(MinecraftServer server, Identifier id) {
|
||||
ageName(id);
|
||||
Long seed = index(server).seed(id);
|
||||
if (seed == null) throw new IllegalArgumentException("Age not active: " + id);
|
||||
return seed;
|
||||
}
|
||||
|
||||
private static String ageName(Identifier id) {
|
||||
if (!id.getNamespace().equals(Rivencraft.MOD_ID) || !id.getPath().startsWith("age/")) {
|
||||
throw new IllegalArgumentException("Expected a rivencraft:age/* dimension ID: " + id);
|
||||
}
|
||||
String name = id.getPath().substring("age/".length());
|
||||
if (name.isBlank() || name.contains("/") || name.equals(".") || name.equals("..")) {
|
||||
throw new IllegalArgumentException("Expected a single safe Age path segment: " + id);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private static ResourceKey<Level> dimensionKey(Identifier id) {
|
||||
return ResourceKey.create(Registries.DIMENSION, id);
|
||||
}
|
||||
|
||||
private static void unloadLoaded(MinecraftServer server, Identifier id) {
|
||||
ResourceKey<Level> key = dimensionKey(id);
|
||||
ServerLevel level = server.getLevel(key);
|
||||
if (level == null) throw new IllegalStateException("Age not loaded: " + id);
|
||||
if (!level.players().isEmpty()) {
|
||||
throw new IllegalStateException("Age still contains players; move them before unloading: " + id);
|
||||
}
|
||||
// Match the server shutdown sequence so forced tickets are persisted
|
||||
// as inactive, rather than holding stale references to this level.
|
||||
level.getChunkSource().deactivateTicketsOnClosing();
|
||||
level.getChunkSource().tick(() -> true, false);
|
||||
level.save(null, true, false);
|
||||
try {
|
||||
level.close();
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Could not close Age: " + id, exception);
|
||||
}
|
||||
server.levels.remove(key);
|
||||
NeoForge.EVENT_BUS.post(new LevelEvent.Unload(level));
|
||||
Rivencraft.LOGGER.info("Unloaded 26.3 prototype Age {}", id);
|
||||
}
|
||||
|
||||
private static ArchivePaths archivePaths(MinecraftServer server, String name) {
|
||||
Path world = server.getWorldPath(LevelResource.ROOT).toAbsolutePath().normalize();
|
||||
Path dimensions = world.resolve("dimensions");
|
||||
Path namespace = dimensions.resolve(Rivencraft.MOD_ID);
|
||||
Path ageRoot = namespace.resolve("age");
|
||||
Path live = ageRoot.resolve(name).normalize();
|
||||
Path archiveRoot = world.getParent().resolve(world.getFileName() + "-retired-ages").normalize();
|
||||
Path archive = archiveRoot.resolve(name).normalize();
|
||||
if (!live.getParent().equals(ageRoot) || !archive.getParent().equals(archiveRoot)
|
||||
|| Files.isSymbolicLink(world) || Files.isSymbolicLink(dimensions)
|
||||
|| Files.isSymbolicLink(namespace) || Files.isSymbolicLink(ageRoot)
|
||||
|| Files.isSymbolicLink(live) || Files.isSymbolicLink(archiveRoot)
|
||||
|| Files.exists(archive, LinkOption.NOFOLLOW_LINKS)) {
|
||||
throw new IllegalStateException("Unsafe or existing Age archive path for " + name);
|
||||
}
|
||||
return new ArchivePaths(live, archive);
|
||||
}
|
||||
|
||||
private record ArchivePaths(Path live, Path archive) {
|
||||
}
|
||||
|
||||
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) {
|
||||
return load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
|
||||
terrainFeatures, index(server).lighting(id), index(server).sky(id), index(server).colors(id), index(server).dayCycle(id));
|
||||
}
|
||||
|
||||
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
|
||||
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
|
||||
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
|
||||
PortTerrainFeaturePlan terrainFeatures, PortLighting lighting, PortSkyStyle sky,
|
||||
PortAgeColors colors, PortDayCycle dayCycle) {
|
||||
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
|
||||
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
|
||||
|
||||
// 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");
|
||||
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(lighting.dimensionType(server, terrain, overworldStem.type(), sky),
|
||||
generator, OptionalLong.of(seed));
|
||||
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
||||
ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain);
|
||||
((PortAgeLevel) level).configureColors(colors);
|
||||
((PortAgeLevel) level).configureDayCycle(dayCycle);
|
||||
|
||||
// Publish only after the constructor succeeds. If the load event fails,
|
||||
// roll back the map and close the newly opened chunk/entity stores.
|
||||
try {
|
||||
server.levels.put(key, level);
|
||||
level.getWorldBorder().setAbsoluteMaxSize(server.getAbsoluteMaxWorldSize());
|
||||
server.getPlayerList().addWorldborderListener(level);
|
||||
NeoForge.EVENT_BUS.post(new LevelEvent.Load(level));
|
||||
TicketStorage tickets = level.getDataStorage().get(TicketStorage.TYPE);
|
||||
if (tickets != null) {
|
||||
tickets.activateAllDeactivatedTickets();
|
||||
ForcedChunkManager.activateAllDeactivatedTickets(level, tickets);
|
||||
}
|
||||
Rivencraft.LOGGER.info("Loaded 26.3 prototype Age {} with seed {}", id, seed);
|
||||
return level;
|
||||
} catch (RuntimeException exception) {
|
||||
server.levels.remove(key);
|
||||
try {
|
||||
level.close();
|
||||
} catch (IOException closeException) {
|
||||
exception.addSuppressed(closeException);
|
||||
}
|
||||
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,45 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.block.AmethystBlock;
|
||||
import net.minecraft.world.level.block.AmethystClusterBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.BuddingAmethystBlock;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.material.Fluids;
|
||||
|
||||
/** Vanilla amethyst growth, using Rivencraft's blue crystal stages. */
|
||||
public final class BuddingCrystalBlock extends AmethystBlock {
|
||||
public BuddingCrystalBlock(BlockBehaviour.Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void randomTick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) {
|
||||
if (random.nextInt(5) != 0) return;
|
||||
Direction direction = Direction.values()[random.nextInt(Direction.values().length)];
|
||||
BlockPos target = pos.relative(direction);
|
||||
BlockState existing = level.getBlockState(target);
|
||||
Block next = nextStage(existing, direction);
|
||||
if (next == null) return;
|
||||
level.setBlockAndUpdate(target, next.defaultBlockState()
|
||||
.setValue(AmethystClusterBlock.FACING, direction)
|
||||
.setValue(AmethystClusterBlock.WATERLOGGED,
|
||||
existing.getFluidState().getType() == Fluids.WATER));
|
||||
}
|
||||
|
||||
public static Block nextStage(BlockState existing, Direction direction) {
|
||||
if (BuddingAmethystBlock.canClusterGrowAtState(existing)) return Rivencraft.SMALL_CRYSTAL_BUD.get();
|
||||
if (!existing.hasProperty(AmethystClusterBlock.FACING)
|
||||
|| existing.getValue(AmethystClusterBlock.FACING) != direction) return null;
|
||||
if (existing.is(Rivencraft.SMALL_CRYSTAL_BUD)) return Rivencraft.MEDIUM_CRYSTAL_BUD.get();
|
||||
if (existing.is(Rivencraft.MEDIUM_CRYSTAL_BUD)) return Rivencraft.LARGE_CRYSTAL_BUD.get();
|
||||
if (existing.is(Rivencraft.LARGE_CRYSTAL_BUD)) return Rivencraft.CRYSTAL_CLUSTER.get();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.Containers;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
/** 26.3 API adaptation of the 1.21.1 Ink Mixer block. */
|
||||
public final class InkMixerBlock extends BaseEntityBlock {
|
||||
public InkMixerBlock(Properties properties) { super(properties); }
|
||||
|
||||
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
|
||||
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new InkMixerBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit) {
|
||||
if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer
|
||||
&& level.getBlockEntity(pos) instanceof InkMixerBlockEntity mixer) {
|
||||
serverPlayer.openMenu(new SimpleMenuProvider(
|
||||
(id, inventory, ignored) -> new InkMixerMenu(id, inventory, mixer),
|
||||
Component.translatable("container.rivencraft.ink_mixer")),
|
||||
buffer -> buffer.writeBlockPos(pos));
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
@Override public void playerDestroy(ServerLevel level, ServerPlayer player, BlockPos pos,
|
||||
BlockState state, BlockEntity entity, ItemStack tool) {
|
||||
if (entity instanceof InkMixerBlockEntity mixer) Containers.dropContents(level, pos, mixer);
|
||||
super.playerDestroy(level, player, pos, state, entity, tool);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.Containers;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.SimpleMenuProvider;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public final class PortBookBinderBlock extends BaseEntityBlock {
|
||||
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;
|
||||
|
||||
public PortBookBinderBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(stateDefinition.any().setValue(FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override protected void createBlockStateDefinition(StateDefinition.Builder<net.minecraft.world.level.block.Block, BlockState> builder) {
|
||||
builder.add(FACING);
|
||||
}
|
||||
@Override public BlockState getStateForPlacement(BlockPlaceContext context) {
|
||||
return defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
|
||||
}
|
||||
@Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; }
|
||||
@Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new PortBookBinderBlockEntity(pos, state);
|
||||
}
|
||||
@Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit) {
|
||||
if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer
|
||||
&& level.getBlockEntity(pos) instanceof PortBookBinderBlockEntity binder) {
|
||||
serverPlayer.openMenu(new SimpleMenuProvider(
|
||||
(id, inventory, ignored) -> new PortBookBinderMenu(id, inventory, binder),
|
||||
Component.translatable("container.rivencraft.book_binder")),
|
||||
buffer -> {
|
||||
buffer.writeBlockPos(pos);
|
||||
buffer.writeUtf(binder.title(), 32);
|
||||
});
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
@Override public void playerDestroy(ServerLevel level, ServerPlayer player, BlockPos pos,
|
||||
BlockState state, BlockEntity entity, ItemStack tool) {
|
||||
if (entity instanceof PortBookBinderBlockEntity binder) Containers.dropContents(level, pos, binder);
|
||||
super.playerDestroy(level, player, pos, state, entity, tool);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 456 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user