Public Access
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48fbae9be9 | ||
|
|
c223f3fdca | ||
|
|
d75154fa9b | ||
|
|
9ccec18c18 | ||
|
|
d9464fad76 | ||
|
|
d6e9a58a24 | ||
|
|
fa294be685 | ||
|
|
cb31156584 | ||
|
|
c23b057f51 | ||
|
|
9ab8d3de4a | ||
|
|
56005ac512 | ||
|
|
9ff931afa9 | ||
|
|
eda19de43c | ||
|
|
0fb2f7c1bb | ||
|
|
4f07f675a4 | ||
|
|
b8be99606f | ||
|
|
98595c83ea |
No files matched your search
@@ -32,6 +32,10 @@ bin/
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
### Python tools ###
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
### Minecraft Modding ###
|
||||
run/
|
||||
logs/
|
||||
|
||||
@@ -7,9 +7,19 @@ of [Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to
|
||||
Minecraft 1.21.1 on NeoForge. Mystcraft veterans will recognize the writing
|
||||
flow, D'ni-inspired glyphs, libraries, crystals, and instability. Rivencraft
|
||||
also lets you author modern terrain, biomes, structures, seasons, and celestial
|
||||
bodies. It is an active pre-public-release project, not a byte-for-byte copy of
|
||||
bodies. It is an independently developed project, not a byte-for-byte copy of
|
||||
the 1.12.2 mod.
|
||||
|
||||
The gameplay guide below describes the **Minecraft 1.21.1 release**. A separate,
|
||||
incomplete **Minecraft 26.3 beta** is available: Java 25, NeoForge 26.3.0.99,
|
||||
and a first-party Age backend (no Dynamic Dimensions or Bad Packets). It now
|
||||
supports authored terrain/biomes, structure and vegetation pages, independent
|
||||
Age weather, and a growing set of generation features,
|
||||
but does not yet provide full 1.21.1 gameplay or optional-integration parity.
|
||||
See the [26.3 beta guide](ports/mc263/README.md) and
|
||||
[latest beta release notes](ports/mc263/RELEASE_NOTES_1.0.1.65-beta.1.md).
|
||||
Keep the builds and their saves separate, and back up worlds before updating.
|
||||
|
||||
## Getting started
|
||||
|
||||
1. Make an Ink Vial from two black dyes, a glass bottle, and a water bucket.
|
||||
@@ -87,14 +97,21 @@ season enrollment, and instability controls. The client setting
|
||||
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.
|
||||
|
||||
## Project and development
|
||||
|
||||
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://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/issues).
|
||||
Milestone 7 is in progress; its [regression matrix](docs/MILESTONE_7_REGRESSION.md)
|
||||
distinguishes automated checks from manual checks and known limitations.
|
||||
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.
|
||||
|
||||
```sh
|
||||
./gradlew build
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 238 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
@@ -1,6 +1,6 @@
|
||||
# Current implementation (Milestone 7 development)
|
||||
|
||||
This is the maintained technical overview of Rivencraft as of the 0.6.11.0
|
||||
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
|
||||
@@ -41,9 +41,11 @@ and [Age administration](AGES.md).
|
||||
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. Symbol Pages are composed at runtime
|
||||
from Legacy glyph assets; number pages 0–24 set color strength and, when
|
||||
immediately before a Length, Angle, or Phase modifier, their numeric value.
|
||||
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
|
||||
@@ -92,5 +94,5 @@ 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. Issue [#84](https://truenas-scale.tail180fb1.ts.net:8443/dnorberg/rivencraft/issues/84)
|
||||
is the final Milestone 7 acceptance and release gate.
|
||||
combinations. The [Milestone 7 acceptance record](MILESTONE_7_ACCEPTANCE.md)
|
||||
records release evidence and remaining validation limits.
|
||||
+6
-5
@@ -54,12 +54,13 @@ 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 (in progress)
|
||||
## 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 covers regression, migration, and document
|
||||
consolidation; issue #84 is the final milestone acceptance gate. See the
|
||||
[M7 Legacy audit](LEGACY_M7_AUDIT.md), [M7 regression matrix](MILESTONE_7_REGRESSION.md),
|
||||
and [M7 compatibility review](MILESTONE_7_COMPATIBILITY_REVIEW.md).
|
||||
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).
|
||||
@@ -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.
|
||||
@@ -1,7 +1,7 @@
|
||||
# Milestone 7 regression and migration matrix (issue #83)
|
||||
|
||||
This is a **working** acceptance record for the 0.6.11.0 candidate, not the
|
||||
Milestone 7 completion sign-off. Issue #84 owns the final gate. A passing
|
||||
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.
|
||||
|
||||
@@ -11,7 +11,7 @@ multiplayer timing, or every optional-mod combination.
|
||||
| --- | --- | --- |
|
||||
| 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 current candidate, including issue #94's number-page check; 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. |
|
||||
@@ -29,7 +29,14 @@ 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.
|
||||
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)
|
||||
|
||||
@@ -60,6 +67,7 @@ this candidate.
|
||||
| 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
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,913 @@
|
||||
# Minecraft 26.3 port status (issue #96)
|
||||
|
||||
Checked 2026-10-26. Minecraft Java 26.3 is the newest stable Java release with
|
||||
NeoForge support. This is an engineering audit and the start of the port, **not
|
||||
a claim that a 26.3 Rivencraft JAR is ready**. The 1.21.1 build remains the
|
||||
release target until a separate 26.3 client and dedicated-server build passes.
|
||||
The user wants both Minecraft lines maintained after the port, not a one-time
|
||||
26.3 release. Gitea milestone "Dual-version support: 1.21.1 and 26.3"
|
||||
tracks issues #97–#103 under parent #96.
|
||||
|
||||
## Version boundary
|
||||
|
||||
Minecraft 26.3/NeoForge uses Java 25, while Rivencraft 1.21.1 uses Java 21.
|
||||
Build and publish separate artifacts; do not widen the Minecraft dependency
|
||||
range of the 1.21.1 JAR. The official [26.3 release notes](https://www.minecraft.net/en-us/article/minecraft-java-edition-26-3),
|
||||
[NeoForge 26.3 Maven metadata](https://maven.neoforged.net/releases/net/neoforged/neoforge/maven-metadata.xml),
|
||||
[NeoForge Java 25 setting](https://github.com/neoforged/NeoForge/blob/26.3.x/gradle.properties),
|
||||
and [NeoForge migration primers](https://docs.neoforged.net/primer/docs/)
|
||||
are the version sources. NeoForge 26.3.0.99 is the newest listed stable build
|
||||
at the time of this check.
|
||||
|
||||
## Dependency availability
|
||||
|
||||
The entries below mean that a NeoForge 26.3 version is advertised by the
|
||||
project, not that its API or runtime behavior has been validated with
|
||||
Rivencraft. Recheck immediately before choosing versions for a port build.
|
||||
|
||||
| Integration | Role in 1.21.1 | 26.3 availability | Port decision |
|
||||
| --- | --- | --- | --- |
|
||||
| [Dynamic Dimensions AR Edition](https://modrinth.com/mod/dynamic-dimensions-ar-edition) | Required runtime Age loading | No; 1.21.1 only | Keep it for 1.21.1. Use a Rivencraft-owned runtime-dimension backend for 26.3. |
|
||||
| [bad packets](https://modrinth.com/mod/badpackets) | Declared runtime dependency; no direct Java imports in Rivencraft | No advertised 26.3 version | Determine whether the new dimension backend needs it; otherwise remove it from the 26.3 artifact. Use NeoForge's own networking for Rivencraft packets. |
|
||||
| [JEI](https://modrinth.com/mod/jei) | Optional recipe/page guides | Yes | Port and test its plugin against the 26.3 API. |
|
||||
| [TerraBlender](https://modrinth.com/mod/terrablender) | Optional modded biome support | Yes | Port and verify biome discovery and Age generation. |
|
||||
| [Serene Seasons](https://modrinth.com/mod/serene-seasons) and [GlitchCore](https://modrinth.com/mod/glitchcore) | Optional seasons | Yes | Port and repeat the per-Age seasonal matrix. |
|
||||
| [Biomes O' Plenty](https://modrinth.com/mod/biomes-o-plenty) | Optional TerraBlender test addon | Yes | Test modded biome registration and decoration. |
|
||||
| [Ruins (Structure Spawning System)](https://www.curseforge.com/minecraft/mc-mods/ruins-structure-spawning-system) | Optional authored ruins integration | Yes | Port and verify its generation hooks. |
|
||||
| [Create](https://modrinth.com/mod/create) / bundled Ponder | Optional scene renderer | No advertised 26.3 version | Use Rivencraft's existing legacy-animation preview fallback for this target until a compatible release exists. |
|
||||
| [Immersive Portals for (Neo)Forge](https://modrinth.com/mod/immersive-portals-neoforge) | Optional live views and portal teleport integration | No advertised 26.3 version | Use the fallback portal surface and normal Rivencraft linking. Do not claim live destination views on 26.3. |
|
||||
|
||||
Ecliptic Seasons publishes 26.3, but Rivencraft's Ecliptic integration was
|
||||
deferred and is not a replacement for the current Serene integration.
|
||||
|
||||
The alternatives checked for runtime Age loading do not meet the 26.3
|
||||
NeoForge requirement: [upstream Dynamic Dimensions](https://github.com/TeamGalacticraft/DynamicDimensions)
|
||||
and its [AR fork](https://github.com/dercodeKoenig/DynamicDimensions)
|
||||
have no published 26.3 branch or artifact;
|
||||
[DimLib](https://modrinth.com/mod/dimlib) is Fabric-only and stops at 1.21.1;
|
||||
and [DimensionsCore](https://www.curseforge.com/minecraft/mc-mods/dimensionscore-dynamic-dimension-world-management)
|
||||
currently publishes only a Forge 1.20.1 file. Mods that add fixed dimensions
|
||||
or portals do not supply the arbitrary per-book runtime worlds Rivencraft
|
||||
needs. This is a search result, not proof that no other project exists.
|
||||
|
||||
## 26.3 backend design
|
||||
|
||||
The 26.3 port is a **fresh-save target**. It does not need to import 1.21.1
|
||||
Age worlds or migrate Dynamic Dimensions' saved registry data. The 1.21.1
|
||||
artifact will continue using that library. Within 26.3, however, a created
|
||||
Age must retain its identity, seed, chunks and access across save/restart.
|
||||
|
||||
1. Keep the existing `AgeDimensionService`/`AgeDimensionBackend` boundary.
|
||||
Implement a target-specific 26.3 backend without importing Dynamic
|
||||
Dimensions or its seed mixin. The backend contract needs registration at
|
||||
server start, create/load, unload and delete. Return a failure without
|
||||
publishing a half-created Age if any step fails.
|
||||
2. At server startup, reconstruct all non-retired Ages from Rivencraft's
|
||||
`AgeRegistrySavedData` **before** players can join. For a newly bound book,
|
||||
register its unique `rivencraft:age/...` dimension and construct a
|
||||
`ServerLevel` from its authored chunk generator, dimension type and saved
|
||||
Age seed. The seed must feed chunk random state, not only `getSeed()`.
|
||||
3. Register the level with the server's level map and the appropriate
|
||||
dimension registries, then synchronize the new dimension definition to
|
||||
already-connected clients before sending a travel packet. Handle a
|
||||
connecting client receiving every existing Age. Determine the minimal
|
||||
26.3 registry and network hooks from the current NeoForge source; do not
|
||||
assume `/reload` creates new running levels.
|
||||
4. Initialize world border, view/simulation distance, forced chunks, spawn
|
||||
settings, per-Age data storage and tick/save lifecycle as a normal level.
|
||||
On unload or retirement, refuse while players occupy the Age, move any
|
||||
remaining entities safely, flush pending chunk writes, detach the level,
|
||||
and synchronize removal. Retirement must not delete another Age's data.
|
||||
5. Review the MIT-licensed Dynamic Dimensions code as an implementation
|
||||
reference. If any code is adapted, retain its copyright/license notice and
|
||||
identify the derived files. Prefer the smallest backend Rivencraft needs
|
||||
over a general-purpose library fork.
|
||||
|
||||
For gameplay systems, treat the 1.21.1 Rivencraft source and assets as the
|
||||
porting baseline. Carry across unchanged rules, probabilities, recipes, and
|
||||
textures directly; adapt only the Minecraft/NeoForge APIs that changed in
|
||||
26.3. Record deliberate behavior gaps (such as prototype Age generation)
|
||||
instead of silently replacing established mechanics with new ones.
|
||||
|
||||
The release gate is a 26.3 integrated-client **and** dedicated-server matrix:
|
||||
create two Ages with distinct seeds, explore chunks, travel while another
|
||||
client remains online, reconnect, restart, revisit saved chunks, reseed for
|
||||
future chunks, unload/reload, retire one Age without harming the other, and
|
||||
verify normal travel without optional mods. Then repeat relevant paths with
|
||||
available integrations. A failed synchronization must be reported and leave
|
||||
the player in their current level, not crash or strand them.
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. Keep `AgeDimensionService` independent of a concrete runtime-dimension
|
||||
implementation. The 1.21.1 Dynamic Dimensions calls now live in
|
||||
`AgeDimensionBackend`; the `AgeDimensionSeedMixin` is 1.21.1-only.
|
||||
2. Create a separate 26.3/Java 25 build target. Replace the dimension backend,
|
||||
then migrate the main sources through the official NeoForge primers. Do not
|
||||
publish a partial JAR with missing Age generation.
|
||||
3. Port optional integration classes one at a time. Keep unavailable mods
|
||||
optional and preserve their existing fallbacks. Upgrade JEI, TerraBlender,
|
||||
Serene Seasons/GlitchCore, Biomes O' Plenty, and Ruins only in the 26.3
|
||||
target; do not change the validated 1.21.1 dependency set.
|
||||
4. Run compile, data/resource validation, new-world Age generation, Age
|
||||
save/restart/reload, direct and portal/book travel, client rendering, and
|
||||
dedicated-server tests on both targets. Test without optional mods, then
|
||||
with each available integration. Require an in-game check before release.
|
||||
|
||||
The core blocker is **not** a version number change: 26.3 makes a major API
|
||||
jump and the required 1.21.1 runtime-dimension library has no 26.3 build.
|
||||
|
||||
## Build isolation started in #97
|
||||
|
||||
The repository root remains the playable 1.21.1/Java 21 build. A separate
|
||||
`ports/mc263` Gradle build now resolves NeoForge 26.3.0.99 and compiles with
|
||||
Java 25. The build workflow has separate Java 21 and Java 25 jobs. Its JAR
|
||||
task remains disabled; this does not satisfy #97's final artifact criteria.
|
||||
|
||||
## First core slice in #98
|
||||
|
||||
The 26.3 target now has real Rivencraft metadata and an entrypoint. It
|
||||
registers the Crystal Shard and Crystal Block with their reused 1.21.1
|
||||
textures/models plus the 26.3 item-model definitions. `build` passed, and a
|
||||
dedicated development server loaded the mod and reached `Done` with a new
|
||||
world and no optional mods. The server was then stopped. This validates only
|
||||
the entrypoint/registry/resource foundation. A development client also loaded
|
||||
Rivencraft and reloaded its resources without reported missing models or
|
||||
textures. The client logged the host's missing `libflite.so` narrator library;
|
||||
this is unrelated to Rivencraft. Both test processes were stopped after
|
||||
startup. This does **not** validate in-world client rendering or Age
|
||||
functionality. Age generators, data, networking, and client code still
|
||||
need migration; #98 remains open.
|
||||
|
||||
## Runtime-level prototype in #99
|
||||
|
||||
The 26.3 target has a first-party server-side runtime-level loader and a
|
||||
SavedData seed index. It constructs each level with a `LevelStem` seed override,
|
||||
publishes the level only after construction, and restores indexed Ages when
|
||||
the Overworld loads. A dedicated-server probe created two separate Ages with
|
||||
seeds `123456789` and `987654321`; after generating chunks, their terrain
|
||||
fingerprints differed (`2865094` and `3227421`). Both levels, their region
|
||||
files, seeds, and fingerprints survived a server restart, and the startup log
|
||||
shows restoration before `Done`. The temporary operator-only
|
||||
`rivencraft_port age` command exists solely to test this non-distributable
|
||||
port workspace.
|
||||
|
||||
This is **not** issue #99 completion. The loader currently reuses the vanilla
|
||||
Overworld generator because issue #98 has not ported authored generators or
|
||||
Age definitions. The synchronized operator travel probe below does not port
|
||||
normal book travel. The 26.3 JAR task remains disabled.
|
||||
|
||||
## Runtime client-catalog prototype in #100
|
||||
|
||||
The 26.3 target now sends the current dimension-key catalog to each joining
|
||||
client and broadcasts a new catalog when a prototype Age is created or
|
||||
retired. Clients update their known-level set and acknowledge a version; the
|
||||
server's `rivencraft_port age enter <name>` probe refuses travel until that
|
||||
acknowledgement arrives and the target is still loaded. A stale acknowledgement
|
||||
does not satisfy the gate. This follows NeoForge's play-payload registration
|
||||
and client-handler separation. It is not a general dynamic-registry sync for
|
||||
authored dimension types, which have not been ported.
|
||||
|
||||
The dedicated-server probe used two simultaneous 26.3 clients. Both
|
||||
acknowledged a newly created Age without reconnecting; a second client entered
|
||||
an existing runtime Age; a player saved inside an Age reconnected after server
|
||||
restart and remained there. The operator travel probe searches near origin for
|
||||
safe footing and creates a small Crystal Block platform if none is found.
|
||||
This probe is not the final book landing implementation.
|
||||
|
||||
`rivencraft_port age retire <name>` refuses to retire an Age containing a
|
||||
player, then saves and closes it, removes it from the active index, and
|
||||
broadcasts the reduced catalog. On the dedicated test server, the client
|
||||
acknowledged creation and retirement even when they happened back-to-back; a
|
||||
late acknowledgement for the intermediate catalog was ignored. Travel to the
|
||||
retired Age was refused without moving the player, and retiring an occupied
|
||||
Age was refused. The #101 lifecycle slice below now archives exact Age data.
|
||||
|
||||
This remains a **prototype**, not completion of #100: normal Linking and
|
||||
Descriptive Book travel, authored dimension-type/generator registration, full
|
||||
client registry synchronization, and an integrated-client regression pass
|
||||
must follow the core port. The 26.3 JAR remains disabled. The local test
|
||||
server was stopped after verification.
|
||||
|
||||
## Runtime lifecycle prototype in #101
|
||||
|
||||
The operator-only backend probe now supports `age unload`, `reload`, `reseed`,
|
||||
and `retire` in addition to `create`, `list`, `sample`, and `enter`. Unload and
|
||||
retirement refuse a level containing players. A full chunk/entity save and
|
||||
close handles other entities; Minecraft's shutdown-style ticket deactivation
|
||||
and startup-style activation preserve forced chunks across runtime reloads.
|
||||
The loaded-dimension catalog is rebroadcast after unload/reload/reseed and
|
||||
retirement, with stale acknowledgements ignored.
|
||||
|
||||
On the dedicated server, a generated Age unloaded and reloaded with the same
|
||||
terrain fingerprint. Reseeding `lifetest` from `24680` to `13579` kept the
|
||||
previously generated origin fingerprint `2898472`; previously ungenerated
|
||||
chunks at offset 100 produced fingerprint `2897795`, identical to a fresh
|
||||
`13579` control Age and different from a fresh `24680` control (`2863099`).
|
||||
After restart, `lifetest` still reported seed `13579` and its original origin
|
||||
fingerprint. Three consecutive unload/reload cycles with a connected client
|
||||
completed; that client acknowledged the final catalog and intermediate stale
|
||||
acknowledgements were ignored. Unloading the player's current Age was refused.
|
||||
|
||||
A force-loaded chunk and a summoned armor stand with UUID
|
||||
`[I; -872307724, 883641011, -1239011659, 796091120]` survived repeated
|
||||
runtime unload/reload and server restart. The entity could be queried again
|
||||
after its chunk finished loading. The probe retired a different Age,
|
||||
`seedcontrol`: only its directory moved to the sibling `world-retired-ages`
|
||||
archive, other Age directories stayed in place, and reloading/recreating the
|
||||
retired ID was refused after restart. A second archival probe exercised the
|
||||
tombstone-before-move sequence and again moved only its own directory. The
|
||||
tombstone is persisted before the archive move so an interrupted archival
|
||||
cannot silently revive an old Age; an ordinary move failure attempts to
|
||||
restore the index and level. Ten more consecutive unload/reload cycles
|
||||
completed on the dedicated server, with its open file-descriptor count steady
|
||||
at 218 before and after. That is a narrow smoke check, not proof of no leak.
|
||||
|
||||
This still does **not** close #101. The prototype has not been integrated with
|
||||
normal books or the full 26.3 Age definition, nor has a long-running resource
|
||||
leak test or interrupted-archive recovery drill been completed. Its dev world
|
||||
and archive are under ignored `ports/mc263/build`, not a release save.
|
||||
|
||||
## Optional-integration probe in #102
|
||||
|
||||
On 2026-10-27, the published NeoForge 26.3 versions checked were JEI
|
||||
31.17.0.95, TerraBlender 26.3.0.0.9, Biomes O' Plenty 26.3.0.0.15,
|
||||
GlitchCore 26.3.0.0.5, and Serene Seasons 26.1.2.0.7 (its file is labelled
|
||||
for Minecraft 26.3). Ruins advertises 26.3.1NF. Create/Ponder and
|
||||
Immersive Portals still had no advertised compatible version. These are
|
||||
availability checks, not API-compatibility claims.
|
||||
|
||||
The 26.3 development target now has the operator-only
|
||||
`/rivencraft_port compat` command. It reports loaded optional mod IDs and
|
||||
the count of registered biome keys by namespace, so integration smoke tests
|
||||
can distinguish a missing addon from a missing biome registration. It does
|
||||
not change Age generation. A clean dedicated server loaded with no optional
|
||||
mods and reported 67 vanilla biomes. A separate dedicated-server launch with
|
||||
TerraBlender, Biomes O' Plenty, GlitchCore, and Serene Seasons loaded all four;
|
||||
the probe reported 68 `biomesoplenty` biome keys, and TerraBlender logged its
|
||||
Overworld and Nether regions. Both launches stopped cleanly.
|
||||
|
||||
Issue #102 remains open. The 26.3 target still lacks authored Ages, JEI
|
||||
guides, per-Age season policy, Ruins hooks, book previews and crystal portals,
|
||||
so the relevant integration and fallback acceptance tests cannot yet pass.
|
||||
The probe must not be mistaken for feature support. The existing 1.21.1
|
||||
integration code and dependencies remain unchanged.
|
||||
|
||||
## First player-facing bridge in #104
|
||||
|
||||
The 26.3 port now registers a Descriptive Book with the Legacy item texture
|
||||
and a persistent, network-synchronized Age-ID data component. The operator-only
|
||||
`/rivencraft_port age book <name>` probe grants a book for an already indexed
|
||||
Age; an unbound book does not create a world. Using the bound book calls the
|
||||
same server-authoritative, catalog-acknowledged landing path as the existing
|
||||
`age enter` probe. A missing or unloaded Age refuses travel. This avoids
|
||||
trusting a book-supplied seed or creating a new Age from a forged item.
|
||||
|
||||
The dedicated-server smoke test still passed after this bridge, and a 26.3
|
||||
development client loaded the new item model and texture without a missing
|
||||
resource warning. The host still reports a missing Linux `libflite.so`
|
||||
narrator library; this is not a Rivencraft item error. The client was stopped.
|
||||
The player confirmed right-click travel in game. Book component persistence
|
||||
across restart still needs a player test; the headless smoke test cannot
|
||||
exercise it. To test the
|
||||
bridge in a disposable 26.3 world as an operator:
|
||||
|
||||
1. `/rivencraft_port age create booktest 123456789`
|
||||
2. Wait for the Age catalog acknowledgement, then
|
||||
`/rivencraft_port age book booktest`.
|
||||
3. Right-click the granted book and verify arrival in
|
||||
`rivencraft:age/booktest`. Restart client and server, then use the same
|
||||
book again and verify it targets the same saved Age.
|
||||
|
||||
This is deliberately a **port probe**, not the final book workflow: the Book
|
||||
Binder, naming UI, GUI preview, automatic return link, dropped-book entity, authored Age
|
||||
plan, and Legacy landing rules are not yet connected. The bound book currently
|
||||
remains in hand after travel. Do not distribute this target or mark #104 done.
|
||||
For a local player test only, the normally disabled port JAR can be built with
|
||||
`./gradlew -p ports/mc263 build -PallowIncompletePortJar=true`; its filename
|
||||
retains `26.3-INCOMPLETE` and a `port-dev` version marker. This does not enable the normal
|
||||
release artifact or imply that the full mod has been ported.
|
||||
|
||||
The first Prism test exposed a production-only catalog bug: Minecraft 26.3's
|
||||
`ClientPacketListener.levels()` returned an immutable set, and the client
|
||||
handler attempted to clear it. That threw `UnsupportedOperationException`
|
||||
before the client could acknowledge the Age, so book travel was refused. The
|
||||
handler now replaces the entire level set through a narrow access transformer
|
||||
and acknowledges only after replacement. The corrected local test JAR is
|
||||
`rivencraft-26.3-INCOMPLETE-0.0.0.1-port-dev.jar`. It built successfully;
|
||||
the player used that JAR to verify actual travel in game on 2026-10-27.
|
||||
|
||||
### Linking Book return-path slice
|
||||
|
||||
The separate 26.3 target now registers a Linking Book using the Legacy bound
|
||||
book sprite. `/rivencraft_port link book` grants an operator-only probe book
|
||||
bound to the player's exact current dimension, coordinates, and facing. Its
|
||||
destination is a persistent data component resolved by the server on use. A
|
||||
missing dimension, unacknowledged catalog, same-dimension target without
|
||||
Intra-Linking, out-of-bounds target, or obstructed arrival is rejected without
|
||||
moving the player. This path does **not** generate a landing platform.
|
||||
|
||||
For a test, stand at an unobstructed spot in the Overworld and run
|
||||
`/rivencraft_port link book`. Create and bind an Age Descriptive Book using the
|
||||
steps above, travel into that Age, then right-click the Linking Book. It
|
||||
should return to the exact bound spot and facing. Same-dimension use should
|
||||
refuse. Repeat after a server restart to check component persistence. This
|
||||
slice is `0.0.0.2-port-dev` and remains explicitly incomplete: normal
|
||||
crafting/binding, modifiers, book GUI, origin-drop entity, and safe fallback
|
||||
are still unported. The probe book remains in inventory after travel.
|
||||
|
||||
### Age-index durability after the first in-game return-link check
|
||||
|
||||
The first return-link test exposed a recoverable prototype failure: Seamless
|
||||
Portals rejected a newly added runtime Age, the integrated server exited
|
||||
abnormally, and a second Age created just before the exit was not present in
|
||||
Rivencraft's saved index on restart. Its directory contained no saved chunks.
|
||||
The `0.0.0.3-port-dev` slice flushes the index synchronously before announcing
|
||||
newly created Ages to clients. It also flushes reseed changes. The dedicated
|
||||
server smoke test now checks that both new Age IDs are already in the on-disk
|
||||
index **before** it issues `stop`, then verifies both seeds/terrain after a
|
||||
restart. This closes the ordinary interrupted-shutdown gap but is not a
|
||||
substitute for a power-loss durability test or integration with other mods'
|
||||
dynamic-dimension catalogs.
|
||||
|
||||
### Handheld Linking Book binding slice
|
||||
|
||||
The `0.0.0.4-port-dev` target adds a client-only book screen using the Legacy
|
||||
book artwork. An unbound Linking Book accepts a player-chosen title; a
|
||||
serverbound packet binds the held book to the player's *current* dimension,
|
||||
coordinates and facing only after checking the book, title length and empty
|
||||
target. A bound book opens the same screen and uses a server-validated
|
||||
destination-panel click for travel. This replaces the earlier direct item-use
|
||||
probe. The panel is a visible placeholder, **not** a live snapshot. The
|
||||
rotating preview, completed book behavior (including origin drop), recipe,
|
||||
modifiers, stand handling and other port systems remain unported.
|
||||
|
||||
To test this slice, use `/give @s rivencraft:linking_book` in a 26.3 test
|
||||
world, stand at the intended return point, right-click the unbound book, enter
|
||||
a name and bind. Travel to an Age, right-click the book again and click its
|
||||
right-hand destination panel. Verify the named book persists across restart,
|
||||
same-dimension travel is refused, and a replaced/removed held book cannot be
|
||||
bound or used by a delayed screen packet. Do not treat this partial target as
|
||||
a playable release.
|
||||
|
||||
The first `0.0.0.4-port-dev` in-game right-click exposed a 26.3 screen API
|
||||
mistake: `Screen.extractRenderStateWithTooltipAndSubtitles` already calls
|
||||
`extractBackground`, so the screen's second call raised `Can only blur once per
|
||||
frame`. The `0.0.0.5-port-dev` fix removes that duplicate background call.
|
||||
The next in-game check found that a bound Linking Book's right-hand panel did
|
||||
not trigger travel: its target component was persistent but lacked a network
|
||||
codec, so the client screen could not identify the book as bound. The
|
||||
`0.0.0.6-port-dev` slice adds stream codecs for the Linking Book target and
|
||||
Descriptive Book Age ID. Server-side held-item and destination checks remain
|
||||
authoritative. This needs another in-game check before considering the slice
|
||||
validated.
|
||||
The `0.0.0.6-port-dev` retest showed the bound-book panel but left-clicking
|
||||
did not even close the GUI. Minecraft 26.3's primary mouse button is
|
||||
`InputConstants.MOUSE_BUTTON_LEFT` (value 1), while the port handler still
|
||||
checked the 1.21.1 value 0. The `0.0.0.7-port-dev` slice uses the 26.3
|
||||
constant. This still requires in-game verification of the full click/travel
|
||||
path.
|
||||
The player confirmed the `0.0.0.7-port-dev` bound Linking Book click/travel
|
||||
path passes in game on 2026-10-27. The 26.3 target remains incomplete and is
|
||||
not ready for a public release or closure of the parent port issue.
|
||||
|
||||
### Explicit technical alpha exception
|
||||
|
||||
At the player's request, `1.0.1.2-alpha.1` packages this limited 26.3 port as
|
||||
an experimental alpha. It is **not** the full mod and does not pass the stable
|
||||
release gate above. The alpha label and release notes must state that Ages use
|
||||
prototype vanilla terrain, normal authoring and most content are absent, and
|
||||
worlds should be disposable or backed up. The 1.21.1 stable release remains
|
||||
independent; issues #96–#104 stay open until the full port and validation pass.
|
||||
|
||||
The next development slice, `1.0.1.3-alpha.1`, adds a basic Descriptive Book
|
||||
screen and server-validated binding. Naming an unbound book creates a unique
|
||||
prototype Age with a random seed; clicking the bound book's placeholder panel
|
||||
uses the acknowledged-catalog travel gate. This is not authored Page binding
|
||||
or a live destination preview. The player confirmed the Descriptive Book
|
||||
binding and travel interaction in game on 2026-10-29.
|
||||
|
||||
The following `1.0.1.4-alpha.1` development iteration registers a small
|
||||
Rivencraft creative tab for the two Books, Crystal Block, and Crystal Shard.
|
||||
It builds and passes the dedicated-server Age smoke test. The player confirmed
|
||||
the tab and four entries in game on 2026-10-29. No Page categories or
|
||||
unported items are advertised in the 26.3 tab.
|
||||
|
||||
The next local `1.0.1.5-alpha.1` iteration selects Legacy's unwritten
|
||||
Linking Book sprite until a target is bound, then the bound sprite. It also
|
||||
restores the four-Crystal-Shard crafting recipe for a Crystal Block. Both
|
||||
changes passed the player's in-game visual/crafting check on 2026-10-29.
|
||||
|
||||
The local `1.0.1.6-alpha.1` iteration ports Budding Crystal and the four
|
||||
blue crystal growth stages, reusing the 1.21.1 sprites and vanilla amethyst
|
||||
growth/harvest rules. Crystal clusters yield Shards, but natural crystal
|
||||
formation generation remains unported. Test growth with a Creative or
|
||||
command-placed Budding Crystal; the player passed the in-game check.
|
||||
|
||||
The local `1.0.1.7-alpha.1` iteration adds one persistent Page item with
|
||||
unwritten and Link Panel variants. It reuses the existing Page/Link Panel
|
||||
artwork and exposes only the Link Panel variant in its own creative tab,
|
||||
matching the 1.21.1 separation of Link Pages from symbol pages. Blank Pages
|
||||
remain command-only bases; the Ink Mixer, Book Binder and symbol writing are
|
||||
not yet ported. The player passed the in-game visual and persistence check.
|
||||
|
||||
The local `1.0.1.8-alpha.1` iteration ports the Ink Vial item, its animated
|
||||
sprite, and the black-dye/glass-bottle/water-bucket crafting recipe. Using
|
||||
it with paper in the other hand prepares a Link Panel at the current 1.21.1
|
||||
default cost of 2 ink from a 16-use vial. Ink remaining persists per vial;
|
||||
an empty vial yields a glass bottle. The Ink Mixer, configurable ink cost,
|
||||
and Book Binder remain unported. The player passed the in-game craft/use check.
|
||||
|
||||
The local `1.0.1.9-alpha.1` iteration ports the special shapeless leather +
|
||||
Link Panel recipe for an unwritten Linking Book. The 26.3 recipe serializer
|
||||
rejects blank Pages and extra ingredients and preserves a panel's custom name
|
||||
without carrying its Page identity onto the Book. No Link Panel modifiers are
|
||||
ported yet. The player passed the in-game crafting check on 2026-10-29. The
|
||||
full Book Binder GUI and Age authoring remain the next larger port step.
|
||||
|
||||
The local `1.0.1.10-alpha.1` iteration adds the first three Link Panel
|
||||
modifiers as a prerequisite to Binder authoring. All eight combinations of
|
||||
Disarm, Maintain Momentum, and Intra-Linking are testable in the Link Pages
|
||||
creative tab and persist as a synced component through leather crafting and
|
||||
book binding. The server uses the book's flags when linking: Intra-Linking
|
||||
allows same-dimension travel, Maintain Momentum rotates horizontal velocity
|
||||
to the destination yaw, and Disarm drops player inventory at the departure
|
||||
point only after a successful teleport. The Ink Mixer, additional Link Modifier
|
||||
flags, and Descriptive Book panel propagation are not yet ported. In-game
|
||||
testing of each modifier and their combinations passed in game. This local slice
|
||||
must not be committed, pushed or published without the player's explicit request.
|
||||
|
||||
The next local `1.0.1.11-alpha.1` slice introduces the Book Binder block and
|
||||
its first server-authoritative binding workflow. Leather and a Link Panel
|
||||
produce a named, bound Descriptive Book for a random prototype Age; the output
|
||||
is persisted in the Binder until collected. The panel's Disarm and Maintain
|
||||
Momentum flags carry onto the book and apply after successful Age travel;
|
||||
Intra-Linking is inert for Descriptive Books. This is not yet symbol-page
|
||||
authoring or the full Legacy Binder interface. Build and dedicated-server
|
||||
smoke pass. The player passed basic Binder crafting and travel in game, but
|
||||
reported that typing E closed the GUI and asked us to reuse the 1.21.1 GUI
|
||||
more faithfully. No
|
||||
commit, push, or publication without an explicit request during alpha.
|
||||
|
||||
The local `1.0.1.12-alpha.1` follow-up ports the existing Binder layout's
|
||||
top title field, flank slots, center Page strip, and pulsing red missing-panel
|
||||
cue. It persists the title on the block entity and consumes focused keyboard
|
||||
events before the container can treat E as inventory-close. Click the output
|
||||
once to bind and again to collect the result. The full authored-symbol strip
|
||||
and 1.21.1 grammar still await later port slices. The same slice adapts the
|
||||
existing 1.21.1 Ink Mixer block, persistent basin, additive probabilities,
|
||||
16-ink-per-panel exchange, virtual output, Legacy atlas and animated glyph to
|
||||
the 26.3 storage, menu and GUI APIs. The build and dedicated-server Age smoke
|
||||
pass. In-game Binder and Mixer verification is
|
||||
pending; do not commit, push, or publish without explicit authorization.
|
||||
|
||||
The local `1.0.1.13-alpha.1` iteration moves the Binder's freshly assembled
|
||||
book directly to the cursor so one output click both binds and takes it. Its
|
||||
central strip now displays five Legacy page thumbnails, while only the Link
|
||||
Panel is currently writable. The Mixer recomputes its virtual output in the
|
||||
same server click as an input change to address an intermittent first-attempt
|
||||
delay. The user's second Mixer attempt succeeded but exposed a cost mismatch:
|
||||
the Mixer now spends one ink and one paper per Link Panel and resets its
|
||||
additive chances on output pickup. Build passes; in-game retest is
|
||||
pending. This alpha remains local until explicitly authorized for release.
|
||||
|
||||
The local `1.0.1.14-alpha.1` visual follow-up uses the new Binder dev images
|
||||
as a reference: the page strip now grows only when a Link Panel is present or
|
||||
carried, its required-panel cue appears above the strip, and the first page
|
||||
can be clicked to place/take the panel. The stored page has a tooltip. The
|
||||
rest of the 1.21.1 strip (symbol pages and scrolling) depends on porting the
|
||||
symbol-page data and authoring pipeline, not just drawing more placeholders.
|
||||
The local `1.0.1.15-alpha.1` follow-up changes the missing-panel cue to the
|
||||
Link Panel's 16×16 item icon while preserving its red pulse and tooltip.
|
||||
|
||||
The local `1.0.1.33-beta.1` slice adds Flat and Void terrain to authored Binder
|
||||
books. Flat uses vanilla superflat layers and can use one authored Biome; Void
|
||||
generates no terrain but can use one authored Biome for environmental behavior.
|
||||
Without a Biome page, Void uses `minecraft:the_void`. Books with multiple Biome
|
||||
pages and Flat/Void terrain remain unassembled until their custom distribution
|
||||
generator is ported. The dedicated-server smoke creates, samples, and reloads
|
||||
both terrain families in a disposable world.
|
||||
|
||||
The local `1.0.1.34-beta.1` follow-up moves Flat ground to Y=0 and targets its
|
||||
surface for travel. `1.0.1.35-beta.1` enables the selected biome's decoration
|
||||
on Flat. `1.0.1.36-beta.1` ports a multi-biome Flat/Void chunk generator and
|
||||
accepts the existing authored distribution pages for those terrains. New Flat
|
||||
chunks can vary vegetation by biome; Void keeps biome climate without adding
|
||||
a floor or decorations. These are beta-port slices, not full 1.21.1 parity.
|
||||
|
||||
The local `1.0.1.37-beta.1` slice ports Cave terrain authoring using the
|
||||
26.3 caves noise preset. Cave and Nether Biome pages can be distributed
|
||||
through an Age, and travel searches for footing from Y=64 downward rather
|
||||
than mistaking the cave roof for the surface. Nether-style cave material
|
||||
replacement from 1.21.1 remains a separate port task. The local
|
||||
`1.0.1.38-beta.1` follow-up
|
||||
keeps Cave Ages dry by default: the vanilla 26.3 Cave preset otherwise fills
|
||||
low openings with water. This changes newly generated chunks only; a future
|
||||
Flooded page will deliberately set an authored water level in both maintained
|
||||
versions.
|
||||
The local `1.0.1.39-beta.1` follow-up makes the 26.3 landing search reject
|
||||
fluid at feet and head height and prevents a submerged fallback platform.
|
||||
|
||||
The local `1.0.1.40-beta.1` slice gives pageless Descriptive Books a
|
||||
seed-selected terrain and biome plan. It follows the 1.21.1 rank weights for
|
||||
terrain and biome distribution, selects registered Cave/Nether biomes for Cave
|
||||
Ages and overworld-source biomes for other non-Void terrains, and persists the
|
||||
resolved choices. Other random Age traits remain a later port task.
|
||||
|
||||
The local `1.0.1.41-beta.1` slice adds the first Age-scoped feature wrapper to
|
||||
the 26.3 generator. A Block page followed by Rocks now places clusters of the
|
||||
chosen block in new Standard, Amplified, Island, or Flat terrain chunks. The
|
||||
Age index persists that selection, and the wrapper delegates all vanilla
|
||||
terrain, biome, and structure generation. Cave and Void are intentionally
|
||||
excluded until their feature placement rules are ported. This is not yet the
|
||||
full 1.21.1 feature grammar or instability system.
|
||||
|
||||
The local `1.0.1.42-beta.1` follow-up resolves unmodified Rocks to a
|
||||
seed-shuffled, full-block material and accepts up to 32 ordered Rocks pages.
|
||||
Each page adds an independent, deterministic surface placement attempt.
|
||||
Explicit Block modifiers still bind only to the immediately following Rocks
|
||||
page. The material sequence persists in the Age index; the codec also reads
|
||||
the single-material format from the previous beta. The remaining feature
|
||||
families and instability scoring are not yet ported.
|
||||
|
||||
The local `1.0.1.43-beta.1` slice uses 26.3's `minecraft:forest_rock`
|
||||
placed-feature rules and `BlockBlobFeature` for authored Rocks, supplying the
|
||||
Age's selected material and removing only the vanilla biome filter. This
|
||||
replaces the port's temporary 3×3 pile algorithm and follows the 1.21.1
|
||||
generator's authored-placement approach. It affects new chunks only.
|
||||
|
||||
The local `1.0.1.44-beta.1` slice extends this Age-scoped feature wrapper to
|
||||
Flowers, Trees, and Huge Trees. Ordered authored pages retain independent
|
||||
attempts and use vanilla 26.3 placed-feature rules after biome decoration.
|
||||
The vegetation plan is persisted alongside terrain, biome, and Rocks plans;
|
||||
the dedicated-server smoke compares generated flowers and logs against a
|
||||
same-seed Flat control and checks persistence after restart.
|
||||
|
||||
The local `1.0.1.45-beta.1` slice adds authored Dungeons separately from
|
||||
surface vegetation. Its 26.3 placed-feature JSON mirrors the 1.21.1 eight
|
||||
monster-room attempts over the world height; ordered duplicate pages add
|
||||
attempts. The count persists in the Age index and generator codec. The server
|
||||
smoke creates and samples a dungeon Age, counts monster spawners across a
|
||||
4×4-chunk area, and verifies the count after restart. Room appearance and
|
||||
normal book authoring remain in-game checks.
|
||||
|
||||
The local `1.0.1.46-beta.1` slice ports Surface Lakes, Deep Lakes, and Small
|
||||
Lakes, including ordered Fluid modifiers. The 26.3 LakeFeature retains its
|
||||
vanilla shape and replacement predicates while using the Age's frozen fluid
|
||||
provider. Surface placement keeps Rivencraft 1.21.1's one-in-twelve rarity;
|
||||
underground lakes use vanilla placement. Repeated unmodified pages resolve
|
||||
their fluids independently from a seeded, shuffled source-fluid pool. Lake
|
||||
plans participate in restore, unload/reload, reseed, and retirement rollback.
|
||||
The server smoke compares water generation against a same-seed control and
|
||||
checks explicit/random plans and fluid blocks across restart.
|
||||
|
||||
The local `1.0.1.47-beta.1` slice adapts the 1.21.1 FloatingIslandFeature
|
||||
algorithm to 26.3 chunk access and the biomes already assigned to each chunk.
|
||||
It retains source range 3, one-in-128 candidate rarity, Y=168..215 center
|
||||
selection, radii 24..41, coordinate-noise contours, air-only filling, and the
|
||||
biome-dependent soil cap. It supports Flat/Void in addition to noise terrains;
|
||||
Block modifiers and independent seeded random materials are frozen in an
|
||||
ordered terrain-feature plan. Rocks and islands share material resolution.
|
||||
|
||||
26.3's final NoiseBasedChunkGenerator merges noise, surface, and carving and
|
||||
does not expose a pre-surface hook. This port appends islands to the completed
|
||||
terrain future before decoration; existing terrain is preserved, but base
|
||||
carvers do not cut the added islands. The smoke verifies actual high-altitude
|
||||
material placement on noise, Flat, and Void terrain, distinct random material
|
||||
choices, authored biome preservation, and block fingerprints across restart.
|
||||
|
||||
The local `1.0.1.48-beta.1` slice ports TendrilFeature's Legacy-derived
|
||||
branching solid volumes using the 26.3 chunk mutation and height APIs. Source
|
||||
range 8, rate 15, size 18, nested node/height selection, branch splitting, and
|
||||
bedrock preservation match the 1.21.1 implementation. Chunk-local writes and
|
||||
neighbor-source seeds preserve continuous paths across chunk borders.
|
||||
|
||||
The existing terrain-feature plan now supports Tendrils on all terrain types,
|
||||
including Cave and Void. Compilation retains mixed feature order and applies
|
||||
the 32-occurrence limit per feature. Block modifiers and seeded random choices
|
||||
use the shared solid-material resolver; existing save/lifecycle handling carries
|
||||
both feature types. World validation is manual at the player's request; no
|
||||
development server is started for this slice.
|
||||
|
||||
The local `1.0.1.49-beta.1` slice ports the Legacy-derived Skylands filter
|
||||
from Rivencraft 1.21.1 with 26.3 chunk height/mutation APIs. The cutoff uses
|
||||
the same salt, seven octaves, initial cell width 128, amplitude 12, and base
|
||||
Y=76. Solid/liquid removal and world-height coverage match the reference.
|
||||
It runs once after base terrain construction and before authored solid
|
||||
volumes, retaining the feature ordering used by the 1.21.1 implementation.
|
||||
|
||||
Skylands is a material-free feature, accepted alongside every terrain type.
|
||||
Repeated pages retain the same idempotent effect. The existing terrain-feature
|
||||
plan stores an optional Skylands flag, defaulting to false for earlier beta
|
||||
Ages; no separate backend/lifecycle path is introduced. A Skylands-only plan
|
||||
can contain no material choices. Build and static checks are automated; world
|
||||
generation and restart validation are left to the player.
|
||||
|
||||
The local `1.0.1.50-beta.1` slice adapts the 1.21.1 SpheresFeature and
|
||||
SpikesFeature to 26.3. Spheres retain their source range 8, five-percent
|
||||
source chance, nested Y selection starting at 32, node geometry, and bedrock
|
||||
protection. They run after the base terrain/Skylands filter and work on every
|
||||
terrain type. Spikes retain their one-in-18 chunk chance, 6..37 height roll,
|
||||
width 1..4, solid-footprint checks, and chunk-local writes during decoration.
|
||||
They remain unavailable on Cave and Void terrain, matching 1.21.1.
|
||||
|
||||
Both page types use ordered saved solid-feature choices, preceding Block
|
||||
modifiers, and independent seeded materials/occurrences. No backend lifecycle
|
||||
branch or generated artwork is added. Compilation and static review are the
|
||||
automated checks; the player handles generation and restart validation.
|
||||
|
||||
The local `1.0.1.51-beta.1` slice ports ObelisksFeature using the 26.3 chunk
|
||||
coordinates and world-height APIs. The site salt, seed mixing, one-in-128
|
||||
chance, chunk-local site selection, 4×4 plinth, and 2×12×2 shaft match 1.21.1.
|
||||
Every footing column must be sturdy, dry, and free of block entities; the
|
||||
base surface can vary by at most two blocks and the full shaft must be open.
|
||||
No partial structure is written when a placement check fails.
|
||||
|
||||
The page joins the existing material-feature plan and shared material resolver.
|
||||
It runs during surface decoration, with its own occurrence counter, on all
|
||||
surface terrain types including Flat; Cave and Void remain unsupported.
|
||||
Spikes and Obelisks use shared stage/restriction classification. Build/static
|
||||
checks are automated; generation and restart checks remain with the player.
|
||||
|
||||
The local `1.0.1.52-beta.1` slice ports CrystalFormationsFeature directly
|
||||
from 1.21.1 with only registry, ChunkPos, and height API substitutions. It
|
||||
retains the site salt, one-in-fifteen rarity, 3×3 ground/clearance checks,
|
||||
three-block center height, randomized outer heights, budding hearts, and
|
||||
initial small buds. Existing blue crystal growth, sprites, drops, and sounds
|
||||
are reused without adding assets.
|
||||
|
||||
Crystal Formations is an ordered surface feature with fixed crystal material,
|
||||
not a consumer of Block/Fluid modifiers. It shares the existing saved feature
|
||||
plan and surface-terrain restrictions (including Flat, excluding Cave/Void),
|
||||
with independent occurrence seeds and the current 32-page cap. No backend
|
||||
lifecycle changes are needed. The Obelisks slice passed the player's in-game
|
||||
check; Crystal Formations build/static checks pass, with generation, growth,
|
||||
and restart validation handed to the player. No test server is started.
|
||||
|
||||
The local `1.0.1.53-beta.1` slice adapts 1.21.1 AgeDenseOres to modern
|
||||
`AbstractOreFeature` targets and `FeaturePlacer.placeWithBiomeCheck`. It keeps
|
||||
the common ore tag/name fallback, explicit `rivencraft:dense_ores` placed-
|
||||
feature opt-in tag, nearby biome discovery, stable registry-ID ordering,
|
||||
and independent per-occurrence seeds. Registered modded ore features remain
|
||||
eligible without depending on an ore-providing mod. Biomes and ore placements
|
||||
are collected once per chunk rather than once per repeated page.
|
||||
|
||||
The optional Dense Ores count is saved in the existing terrain-feature plan;
|
||||
older plans default to zero. Grammar accepts 1–32 occurrences on noise terrain
|
||||
(including Cave), rejects Flat/Void and material modifiers, and checks explicit
|
||||
biome pools for at least one eligible ore feature. No extra lifecycle path or
|
||||
asset is introduced. Random feature selection and instability remain later
|
||||
port work. Crystal Formations passed the player's in-game check; this slice
|
||||
passes build/static checks, with ore generation and restart validation manual.
|
||||
|
||||
The local `1.0.1.54-beta.1` slice ports Star Fissures. Its footprint matches
|
||||
1.21.1 exactly (including Legacy attribution); carving uses the dimension's
|
||||
minimum Y, clears fluids, and protects Cave terrain's roof. The authored flag
|
||||
is optional in the existing feature-plan codec. Duplicate pages retain the
|
||||
1.21.1 single-fissure result; all terrains are accepted, with no material
|
||||
modifier. Random feature selection is not introduced in this slice.
|
||||
|
||||
First arrival generates the fissure and stores its spawn/completion in per-Age
|
||||
SavedData. Re-entry/restart does not repeat carving; an obstructed saved arrival
|
||||
is rechecked for safe footing. The return block ports modern entity-inside and
|
||||
TeleportTransition APIs, has a registered Rivencraft block-entity type, and
|
||||
uses Minecraft's starfield renderer with 1.21.1 face offsets. Shared Overworld
|
||||
spawn safety search and fallback match 1.21.1, rather than respawning at a bed.
|
||||
The existing fissure OGG/model are reused. Client renderer registration stays
|
||||
client-only; the block has no item/drop and is inert in the Overworld.
|
||||
|
||||
Dense Ores passed the player's in-game check. Build, footprint comparison,
|
||||
resource packaging, and static checks pass for this slice. Generation, visual
|
||||
rendering, actual return travel, and restart checks are left to the player;
|
||||
no development server is started.
|
||||
|
||||
The local `1.0.1.55-beta.1` slice begins the authored structure-set port with
|
||||
Villages, Mineshafts, and Strongholds. Saved page occurrences use the existing
|
||||
feature plan's new optional structure list. Terrain gates, random-spread
|
||||
frequency/spacing scaling, and bounded concentric-ring count/spread follow
|
||||
1.21.1. Modern `AbstractSpreadingStructurePlacement` accessors replace the old
|
||||
placement-class accessors. Scaled holder copies never mutate global registries.
|
||||
|
||||
Structure-state planning includes authored eligibility biomes while continuing
|
||||
to sample the actual terrain source. A scoped Structure mixin carries over
|
||||
1.21.1's authored biome override. The wrapper handles structure starts itself
|
||||
when a structure plan exists, allowing the hook to recognize the authored Age;
|
||||
unrelated/unplanned generators remain delegated. Only these three controlled
|
||||
families are gated in this slice; remaining structure families, random picks,
|
||||
and biome-mismatch instability remain unported. Existing plans without the
|
||||
optional list retain their previous structure path. Mod metadata now correctly
|
||||
calls this target Beta rather than Alpha.
|
||||
|
||||
The player passed Star Fissures. Build/static checks pass for this new slice.
|
||||
A disposable dedicated-server startup reached `Done` without mixin-loading
|
||||
errors. Its console `/stop` command hit a null-level CommandSourceStack error;
|
||||
the exact test JVM was then stopped through SIGTERM and saved/shut down. This
|
||||
was a startup check, not authored generation or density validation. The
|
||||
disposable world is removed; client authoring, actual structures, density,
|
||||
biome overrides, and restart checks are handed to the player.
|
||||
|
||||
The local `1.0.1.56-beta.1` follow-up corrects the structure biome override.
|
||||
Client logs confirmed the build-55 mixin was applied, but 26.3 JigsawPlacement
|
||||
checks `GenerationContext.couldStructureExistInColumn` inside generation,
|
||||
before the final check bypassed by the 1.21.1 hook. The replacement ModifyArg
|
||||
changes only the biome predicate passed to the GenerationContext constructor
|
||||
in Structure.generate for explicitly authored Age structures. Both early
|
||||
and final checks now see the override; all other generation constraints and
|
||||
unrelated generators retain their existing behavior.
|
||||
|
||||
Build/static checks pass. An isolated `--initSettings` bootstrap initialized
|
||||
settings without a mixin application error, then FML exited nonzero because
|
||||
that settings-only mode creates no server thread. This is not a full server
|
||||
or generation pass. No world was created, and the temporary settings directory
|
||||
is removed. In-game validation requires a new Age or ungenerated chunks;
|
||||
previously rejected chunks are not regenerated. No commit/release is made.
|
||||
|
||||
The local `1.0.1.57-beta.1` slice completes the existing 1.21.1 structure-page
|
||||
mapping (Dungeons remains its separate placed feature) and enables 26.3's
|
||||
Abandoned Camps page. The shared planner now recognizes 15 structure sets.
|
||||
Page/terrain restrictions match AgeStructureSelection for the 1.21.1 families;
|
||||
Abandoned Camps follows surface noise terrain. No material modifiers are added.
|
||||
|
||||
The separate Fortress/Bastion set resources are reused directly from 1.21.1.
|
||||
The controlled-set gate includes the vanilla combined Nether set, preventing
|
||||
unrequested cross-family placement. All structure pages share the previously
|
||||
validated saved list, density logic, and corrected context-level biome override.
|
||||
Unknown non-spreading placements are left unchanged rather than cast unsafely.
|
||||
Unmodified/no-authored-structure Ages retain their prior native path; previously
|
||||
saved authored plans now gate all supported families in new chunks. Instability
|
||||
and random feature selection remain pending.
|
||||
|
||||
The player passed the build-56 biome fix. Build, diff checks, all 15 registry
|
||||
and Creative-page mappings, and target structure resource existence checks pass.
|
||||
Packaged Fortress/Bastion definitions match 1.21.1 byte-for-byte. No server is
|
||||
started; actual structure placement, terrain-specific limits, repeated density,
|
||||
and restart validation are handed to the player. Changes remain local.
|
||||
|
||||
The local `1.0.1.58-beta.1` slice ports Caves/Ravines feature pages separately
|
||||
from Cave terrain. A new optional saved carver-page list participates in the
|
||||
existing feature-plan lifecycle. Grammar accepts noise terrain including Cave,
|
||||
rejects Flat/Void and material modifiers, and checks the modern CARVER registry.
|
||||
|
||||
26.3 combines carving with terrain construction and exposes carvers that emit
|
||||
geometry rather than mutate blocks. A scoped NoiseBasedChunkGenerator mixin
|
||||
adds authored geometry to its native CarvingMask immediately before that mask
|
||||
is tested/applied. It only acts for the matching runtime Age wrapper, using
|
||||
its frozen seed and actual biome resolver. Vanilla applies the combined mask
|
||||
with its own aquifer/surface rules; no closed NoiseChunk is accessed afterward.
|
||||
The source range, salts, Nether cave selection, and Nether ravine skip match
|
||||
1.21.1. Duplicate Caves/Ravines repeat identical seeds in the baseline, so the
|
||||
port avoids redundant identical mask passes while retaining authored pages.
|
||||
Added terrain volumes still follow the combined terrain operation, as noted
|
||||
for the earlier Floating Islands port.
|
||||
|
||||
The player passed structure slice 57. Build/static checks and packaging checks
|
||||
for the hook, Creative entries, and three required vanilla carvers pass. A
|
||||
settings-only bootstrap completed initialization without a mixin application
|
||||
error; FML then returned nonzero because no server thread exists in that mode.
|
||||
This is not world-generation validation. No world was created; the temporary
|
||||
settings directory is removed. Actual carving and restart checks are manual,
|
||||
and no commit/push/publication is performed.
|
||||
|
||||
The local `1.0.1.59-beta.1` slice replaces stale per-page port information
|
||||
and adds the requested red hue for unsupported pages. A shared
|
||||
PortAuthoredWriting.isPageImplemented classifier uses the same terrain,
|
||||
controller, material/biome, and feature recognizers as the grammar, without
|
||||
misclassifying context-dependent modifiers/controllers by testing them alone.
|
||||
Page tooltips, the extracted item-render color, and the Binder background use
|
||||
this single availability check. Tinting does not generate extra textures or
|
||||
change stored page data. Corrupted-page names/descriptions remain obfuscated.
|
||||
|
||||
Port language overrides remove the old Standard/one-Biome claim, distinguish
|
||||
implemented/unimplemented pages and invalid combinations, and correct all
|
||||
15 structure-family descriptions, material adjacency, and selected terrain/
|
||||
feature restrictions. The 1.21.1 language assets are unchanged. This is a UI
|
||||
and information slice, not new generation functionality.
|
||||
|
||||
The player passed Caves/Ravines. Build, diff checks, unique language-key checks,
|
||||
and packaged-language override checks pass. No server/world is started.
|
||||
Inventory tint, Binder tint/layering, and tooltip readability await the
|
||||
player's visual check. Changes remain local and unpublished.
|
||||
|
||||
The local `1.0.1.60-beta.1` slice implements the four requested 26.3-only
|
||||
vegetation pages: Poplar Trees, Fallen Poplar Trees, Red Shrubs, and Shelf
|
||||
Mushrooms. They extend PortVegetationPlan recognition, so grammar, saved plans,
|
||||
Creative status, item tint, and tooltip availability agree without another
|
||||
page list. The existing ordered vegetation cap and surface-terrain/biome
|
||||
restrictions are retained. The 1.21.1 code/assets are unchanged.
|
||||
|
||||
Poplar Trees selects only the three vanilla poplar variants, retaining their
|
||||
relative native weights and six placement attempts. Fallen poplars reuse the
|
||||
vanilla feature but add authored in-square/surface placement before its sapling
|
||||
check; the vanilla placed feature alone has no surface-height placement. Red
|
||||
Shrubs directly reuses its registered patch. No tree/shrub algorithm or artwork
|
||||
is duplicated.
|
||||
|
||||
Shelf Mushrooms is a tree decorator in 26.3, not a placed feature. The authored
|
||||
page therefore applies ShelfMushroomDecorator to existing connected log hosts
|
||||
at its position in the vegetation sequence. Section palettes skip log-free
|
||||
sections, repeated pages reuse the host scan, and tree placements invalidate
|
||||
that cache. Host ordering and page seeds are deterministic; callback writes
|
||||
are chunk-local, fluid-free, supported, and height-bounded. The decorator keeps
|
||||
its native standing/fallen logic and side/age/adjacency checks. This is an
|
||||
authored extension using the native decorator, not a new vanilla feature ID.
|
||||
|
||||
Build/static and resource-reference checks pass. The four pages' descriptions
|
||||
explain their actual effects and requirements. No server/world is started;
|
||||
the player validates vegetation appearance, support/tint, ordering, and restart.
|
||||
Changes remain local and unpublished.
|
||||
|
||||
The local `1.0.1.61-beta.1` follow-up separates log-hosted Shelf Mushrooms
|
||||
from surface plants. A shared requiresSurface classification now governs
|
||||
terrain/explicit-biome validation; decoration handles Shelf Mushrooms before
|
||||
the surface-biome skip. Trees, shrubs, and flowers retain their restrictions.
|
||||
Void/Cave/End-biome log hosts are no longer excluded by the vegetation path.
|
||||
|
||||
The host scan is a stable log-position list rather than connected tree groups.
|
||||
Native tree decorators assume a meaningful base and only decorate a limited
|
||||
height band, which is unsuitable for log tendrils. The authored extension
|
||||
therefore checks exposed log faces at all heights, reusing the native decorator's
|
||||
replaceability/water predicate and block facing/age/survival rules, with spacing
|
||||
and chunk bounds. Cached scans still avoid repeated full-volume work. No tree
|
||||
page or log-producing page is required, but actual log hosts must exist during
|
||||
chunk generation. This supersedes slice 60's whole-tree height-band behavior.
|
||||
|
||||
Build/static checks pass. Manual validation: new Void + Oak Log + Tendrils +
|
||||
Shelf Mushrooms Age, without any tree page; inspect exposed log faces and
|
||||
restart/new-chunk generation. No server is started and nothing is published.
|
||||
|
||||
The player passed the local `1.0.1.62-beta.1` sound slice: the complete 1.21.1
|
||||
sound manifest/OGGs are reused, book travel emits modifier-selected audio and
|
||||
cloud bursts, and Star Fissures play audio after arrival as well as departure.
|
||||
Portal/meteor events are registered but their gameplay ports remain separate.
|
||||
|
||||
The local `1.0.1.63-beta.1` slice adds authored Normal/Fast/Slow/Off/Always/Storm
|
||||
weather policies. One policy is accepted per book, with availability/tint and
|
||||
tooltips following the shared classifier. Cave terrain always suppresses it.
|
||||
An omitted policy defaults to Normal for now; random-trait selection is pending.
|
||||
|
||||
Unlike 1.21.1, 26.3 ServerLevel.getWeatherData delegates to the server's shared
|
||||
weather object. A scoped Runtime Age ServerLevel subclass instead loads native
|
||||
WeatherData from that Age's data storage, alongside its saved policy. Native
|
||||
weather ticking and client packets remain in charge; only the data owner and
|
||||
Legacy-style duration policy change. The constructor replaces inherited global
|
||||
rain/thunder visual levels before publishing the level. Retirement, reseeding,
|
||||
reload and restart retain the policy through existing per-Age storage lifecycle.
|
||||
Vanilla /weather remains a global server command; the Age policy is not a
|
||||
global command override. Native biome precipitation rules still apply.
|
||||
|
||||
Build/diff checks pass. A disposable dedicated server created all six weather
|
||||
policies: every Age reported independent native data and the expected flags/
|
||||
duration ranges. Setting global thunder left the Off Age clear. Unload/reload
|
||||
retained Storm, and a full stop/restart restored all six policies and their
|
||||
live timers. Both servers shut down cleanly; the temporary test world is
|
||||
deleted after completion. Client appearance, cross-Age travel, authoring/tint,
|
||||
and Cave suppression remain manual checks.
|
||||
No commit/push/publication is authorized by the player's testing pass alone.
|
||||
|
||||
The local `1.0.1.64-beta.1` follow-up fixes weather-only authoring. Build 63's
|
||||
server weather checks bypassed the Binder's biome compiler, whose exclusion
|
||||
list did not yet include weather. As a result weather/storm was incorrectly
|
||||
resolved as a biome ID despite grammar acceptance. The biome compiler now
|
||||
selects actual biome-category entries after full-book grammar validation;
|
||||
controllers retain their separate handling. This avoids repeating the same
|
||||
missed-exclusion bug as more non-biome page families are ported. The operator
|
||||
weather fixture now calls that compiler before backend creation. All six
|
||||
weather-only symbol lists compiled and created their expected weather states
|
||||
on a fresh dedicated server. Build/diff checks pass. The server shut down
|
||||
cleanly and its disposable world was removed; player GUI validation remains.
|
||||
|
||||
The local `1.0.1.65-beta.1` spawn correction traces non-Cave arrival columns
|
||||
from maximum build height, inspecting actual fully generated chunk blocks.
|
||||
The configured Flat-layer shortcut could select ground below later authored
|
||||
terrain, and the old heightmap-only path did not explicitly ensure an exposed
|
||||
surface. The new path stops at the first fluid/leaves/collision, requires dry
|
||||
footing and headroom, and otherwise tries the next column rather than searching
|
||||
below an obstruction. Chunk lookup is cached per column and a mutable position
|
||||
avoids per-air-block allocations. A fallback platform goes above the actual
|
||||
highest obstruction; empty Void retains floor Y=63. Surface footing accepts
|
||||
leaves, rejects fluid-bearing floors, and checks build/border bounds.
|
||||
|
||||
Stored non-Cave Star Fissure arrivals also use the surface search rather than
|
||||
accepting old underground pockets. Cave terrain keeps its Y=64-down search.
|
||||
Exact bound Linking Books and the general fissure Overworld-return anchor
|
||||
search are not redirected to the sky. A read-only surface probe command helps
|
||||
validate the chosen column/nearby surface (chunk generation is possible).
|
||||
Build/diff checks pass. A disposable dedicated-server probe selected Y=4 on
|
||||
plain Flat ground, Y=141 above leaves placed at Y=140, and a neighboring dry
|
||||
column instead of descending below water added above those leaves. A larger
|
||||
roof-fixture fill was rejected by vanilla because some target chunks were not
|
||||
loaded; that fixture is not claimed as tested. The server shut down cleanly,
|
||||
and its temporary world was deleted. Actual player arrival in the affected
|
||||
Age and the unchanged Cave/Linking Book paths remain manual checks.
|
||||
|
||||
## Verification of this first slice
|
||||
|
||||
The 1.21.1 `compileJava` and `build` tasks passed offline after the backend
|
||||
extraction. The GameTest server ran 93 tests; 92 passed, while
|
||||
`twoblockdeskdropsonce` failed because breaking either desk half dropped zero
|
||||
desks instead of one. That desk failure is outside the dimension-backend edit
|
||||
and must be resolved or rechecked before a release. The 1.21.1 `build` task
|
||||
passed again after the 26.3 registry slice.
|
||||
On 2026-11-01, the default 1.21.1 GameTest server was rerun and all 93
|
||||
required tests passed, including the desk test. Issue #97 was then closed;
|
||||
remaining 26.3 parity work stays under #98–#104.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Publishing Rivencraft with the Gitea, Modrinth, and CurseForge APIs
|
||||
|
||||
The local helper is [`scripts/publish_mod_sites.py`](../scripts/publish_mod_sites.py).
|
||||
It previews a release unless `--publish` is explicitly supplied. The current
|
||||
configuration is for the Minecraft **26.3** artifact only. The 1.21.1 line needs
|
||||
its required external dependencies mapped on both sites before API publishing
|
||||
is enabled for it.
|
||||
|
||||
## One-time token setup
|
||||
|
||||
For Gitea, an existing personal access token is sufficient if it has
|
||||
`write:repository` scope. That scope covers release creation and attachment
|
||||
upload; do not grant `all` if a narrower token is available. Create or inspect
|
||||
tokens under [Gitea user settings → Applications](https://git.paddedcell.org/user/settings/applications).
|
||||
The helper checks token identity and scope without displaying the secret.
|
||||
|
||||
Create a [Modrinth personal access token](https://docs.modrinth.com/api/#authentication)
|
||||
in account settings with `VERSION_CREATE` (upload) and `USER_READ` (read-only
|
||||
credential check) scopes. Create a
|
||||
[CurseForge Upload API token](https://support.curseforge.com/support/solutions/articles/9000197321)
|
||||
from the author's API Tokens page. Treat both as passwords: never put them in
|
||||
git, chat, a release note, or a command-line argument.
|
||||
|
||||
In a terminal at the repository root, run these one at a time and paste the
|
||||
corresponding token into the hidden prompt:
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_mod_sites.py store-token modrinth
|
||||
python3 scripts/publish_mod_sites.py store-token curseforge
|
||||
python3 scripts/publish_mod_sites.py store-token gitea
|
||||
```
|
||||
|
||||
The tokens are stored in the desktop Secret Service keyring under
|
||||
`application=rivencraft-publishing`, separately by site. The script also accepts
|
||||
`RIVENCRAFT_MODRINTH_TOKEN` or `RIVENCRAFT_CURSEFORGE_TOKEN` environment variables,
|
||||
but these take precedence over keyring entries and should not be put in shell
|
||||
history or startup files.
|
||||
|
||||
Verify the credentials without uploading anything:
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_mod_sites.py check-token modrinth
|
||||
python3 scripts/publish_mod_sites.py check-token curseforge
|
||||
python3 scripts/publish_mod_sites.py check-token gitea
|
||||
```
|
||||
|
||||
## Release workflow
|
||||
|
||||
Build the JAR, write a Markdown changelog, and preview the metadata:
|
||||
|
||||
```sh
|
||||
python3 scripts/publish_mod_sites.py release \
|
||||
--jar ports/mc263/build/libs/rivencraft-26.3-1.0.1.32-beta.1.jar \
|
||||
--changelog ports/mc263/RELEASE_NOTES_1.0.1.32-beta.1.md \
|
||||
--version 1.0.1.32-beta.1 \
|
||||
--name 'Rivencraft 1.0.1.32-beta.1 — NeoForge 26.3' \
|
||||
--minecraft 26.3 --java 25 --channel beta
|
||||
```
|
||||
|
||||
For a *new* version only, append `--publish` to upload to both sites. Use
|
||||
`--site modrinth` or `--site curseforge` to target one site. Modrinth rejects an
|
||||
existing version number; CurseForge may not, so check its file list before any
|
||||
retry after a partial failure. CurseForge approval may take additional time.
|
||||
|
||||
The default destination is now all three sites. The Gitea release step requires
|
||||
the annotated version tag to have been pushed first. Gitea's `Vx.y.z.b-mc26.3-beta.1`
|
||||
tag convention is derived from the version and channel; an existing release
|
||||
with different title or notes is never overwritten. If release creation
|
||||
succeeds but its JAR upload fails, rerun with `--site gitea --publish` to resume
|
||||
only the missing attachment. `--site both` retains the earlier Modrinth and
|
||||
CurseForge-only behavior.
|
||||
|
||||
The helper uses Gitea's documented
|
||||
[`POST /repos/{owner}/{repo}/releases`](https://docs.gitea.com/api/operations/repo-create-release/)
|
||||
and [release attachment endpoint](https://docs.gitea.com/api/operations/repo-create-release-attachment/),
|
||||
plus Modrinth's documented multipart
|
||||
[`POST /version`](https://docs.modrinth.com/api/operations/createversion/) and
|
||||
CurseForge's documented
|
||||
[`POST /api/projects/{projectId}/upload-file`](https://support.curseforge.com/support/solutions/articles/9000197321)
|
||||
with the JAR, changelog, version channel, NeoForge, Minecraft version, Java
|
||||
version, and client/server environment. It sets no external dependencies for
|
||||
26.3, which uses Rivencraft's first-party Age backend.
|
||||
+1
-1
@@ -13,7 +13,7 @@ former behavior or loader version.
|
||||
| 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 | [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) |
|
||||
| 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
|
||||
|
||||
@@ -35,6 +35,7 @@ The Writing Desk retains 88 collection tabs, numbered from zero as in Legacy. Le
|
||||
| `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. |
|
||||
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ 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.6.11.0
|
||||
mod_version=1.0.1.1
|
||||
immersive_portals_version=6.0.7
|
||||
terrablender_version=4.1.0.8
|
||||
serene_seasons_version_id=pHEgQQUE
|
||||
|
||||
@@ -0,0 +1,581 @@
|
||||
# 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. Celestial/lighting 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.
|
||||
|
||||
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,202 @@
|
||||
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
|
||||
|
||||
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.65-beta.1
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"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.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,29 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
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) 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,
|
||||
AgeCatalogPayload::new
|
||||
);
|
||||
|
||||
public AgeCatalogPayload {
|
||||
dimensions = List.copyOf(dimensions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type<AgeCatalogPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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("1");
|
||||
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));
|
||||
}
|
||||
|
||||
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,172 @@
|
||||
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)
|
||||
).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 AgeLevelIndex() {
|
||||
this.ages = new LinkedHashMap<>();
|
||||
this.retired = new LinkedHashSet<>();
|
||||
this.biomeOverrides = new LinkedHashMap<>();
|
||||
this.biomePlans = new LinkedHashMap<>();
|
||||
this.terrain = new LinkedHashMap<>();
|
||||
this.rocks = new LinkedHashMap<>();
|
||||
this.vegetation = new LinkedHashMap<>();
|
||||
this.dungeons = new LinkedHashMap<>();
|
||||
this.lakes = new LinkedHashMap<>();
|
||||
this.terrainFeatures = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
|
||||
Map<Identifier, Identifier> biomeOverrides,
|
||||
Map<Identifier, PortBiomePlan> biomePlans,
|
||||
Map<Identifier, PortAgeTerrain> terrain,
|
||||
Map<Identifier, PortRockPlan> rocks,
|
||||
Map<Identifier, PortVegetationPlan> vegetation,
|
||||
Map<Identifier, Integer> dungeons,
|
||||
Map<Identifier, PortLakePlan> lakes,
|
||||
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures) {
|
||||
this.ages = new LinkedHashMap<>(ages);
|
||||
this.retired = new LinkedHashSet<>(retired);
|
||||
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
|
||||
this.biomePlans = new LinkedHashMap<>(biomePlans);
|
||||
this.terrain = new LinkedHashMap<>(terrain);
|
||||
this.rocks = new LinkedHashMap<>(rocks);
|
||||
this.vegetation = new LinkedHashMap<>(vegetation);
|
||||
this.dungeons = new LinkedHashMap<>(dungeons);
|
||||
this.lakes = new LinkedHashMap<>(lakes);
|
||||
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
|
||||
}
|
||||
|
||||
Map<Identifier, Long> entries() {
|
||||
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); }
|
||||
|
||||
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||
if (contains(id)) {
|
||||
throw new IllegalArgumentException("Age already exists: " + id);
|
||||
}
|
||||
ages.put(id, seed);
|
||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void retire(Identifier id) {
|
||||
if (ages.remove(id) == null) throw new IllegalArgumentException("Age not indexed: " + id);
|
||||
biomeOverrides.remove(id);
|
||||
biomePlans.remove(id);
|
||||
terrain.remove(id);
|
||||
rocks.remove(id);
|
||||
vegetation.remove(id);
|
||||
dungeons.remove(id);
|
||||
lakes.remove(id);
|
||||
terrainFeatures.remove(id);
|
||||
retired.add(id);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
|
||||
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
|
||||
PortVegetationPlan vegetationPlan, int dungeonCount,
|
||||
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
|
||||
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
|
||||
ages.put(id, seed);
|
||||
if (biomePlan != null) biomePlans.put(id, biomePlan);
|
||||
if (ageTerrain != PortAgeTerrain.STANDARD) terrain.put(id, ageTerrain);
|
||||
if (rockPlan != null) rocks.put(id, rockPlan);
|
||||
if (vegetationPlan != null) vegetation.put(id, vegetationPlan);
|
||||
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
|
||||
if (lakePlan != null) lakes.put(id, lakePlan);
|
||||
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
|
||||
setDirty();
|
||||
}
|
||||
|
||||
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,668 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.brigadier.arguments.LongArgumentType;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
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.util.Prediction;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.tags.BlockTags;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||
import net.neoforged.fml.ModList;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Operator-only backend and synchronized-travel probes, not normal book behavior. */
|
||||
public final class PortAgeCommands {
|
||||
private PortAgeCommands() {
|
||||
}
|
||||
|
||||
public static void register(RegisterCommandsEvent event) {
|
||||
event.getDispatcher().register(Commands.literal("rivencraft_port")
|
||||
.requires(Commands.hasPermission(Commands.LEVEL_ADMINS))
|
||||
.then(Commands.literal("compat")
|
||||
.executes(context -> {
|
||||
StringBuilder mods = new StringBuilder("Optional mods: ");
|
||||
for (String id : new String[]{"jei", "terrablender", "biomesoplenty",
|
||||
"sereneseasons", "glitchcore", "ruins", "create",
|
||||
"ponder", "immersive_portals_core"}) {
|
||||
if (mods.length() > "Optional mods: ".length()) mods.append(", ");
|
||||
mods.append(id).append('=').append(ModList.get().isLoaded(id));
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(mods.toString()), false);
|
||||
Map<String, Integer> biomeCounts = new TreeMap<>();
|
||||
context.getSource().getServer().registryAccess()
|
||||
.lookupOrThrow(Registries.BIOME).listElementIds()
|
||||
.forEach(key -> biomeCounts.merge(key.identifier().getNamespace(), 1, Integer::sum));
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Registered biomes by namespace: " + biomeCounts), false);
|
||||
return 1;
|
||||
}))
|
||||
.then(Commands.literal("link")
|
||||
.then(Commands.literal("book")
|
||||
.executes(context -> {
|
||||
ServerPlayer player = context.getSource().getPlayerOrException();
|
||||
ItemStack book = new ItemStack(Rivencraft.LINKING_BOOK.get());
|
||||
LinkBookTarget target = LinkBookTarget.at(player);
|
||||
book.set(Rivencraft.LINK_BOOK_TARGET.get(), target);
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal(
|
||||
"Link to " + target.dimension()));
|
||||
if (!player.getInventory().add(book)) player.drop(book, false, Prediction.SERVER_ONLY);
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Granted Linking Book bound to " + target.dimension() + " at "
|
||||
+ player.blockPosition()), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("age")
|
||||
.then(Commands.literal("surface_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||
.then(Commands.argument("z", IntegerArgumentType.integer())
|
||||
.executes(PortAgeCommands::probeSurface)))))
|
||||
.then(Commands.literal("create_weather")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("mode", StringArgumentType.word())
|
||||
.suggests((context, builder) -> net.minecraft.commands.SharedSuggestionProvider.suggest(
|
||||
java.util.Arrays.stream(PortWeatherMode.values())
|
||||
.map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder))
|
||||
.executes(PortAgeCommands::createWeatherAge)))))
|
||||
.then(Commands.literal("weather_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeWeather)))
|
||||
.then(Commands.literal("create")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.STANDARD))
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
PortAgeTerrain terrain;
|
||||
try {
|
||||
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(
|
||||
context, "terrain").toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal(
|
||||
"Unknown terrain; use standard, amplified, island, cave, flat, or void"));
|
||||
return 0;
|
||||
}
|
||||
return createAge(context, terrain);
|
||||
})))))
|
||||
.then(Commands.literal("create_multi")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
PortAgeTerrain terrain;
|
||||
try {
|
||||
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(
|
||||
context, "terrain").toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal(
|
||||
"Unknown terrain; use cave, flat, or void"));
|
||||
return 0;
|
||||
}
|
||||
if (terrain != PortAgeTerrain.CAVE
|
||||
&& terrain != PortAgeTerrain.FLAT
|
||||
&& terrain != PortAgeTerrain.VOID) {
|
||||
context.getSource().sendFailure(Component.literal(
|
||||
"Multi-biome probe supports cave, flat, or void only"));
|
||||
return 0;
|
||||
}
|
||||
return createAge(context, terrain, new PortBiomePlan(
|
||||
PortBiomePlan.Distribution.TILED,
|
||||
terrain == PortAgeTerrain.CAVE
|
||||
? java.util.List.of(Identifier.withDefaultNamespace("lush_caves"),
|
||||
Identifier.withDefaultNamespace("dripstone_caves"))
|
||||
: java.util.List.of(Identifier.withDefaultNamespace("plains"),
|
||||
Identifier.withDefaultNamespace("forest"))));
|
||||
})))))
|
||||
.then(Commands.literal("create_random")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
|
||||
context.getSource().getServer(), seed);
|
||||
int created = createAge(context, selected.terrain(), selected.biomes());
|
||||
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Random Age plan: terrain=" + selected.terrain()
|
||||
+ " biomes=" + selected.biomes()), false);
|
||||
return created;
|
||||
}))))
|
||||
.then(Commands.literal("create_rocks")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.FLAT,
|
||||
null, new PortRockPlan(Identifier.withDefaultNamespace("gold_block")))))))
|
||||
.then(Commands.literal("create_rocks_multi")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||
fourGoldRocks(LongArgumentType.getLong(context, "seed")))))))
|
||||
.then(Commands.literal("create_rocks_random")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(
|
||||
java.util.List.of(Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/rocks")),
|
||||
LongArgumentType.getLong(context, "seed")))))))
|
||||
.then(Commands.literal("create_vegetation")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.FLAT, null,
|
||||
null, testVegetation())))))
|
||||
.then(Commands.literal("create_dungeons")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||
null, null, null, testDungeons())))))
|
||||
.then(Commands.literal("create_lakes")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||
null, null, null, 0, testLakes(
|
||||
LongArgumentType.getLong(context, "seed"), false))))))
|
||||
.then(Commands.literal("create_lakes_random")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||
null, null, null, 0, testLakes(
|
||||
LongArgumentType.getLong(context, "seed"), true))))))
|
||||
.then(Commands.literal("create_islands")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(context -> createIslandAge(context, false))))))
|
||||
.then(Commands.literal("create_islands_random")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(context -> createIslandAge(context, true))))))
|
||||
.then(Commands.literal("list")
|
||||
.executes(context -> {
|
||||
var ages = RuntimeAgeBackend.savedAges(context.getSource().getServer());
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Saved prototype Ages: " + ages), false);
|
||||
return ages.size();
|
||||
}))
|
||||
.then(Commands.literal("retire")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
String name = StringArgumentType.getString(context, "name");
|
||||
Identifier id = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "age/" + name);
|
||||
try {
|
||||
RuntimeAgeBackend.retire(context.getSource().getServer(), id);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Retired " + id + "; exact Age data archived"), true);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("unload")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
Identifier id = ageId(context);
|
||||
try {
|
||||
RuntimeAgeBackend.unload(context.getSource().getServer(), id);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal("Unloaded " + id), true);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("reload")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
Identifier id = ageId(context);
|
||||
try {
|
||||
RuntimeAgeBackend.reload(context.getSource().getServer(), id);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal("Reloaded " + id), true);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("reseed")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> {
|
||||
Identifier id = ageId(context);
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
try {
|
||||
RuntimeAgeBackend.reseed(context.getSource().getServer(), id, seed);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Reseeded " + id + " to " + seed + " for future chunks"), true);
|
||||
return 1;
|
||||
}))))
|
||||
.then(Commands.literal("enter")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
ServerPlayer player = context.getSource().getPlayerOrException();
|
||||
Identifier id = ageId(context);
|
||||
String failure = PortAgeTravel.enter(player, id);
|
||||
if (failure != null) {
|
||||
context.getSource().sendFailure(Component.literal(failure));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Entered " + id + " after Age catalog acknowledgement"), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("book")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> {
|
||||
ServerPlayer player = context.getSource().getPlayerOrException();
|
||||
Identifier id = ageId(context);
|
||||
if (!RuntimeAgeBackend.savedAges(context.getSource().getServer()).containsKey(id)) {
|
||||
context.getSource().sendFailure(Component.literal("Age not active: " + id));
|
||||
return 0;
|
||||
}
|
||||
ItemStack book = new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
book.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal(
|
||||
StringArgumentType.getString(context, "name")));
|
||||
if (!player.getInventory().add(book)) player.drop(book, false, Prediction.SERVER_ONLY);
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Granted bound Descriptive Book for " + id), false);
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("sample")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> sampleAge(context, 0))
|
||||
.then(Commands.argument("chunkOffset", IntegerArgumentType.integer(0))
|
||||
.executes(context -> sampleAge(context,
|
||||
IntegerArgumentType.getInteger(context, "chunkOffset"))))))
|
||||
.then(Commands.literal("rocks_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeRocks)))
|
||||
.then(Commands.literal("vegetation_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeVegetation)))
|
||||
.then(Commands.literal("dungeons_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeDungeons)))
|
||||
.then(Commands.literal("lakes_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeLakes)))
|
||||
.then(Commands.literal("islands_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeIslands)))
|
||||
.then(Commands.literal("biome")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("x", IntegerArgumentType.integer())
|
||||
.then(Commands.argument("z", IntegerArgumentType.integer())
|
||||
.executes(PortAgeCommands::sampleBiome)))))));
|
||||
}
|
||||
|
||||
private static int probeSurface(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
int x = IntegerArgumentType.getInteger(context, "x");
|
||||
int z = IntegerArgumentType.getInteger(context, "z");
|
||||
var obstruction = PortAgeTravel.highestObstruction(level, x, z);
|
||||
var landing = PortAgeTravel.findSurfaceLanding(level, x, z);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Surface probe " + id
|
||||
+ " highest=" + obstruction + " landing=" + landing), false);
|
||||
return landing == null ? 0 : 1;
|
||||
}
|
||||
|
||||
private static int createWeatherAge(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
PortWeatherMode mode = PortWeatherMode.valueOf(
|
||||
StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT));
|
||||
Identifier id = ageId(context);
|
||||
var server = context.getSource().getServer();
|
||||
// Exercise the Binder's page compiler too, not just direct backend creation.
|
||||
var pages = java.util.List.of(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"weather/" + mode.name().toLowerCase(Locale.ROOT)));
|
||||
var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
pages, server.registryAccess());
|
||||
RuntimeAgeBackend.create(server, id,
|
||||
LongArgumentType.getLong(context, "seed"), biomePlan, PortAgeTerrain.FLAT,
|
||||
null, null, 0, null, null, mode);
|
||||
return probeWeather(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int probeWeather(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
var server = context.getSource().getServer();
|
||||
ServerLevel loaded = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (!(loaded instanceof PortAgeLevel level)) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
var weather = level.getWeatherData();
|
||||
context.getSource().sendSuccess(() -> Component.literal("Weather probe " + id
|
||||
+ " mode=" + level.weatherMode() + " raining=" + weather.isRaining()
|
||||
+ " thundering=" + weather.isThundering() + " rainTicks=" + weather.getRainTime()
|
||||
+ " thunderTicks=" + weather.getThunderTime()
|
||||
+ " independent=" + (weather != server.getWeatherData())
|
||||
+ " overworldRaining=" + server.getWeatherData().isRaining()
|
||||
+ " overworldThundering=" + server.getWeatherData().isThundering()), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context, PortAgeTerrain terrain) {
|
||||
return createAge(context, terrain, null);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan) {
|
||||
return createAge(context, terrain, biomePlan, null);
|
||||
}
|
||||
|
||||
private static int probeRocks(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|
||||
|| features.rocks() == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age has no loaded Rocks generator: " + id));
|
||||
return 0;
|
||||
}
|
||||
var materials = features.rocks().materials().stream()
|
||||
.map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
|
||||
int count = 0;
|
||||
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
||||
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING, x, z) - 1;
|
||||
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
|
||||
if (materials.stream().anyMatch(state::is)) count++;
|
||||
}
|
||||
int placed = count;
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Rocks probe " + id + " materials=" + features.rocks().materials() + " count=" + placed), false);
|
||||
return placed;
|
||||
}
|
||||
|
||||
private static int probeVegetation(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
int flowers = 0;
|
||||
int logs = 0;
|
||||
for (int x = 0; x < 64; x++) for (int z = 0; z < 64; z++) {
|
||||
for (int y = 4; y < 32; y++) {
|
||||
var state = level.getBlockState(new net.minecraft.core.BlockPos(x, y, z));
|
||||
if (state.is(BlockTags.FLOWERS)) flowers++;
|
||||
if (state.is(BlockTags.LOGS)) logs++;
|
||||
}
|
||||
}
|
||||
int flowerCount = flowers;
|
||||
int logCount = logs;
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Vegetation probe " + id + " flowers=" + flowerCount + " logs=" + logCount), false);
|
||||
return flowerCount + logCount;
|
||||
}
|
||||
|
||||
private static PortVegetationPlan testVegetation() {
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/flowers"));
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/trees"));
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/huge_trees"));
|
||||
}
|
||||
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.vegetationPlan(pages);
|
||||
}
|
||||
|
||||
private static int testDungeons() {
|
||||
Identifier page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dungeons");
|
||||
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(
|
||||
java.util.Collections.nCopies(8, page));
|
||||
}
|
||||
|
||||
private static int probeDungeons(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
|
||||
context.getSource().sendFailure(Component.literal("Age has no loaded feature generator: " + id));
|
||||
return 0;
|
||||
}
|
||||
int count = features.dungeons();
|
||||
int spawners = 0;
|
||||
for (int chunkX = 0; chunkX < 4; chunkX++) for (int chunkZ = 0; chunkZ < 4; chunkZ++) {
|
||||
var chunk = level.getChunk(chunkX, chunkZ);
|
||||
for (var pos : chunk.getBlockEntitiesPos()) {
|
||||
if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++;
|
||||
}
|
||||
}
|
||||
int placed = spawners;
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Dungeons probe " + id + " pages=" + count + " spawners=" + placed), false);
|
||||
return placed;
|
||||
}
|
||||
|
||||
private static PortLakePlan testLakes(long seed, boolean randomFluids) {
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 8; i++) {
|
||||
for (String kind : java.util.List.of("surface_lakes", "deep_lakes", "lava_lakes")) {
|
||||
if (!randomFluids) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||
Identifier.withDefaultNamespace("water")));
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/" + kind));
|
||||
}
|
||||
}
|
||||
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed);
|
||||
}
|
||||
|
||||
private static int probeLakes(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
PortLakePlan lakes = level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features
|
||||
? features.lakes() : null;
|
||||
// Finish neighboring features before counting, since lake bowls can
|
||||
// cross a chunk boundary. Include underground lakes in the probe.
|
||||
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
|
||||
int[] fluids = new int[2];
|
||||
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
|
||||
level.getChunk(x, z).findBlocks(state -> state.is(Blocks.WATER) || state.is(Blocks.LAVA),
|
||||
(pos, state) -> fluids[state.is(Blocks.WATER) ? 0 : 1]++);
|
||||
}
|
||||
int waterCount = fluids[0];
|
||||
int lavaCount = fluids[1];
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Lakes probe " + id + " pages=" + (lakes == null ? 0 : lakes.choices().size())
|
||||
+ " fluids=" + (lakes == null ? java.util.List.of() : lakes.choices().stream()
|
||||
.map(PortLakePlan.Choice::fluid).distinct().toList())
|
||||
+ " plan=" + (lakes == null ? 0 : lakes.choices().hashCode())
|
||||
+ " water=" + waterCount + " lava=" + lavaCount), false);
|
||||
return waterCount + lavaCount;
|
||||
}
|
||||
|
||||
private static int createIslandAge(CommandContext<CommandSourceStack> context, boolean randomMaterials) {
|
||||
PortAgeTerrain terrain;
|
||||
try {
|
||||
terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal("Unknown island test terrain"));
|
||||
return 0;
|
||||
}
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
|
||||
pages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/forest"));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (!randomMaterials) pages.add(net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||
Identifier.withDefaultNamespace("gold_block")));
|
||||
pages.add(PortTerrainFeaturePlan.FLOATING_ISLANDS);
|
||||
}
|
||||
try {
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(
|
||||
pages, context.getSource().getServer().registryAccess());
|
||||
var features = net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed);
|
||||
return createAge(context, terrain, biomes, null, null, 0, null, features);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int probeIslands(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)
|
||||
|| features.terrainFeatures() == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age has no loaded island generator: " + id));
|
||||
return 0;
|
||||
}
|
||||
var choices = features.terrainFeatures().choices();
|
||||
var materials = choices.stream().map(PortTerrainFeaturePlan.Choice::material).distinct().toList();
|
||||
var blocks = materials.stream().map(net.minecraft.core.registries.BuiltInRegistries.BLOCK::getValue).toList();
|
||||
for (int x = -1; x <= 4; x++) for (int z = -1; z <= 4; z++) level.getChunk(x, z);
|
||||
int[] count = {0};
|
||||
long[] fingerprint = {0};
|
||||
for (int x = 0; x < 4; x++) for (int z = 0; z < 4; z++) {
|
||||
level.getChunk(x, z).findBlocks(state -> !state.isAir(), (pos, state) -> {
|
||||
if (pos.getY() < 128 || pos.getY() >= 240) return;
|
||||
if (blocks.stream().anyMatch(state::is)) count[0]++;
|
||||
fingerprint[0] += pos.asLong() * 0x9E3779B97F4A7C15L ^ state.toString().hashCode();
|
||||
});
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Islands probe " + id + " pages=" + choices.size() + " materials=" + materials
|
||||
+ " count=" + count[0] + " fingerprint=" + fingerprint[0]), false);
|
||||
return count[0];
|
||||
}
|
||||
|
||||
/** Exercise the actual ordered writing parser, not only the generator codec. */
|
||||
private static PortRockPlan fourGoldRocks(long seed) {
|
||||
Identifier block = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||
Identifier.withDefaultNamespace("gold_block"));
|
||||
Identifier feature = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/rocks");
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 4; i++) {
|
||||
pages.add(block);
|
||||
pages.add(feature);
|
||||
}
|
||||
return net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan, PortRockPlan rockPlan) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, null);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, 0);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, null);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons, PortLakePlan lakes) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, null);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) {
|
||||
String name = StringArgumentType.getString(context, "name");
|
||||
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
ServerLevel level;
|
||||
try {
|
||||
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
|
||||
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Created " + id + " with seed " + level.getSeed() + " terrain=" + terrain), true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int sampleAge(com.mojang.brigadier.context.CommandContext<net.minecraft.commands.CommandSourceStack> context,
|
||||
int chunkOffset) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(
|
||||
ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
int fingerprint = 1;
|
||||
for (int x = 0; x < 2; x++) {
|
||||
for (int z = 0; z < 2; z++) {
|
||||
int chunkX = chunkOffset + x * 4;
|
||||
int chunkZ = chunkOffset + z * 4;
|
||||
level.getChunk(chunkX, chunkZ);
|
||||
fingerprint = 31 * fingerprint + level.getHeight(
|
||||
Heightmap.Types.WORLD_SURFACE, chunkX * 16, chunkZ * 16);
|
||||
}
|
||||
}
|
||||
int result = fingerprint;
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
id + " seed=" + level.getSeed() + " chunkOffset=" + chunkOffset + " terrain=" + result), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int sampleBiome(CommandContext<CommandSourceStack> context) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
|
||||
return 0;
|
||||
}
|
||||
int x = IntegerArgumentType.getInteger(context, "x");
|
||||
int z = IntegerArgumentType.getInteger(context, "z");
|
||||
level.getChunk(x >> 4, z >> 4);
|
||||
Identifier biome = level.getBiome(new net.minecraft.core.BlockPos(x, 4, z))
|
||||
.unwrapKey().orElseThrow().identifier();
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
id + " biome at " + x + "," + z + "=" + biome), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static Identifier ageId(com.mojang.brigadier.context.CommandContext<net.minecraft.commands.CommandSourceStack> context) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"age/" + StringArgumentType.getString(context, "name"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.List;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
import net.minecraft.world.level.saveddata.WeatherData;
|
||||
import net.minecraft.world.level.storage.DerivedLevelData;
|
||||
|
||||
/** Runtime Age with independent native weather, rather than 26.3's shared server cycle. */
|
||||
final class PortAgeLevel extends ServerLevel {
|
||||
private final WeatherData ageWeather;
|
||||
private final PortWeatherSettings weatherSettings;
|
||||
private final boolean cave;
|
||||
|
||||
PortAgeLevel(MinecraftServer server, ResourceKey<Level> key, LevelStem stem,
|
||||
long biomeZoomSeed, PortAgeTerrain terrain) {
|
||||
super(server, server.executor, server.storageSource,
|
||||
new DerivedLevelData(server.getWorldData(), server.getWorldData().overworldData()),
|
||||
key, stem, server.getWorldData().isDebugWorld(), biomeZoomSeed, List.of(), false);
|
||||
ageWeather = getDataStorage().computeIfAbsent(WeatherData.TYPE);
|
||||
weatherSettings = getDataStorage().computeIfAbsent(PortWeatherSettings.TYPE);
|
||||
cave = terrain == PortAgeTerrain.CAVE;
|
||||
PortAgeWeather.apply(this);
|
||||
// The base constructor initializes visual levels from server weather.
|
||||
// Replace them before publishing this Age or sending its join packets.
|
||||
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WeatherData getWeatherData() {
|
||||
// The base constructor may query weather before our fields are initialized.
|
||||
return ageWeather == null ? super.getWeatherData() : ageWeather;
|
||||
}
|
||||
|
||||
PortWeatherMode weatherMode() { return cave ? PortWeatherMode.OFF : weatherSettings.mode(); }
|
||||
|
||||
void configureWeather(PortWeatherMode mode) {
|
||||
weatherSettings.setMode(mode);
|
||||
PortAgeWeather.apply(this);
|
||||
setRainLevel(ageWeather.isRaining() ? 1.0F : 0.0F);
|
||||
setThunderLevel(ageWeather.isThundering() ? 1.0F : 0.0F);
|
||||
getDataStorage().saveAndJoin();
|
||||
}
|
||||
}
|
||||
@@ -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,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,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,91 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** 26.3 port of Rivencraft 1.21.1's Legacy-style 4×4 plinth and 2×12×2 shaft. */
|
||||
final class PortObelisksFeature {
|
||||
private static final long SALT = 0x4F42454C49534B53L;
|
||||
private static final int CHANCE = 128;
|
||||
private static final int BASE = 4;
|
||||
private static final int HEIGHT = 12;
|
||||
|
||||
private PortObelisksFeature() {}
|
||||
|
||||
record Site(int x, int z) {}
|
||||
|
||||
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||
^ (long) chunk.x() * 341873128712L ^ (long) chunk.z() * 132897987541L
|
||||
^ (long) occurrence * 0x9E3779B97F4A7C15L));
|
||||
if (random.nextInt(CHANCE) != 0) return null;
|
||||
return new Site(chunk.getMinBlockX() + 4 + random.nextInt(8),
|
||||
chunk.getMinBlockZ() + 4 + random.nextInt(8));
|
||||
}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed, BlockState material, int occurrence) {
|
||||
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||
if (site == null) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
int minSurface = Integer.MAX_VALUE;
|
||||
int maxSurface = Integer.MIN_VALUE;
|
||||
int[][] surface = new int[BASE][BASE];
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
int x = site.x() + dx;
|
||||
int z = site.z() + dz;
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||
if (y <= level.getMinY() + 1) return;
|
||||
BlockState ground = level.getBlockState(pos.set(x, y - 1, z));
|
||||
if (!ground.isFaceSturdy(level, pos, Direction.UP)
|
||||
|| !ground.getFluidState().isEmpty() || ground.hasBlockEntity()) return;
|
||||
surface[dx][dz] = y;
|
||||
minSurface = Math.min(minSurface, y);
|
||||
maxSurface = Math.max(maxSurface, y);
|
||||
}
|
||||
}
|
||||
if (maxSurface - minSurface > 2 || maxSurface + HEIGHT >= level.getMaxY()) return;
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean open(BlockState state) {
|
||||
return state.getFluidState().isEmpty() && (state.isAir() || state.canBeReplaced());
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
|
||||
/** Seed-frozen terrain and biome choices for a book with no authored symbols. */
|
||||
public final class PortRandomAgeSelection {
|
||||
private PortRandomAgeSelection() {}
|
||||
|
||||
public record Selection(PortAgeTerrain terrain, PortBiomePlan biomes) {}
|
||||
|
||||
public static Selection select(MinecraftServer server, long seed) {
|
||||
// Match the 1.21.1 Legacy-rank choices: Standard and Flat are common;
|
||||
// the other terrain families are rarer. The sorted order is deliberate.
|
||||
PortAgeTerrain terrain = weighted(new Random(seed ^ 0x5445525241494E4CL),
|
||||
List.of(PortAgeTerrain.AMPLIFIED, PortAgeTerrain.CAVE, PortAgeTerrain.FLAT,
|
||||
PortAgeTerrain.ISLAND, PortAgeTerrain.STANDARD, PortAgeTerrain.VOID),
|
||||
new int[]{1, 1, 5, 1, 6, 1});
|
||||
if (terrain == PortAgeTerrain.VOID)
|
||||
return new Selection(terrain, new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
||||
List.of(Identifier.withDefaultNamespace("the_void"))));
|
||||
|
||||
PortBiomePlan.Distribution distribution = weighted(new Random(seed ^ 0x524956454E435241L),
|
||||
List.of(PortBiomePlan.Distribution.NATIVE, PortBiomePlan.Distribution.SINGLE,
|
||||
PortBiomePlan.Distribution.MEDIUM, PortBiomePlan.Distribution.TILED,
|
||||
PortBiomePlan.Distribution.GRID, PortBiomePlan.Distribution.TINY,
|
||||
PortBiomePlan.Distribution.SMALL, PortBiomePlan.Distribution.LARGE,
|
||||
PortBiomePlan.Distribution.HUGE),
|
||||
new int[]{3, 3, 3, 1, 1, 1, 1, 1, 1});
|
||||
List<Identifier> eligible = eligibleBiomes(server, terrain);
|
||||
if (eligible.size() < 2 && distribution != PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
// 26.3 has no native multi-cave biome source; choose a seeded Cave
|
||||
// biome rather than silently using Lush Caves for every random Age.
|
||||
if (terrain == PortAgeTerrain.CAVE && distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
distribution = PortBiomePlan.Distribution.SINGLE;
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, List.of()));
|
||||
|
||||
int count = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
if (count > 1) {
|
||||
Random countRandom = new Random(seed ^ 0x42494F4D45434F55L);
|
||||
while (count < eligible.size() && countRandom.nextInt(3) != 0) count++;
|
||||
}
|
||||
List<Identifier> shuffled = new ArrayList<>(eligible);
|
||||
Collections.shuffle(shuffled, new Random(seed ^ 0x42494F4D45504F4FL));
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
||||
}
|
||||
|
||||
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||
if (terrain == PortAgeTerrain.CAVE) {
|
||||
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(PortRegistryPages::isCaveBiome)
|
||||
.map(holder -> holder.key().identifier())
|
||||
.sorted().toList();
|
||||
return caves.isEmpty() ? List.of(Identifier.withDefaultNamespace("lush_caves")) : caves;
|
||||
}
|
||||
List<Identifier> surface = server.overworld().getChunkSource().getGenerator().getBiomeSource()
|
||||
.possibleBiomes().stream().flatMap(holder -> holder.unwrapKey().stream())
|
||||
.map(key -> key.identifier()).distinct().sorted(Comparator.naturalOrder()).toList();
|
||||
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
|
||||
}
|
||||
|
||||
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
|
||||
int total = 0;
|
||||
for (int weight : weights) total += weight;
|
||||
int ticket = random.nextInt(total);
|
||||
for (int index = 0; index < choices.size(); index++) {
|
||||
ticket -= weights[index];
|
||||
if (ticket < 0) return choices.get(index);
|
||||
}
|
||||
throw new IllegalStateException("Random Age ticket was not consumed");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen, ordered Rocks materials. Old single-material beta Ages remain readable. */
|
||||
public record PortRockPlan(List<Identifier> materials) {
|
||||
public static final Codec<PortRockPlan> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.optionalFieldOf("material").forGetter(plan -> Optional.empty()),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("materials", List.of()).forGetter(PortRockPlan::materials)
|
||||
).apply(instance, (old, current) -> new PortRockPlan(current.isEmpty()
|
||||
? old.map(List::of).orElse(List.of()) : current)));
|
||||
|
||||
public PortRockPlan {
|
||||
materials = List.copyOf(materials);
|
||||
if (materials.isEmpty() || materials.size() > 32)
|
||||
throw new IllegalArgumentException("Rocks plan must contain 1–32 materials");
|
||||
}
|
||||
|
||||
public PortRockPlan(Identifier material) { this(List.of(material)); }
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeResolver;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
|
||||
/** 26.3 resolver port of the seeded 1.21.1 Mystcraft-style region distribution. */
|
||||
public final class PortScaledBiomeSource extends BiomeSource implements BiomeResolver {
|
||||
public static final MapCodec<PortScaledBiomeSource> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group(
|
||||
Biome.LIST_CODEC.fieldOf("biomes").forGetter(source -> source.biomes),
|
||||
Codec.LONG.fieldOf("seed").forGetter(source -> source.seed),
|
||||
Codec.intRange(0, 4).fieldOf("scale").forGetter(source -> source.scale)
|
||||
).apply(instance, PortScaledBiomeSource::new));
|
||||
|
||||
private final HolderSet<Biome> biomes;
|
||||
private final long seed;
|
||||
private final int scale;
|
||||
private final int cellWidth;
|
||||
|
||||
public PortScaledBiomeSource(HolderSet<Biome> biomes, long seed, int scale) {
|
||||
if (biomes.size() < 2 || scale < 0 || scale > 4)
|
||||
throw new IllegalArgumentException("Scaled regions need two biomes and scale 0..4");
|
||||
this.biomes = biomes;
|
||||
this.seed = seed;
|
||||
this.scale = scale;
|
||||
// Quart coordinates; Tiny starts at 16 blocks and each scale doubles.
|
||||
this.cellWidth = 4 << scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BiomeSource> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Stream<Holder<Biome>> collectPossibleBiomes() {
|
||||
return biomes.stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BiomeResolver createResolver(Climate.Sampler sampler) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Holder<Biome> getNoiseBiome(int x, int y, int z) {
|
||||
int baseX = Math.floorDiv(x, cellWidth);
|
||||
int baseZ = Math.floorDiv(z, cellWidth);
|
||||
int closestX = baseX;
|
||||
int closestZ = baseZ;
|
||||
double closestDistance = Double.POSITIVE_INFINITY;
|
||||
for (int cellX = baseX - 1; cellX <= baseX + 1; cellX++) {
|
||||
for (int cellZ = baseZ - 1; cellZ <= baseZ + 1; cellZ++) {
|
||||
long cellSeed = mix(seed, cellX, cellZ);
|
||||
double centerX = (cellX + 0.5D + jitter(cellSeed) * 0.8D) * cellWidth;
|
||||
double centerZ = (cellZ + 0.5D + jitter(mix64(cellSeed)) * 0.8D) * cellWidth;
|
||||
double deltaX = x - centerX;
|
||||
double deltaZ = z - centerZ;
|
||||
double distance = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestX = cellX;
|
||||
closestZ = cellZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
return biomes.get(Math.floorMod((int) mix(seed ^ 0x42494F4D45534CL,
|
||||
closestX, closestZ), biomes.size()));
|
||||
}
|
||||
|
||||
private static double jitter(long value) {
|
||||
return ((value >>> 11) * 0x1.0p-53) - 0.5D;
|
||||
}
|
||||
|
||||
private static long mix(long seed, int x, int z) {
|
||||
return mix64(seed ^ (long) x * 0x9E3779B97F4A7C15L ^ (long) z * 0xC2B2AE3D27D4EB4FL);
|
||||
}
|
||||
|
||||
private static long mix64(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,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,33 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** The six active weather pages in the 1.21.1 writing system. */
|
||||
public enum PortWeatherMode {
|
||||
NORMAL, FAST, SLOW, OFF, ALWAYS, STORM;
|
||||
|
||||
public static final Codec<PortWeatherMode> CODEC = Codec.STRING.xmap(
|
||||
name -> valueOf(name.toUpperCase(java.util.Locale.ROOT)),
|
||||
mode -> mode.name().toLowerCase(java.util.Locale.ROOT));
|
||||
|
||||
public static PortWeatherMode forPage(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
|
||||
return switch (page.getPath()) {
|
||||
case "weather/normal" -> NORMAL;
|
||||
case "weather/fast" -> FAST;
|
||||
case "weather/slow" -> SLOW;
|
||||
case "weather/off" -> OFF;
|
||||
case "weather/always" -> ALWAYS;
|
||||
case "weather/storm" -> STORM;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static PortWeatherMode fromPages(List<Identifier> pages) {
|
||||
return pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
|
||||
.findFirst().orElse(NORMAL);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.saveddata.SavedData;
|
||||
import net.minecraft.world.level.saveddata.SavedDataType;
|
||||
|
||||
/** Age-local policy; native WeatherData separately persists its live cycle. */
|
||||
final class PortWeatherSettings extends SavedData {
|
||||
private static final Codec<PortWeatherSettings> CODEC = PortWeatherMode.CODEC
|
||||
.fieldOf("mode").codec().xmap(PortWeatherSettings::new, settings -> settings.mode);
|
||||
static final SavedDataType<PortWeatherSettings> TYPE = new SavedDataType<>(
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "weather_policy"),
|
||||
() -> new PortWeatherSettings(PortWeatherMode.NORMAL), CODEC);
|
||||
|
||||
private PortWeatherMode mode;
|
||||
|
||||
private PortWeatherSettings(PortWeatherMode mode) { this.mode = mode; }
|
||||
PortWeatherMode mode() { return mode; }
|
||||
void setMode(PortWeatherMode mode) {
|
||||
this.mode = mode;
|
||||
setDirty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,450 @@
|
||||
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) {
|
||||
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);
|
||||
((PortAgeLevel) level).configureWeather(weather);
|
||||
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
|
||||
// A newly bound book may be used immediately. Persist its Age identity
|
||||
// before publishing the catalog so a later crash cannot leave that
|
||||
// book pointing at a world omitted from the on-disk index.
|
||||
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);
|
||||
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);
|
||||
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();
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
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(overworldStem.type(), generator, OptionalLong.of(seed));
|
||||
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
|
||||
ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.dnorberg.rivencraft.block;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.InsideBlockEffectApplier;
|
||||
import net.minecraft.world.level.BlockGetter;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.EndPortalBlock;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.portal.TeleportTransition;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.phys.shapes.CollisionContext;
|
||||
import net.minecraft.world.phys.shapes.VoxelShape;
|
||||
|
||||
/** Non-obtainable starfield portal returning entities to the shared Overworld spawn. */
|
||||
public final class StarFissureBlock extends EndPortalBlock {
|
||||
private static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 1.6, 16);
|
||||
|
||||
public StarFissureBlock(BlockBehaviour.Properties properties) { super(properties); }
|
||||
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new StarFissureBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void animateTick(BlockState state, Level level, BlockPos pos, net.minecraft.util.RandomSource random) {
|
||||
// Legacy fissures have a starfield, not vanilla End portal smoke.
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
|
||||
return SHAPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entityInside(BlockState state, Level level, BlockPos pos, Entity entity,
|
||||
InsideBlockEffectApplier effects, boolean precise) {
|
||||
// Do not inherit End credits or End-dimension travel from the vanilla block.
|
||||
if (entity.canUsePortal(false)) entity.setAsInsidePortal(this, pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) {
|
||||
if (level.dimension() == Level.OVERWORLD) return null;
|
||||
ServerLevel overworld = level.getServer().overworld();
|
||||
if (overworld == null) return null;
|
||||
BlockPos spawn = overworld.getRespawnData().pos();
|
||||
BlockPos landing = net.dnorberg.rivencraft.age.PortAgeTravel.findFootedDestination(overworld, spawn).orElse(spawn);
|
||||
level.playSound(null, pos, Rivencraft.LINK_FISSURE.get(), SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||
return new TeleportTransition(overworld, Vec3.atBottomCenterOf(landing), entity.getDeltaMovement(),
|
||||
entity.getYRot(), entity.getXRot(), TeleportTransition.PLACE_PORTAL_TICKET.then(arrived -> {
|
||||
// Departure audio can be discarded when the client switches levels.
|
||||
// Play after arrival too so the traveler hears the successful return.
|
||||
arrived.level().playSound(null, arrived.blockPosition(), Rivencraft.LINK_FISSURE.get(),
|
||||
SoundSource.BLOCKS, 1.0F, 1.0F);
|
||||
}));
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.InkVialExchange;
|
||||
import net.dnorberg.rivencraft.writing.InkModifierIngredients;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.ContainerHelper;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
/** Server-owned Link Panel mixer with a persistent, Legacy-style additive mixture. */
|
||||
public final class InkMixerBlockEntity extends BlockEntity implements Container {
|
||||
public static final int INK = 0;
|
||||
public static final int PAPER = 1;
|
||||
public static final int BOTTLES = 2;
|
||||
public static final int SIZE = 3;
|
||||
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
private int inkRemaining;
|
||||
private long nextModifierSeed = new Random().nextLong();
|
||||
private final double[] modifierChances = new double[3];
|
||||
private static final int[] FLAGS = {LinkProperties.DISARM,
|
||||
LinkProperties.MAINTAIN_MOMENTUM, LinkProperties.INTRA_LINKING};
|
||||
private static final String[] CHANCE_KEYS = {"DisarmChance", "MomentumChance", "IntraLinkingChance"};
|
||||
private static final int[] OLD_CHANCE_INDEX = {0, 2, 3};
|
||||
|
||||
public InkMixerBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(Rivencraft.INK_MIXER_BLOCK_ENTITY.get(), pos, state);
|
||||
}
|
||||
|
||||
public int inkRemaining() { return inkRemaining; }
|
||||
|
||||
public int modifierChancePermille(int index) {
|
||||
return Math.clamp((int) Math.round(modifierChances[index] * 1000), 0, 1000);
|
||||
}
|
||||
|
||||
/** Infuses a carried ingredient into the current ink pool; only the server calls this. */
|
||||
public boolean addModifier(ItemStack carried, int amount) {
|
||||
InkModifierIngredients.Ingredient ingredient = InkModifierIngredients.find(carried);
|
||||
if (ingredient == null || inkRemaining == 0 || amount <= 0 || carried.getCount() < amount)
|
||||
return false;
|
||||
for (InkModifierIngredients.Effect effect : ingredient.effects())
|
||||
for (int index = 0; index < FLAGS.length; index++)
|
||||
if (FLAGS[index] == effect.flag())
|
||||
modifierChances[index] = 1.0 - (1.0 - modifierChances[index])
|
||||
* Math.pow(1.0 - effect.chance(), amount);
|
||||
carried.shrink(amount);
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ItemStack previewLinkPanel() {
|
||||
if (!canCreateLinkPanel()) return ItemStack.EMPTY;
|
||||
ItemStack panel = PageItem.createLinkPanelPage();
|
||||
LinkProperties properties = rollProperties(nextModifierSeed).properties();
|
||||
if (properties.flags() != 0) panel.set(Rivencraft.LINK_PROPERTIES.get(), properties);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private record Roll(LinkProperties properties, long nextSeed) {}
|
||||
|
||||
private Roll rollProperties(long seed) {
|
||||
Random random = new Random(seed);
|
||||
int selected = 0;
|
||||
for (int index = 0; index < FLAGS.length; index++)
|
||||
if (modifierChances[index] > 0 && random.nextDouble() < modifierChances[index])
|
||||
selected |= FLAGS[index];
|
||||
return new Roll(new LinkProperties(selected), random.nextLong());
|
||||
}
|
||||
|
||||
@Override public int getContainerSize() { return SIZE; }
|
||||
@Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::isEmpty); }
|
||||
@Override public ItemStack getItem(int slot) { return items.get(slot); }
|
||||
@Override public ItemStack removeItem(int slot, int amount) {
|
||||
ItemStack removed = ContainerHelper.removeItem(items, slot, amount);
|
||||
if (!removed.isEmpty()) {
|
||||
setChanged();
|
||||
if (slot == BOTTLES) refillEmptyBasin();
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
@Override public ItemStack removeItemNoUpdate(int slot) { return ContainerHelper.takeItem(items, slot); }
|
||||
@Override public void setItem(int slot, ItemStack stack) {
|
||||
items.set(slot, stack);
|
||||
if (stack.getCount() > getMaxStackSize()) stack.setCount(getMaxStackSize());
|
||||
setChanged();
|
||||
if (slot == INK || slot == BOTTLES) refillEmptyBasin();
|
||||
}
|
||||
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
|
||||
return switch (slot) {
|
||||
case INK -> stack.is(Rivencraft.INK_VIAL.get());
|
||||
case PAPER -> stack.is(Items.PAPER);
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return level != null && level.isLoaded(worldPosition) && level.getBlockEntity(worldPosition) == this
|
||||
&& player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5,
|
||||
worldPosition.getZ() + 0.5) <= 64.0;
|
||||
}
|
||||
|
||||
/** No server tick: refill only when ink, bottle space, or consumption changes. */
|
||||
private void refillEmptyBasin() {
|
||||
if (level != null && !level.isClientSide() && inkRemaining == 0) {
|
||||
int addedInk = InkVialExchange.exchange(items, INK, BOTTLES);
|
||||
if (addedInk > 0) {
|
||||
inkRemaining = addedInk;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean canCreateLinkPanel() {
|
||||
if (!items.get(PAPER).is(Items.PAPER)) return false;
|
||||
return InkVialExchange.canPay(items, INK, BOTTLES, inkRemaining, 1);
|
||||
}
|
||||
|
||||
/** Commit inputs only when a virtual output is actually taken. */
|
||||
public boolean consumeLinkPanelMaterials() {
|
||||
if (!canCreateLinkPanel()) return false;
|
||||
int cost = 1;
|
||||
if (inkRemaining < cost) {
|
||||
inkRemaining += InkVialExchange.exchange(items, INK, BOTTLES);
|
||||
}
|
||||
inkRemaining -= cost;
|
||||
items.get(PAPER).shrink(1);
|
||||
if (items.get(PAPER).isEmpty()) items.set(PAPER, ItemStack.EMPTY);
|
||||
nextModifierSeed = rollProperties(nextModifierSeed).nextSeed();
|
||||
java.util.Arrays.fill(modifierChances, 0);
|
||||
setChanged();
|
||||
if (inkRemaining == 0) refillEmptyBasin();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override public void setLevel(Level level) {
|
||||
super.setLevel(level);
|
||||
refillEmptyBasin();
|
||||
}
|
||||
@Override public void clearContent() {
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
inkRemaining = 0;
|
||||
java.util.Arrays.fill(modifierChances, 0);
|
||||
setChanged();
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
ContainerHelper.loadAllItems(input, items);
|
||||
inkRemaining = Math.clamp(input.getIntOr("InkRemaining", 0), 0, InkVialItem.CAPACITY);
|
||||
nextModifierSeed = input.getLongOr("NextModifierSeed", nextModifierSeed);
|
||||
for (int index = 0; index < modifierChances.length; index++) {
|
||||
modifierChances[index] = Math.clamp(input.getDoubleOr(CHANCE_KEYS[index],
|
||||
input.getDoubleOr("ModifierChance" + OLD_CHANCE_INDEX[index], 0.0)), 0.0, 1.0);
|
||||
}
|
||||
}
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
ContainerHelper.saveAllItems(output, items);
|
||||
output.putInt("InkRemaining", inkRemaining);
|
||||
output.putLong("NextModifierSeed", nextModifierSeed);
|
||||
for (int index = 0; index < modifierChances.length; index++)
|
||||
output.putDouble(CHANCE_KEYS[index], modifierChances[index]);
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.NonNullList;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.ContainerHelper;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
/** Persistent, server-owned Binder inputs. */
|
||||
public final class PortBookBinderBlockEntity extends BlockEntity implements Container {
|
||||
public static final int COVER = 0;
|
||||
public static final int PANEL = 1;
|
||||
public static final int OUTPUT = 2;
|
||||
public static final int FIRST_PAGE = 3;
|
||||
public static final int PAGE_COUNT = 252;
|
||||
public static final int SIZE = FIRST_PAGE + PAGE_COUNT;
|
||||
private NonNullList<ItemStack> items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
private String title = "";
|
||||
|
||||
public PortBookBinderBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(Rivencraft.BOOK_BINDER_BLOCK_ENTITY.get(), pos, state);
|
||||
}
|
||||
|
||||
public String title() { return title; }
|
||||
|
||||
public void setTitle(String value) {
|
||||
String normalized = value == null ? "" : value.strip();
|
||||
if (normalized.length() > 32) return;
|
||||
if (!title.equals(normalized)) {
|
||||
title = normalized;
|
||||
setChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override public int getContainerSize() { return SIZE; }
|
||||
@Override public boolean isEmpty() { return items.stream().allMatch(ItemStack::isEmpty); }
|
||||
@Override public void clearContent() {
|
||||
items.clear();
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
setChanged();
|
||||
}
|
||||
@Override public ItemStack getItem(int slot) { return items.get(slot); }
|
||||
@Override public ItemStack removeItem(int slot, int amount) {
|
||||
ItemStack removed = ContainerHelper.removeItem(items, slot, amount);
|
||||
if (!removed.isEmpty()) setChanged();
|
||||
return removed;
|
||||
}
|
||||
@Override public ItemStack removeItemNoUpdate(int slot) {
|
||||
ItemStack removed = ContainerHelper.takeItem(items, slot);
|
||||
if (!removed.isEmpty()) setChanged();
|
||||
return removed;
|
||||
}
|
||||
@Override public void setItem(int slot, ItemStack stack) {
|
||||
items.set(slot, stack);
|
||||
setChanged();
|
||||
}
|
||||
@Override public boolean canPlaceItem(int slot, ItemStack stack) {
|
||||
if (slot == COVER) return stack.is(Items.LEATHER);
|
||||
if (slot == PANEL) return PageItem.isLinkPanel(stack);
|
||||
return slot >= FIRST_PAGE && slot < SIZE && validSymbolPage(stack);
|
||||
}
|
||||
private static boolean validSymbolPage(ItemStack stack) {
|
||||
return stack.is(Rivencraft.PAGE.get()) && !PageItem.isLinkPanel(stack)
|
||||
&& stack.has(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
}
|
||||
|
||||
public boolean hasSymbolPages() {
|
||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++)
|
||||
if (!items.get(slot).isEmpty()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<Identifier> symbolPages() {
|
||||
List<Identifier> pages = new ArrayList<>();
|
||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++) {
|
||||
Identifier symbol = items.get(slot).get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (symbol != null) pages.add(symbol);
|
||||
}
|
||||
return List.copyOf(pages);
|
||||
}
|
||||
|
||||
public void clearSymbolPages() {
|
||||
for (int slot = FIRST_PAGE; slot < SIZE; slot++) items.set(slot, ItemStack.EMPTY);
|
||||
setChanged();
|
||||
}
|
||||
|
||||
public int nextPageIndex() {
|
||||
for (int index = PAGE_COUNT - 1; index >= 0; index--)
|
||||
if (!items.get(FIRST_PAGE + index).isEmpty()) return index + 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean insertPage(int index, ItemStack page) {
|
||||
int end = nextPageIndex();
|
||||
if (index < 0 || index > end || end >= PAGE_COUNT || page.getCount() != 1
|
||||
|| !validSymbolPage(page)) return false;
|
||||
for (int current = end; current > index; current--)
|
||||
items.set(FIRST_PAGE + current, items.get(FIRST_PAGE + current - 1));
|
||||
items.set(FIRST_PAGE + index, page.copy());
|
||||
setChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
public ItemStack takePage(int index) {
|
||||
int end = nextPageIndex();
|
||||
if (index < 0 || index >= end) return ItemStack.EMPTY;
|
||||
ItemStack removed = items.get(FIRST_PAGE + index);
|
||||
for (int current = index; current < end - 1; current++)
|
||||
items.set(FIRST_PAGE + current, items.get(FIRST_PAGE + current + 1));
|
||||
items.set(FIRST_PAGE + end - 1, ItemStack.EMPTY);
|
||||
setChanged();
|
||||
return removed;
|
||||
}
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return level != null && level.getBlockEntity(worldPosition) == this
|
||||
&& player.distanceToSqr(worldPosition.getX() + 0.5, worldPosition.getY() + 0.5,
|
||||
worldPosition.getZ() + 0.5) <= 64.0;
|
||||
}
|
||||
@Override protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
ContainerHelper.saveAllItems(output, items);
|
||||
output.putString("Title", title);
|
||||
}
|
||||
@Override protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
items = NonNullList.withSize(SIZE, ItemStack.EMPTY);
|
||||
ContainerHelper.loadAllItems(input, items);
|
||||
title = input.getStringOr("Title", "");
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package net.dnorberg.rivencraft.block.entity;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.world.level.block.entity.TheEndPortalBlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
public final class StarFissureBlockEntity extends TheEndPortalBlockEntity {
|
||||
public StarFissureBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), pos, state);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.AgeCatalogAckPayload;
|
||||
import net.dnorberg.rivencraft.age.AgeCatalogPayload;
|
||||
import net.minecraft.client.multiplayer.ClientPacketListener;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.network.event.RegisterClientPayloadHandlersEvent;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class AgeCatalogClient {
|
||||
private AgeCatalogClient() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void register(RegisterClientPayloadHandlersEvent event) {
|
||||
event.register(AgeCatalogPayload.TYPE, (payload, context) -> {
|
||||
if (!(context.listener() instanceof ClientPacketListener listener)) return;
|
||||
listener.levels = payload.dimensions().stream()
|
||||
.map(id -> ResourceKey.<Level>create(Registries.DIMENSION, id))
|
||||
.collect(Collectors.toUnmodifiableSet());
|
||||
context.reply(new AgeCatalogAckPayload(payload.version()));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.InkVialItem;
|
||||
import net.dnorberg.rivencraft.menu.InkMixerMenu;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
/** 1.21.1 Legacy atlas and mixer animation adapted to the 26.3 GUI API. */
|
||||
public final class InkMixerScreen extends AbstractContainerScreen<InkMixerMenu> {
|
||||
private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "textures/gui/ink_mixer_legacy.png");
|
||||
private static final int BASIN_X = 88;
|
||||
private static final int BASIN_Y = 49;
|
||||
private static final int BASIN_RADIUS = 30;
|
||||
private static final int[] PROPERTY_COLORS = {0xFFE84848, 0xFF5555FF, 0xFF55EF55};
|
||||
private final int[] lastChances = new int[3];
|
||||
private boolean basinClickPendingRelease;
|
||||
private long lastAdditionTime;
|
||||
|
||||
public InkMixerScreen(InkMixerMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 176, 181);
|
||||
inventoryLabelY = 87;
|
||||
}
|
||||
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
|
||||
@Override public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.extractBackground(graphics, mouseX, mouseY, partialTick);
|
||||
int left = (width - imageWidth) / 2;
|
||||
int top = (height - imageHeight) / 2;
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left + 54, top + 16,
|
||||
179, 16, 66, 65, 256, 256);
|
||||
if (menu.inkRemaining() > 0) {
|
||||
graphics.fill(left + 54, top + 16, left + 120, top + 81, 0xFF101014);
|
||||
renderPropertyGlyph(graphics);
|
||||
}
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left, top,
|
||||
0, 0, imageWidth, imageHeight, 256, 256);
|
||||
}
|
||||
|
||||
@Override protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0xFF404040, false);
|
||||
}
|
||||
|
||||
@Override protected void extractTooltip(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
super.extractTooltip(graphics, mouseX, mouseY);
|
||||
if (overBasin(mouseX, mouseY)) {
|
||||
var lines = new java.util.ArrayList<Component>();
|
||||
lines.add(Component.translatable("screen.rivencraft.ink_mixer.ink",
|
||||
menu.inkRemaining(), InkVialItem.CAPACITY));
|
||||
lines.add(Component.translatable("screen.rivencraft.ink_mixer.additive_hint"));
|
||||
for (int index = 0; index < 3; index++) {
|
||||
int chance = menu.modifierChancePermille(index);
|
||||
if (chance > 0) lines.add(Component.translatable("screen.rivencraft.ink_mixer.chance",
|
||||
switch (index) {
|
||||
case 0 -> Component.translatable("item.rivencraft.link_property.disarm");
|
||||
case 1 -> Component.translatable("item.rivencraft.link_property.maintain_momentum");
|
||||
default -> Component.translatable("item.rivencraft.link_property.intra_linking");
|
||||
}, chance / 10.0));
|
||||
}
|
||||
graphics.setComponentTooltipForNextFrame(font, lines, mouseX, mouseY, ItemStack.EMPTY, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if ((event.button() == InputConstants.MOUSE_BUTTON_LEFT
|
||||
|| event.button() == InputConstants.MOUSE_BUTTON_RIGHT)
|
||||
&& overBasin(event.x(), event.y()) && !menu.getCarried().isEmpty()) {
|
||||
minecraft.gameMode.handleContainerInput(menu.containerId, InkMixerMenu.MODIFIER,
|
||||
event.button() == InputConstants.MOUSE_BUTTON_RIGHT ? 1 : 0,
|
||||
ContainerInput.PICKUP, minecraft.player);
|
||||
basinClickPendingRelease = true;
|
||||
return true;
|
||||
}
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override public boolean mouseReleased(MouseButtonEvent event) {
|
||||
if (basinClickPendingRelease) {
|
||||
basinClickPendingRelease = false;
|
||||
return true;
|
||||
}
|
||||
return super.mouseReleased(event);
|
||||
}
|
||||
|
||||
private boolean overBasin(double mouseX, double mouseY) {
|
||||
double x = mouseX - leftPos - BASIN_X;
|
||||
double y = mouseY - topPos - BASIN_Y;
|
||||
return x * x + y * y < BASIN_RADIUS * BASIN_RADIUS;
|
||||
}
|
||||
|
||||
/** A GUI-scale projection of Legacy's colored, rotating D'ni ink effect. */
|
||||
private void renderPropertyGlyph(GuiGraphicsExtractor graphics) {
|
||||
long now = System.currentTimeMillis();
|
||||
int[] active = new int[3];
|
||||
int count = 0;
|
||||
int strongest = 0;
|
||||
for (int index = 0; index < lastChances.length; index++) {
|
||||
int chance = menu.modifierChancePermille(index);
|
||||
if (chance > lastChances[index]) lastAdditionTime = now;
|
||||
lastChances[index] = chance;
|
||||
if (chance > 0) {
|
||||
active[count++] = index;
|
||||
strongest = Math.max(strongest, chance);
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
lastAdditionTime = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
double cycle = now / 1800.0;
|
||||
int first = active[(int) cycle % count];
|
||||
int second = active[((int) cycle + 1) % count];
|
||||
double blend = (1.0 - Math.cos((cycle % 1.0) * Math.PI)) / 2.0;
|
||||
int rgb = blendRgb(PROPERTY_COLORS[first], PROPERTY_COLORS[second], blend);
|
||||
double pulse = Math.max(0.0, 1.0 - (now - lastAdditionTime) / 700.0);
|
||||
int alpha = Math.clamp((int) (70 + strongest * 0.14 + pulse * 45), 0, 255);
|
||||
int color = alpha << 24 | rgb;
|
||||
int centerX = leftPos + BASIN_X;
|
||||
int centerY = topPos + BASIN_Y;
|
||||
double turn = now / 1200.0;
|
||||
|
||||
for (int spoke = 0; spoke < 4; spoke++) {
|
||||
for (int step = 0; step <= 30; step++) {
|
||||
double progress = step / 30.0;
|
||||
double angle = turn + spoke * Math.PI / 2.0 + progress * 0.9;
|
||||
double radius = 3.0 + progress * (15.0 + pulse * 3.0);
|
||||
plot(graphics, centerX + Math.cos(angle) * radius,
|
||||
centerY + Math.sin(angle) * radius, color);
|
||||
}
|
||||
}
|
||||
for (int step = 0; step < 96; step++) {
|
||||
double angle = step * Math.PI / 48.0 + turn * 0.3;
|
||||
plot(graphics, centerX + Math.cos(angle) * 19.0,
|
||||
centerY + Math.sin(angle) * 19.0, color);
|
||||
if (strongest >= 990 && step % 2 == 0) {
|
||||
plot(graphics, centerX + Math.cos(angle) * 23.0,
|
||||
centerY + Math.sin(angle) * 23.0, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int blendRgb(int first, int second, double amount) {
|
||||
int red = (int) (((first >> 16) & 0xFF) * (1.0 - amount)
|
||||
+ ((second >> 16) & 0xFF) * amount);
|
||||
int green = (int) (((first >> 8) & 0xFF) * (1.0 - amount)
|
||||
+ ((second >> 8) & 0xFF) * amount);
|
||||
int blue = (int) ((first & 0xFF) * (1.0 - amount) + (second & 0xFF) * amount);
|
||||
return red << 16 | green << 8 | blue;
|
||||
}
|
||||
|
||||
private static void plot(GuiGraphicsExtractor graphics, double x, double y, int color) {
|
||||
int pixelX = (int) Math.round(x);
|
||||
int pixelY = (int) Math.round(y);
|
||||
graphics.fill(pixelX, pixelY, pixelX + 1, pixelY + 1, color);
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
|
||||
import net.neoforged.neoforge.client.event.RegisterSpecialModelRendererEvent;
|
||||
import net.neoforged.neoforge.client.event.AddClientReloadListenersEvent;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.ResourceManagerReloadListener;
|
||||
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class PortBookBinderClientEvents {
|
||||
private PortBookBinderClientEvents() {}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerBlockEntityRenderers(
|
||||
net.neoforged.neoforge.client.event.EntityRenderersEvent.RegisterRenderers event) {
|
||||
event.registerBlockEntityRenderer(Rivencraft.STAR_FISSURE_BLOCK_ENTITY.get(), context -> new StarFissureRenderer());
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerScreens(RegisterMenuScreensEvent event) {
|
||||
event.register(Rivencraft.BOOK_BINDER_MENU.get(), PortBookBinderScreen::new);
|
||||
event.register(Rivencraft.INK_MIXER_MENU.get(), InkMixerScreen::new);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerPageRenderer(RegisterSpecialModelRendererEvent event) {
|
||||
event.register(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "symbol_page"),
|
||||
PortSymbolPageRenderer.Unbaked.MAP_CODEC);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void registerPageTextureReload(AddClientReloadListenersEvent event) {
|
||||
event.addListener(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "symbol_page_textures"),
|
||||
(ResourceManagerReloadListener) manager -> PortSymbolPageTextures.clear());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
import net.dnorberg.rivencraft.network.AssembleBinderPayload;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.network.SetBinderTitlePayload;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
import net.minecraft.client.input.KeyEvent;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
||||
|
||||
/** Legacy-sized Binder with a page strip and single-click book assembly. */
|
||||
public final class PortBookBinderScreen extends AbstractContainerScreen<PortBookBinderMenu> {
|
||||
private static final Identifier BACKGROUND = Identifier.fromNamespaceAndPath(
|
||||
"rivencraft", "textures/gui/book_binder_legacy.png");
|
||||
private static final Identifier PAGE_ATLAS = Identifier.fromNamespaceAndPath(
|
||||
"rivencraft", "textures/gui/notebook_pages.png");
|
||||
private static final int PAGE_Y = 48;
|
||||
private static final int PAGE_WIDTH = 26;
|
||||
private static final int PAGE_HEIGHT = 34;
|
||||
private static final int PAGE_STEP = 28;
|
||||
private static final int VISIBLE_PAGES = 5;
|
||||
private static final int STRIP_X = 7;
|
||||
private static final int STRIP_Y = 45;
|
||||
private static final int STRIP_WIDTH = 162;
|
||||
private static final int STRIP_HEIGHT = 40;
|
||||
private static final int MISSING_PANEL_X = 27;
|
||||
private static final int MISSING_PANEL_Y = 26;
|
||||
private static final int MISSING_PANEL_SIZE = 16;
|
||||
private static final ItemStack MISSING_PANEL_ICON = PageItem.createLinkPanelPage();
|
||||
private EditBox titleInput;
|
||||
private boolean stripClickPendingRelease;
|
||||
private int firstVisible;
|
||||
|
||||
public PortBookBinderScreen(PortBookBinderMenu menu, Inventory inventory, Component title) {
|
||||
super(menu, inventory, title, 176, 181);
|
||||
inventoryLabelY = 87;
|
||||
}
|
||||
|
||||
@Override public boolean isPauseScreen() { return false; }
|
||||
|
||||
@Override protected void init() {
|
||||
super.init();
|
||||
int left = (width - imageWidth) / 2;
|
||||
int top = (height - imageHeight) / 2;
|
||||
titleInput = new EditBox(font, left + 9, top + 10, 112, 12, Component.literal("Age name"));
|
||||
titleInput.setBordered(false);
|
||||
titleInput.setTextColor(0xFFFFFFFF);
|
||||
titleInput.setMaxLength(PortBookNetworking.MAX_BOOK_TITLE_LENGTH);
|
||||
titleInput.setValue(menu.title());
|
||||
titleInput.setHint(Component.literal("Name this Age"));
|
||||
titleInput.setResponder(value -> ClientPacketDistributor.sendToServer(
|
||||
new SetBinderTitlePayload(menu.containerId, value)));
|
||||
addRenderableWidget(titleInput);
|
||||
setInitialFocus(titleInput);
|
||||
}
|
||||
|
||||
@Override public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
super.extractBackground(graphics, mouseX, mouseY, partialTick);
|
||||
int left = (width - imageWidth) / 2;
|
||||
int top = (height - imageHeight) / 2;
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, BACKGROUND, left, top, 0, 0,
|
||||
imageWidth, imageHeight, 256, 256);
|
||||
graphics.fill(left + 7, top + 9, left + 123, top + 23, 0xFF000000);
|
||||
firstVisible = Math.min(firstVisible, maxFirstVisible());
|
||||
int visibleCount = Math.min(VISIBLE_PAGES, visiblePageCount() - firstVisible);
|
||||
for (int visible = 0; visible < visibleCount; visible++) {
|
||||
int x = left + 16 + visible * PAGE_STEP;
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, PAGE_ATLAS,
|
||||
x, top + PAGE_Y, 156, 0, PAGE_WIDTH, PAGE_HEIGHT, 256, 256);
|
||||
}
|
||||
// GUI extraction batches textures within a stratum. The glyphs need a
|
||||
// later stratum so another atlas page cannot cover their transparent quad.
|
||||
graphics.nextStratum();
|
||||
for (int visible = 0; visible < visibleCount; visible++) {
|
||||
int index = firstVisible + visible;
|
||||
int x = left + 16 + visible * PAGE_STEP;
|
||||
int y = top + PAGE_Y;
|
||||
ItemStack page = pageAt(index);
|
||||
if (page.isEmpty()) {
|
||||
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0xAA000000);
|
||||
} else if (PageItem.isLinkPanel(page)) {
|
||||
graphics.fill(x + 5, y + 6, x + 22, y + 17, 0xFF000000);
|
||||
} else {
|
||||
if (!PortAuthoredWriting.isPageImplemented(page.get(Rivencraft.SYMBOL_PAGE_ID.get())))
|
||||
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0x88FF3333);
|
||||
// Render the full-size transparent glyph through the working
|
||||
// item pipeline, without its small parchment-backed icon.
|
||||
ItemStack glyphOnly = page.copy();
|
||||
glyphOnly.set(Rivencraft.PAGE_GLYPH_ONLY.get(), true);
|
||||
graphics.pose().pushMatrix();
|
||||
graphics.pose().translate(x + 1, y + 5);
|
||||
graphics.pose().scale(1.5F, 1.5F);
|
||||
graphics.fakeItem(glyphOnly, 0, 0);
|
||||
graphics.pose().popMatrix();
|
||||
}
|
||||
}
|
||||
graphics.fill(left + STRIP_X, top + STRIP_Y, left + STRIP_X + 8,
|
||||
top + STRIP_Y + STRIP_HEIGHT, firstVisible == 0 ? 0x33000000 : 0xAA000000);
|
||||
graphics.fill(left + STRIP_X + STRIP_WIDTH - 8, top + STRIP_Y,
|
||||
left + STRIP_X + STRIP_WIDTH, top + STRIP_Y + STRIP_HEIGHT,
|
||||
firstVisible >= maxFirstVisible() ? 0x33000000 : 0xAA000000);
|
||||
boolean panelPresent = !menu.getSlot(PortBookBinderBlockEntity.PANEL).getItem().isEmpty();
|
||||
if (titleInput == null || titleInput.getValue().isBlank()) {
|
||||
graphics.fill(left + 7, top + 9, left + 123, top + 10, 0xFFFF3030);
|
||||
graphics.fill(left + 7, top + 22, left + 123, top + 23, 0xFFFF3030);
|
||||
}
|
||||
if (!panelPresent) {
|
||||
graphics.fakeItem(MISSING_PANEL_ICON, left + MISSING_PANEL_X, top + MISSING_PANEL_Y);
|
||||
float phase = (System.currentTimeMillis() % 4000L) / 2000.0F;
|
||||
float fade = phase > 1.0F ? 2.0F - phase : phase;
|
||||
int alpha = 40 + (int) (125 * fade);
|
||||
graphics.fill(left + MISSING_PANEL_X, top + MISSING_PANEL_Y,
|
||||
left + MISSING_PANEL_X + MISSING_PANEL_SIZE,
|
||||
top + MISSING_PANEL_Y + MISSING_PANEL_SIZE,
|
||||
(alpha << 24) | 0x00B02020);
|
||||
}
|
||||
if (hasUnsupportedPages()) graphics.text(font,
|
||||
Component.translatable("screen.rivencraft.book_binder.authored_pending"),
|
||||
left + 75, top + 31, 0xFFB84B42, false);
|
||||
if (!hasUnsupportedPages() && menu.getSlot(2).getItem().isEmpty() && !menu.getSlot(0).getItem().isEmpty()
|
||||
&& !menu.getSlot(1).getItem().isEmpty() && titleInput != null
|
||||
&& !titleInput.getValue().isBlank()) {
|
||||
graphics.fakeItem(new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get()), left + 152, top + 27);
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
graphics.text(font, playerInventoryTitle, inventoryLabelX, inventoryLabelY, 0xFF404040, false);
|
||||
}
|
||||
|
||||
@Override protected void extractTooltip(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
|
||||
super.extractTooltip(graphics, mouseX, mouseY);
|
||||
int x = mouseX - leftPos;
|
||||
int y = mouseY - topPos;
|
||||
if (x >= MISSING_PANEL_X && x < MISSING_PANEL_X + MISSING_PANEL_SIZE
|
||||
&& y >= MISSING_PANEL_Y && y < MISSING_PANEL_Y + MISSING_PANEL_SIZE
|
||||
&& menu.getSlot(1).getItem().isEmpty()) {
|
||||
graphics.setTooltipForNextFrame(font,
|
||||
Component.translatable("screen.rivencraft.book_binder.missing_panel"), mouseX, mouseY);
|
||||
} else {
|
||||
int index = pageIndexAt(mouseX, mouseY);
|
||||
if (index >= 0 && !pageAt(index).isEmpty())
|
||||
graphics.setTooltipForNextFrame(font, pageAt(index), mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
private ItemStack pageAt(int index) {
|
||||
return menu.getSlot(index == 0 ? PortBookBinderBlockEntity.PANEL
|
||||
: PortBookBinderBlockEntity.FIRST_PAGE + index - 1).getItem();
|
||||
}
|
||||
|
||||
private int displayedPages() {
|
||||
for (int index = PortBookBinderBlockEntity.PAGE_COUNT; index >= 1; index--)
|
||||
if (!pageAt(index).isEmpty()) return index + 1;
|
||||
return pageAt(0).isEmpty() ? 0 : 1;
|
||||
}
|
||||
|
||||
private boolean hasUnsupportedPages() {
|
||||
java.util.List<Identifier> pages = new java.util.ArrayList<>();
|
||||
for (int index = 1; index <= PortBookBinderBlockEntity.PAGE_COUNT; index++) {
|
||||
Identifier id = pageAt(index).get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (id != null) pages.add(id);
|
||||
}
|
||||
return !PortAuthoredWriting.supports(pages);
|
||||
}
|
||||
|
||||
private int visiblePageCount() {
|
||||
return Math.min(PortBookBinderBlockEntity.PAGE_COUNT + 1,
|
||||
displayedPages() + (menu.getCarried().isEmpty() ? 0 : 1));
|
||||
}
|
||||
|
||||
private int maxFirstVisible() { return Math.max(0, visiblePageCount() - VISIBLE_PAGES); }
|
||||
|
||||
private int pageIndexAt(double mouseX, double mouseY) {
|
||||
int x = (int) mouseX - leftPos;
|
||||
int y = (int) mouseY - topPos;
|
||||
if (x < 16 || x >= STRIP_X + STRIP_WIDTH - 8 || y < STRIP_Y || y >= STRIP_Y + STRIP_HEIGHT)
|
||||
return -1;
|
||||
int visible = (x - 16) / PAGE_STEP;
|
||||
return visible < VISIBLE_PAGES ? firstVisible + visible : -1;
|
||||
}
|
||||
|
||||
@Override public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
int left = (width - imageWidth) / 2;
|
||||
int top = (height - imageHeight) / 2;
|
||||
if ((event.button() == InputConstants.MOUSE_BUTTON_LEFT
|
||||
|| event.button() == InputConstants.MOUSE_BUTTON_RIGHT)
|
||||
&& event.x() >= left + STRIP_X && event.x() < left + STRIP_X + STRIP_WIDTH
|
||||
&& event.y() >= top + STRIP_Y && event.y() < top + STRIP_Y + STRIP_HEIGHT) {
|
||||
stripClickPendingRelease = true;
|
||||
if (event.x() < left + STRIP_X + 8) firstVisible = Math.max(0, firstVisible - 1);
|
||||
else if (event.x() >= left + STRIP_X + STRIP_WIDTH - 8)
|
||||
firstVisible = Math.min(maxFirstVisible(), firstVisible + 1);
|
||||
else {
|
||||
int index = pageIndexAt(event.x(), event.y());
|
||||
if (index >= 0 && index <= displayedPages()
|
||||
&& (index < displayedPages() || !menu.getCarried().isEmpty()))
|
||||
minecraft.gameMode.handleInventoryButtonClick(menu.containerId,
|
||||
PortBookBinderMenu.STRIP_CLICK_BASE + index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (event.button() == InputConstants.MOUSE_BUTTON_LEFT
|
||||
&& event.x() >= left + 151 && event.x() < left + 170
|
||||
&& event.y() >= top + 26 && event.y() < top + 45
|
||||
&& !hasUnsupportedPages() && menu.getSlot(2).getItem().isEmpty()
|
||||
&& menu.getCarried().isEmpty()) {
|
||||
String name = titleInput.getValue().strip();
|
||||
if (!name.isEmpty()) ClientPacketDistributor.sendToServer(new AssembleBinderPayload(name));
|
||||
return true;
|
||||
}
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override public boolean mouseReleased(MouseButtonEvent event) {
|
||||
if (stripClickPendingRelease) {
|
||||
stripClickPendingRelease = false;
|
||||
return true;
|
||||
}
|
||||
return super.mouseReleased(event);
|
||||
}
|
||||
|
||||
@Override public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
|
||||
if (mouseX >= leftPos + STRIP_X && mouseX < leftPos + STRIP_X + STRIP_WIDTH
|
||||
&& mouseY >= topPos + STRIP_Y && mouseY < topPos + STRIP_Y + STRIP_HEIGHT) {
|
||||
firstVisible = Math.clamp(firstVisible - (int) Math.signum(scrollY), 0, maxFirstVisible());
|
||||
return true;
|
||||
}
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
|
||||
}
|
||||
|
||||
@Override public boolean keyPressed(KeyEvent event) {
|
||||
if (titleInput != null && titleInput.isFocused()) {
|
||||
if (event.isEscape()) {
|
||||
setFocused(null);
|
||||
titleInput.setFocused(false);
|
||||
} else {
|
||||
titleInput.keyPressed(event);
|
||||
}
|
||||
// The character event inserts text. Never forward a focused key
|
||||
// to the container's inventory/hotbar shortcuts.
|
||||
return true;
|
||||
}
|
||||
return super.keyPressed(event);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
|
||||
|
||||
/** Opens screens only on the physical client, including integrated singleplayer. */
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class PortBookInteractionHandler {
|
||||
private PortBookInteractionHandler() {
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void onRightClickItem(PlayerInteractEvent.RightClickItem event) {
|
||||
if (!event.getEntity().level().isClientSide()) return;
|
||||
boolean linking = event.getItemStack().is(Rivencraft.LINKING_BOOK.get());
|
||||
boolean descriptive = event.getItemStack().is(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
if (!linking && !descriptive) return;
|
||||
event.setCanceled(true);
|
||||
event.setCancellationResult(InteractionResult.SUCCESS);
|
||||
Minecraft.getInstance().setScreenAndShow(linking
|
||||
? new PortLinkingBookScreen(event.getHand(), event.getItemStack().copy())
|
||||
: new PortDescriptiveBookScreen(event.getHand(), event.getItemStack().copy()));
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.network.BindDescriptiveBookPayload;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.network.UseDescriptiveBookPayload;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
||||
|
||||
/** Temporary 26.3 descriptive-book interaction; authored pages and live previews remain unported. */
|
||||
public final class PortDescriptiveBookScreen extends Screen {
|
||||
private static final int WIDTH = 327;
|
||||
private static final int HEIGHT = 199;
|
||||
private static final int TEXT_COLOR = 0xFF3B281B;
|
||||
private static final Identifier COVER = texture("bookui_cover.png");
|
||||
private static final Identifier LEFT_PAGE = texture("notebook_pages.png");
|
||||
private static final Identifier RIGHT_PANEL = texture("bookui_pager.png");
|
||||
|
||||
private final InteractionHand hand;
|
||||
private final boolean bound;
|
||||
private EditBox titleInput;
|
||||
|
||||
public PortDescriptiveBookScreen(InteractionHand hand, ItemStack book) {
|
||||
super(book.getHoverName());
|
||||
this.hand = hand;
|
||||
bound = book.has(Rivencraft.AGE_BOOK_ID.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (bound) return;
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
|
||||
Component.literal("Age name"));
|
||||
titleInput.setMaxLength(PortBookNetworking.MAX_BOOK_TITLE_LENGTH);
|
||||
titleInput.setHint(Component.literal("Name this Age"));
|
||||
addRenderableWidget(titleInput);
|
||||
setInitialFocus(titleInput);
|
||||
addRenderableWidget(Button.builder(Component.literal("Bind"), button -> bind())
|
||||
.bounds(left + 40, top + 100, 53, 20).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
||||
.bounds(left + 97, top + 100, 53, 20).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
||||
blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0);
|
||||
blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0);
|
||||
blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0);
|
||||
graphics.text(font, title, left + 42, top + 29, TEXT_COLOR, false);
|
||||
if (bound) {
|
||||
graphics.fill(left + 173, top + 20, left + 305, top + 103, 0xFF15141D);
|
||||
graphics.text(font, Component.literal("Age preview unavailable"), left + 181, top + 47,
|
||||
0xFFE9DFCB, false);
|
||||
graphics.text(font, Component.literal("Click to enter"), left + 205, top + 65,
|
||||
0xFFE9DFCB, false);
|
||||
} else {
|
||||
graphics.text(font, Component.literal("Bind a prototype Age"), left + 183, top + 39,
|
||||
TEXT_COLOR, false);
|
||||
}
|
||||
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if (bound && event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
if (event.x() >= left + 173 && event.x() < left + 305
|
||||
&& event.y() >= top + 20 && event.y() < top + 103) {
|
||||
ClientPacketDistributor.sendToServer(new UseDescriptiveBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
onClose();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
if (titleInput == null) return;
|
||||
String name = titleInput.getValue().strip();
|
||||
if (name.isEmpty()) return;
|
||||
ClientPacketDistributor.sendToServer(new BindDescriptiveBookPayload(hand == InteractionHand.OFF_HAND, name));
|
||||
onClose();
|
||||
}
|
||||
|
||||
private static Identifier texture(String filename) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/gui/" + filename);
|
||||
}
|
||||
|
||||
private static void blit(GuiGraphicsExtractor graphics, Identifier texture, int x, int y,
|
||||
int width, int height, int u, int v) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, 256, 256);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.InputConstants;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.network.BindLinkingBookPayload;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.network.UseLinkingBookPayload;
|
||||
import net.minecraft.client.gui.GuiGraphicsExtractor;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.client.input.MouseButtonEvent;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.client.network.ClientPacketDistributor;
|
||||
|
||||
/** Minimal 26.3 book interaction using the Legacy artwork; live preview is not ported yet. */
|
||||
public final class PortLinkingBookScreen extends Screen {
|
||||
private static final int WIDTH = 327;
|
||||
private static final int HEIGHT = 199;
|
||||
private static final int TEXT_COLOR = 0xFF3B281B;
|
||||
private static final Identifier COVER = texture("bookui_cover.png");
|
||||
private static final Identifier LEFT_PAGE = texture("notebook_pages.png");
|
||||
private static final Identifier RIGHT_PANEL = texture("bookui_pager.png");
|
||||
|
||||
private final InteractionHand hand;
|
||||
private final boolean bound;
|
||||
private EditBox titleInput;
|
||||
|
||||
public PortLinkingBookScreen(InteractionHand hand, ItemStack book) {
|
||||
super(book.getHoverName());
|
||||
this.hand = hand;
|
||||
bound = book.has(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
if (bound) return;
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
titleInput = new EditBox(font, left + 40, top + 69, 110, 20,
|
||||
Component.literal("Linking Book name"));
|
||||
titleInput.setMaxLength(PortBookNetworking.MAX_BOOK_TITLE_LENGTH);
|
||||
titleInput.setHint(Component.literal("Name this link"));
|
||||
addRenderableWidget(titleInput);
|
||||
setInitialFocus(titleInput);
|
||||
addRenderableWidget(Button.builder(Component.literal("Bind"), button -> bind())
|
||||
.bounds(left + 40, top + 100, 53, 20).build());
|
||||
addRenderableWidget(Button.builder(Component.literal("Cancel"), button -> onClose())
|
||||
.bounds(left + 97, top + 100, 53, 20).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float partialTick) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
blit(graphics, COVER, left, top + 7, 34, 192, 152, 0);
|
||||
blit(graphics, COVER, left + 34, top + 7, 103, 192, 49, 0);
|
||||
blit(graphics, COVER, left + 137, top + 7, 4, 192, 45, 0);
|
||||
blit(graphics, COVER, left + 141, top + 7, 186, 192, 0, 0);
|
||||
if (bound) blit(graphics, LEFT_PAGE, left + 7, top, 156, 195, 0, 0);
|
||||
blit(graphics, RIGHT_PANEL, left + 163, top, 156, 195, 0, 0);
|
||||
graphics.text(font, title, left + 42, top + 29, TEXT_COLOR, false);
|
||||
if (bound) {
|
||||
graphics.fill(left + 173, top + 20, left + 305, top + 103, 0xFF15141D);
|
||||
graphics.text(font, Component.literal("Destination preview"), left + 185, top + 47, 0xFFE9DFCB, false);
|
||||
graphics.text(font, Component.literal("Click to link"), left + 205, top + 65, 0xFFE9DFCB, false);
|
||||
} else {
|
||||
graphics.text(font, Component.literal("Name this Linking Book"), left + 183, top + 39,
|
||||
TEXT_COLOR, false);
|
||||
}
|
||||
super.extractRenderState(graphics, mouseX, mouseY, partialTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
|
||||
if (bound && event.button() == InputConstants.MOUSE_BUTTON_LEFT) {
|
||||
int left = (width - WIDTH) / 2;
|
||||
int top = (height - HEIGHT) / 2;
|
||||
if (event.x() >= left + 173 && event.x() < left + 305
|
||||
&& event.y() >= top + 20 && event.y() < top + 103) {
|
||||
ClientPacketDistributor.sendToServer(new UseLinkingBookPayload(hand == InteractionHand.OFF_HAND));
|
||||
onClose();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.mouseClicked(event, doubleClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPauseScreen() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void bind() {
|
||||
if (titleInput == null) return;
|
||||
String name = titleInput.getValue().strip();
|
||||
if (name.isEmpty()) return;
|
||||
ClientPacketDistributor.sendToServer(new BindLinkingBookPayload(hand == InteractionHand.OFF_HAND, name));
|
||||
onClose();
|
||||
}
|
||||
|
||||
private static Identifier texture(String filename) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/gui/" + filename);
|
||||
}
|
||||
|
||||
private static void blit(GuiGraphicsExtractor graphics, Identifier texture, int x, int y,
|
||||
int width, int height, int u, int v) {
|
||||
graphics.blit(RenderPipelines.GUI_TEXTURED, texture, x, y, u, v, width, height, 256, 256);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.special.SpecialModelRenderer;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
/** Flat item geometry textured with a page composed from its symbol stack. */
|
||||
public final class PortSymbolPageRenderer implements SpecialModelRenderer<PortSymbolPageRenderer.PageArt> {
|
||||
@Override
|
||||
public PageArt extractArgument(ItemStack stack) {
|
||||
Identifier symbol = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
return symbol == null ? null : new PageArt(symbol,
|
||||
stack.getOrDefault(Rivencraft.PAGE_GLYPH_ONLY.get(), false),
|
||||
PortAuthoredWriting.isPageImplemented(symbol) ? 0xFFFFFFFF : 0xFFFF6666);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(PageArt art, PoseStack pose, SubmitNodeCollector collector,
|
||||
int light, int overlay, boolean hasFoil, int outlineColor) {
|
||||
if (art == null) return;
|
||||
Identifier texture = art.glyphOnly()
|
||||
? PortSymbolPageTextures.forPage(art.symbol())
|
||||
: PortSymbolPageTextures.forItem(art.symbol());
|
||||
collector.submitCustomGeometry(pose, RenderTypes.entityCutout(texture),
|
||||
(matrix, vertices) -> {
|
||||
vertex(vertices, matrix, 0, 0, 0, 1, light, overlay, art.color());
|
||||
vertex(vertices, matrix, 1, 0, 1, 1, light, overlay, art.color());
|
||||
vertex(vertices, matrix, 1, 1, 1, 0, light, overlay, art.color());
|
||||
vertex(vertices, matrix, 0, 1, 0, 0, light, overlay, art.color());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getExtents(Consumer<org.joml.Vector3fc> extents) {
|
||||
extents.accept(new Vector3f(0, 0, 0.5F));
|
||||
extents.accept(new Vector3f(1, 1, 0.5F));
|
||||
}
|
||||
|
||||
private static void vertex(VertexConsumer vertices, PoseStack.Pose pose,
|
||||
float x, float y, float u, float v, int light, int overlay, int color) {
|
||||
vertices.addVertex(pose, x, y, 0.5F).setColor(color)
|
||||
.setUv(u, v).setOverlay(overlay).setLight(light).setNormal(pose, 0, 0, 1);
|
||||
}
|
||||
|
||||
public record PageArt(Identifier symbol, boolean glyphOnly, int color) {}
|
||||
|
||||
public record Unbaked() implements SpecialModelRenderer.Unbaked<PageArt> {
|
||||
public static final Unbaked INSTANCE = new Unbaked();
|
||||
public static final MapCodec<Unbaked> MAP_CODEC = MapCodec.unit(INSTANCE);
|
||||
|
||||
@Override public MapCodec<Unbaked> type() { return MAP_CODEC; }
|
||||
@Override public SpecialModelRenderer<PageArt> bake(BakingContext context) {
|
||||
return new PortSymbolPageRenderer();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.platform.NativeImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolNumerals;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.texture.DynamicTexture;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraft.util.ARGB;
|
||||
|
||||
/** The 1.21.1 Legacy-style glyph compositor adapted to 26.3's texture API. */
|
||||
public final class PortSymbolPageTextures {
|
||||
private static final int SIZE = 128;
|
||||
private static final int GLYPH = 51;
|
||||
private static final int ITEM_SYMBOL_SIZE = 15;
|
||||
private static final int NUMBER_GLYPH_SIZE = 64;
|
||||
private static final int CACHE_LIMIT = 256;
|
||||
private static final Identifier PAGE = texture("item/page");
|
||||
private static final Identifier COMPONENTS = texture("gui/symbolcomponents");
|
||||
private static final Identifier FALLBACK = texture("item/page_symbol");
|
||||
private static final Map<String, Identifier> CACHE = new LinkedHashMap<>(16, .75F, true);
|
||||
private static final Map<String, int[]> WORD_CACHE = new LinkedHashMap<>(16, .75F, true);
|
||||
private static NativeImage page;
|
||||
private static NativeImage components;
|
||||
private static int nextId;
|
||||
|
||||
// WordData from Mystcraft Legacy 0.13.7.06, matching the 1.21.1 compositor.
|
||||
private static final Map<String, int[]> WORDS = Map.ofEntries(
|
||||
Map.entry("Terrain", new int[] {6,10,12,13,16,19,24,25,27}),
|
||||
Map.entry("Form", new int[] {4,6,8,9,10,11,13,14,16,23}),
|
||||
Map.entry("Tradition", new int[] {4,6,7,8,9,14,17,18,20}),
|
||||
Map.entry("Flow", new int[] {4,7,12,13,17,18}),
|
||||
Map.entry("Constraint", new int[] {5,6,9,11,13,20,22,23}),
|
||||
Map.entry("Nature", new int[] {5,6,8,10,11,12,15,16,17,22}),
|
||||
Map.entry("Sustain", new int[] {6,16,19,20,21,23}),
|
||||
Map.entry("Ethereal", new int[] {4,6,8,13,14,16,18,21,23}),
|
||||
Map.entry("Dynamic", new int[] {4,5,6,12,13,17,18,22,23}),
|
||||
Map.entry("Cycle", new int[] {4,5,7,16,17,18}),
|
||||
Map.entry("Balance", new int[] {4,6,9,10,16,19}),
|
||||
Map.entry("Transform", new int[] {4,5,6,8,11,14,18,20,21}),
|
||||
Map.entry("Time", new int[] {4,5,7,12,13,19,20,21,22,23}),
|
||||
Map.entry("System", new int[] {10,12,14,15,16,20,21,22}),
|
||||
Map.entry("Void", new int[] {4,5,6,16,17,18,23})
|
||||
);
|
||||
|
||||
private PortSymbolPageTextures() {}
|
||||
|
||||
public static Identifier forItem(Identifier id) {
|
||||
return forSymbol(id, true);
|
||||
}
|
||||
|
||||
/** Transparent, full-size glyphs layered over the Binder's page atlas. */
|
||||
public static Identifier forPage(Identifier id) {
|
||||
return forSymbol(id, false);
|
||||
}
|
||||
|
||||
private static Identifier forSymbol(Identifier id, boolean item) {
|
||||
PortSymbolPages.Entry entry = PortSymbolPages.find(id);
|
||||
if (entry == null) return FALLBACK;
|
||||
String poem = entry.poem();
|
||||
if (!poem.matches("[A-Za-z]+(?: [A-Za-z]+){3}")) poem = "";
|
||||
String key = id + "|" + poem + (item ? "|item" : "|page");
|
||||
Identifier cached = CACHE.get(key);
|
||||
if (cached != null) return cached;
|
||||
try {
|
||||
loadSources(Minecraft.getInstance().getResourceManager());
|
||||
NativeImage image = compose(id, poem, item);
|
||||
Identifier location = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"generated/page_" + nextId++);
|
||||
Minecraft.getInstance().getTextureManager().register(location,
|
||||
new DynamicTexture(() -> "Rivencraft symbol page " + id, image));
|
||||
CACHE.put(key, location);
|
||||
if (CACHE.size() > CACHE_LIMIT) {
|
||||
String eldest = CACHE.keySet().iterator().next();
|
||||
Minecraft.getInstance().getTextureManager().release(CACHE.remove(eldest));
|
||||
}
|
||||
return location;
|
||||
} catch (IOException exception) {
|
||||
Rivencraft.LOGGER.error("Could not compose symbol page {}", id, exception);
|
||||
return FALLBACK;
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
var textures = Minecraft.getInstance().getTextureManager();
|
||||
CACHE.values().forEach(textures::release);
|
||||
CACHE.clear();
|
||||
WORD_CACHE.clear();
|
||||
if (page != null) page.close();
|
||||
if (components != null) components.close();
|
||||
page = null;
|
||||
components = null;
|
||||
}
|
||||
|
||||
private static void loadSources(ResourceManager manager) throws IOException {
|
||||
if (page != null && components != null) return;
|
||||
try (InputStream pageStream = manager.getResourceOrThrow(PAGE).open();
|
||||
InputStream componentsStream = manager.getResourceOrThrow(COMPONENTS).open()) {
|
||||
page = NativeImage.read(pageStream);
|
||||
components = NativeImage.read(componentsStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeImage compose(Identifier id, String poem, boolean item) {
|
||||
NativeImage image = new NativeImage(SIZE, SIZE, true);
|
||||
if (item) {
|
||||
for (int y = 0; y < SIZE; y++) for (int x = 0; x < SIZE; x++) {
|
||||
image.setPixel(x, y, page.getPixel(x * page.getWidth() / SIZE,
|
||||
y * page.getHeight() / SIZE));
|
||||
}
|
||||
}
|
||||
String numberPath = id.getPath().startsWith("number/")
|
||||
? id.getPath().substring("number/".length()) : "";
|
||||
if (id.getNamespace().equals(Rivencraft.MOD_ID)
|
||||
&& numberPath.matches("(?:[0-9]|1[0-9]|2[0-4])")) {
|
||||
int digit = Integer.parseInt(numberPath);
|
||||
int glyphSize = item ? NUMBER_GLYPH_SIZE : GLYPH;
|
||||
int origin = item ? (SIZE - glyphSize) / 2 : 38;
|
||||
for (int component : PortSymbolNumerals.componentsForDigit(digit))
|
||||
blendComponent(image, component, origin, origin, glyphSize);
|
||||
return image;
|
||||
}
|
||||
String[] words = poem.isBlank() ? generatedWords(id) : poem.split(" ");
|
||||
int[] offsetsX = {38, 77, 38, 0};
|
||||
int[] offsetsY = {0, 38, 77, 38};
|
||||
int glyphSize = item ? GLYPH * ITEM_SYMBOL_SIZE / 16 : GLYPH;
|
||||
for (int word = 0; word < 4; word++) {
|
||||
int x = item ? scaleForItem(offsetsX[word]) : offsetsX[word];
|
||||
int y = item ? scaleForItem(offsetsY[word]) : offsetsY[word];
|
||||
for (int component : glyphs(words[word]))
|
||||
blendComponent(image, component, x, y, glyphSize);
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
private static void blendComponent(NativeImage image, int component, int x0, int y0, int size) {
|
||||
int cellX = (component % 8) * 64;
|
||||
int cellY = (component / 8) * 64;
|
||||
for (int y = 0; y < size; y++) for (int x = 0; x < size; x++) {
|
||||
int sourceX = cellX + x * 64 / size;
|
||||
int sourceY = cellY + y * 64 / size;
|
||||
// One source-pixel of coverage keeps the thin Legacy strokes legible
|
||||
// when Minecraft shrinks this 128px composite to a 16px item icon.
|
||||
int alpha = ARGB.alpha(components.getPixel(sourceX, sourceY));
|
||||
if (sourceX > cellX) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX - 1, sourceY)));
|
||||
if (sourceX < cellX + 63) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX + 1, sourceY)));
|
||||
if (sourceY > cellY) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX, sourceY - 1)));
|
||||
if (sourceY < cellY + 63) alpha = Math.max(alpha, ARGB.alpha(components.getPixel(sourceX, sourceY + 1)));
|
||||
if (alpha != 0) image.setPixel(x0 + x, y0 + y,
|
||||
ARGB.alphaBlend(image.getPixel(x0 + x, y0 + y),
|
||||
(alpha << 24) | 0x001A212B));
|
||||
}
|
||||
}
|
||||
|
||||
private static int scaleForItem(int coordinate) {
|
||||
return SIZE / 2 + Math.round((coordinate - SIZE / 2F) * ITEM_SYMBOL_SIZE / 16F);
|
||||
}
|
||||
|
||||
private static String[] generatedWords(Identifier id) {
|
||||
String stem = id.toString();
|
||||
return new String[] {stem + 'a', stem + 'b', stem + 'c', stem + 'd'};
|
||||
}
|
||||
|
||||
private static int[] glyphs(String word) {
|
||||
int[] known = WORDS.get(word);
|
||||
if (known != null) return known;
|
||||
String key = word.toLowerCase(Locale.ROOT);
|
||||
int[] cached = WORD_CACHE.get(key);
|
||||
if (cached != null) return cached;
|
||||
Random random = new Random(key.hashCode());
|
||||
int[] result = new int[random.nextInt(10) + 3];
|
||||
for (int i = 0; i < result.length; i++) result[i] = random.nextInt(20) + 4;
|
||||
WORD_CACHE.put(key, result);
|
||||
if (WORD_CACHE.size() > CACHE_LIMIT) WORD_CACHE.remove(WORD_CACHE.keySet().iterator().next());
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Identifier texture(String path) {
|
||||
return Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "textures/" + path + ".png");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.block.entity.StarFissureBlockEntity;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.AbstractEndPortalRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.state.EndPortalRenderState;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.core.Direction;
|
||||
|
||||
/** Retains 1.21.1's UP-at-zero/DOWN-at-one planes with the modern starfield renderer. */
|
||||
public final class StarFissureRenderer extends AbstractEndPortalRenderer<StarFissureBlockEntity, EndPortalRenderState> {
|
||||
@Override
|
||||
public EndPortalRenderState createRenderState() { return new EndPortalRenderState(); }
|
||||
|
||||
@Override
|
||||
public void submit(EndPortalRenderState state, PoseStack pose, SubmitNodeCollector collector,
|
||||
CameraRenderState camera) {
|
||||
if (state.facesToShow.contains(Direction.UP)) {
|
||||
pose.pushPose();
|
||||
pose.translate(0, -1, 0);
|
||||
submitCube(List.of(Direction.UP), RenderTypes.endPortal(), pose, collector);
|
||||
pose.popPose();
|
||||
}
|
||||
if (state.facesToShow.contains(Direction.DOWN)) {
|
||||
pose.pushPose();
|
||||
pose.translate(0, 1, 0);
|
||||
submitCube(List.of(Direction.DOWN), RenderTypes.endPortal(), pose, collector);
|
||||
pose.popPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** Descriptive Book shell for the 26.3 prototype Age backend. */
|
||||
public final class DescriptiveBookItem extends Item {
|
||||
public DescriptiveBookItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoil(ItemStack stack) {
|
||||
return stack.has(Rivencraft.AGE_BOOK_ID.get()) || super.isFoil(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(Level level, Player player, InteractionHand hand) {
|
||||
// The client opens the GUI. Binding and travel are validated on the
|
||||
// server through dedicated packets; item use cannot bypass them.
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
Identifier id = stack.get(Rivencraft.AGE_BOOK_ID.get());
|
||||
tooltip.accept(Component.literal(id == null ? "Unbound (26.3 port preview)" : "Age: " + id));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.util.Prediction;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
|
||||
/** A stackable 16-use vial; using it with paper in the other hand prepares a Link Panel. */
|
||||
public final class InkVialItem extends Item {
|
||||
public static final int CAPACITY = 16;
|
||||
// Match the 1.21.1 default; port the configurable cost with the full configuration system.
|
||||
public static final int LINK_PANEL_COST = 2;
|
||||
|
||||
public InkVialItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public static int remaining(ItemStack vial) {
|
||||
return vial.getOrDefault(Rivencraft.INK_REMAINING.get(), CAPACITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(Level level, Player player, InteractionHand hand) {
|
||||
InteractionHand paperHand = hand == InteractionHand.MAIN_HAND
|
||||
? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
|
||||
ItemStack vial = player.getItemInHand(hand);
|
||||
ItemStack paper = player.getItemInHand(paperHand);
|
||||
int ink = remaining(vial);
|
||||
if (!vial.is(Rivencraft.INK_VIAL.get()) || !paper.is(Items.PAPER)
|
||||
|| ink < LINK_PANEL_COST || ink > CAPACITY) {
|
||||
return InteractionResult.PASS;
|
||||
}
|
||||
if (level.isClientSide()) return InteractionResult.SUCCESS;
|
||||
|
||||
ItemStack panel = PageItem.createLinkPanelPage();
|
||||
int newInk = ink - LINK_PANEL_COST;
|
||||
ItemStack spentVial = newInk == 0
|
||||
? new ItemStack(Items.GLASS_BOTTLE)
|
||||
: new ItemStack(Rivencraft.INK_VIAL.get());
|
||||
if (newInk > 0) spentVial.set(Rivencraft.INK_REMAINING.get(), newInk);
|
||||
|
||||
if (paper.getCount() == 1) {
|
||||
player.setItemInHand(paperHand, panel);
|
||||
} else {
|
||||
paper.shrink(1);
|
||||
giveOrDrop(player, panel);
|
||||
}
|
||||
if (vial.getCount() == 1) {
|
||||
player.setItemInHand(hand, spentVial);
|
||||
} else {
|
||||
vial.shrink(1);
|
||||
giveOrDrop(player, spentVial);
|
||||
}
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
tooltip.accept(Component.translatable("item.rivencraft.ink_vial.remaining", remaining(stack), CAPACITY));
|
||||
tooltip.accept(Component.translatable("item.rivencraft.ink_vial.usage"));
|
||||
}
|
||||
|
||||
private static void giveOrDrop(Player player, ItemStack stack) {
|
||||
if (!player.getInventory().add(stack)) player.drop(stack, false, Prediction.SERVER_ONLY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
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.server.level.ServerPlayer;
|
||||
|
||||
/** Persistent destination captured when a 26.3 prototype Linking Book is bound. */
|
||||
public record LinkBookTarget(Identifier dimension, double x, double y, double z, float yaw, float pitch) {
|
||||
public static final Codec<LinkBookTarget> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Identifier.CODEC.fieldOf("dimension").forGetter(LinkBookTarget::dimension),
|
||||
Codec.DOUBLE.fieldOf("x").forGetter(LinkBookTarget::x),
|
||||
Codec.DOUBLE.fieldOf("y").forGetter(LinkBookTarget::y),
|
||||
Codec.DOUBLE.fieldOf("z").forGetter(LinkBookTarget::z),
|
||||
Codec.FLOAT.fieldOf("yaw").forGetter(LinkBookTarget::yaw),
|
||||
Codec.FLOAT.fieldOf("pitch").forGetter(LinkBookTarget::pitch)
|
||||
).apply(instance, LinkBookTarget::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, LinkBookTarget> STREAM_CODEC = StreamCodec.composite(
|
||||
Identifier.STREAM_CODEC, LinkBookTarget::dimension,
|
||||
ByteBufCodecs.DOUBLE, LinkBookTarget::x,
|
||||
ByteBufCodecs.DOUBLE, LinkBookTarget::y,
|
||||
ByteBufCodecs.DOUBLE, LinkBookTarget::z,
|
||||
ByteBufCodecs.FLOAT, LinkBookTarget::yaw,
|
||||
ByteBufCodecs.FLOAT, LinkBookTarget::pitch,
|
||||
LinkBookTarget::new);
|
||||
|
||||
public static LinkBookTarget at(ServerPlayer player) {
|
||||
return new LinkBookTarget(player.level().dimension().identifier(),
|
||||
player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
|
||||
}
|
||||
|
||||
public boolean valid() {
|
||||
return Double.isFinite(x) && Double.isFinite(y) && Double.isFinite(z)
|
||||
&& Float.isFinite(yaw) && Float.isFinite(pitch)
|
||||
&& Math.abs(x) <= 30_000_000 && Math.abs(z) <= 30_000_000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
/** The three Ink Mixer modifiers currently supported by the 26.3 port. */
|
||||
public record LinkProperties(int flags) {
|
||||
public static final int DISARM = 1;
|
||||
public static final int MAINTAIN_MOMENTUM = 4;
|
||||
public static final int INTRA_LINKING = 8;
|
||||
public static final int SUPPORTED = DISARM | MAINTAIN_MOMENTUM | INTRA_LINKING;
|
||||
public static final LinkProperties NONE = new LinkProperties(0);
|
||||
public static final Codec<LinkProperties> CODEC = Codec.INT.xmap(LinkProperties::new, LinkProperties::flags);
|
||||
public static final StreamCodec<ByteBuf, LinkProperties> STREAM_CODEC = StreamCodec.of(
|
||||
(buffer, value) -> ByteBufCodecs.VAR_INT.encode(buffer, value.flags()),
|
||||
buffer -> new LinkProperties(ByteBufCodecs.VAR_INT.decode(buffer)));
|
||||
|
||||
public LinkProperties {
|
||||
if (flags < 0 || (flags & ~SUPPORTED) != 0) {
|
||||
throw new IllegalArgumentException("Unsupported Link Panel flags: " + flags);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean has(int flag) {
|
||||
return (flags & flag) != 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.core.particles.ParticleTypes;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Same successful-link feedback and sound precedence as the 1.21.1 target. */
|
||||
public final class LinkVisualEffects {
|
||||
private LinkVisualEffects() {}
|
||||
|
||||
public static SoundEvent sound(LinkProperties properties, boolean portal) {
|
||||
if (properties.has(LinkProperties.DISARM)) return Rivencraft.LINK_DISARM.get();
|
||||
if (portal) return Rivencraft.LINK_PORTAL.get();
|
||||
if (properties.has(LinkProperties.INTRA_LINKING)) return Rivencraft.LINK_INTRA.get();
|
||||
return Rivencraft.LINK.get();
|
||||
}
|
||||
|
||||
public static void play(ServerLevel source, Vec3 departure, ServerPlayer player,
|
||||
LinkProperties properties, boolean portal) {
|
||||
SoundEvent event = sound(properties, portal);
|
||||
burst(source, departure, event);
|
||||
burst(player.level(), player.position(), event);
|
||||
}
|
||||
|
||||
private static void burst(ServerLevel level, Vec3 position, SoundEvent event) {
|
||||
level.playSound(null, position.x, position.y, position.z, event,
|
||||
SoundSource.PLAYERS, 0.8F, level.getRandom().nextFloat() * 0.2F + 0.9F);
|
||||
level.sendParticles(ParticleTypes.CLOUD, position.x, position.y, position.z,
|
||||
50, 0.5, 1.0, 0.5, 0.005);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.AgeCatalogSync;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
/** Handheld Linking Book; destination binding and travel are server-authoritative packets. */
|
||||
public final class LinkingBookItem extends Item {
|
||||
public LinkingBookItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFoil(ItemStack stack) {
|
||||
return stack.has(Rivencraft.LINK_BOOK_TARGET.get()) || super.isFoil(stack);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionResult use(Level level, Player player, InteractionHand hand) {
|
||||
// Client-side interaction opens the book screen. A direct server-side
|
||||
// item-use packet must never bypass the screen's validated payloads.
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
|
||||
public static String travel(ServerPlayer player, ItemStack book) {
|
||||
LinkBookTarget destination = book.get(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
LinkProperties properties = book.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
if (destination == null) return "Unbound Linking Book (26.3 port preview)";
|
||||
if (!destination.valid()) return "Invalid Linking Book destination";
|
||||
if (player.level().dimension().identifier().equals(destination.dimension())
|
||||
&& !properties.has(LinkProperties.INTRA_LINKING)) {
|
||||
return "Same-dimension linking requires an Intra-Linking page";
|
||||
}
|
||||
ServerLevel target = player.level().getServer().getLevel(
|
||||
ResourceKey.create(Registries.DIMENSION, destination.dimension()));
|
||||
if (target == null) return "Destination dimension is unavailable: " + destination.dimension();
|
||||
if (!AgeCatalogSync.ready(player, destination.dimension())) {
|
||||
return "Destination catalog not acknowledged by client";
|
||||
}
|
||||
BlockPos feet = BlockPos.containing(destination.x(), destination.y(), destination.z());
|
||||
if (feet.getY() < target.getMinY() || feet.getY() + 1 >= target.getMaxY()
|
||||
|| !target.getWorldBorder().isWithinBounds(feet)) {
|
||||
return "Destination is outside the world bounds";
|
||||
}
|
||||
// Test the player's actual arrival volume: checking whole block shapes
|
||||
// would incorrectly reject links bound while standing on a slab.
|
||||
if (!target.noCollision(player, player.getBoundingBox().move(
|
||||
destination.x() - player.getX(), destination.y() - player.getY(),
|
||||
destination.z() - player.getZ()))) {
|
||||
return "Linked destination is obstructed";
|
||||
}
|
||||
ServerLevel source = player.level();
|
||||
Vec3 departure = player.position();
|
||||
Vec3 incoming = player.getDeltaMovement();
|
||||
boolean moved = player.teleportTo(target, destination.x(), destination.y(), destination.z(),
|
||||
Set.of(), destination.yaw(), destination.pitch(), true);
|
||||
if (moved) applySuccessfulLink(player, properties, source, departure, incoming, destination.yaw());
|
||||
return moved ? null : "Teleportation failed";
|
||||
}
|
||||
|
||||
/** Link Panel effects are shared by Linking and Descriptive Books. */
|
||||
public static void applySuccessfulLink(ServerPlayer player, LinkProperties properties,
|
||||
ServerLevel source, Vec3 departure, Vec3 incoming, float destinationYaw) {
|
||||
LinkVisualEffects.play(source, departure, player, properties, false);
|
||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM)) {
|
||||
double speed = Math.hypot(incoming.x, incoming.z);
|
||||
double yaw = Math.toRadians(destinationYaw);
|
||||
player.setDeltaMovement(speed * Math.cos(yaw), incoming.y + 0.2,
|
||||
speed * Math.sin(yaw));
|
||||
} else {
|
||||
player.setDeltaMovement(Vec3.ZERO);
|
||||
}
|
||||
if (properties.has(LinkProperties.DISARM)) {
|
||||
for (int slot = 0; slot < player.getInventory().getContainerSize(); slot++) {
|
||||
ItemStack carried = player.getInventory().getItem(slot);
|
||||
if (carried.isEmpty()) continue;
|
||||
player.getInventory().setItem(slot, ItemStack.EMPTY);
|
||||
ItemEntity dropped = new ItemEntity(source, departure.x, departure.y, departure.z,
|
||||
carried.copy());
|
||||
dropped.setDefaultPickUpDelay();
|
||||
source.addFreshEntity(dropped);
|
||||
}
|
||||
player.getInventory().setChanged();
|
||||
player.containerMenu.broadcastChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
LinkBookTarget target = stack.get(Rivencraft.LINK_BOOK_TARGET.get());
|
||||
tooltip.accept(Component.literal(target == null ? "Unbound (26.3 port preview)"
|
||||
: "Destination: " + target.dimension() + " (" + (int) target.x() + ", "
|
||||
+ (int) target.y() + ", " + (int) target.z() + ")"));
|
||||
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
if (properties.has(LinkProperties.DISARM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm"));
|
||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.maintain_momentum"));
|
||||
if (properties.has(LinkProperties.INTRA_LINKING))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.intra_linking"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package net.dnorberg.rivencraft.item;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.ChatFormatting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.TooltipFlag;
|
||||
import net.minecraft.world.item.component.TooltipDisplay;
|
||||
|
||||
/** Shared Page item with persistent Link Panel and authored symbol variants. */
|
||||
public final class PageItem extends Item {
|
||||
public PageItem(Properties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
public static ItemStack createLinkPanelPage() {
|
||||
return createLinkPanelPage(0);
|
||||
}
|
||||
|
||||
public static ItemStack createLinkPanelPage(int flags) {
|
||||
ItemStack stack = new ItemStack(Rivencraft.PAGE.get());
|
||||
stack.set(Rivencraft.LINK_PANEL_PAGE.get(), true);
|
||||
if (flags != 0) stack.set(Rivencraft.LINK_PROPERTIES.get(), new LinkProperties(flags));
|
||||
return stack;
|
||||
}
|
||||
|
||||
public static boolean isLinkPanel(ItemStack stack) {
|
||||
return stack.is(Rivencraft.PAGE.get()) && stack.getOrDefault(Rivencraft.LINK_PANEL_PAGE.get(), false);
|
||||
}
|
||||
|
||||
public static ItemStack createSymbolPage(Identifier id) {
|
||||
if (PortSymbolPages.find(id) == null)
|
||||
throw new IllegalArgumentException("Unknown bundled symbol " + id);
|
||||
ItemStack stack = new ItemStack(Rivencraft.PAGE.get());
|
||||
stack.set(Rivencraft.SYMBOL_PAGE_ID.get(), id);
|
||||
return stack;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getName(ItemStack stack) {
|
||||
Component customName = stack.get(DataComponents.CUSTOM_NAME);
|
||||
if (customName != null) return customName;
|
||||
Identifier symbolId = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (symbolId != null) {
|
||||
PortSymbolPages.Entry entry = PortSymbolPages.find(symbolId);
|
||||
if (entry != null) {
|
||||
Component name = Component.translatableWithFallback(
|
||||
entry.nameKey(), PortSymbolPages.fallbackName(symbolId));
|
||||
return isCorrupted(symbolId) ? name.copy().withStyle(ChatFormatting.OBFUSCATED) : name;
|
||||
}
|
||||
return Component.translatableWithFallback("item.rivencraft.page.unresolved", "Unknown Symbol Page");
|
||||
}
|
||||
return Component.translatable(isLinkPanel(stack)
|
||||
? "item.rivencraft.page.link_panel" : "item.rivencraft.page.blank");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendHoverText(ItemStack stack, TooltipContext context, TooltipDisplay display,
|
||||
Consumer<Component> tooltip, TooltipFlag flag) {
|
||||
Identifier symbolId = stack.get(Rivencraft.SYMBOL_PAGE_ID.get());
|
||||
if (symbolId != null) {
|
||||
PortSymbolPages.Entry entry = PortSymbolPages.find(symbolId);
|
||||
if (entry != null) {
|
||||
Component description = Component.translatableWithFallback(
|
||||
entry.descriptionKey(), PortSymbolPages.fallbackName(symbolId));
|
||||
tooltip.accept(isCorrupted(symbolId)
|
||||
? description.copy().withStyle(ChatFormatting.GRAY, ChatFormatting.OBFUSCATED)
|
||||
: description.copy().withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
boolean implemented = PortAuthoredWriting.isPageImplemented(symbolId);
|
||||
tooltip.accept(Component.translatable(implemented
|
||||
? "item.rivencraft.page.symbol.implemented" : "item.rivencraft.page.symbol.unimplemented")
|
||||
.withStyle(implemented ? ChatFormatting.DARK_GRAY : ChatFormatting.RED));
|
||||
return;
|
||||
}
|
||||
LinkProperties properties = stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE);
|
||||
if (properties.has(LinkProperties.DISARM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.disarm"));
|
||||
if (properties.has(LinkProperties.MAINTAIN_MOMENTUM))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.maintain_momentum"));
|
||||
if (properties.has(LinkProperties.INTRA_LINKING))
|
||||
tooltip.accept(Component.translatable("item.rivencraft.link_property.intra_linking"));
|
||||
tooltip.accept(Component.translatable(isLinkPanel(stack)
|
||||
? "item.rivencraft.page.link_panel.port_status"
|
||||
: "item.rivencraft.page.blank.port_status"));
|
||||
}
|
||||
|
||||
private static boolean isCorrupted(Identifier id) {
|
||||
return id.getNamespace().equals(Rivencraft.MOD_ID) && id.getPath().startsWith("corrupted/");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.InkMixerBlockEntity;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.ContainerInput;
|
||||
import net.minecraft.world.inventory.DataSlot;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Server-owned virtual output; taking it spends paper and the configured ink cost once. */
|
||||
public final class InkMixerMenu extends AbstractContainerMenu {
|
||||
public static final int MODIFIER = InkMixerBlockEntity.SIZE;
|
||||
public static final int RESULT = MODIFIER + 1;
|
||||
private static final int PLAYER_START = RESULT + 1;
|
||||
private final InkMixerBlockEntity mixer;
|
||||
private final Container inputs;
|
||||
private final SimpleContainer result = new SimpleContainer(1);
|
||||
private final boolean clientSide;
|
||||
private int inkRemaining;
|
||||
private final int[] modifierChances = new int[3];
|
||||
|
||||
public InkMixerMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
|
||||
instanceof InkMixerBlockEntity found ? found : null);
|
||||
}
|
||||
|
||||
public InkMixerMenu(int id, Inventory inventory, InkMixerBlockEntity mixer) {
|
||||
super(Rivencraft.INK_MIXER_MENU.get(), id);
|
||||
this.mixer = mixer;
|
||||
inputs = mixer == null ? new SimpleContainer(InkMixerBlockEntity.SIZE) : mixer;
|
||||
clientSide = inventory.player.level().isClientSide();
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() { return clientSide ? inkRemaining : mixer.inkRemaining(); }
|
||||
@Override public void set(int value) { inkRemaining = value; }
|
||||
});
|
||||
for (int index = 0; index < modifierChances.length; index++) {
|
||||
final int chanceIndex = index;
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() {
|
||||
return clientSide ? modifierChances[chanceIndex] : mixer.modifierChancePermille(chanceIndex);
|
||||
}
|
||||
@Override public void set(int value) { modifierChances[chanceIndex] = value; }
|
||||
});
|
||||
}
|
||||
addSlot(new MixerSlot(InkMixerBlockEntity.INK, 8, 27));
|
||||
addSlot(new MixerSlot(InkMixerBlockEntity.PAPER, 8, 48));
|
||||
addSlot(new MixerSlot(InkMixerBlockEntity.BOTTLES, 152, 27));
|
||||
addSlot(new ModifierSlot(79, 40));
|
||||
addSlot(new ResultSlot(152, 48));
|
||||
for (int row = 0; row < 3; row++)
|
||||
for (int column = 0; column < 9; column++)
|
||||
addSlot(new Slot(inventory, column + row * 9 + 9, 8 + column * 18, 99 + row * 18));
|
||||
for (int column = 0; column < 9; column++)
|
||||
addSlot(new Slot(inventory, column, 8 + column * 18, 157));
|
||||
refreshResult();
|
||||
}
|
||||
|
||||
public int inkRemaining() { return inkRemaining; }
|
||||
public int modifierChancePermille(int index) { return modifierChances[index]; }
|
||||
|
||||
@Override public void clicked(int slotId, int button, ContainerInput clickType, Player player) {
|
||||
if (slotId == MODIFIER) {
|
||||
if (!clientSide && stillValid(player) && clickType == ContainerInput.PICKUP
|
||||
&& (button == 0 || button == 1)) {
|
||||
ItemStack carried = getCarried();
|
||||
int amount = button == 1 ? 1 : carried.getCount();
|
||||
if (mixer.addModifier(carried, amount)) {
|
||||
setCarried(carried);
|
||||
refreshResult();
|
||||
broadcastChanges();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
super.clicked(slotId, button, clickType, player);
|
||||
// In 26.3, recompute the virtual result in the same packet that changes
|
||||
// paper or ink. Waiting for a later menu update can require a second click.
|
||||
if (!clientSide) refreshResult();
|
||||
}
|
||||
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return mixer != null && mixer.stillValid(player);
|
||||
}
|
||||
|
||||
@Override public void broadcastChanges() {
|
||||
refreshResult();
|
||||
super.broadcastChanges();
|
||||
}
|
||||
|
||||
private void refreshResult() {
|
||||
if (clientSide) return;
|
||||
ItemStack preview = mixer == null ? ItemStack.EMPTY : mixer.previewLinkPanel();
|
||||
if (!ItemStack.matches(result.getItem(0), preview)) result.setItem(0, preview);
|
||||
}
|
||||
|
||||
@Override public ItemStack quickMoveStack(Player player, int index) {
|
||||
if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY;
|
||||
if (index == RESULT) {
|
||||
refreshResult();
|
||||
if (!mixer.canCreateLinkPanel()) return ItemStack.EMPTY;
|
||||
ItemStack output = result.getItem(0).copy();
|
||||
ItemStack remainder = output.copy();
|
||||
if (!moveItemStackTo(remainder, PLAYER_START, slots.size(), true) || !remainder.isEmpty())
|
||||
return ItemStack.EMPTY;
|
||||
if (!mixer.consumeLinkPanelMaterials()) return ItemStack.EMPTY;
|
||||
refreshResult();
|
||||
return output;
|
||||
}
|
||||
Slot slot = slots.get(index);
|
||||
if (!slot.hasItem()) return ItemStack.EMPTY;
|
||||
ItemStack source = slot.getItem();
|
||||
ItemStack original = source.copy();
|
||||
boolean moved;
|
||||
if (index < MODIFIER) moved = moveItemStackTo(source, PLAYER_START, slots.size(), true);
|
||||
else if (source.is(Rivencraft.INK_VIAL.get()))
|
||||
moved = moveItemStackTo(source, InkMixerBlockEntity.INK, InkMixerBlockEntity.INK + 1, false);
|
||||
else if (source.is(Items.PAPER))
|
||||
moved = moveItemStackTo(source, InkMixerBlockEntity.PAPER, InkMixerBlockEntity.PAPER + 1, false);
|
||||
else moved = false;
|
||||
if (!moved) return ItemStack.EMPTY;
|
||||
if (source.isEmpty()) slot.setByPlayer(ItemStack.EMPTY);
|
||||
else slot.setChanged();
|
||||
refreshResult();
|
||||
return original;
|
||||
}
|
||||
|
||||
private final class MixerSlot extends Slot {
|
||||
MixerSlot(int index, int x, int y) { super(inputs, index, x, y); }
|
||||
@Override public boolean mayPlace(ItemStack stack) { return inputs.canPlaceItem(getContainerSlot(), stack); }
|
||||
}
|
||||
|
||||
private static final class ModifierSlot extends Slot {
|
||||
ModifierSlot(int x, int y) { super(new SimpleContainer(1), 0, x, y); }
|
||||
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||
@Override public boolean mayPickup(Player player) { return false; }
|
||||
}
|
||||
|
||||
private final class ResultSlot extends Slot {
|
||||
ResultSlot(int x, int y) { super(result, 0, x, y); }
|
||||
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||
@Override public boolean mayPickup(Player player) {
|
||||
return clientSide ? hasItem() : stillValid(player) && mixer.canCreateLinkPanel();
|
||||
}
|
||||
@Override public void onTake(Player player, ItemStack stack) {
|
||||
if (!clientSide && mixer.consumeLinkPanelMaterials()) {
|
||||
refreshResult();
|
||||
}
|
||||
super.onTake(player, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package net.dnorberg.rivencraft.menu;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.PortBookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.network.PortBookNetworking;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.Container;
|
||||
import net.minecraft.world.SimpleContainer;
|
||||
import net.minecraft.world.entity.player.Inventory;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.inventory.AbstractContainerMenu;
|
||||
import net.minecraft.world.inventory.Slot;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Binder inputs persist in the block; assembling places the finished book on the cursor. */
|
||||
public final class PortBookBinderMenu extends AbstractContainerMenu {
|
||||
public static final int STRIP_CLICK_BASE = 3000;
|
||||
private final Container binder;
|
||||
private final PortBookBinderBlockEntity blockEntity;
|
||||
private final String initialTitle;
|
||||
|
||||
public PortBookBinderMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
|
||||
instanceof PortBookBinderBlockEntity found ? found : null, data.readUtf(32));
|
||||
}
|
||||
|
||||
public PortBookBinderMenu(int id, Inventory inventory, PortBookBinderBlockEntity blockEntity) {
|
||||
this(id, inventory, blockEntity, blockEntity == null ? "" : blockEntity.title());
|
||||
}
|
||||
|
||||
private PortBookBinderMenu(int id, Inventory inventory, PortBookBinderBlockEntity blockEntity, String title) {
|
||||
super(Rivencraft.BOOK_BINDER_MENU.get(), id);
|
||||
this.blockEntity = blockEntity;
|
||||
initialTitle = title;
|
||||
binder = blockEntity == null ? new SimpleContainer(PortBookBinderBlockEntity.SIZE) : blockEntity;
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.COVER, 8, 27) {
|
||||
@Override public boolean mayPlace(ItemStack stack) { return stack.is(Items.LEATHER); }
|
||||
});
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.PANEL, -1000, -1000) {
|
||||
@Override public boolean mayPlace(ItemStack stack) { return PageItem.isLinkPanel(stack); }
|
||||
@Override public int getMaxStackSize() { return 1; }
|
||||
});
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.OUTPUT, 152, 27) {
|
||||
@Override public boolean mayPlace(ItemStack stack) { return false; }
|
||||
});
|
||||
for (int page = 0; page < PortBookBinderBlockEntity.PAGE_COUNT; page++) {
|
||||
addSlot(new Slot(binder, PortBookBinderBlockEntity.FIRST_PAGE + page, -1000, -1000) {
|
||||
@Override public boolean mayPlace(ItemStack stack) {
|
||||
return binder.canPlaceItem(getContainerSlot(), stack);
|
||||
}
|
||||
@Override public int getMaxStackSize() { return 1; }
|
||||
});
|
||||
}
|
||||
addStandardInventorySlots(inventory, 8, 99);
|
||||
}
|
||||
|
||||
@Override public boolean stillValid(Player player) {
|
||||
return blockEntity != null && blockEntity.stillValid(player);
|
||||
}
|
||||
|
||||
public String title() { return initialTitle; }
|
||||
|
||||
@Override public boolean clickMenuButton(Player player, int id) {
|
||||
if (id < STRIP_CLICK_BASE || id > STRIP_CLICK_BASE + PortBookBinderBlockEntity.PAGE_COUNT
|
||||
|| !stillValid(player)) return false;
|
||||
if (player.level().isClientSide()) return true;
|
||||
int index = id - STRIP_CLICK_BASE;
|
||||
ItemStack carried = getCarried();
|
||||
if (!carried.isEmpty()) {
|
||||
if (index == 0) {
|
||||
if (!PageItem.isLinkPanel(carried)
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.PANEL).isEmpty()) return false;
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, carried.copyWithCount(1));
|
||||
} else if (!blockEntity.insertPage(index - 1, carried.copyWithCount(1))) return false;
|
||||
ItemStack remainder = carried.copy();
|
||||
remainder.shrink(1);
|
||||
setCarried(remainder);
|
||||
} else {
|
||||
ItemStack removed = index == 0
|
||||
? binder.removeItem(PortBookBinderBlockEntity.PANEL, 1)
|
||||
: blockEntity.takePage(index - 1);
|
||||
if (removed.isEmpty()) return false;
|
||||
setCarried(removed);
|
||||
}
|
||||
broadcastChanges();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setTitle(ServerPlayer player, String value) {
|
||||
if (stillValid(player) && value != null && value.length() <= PortBookNetworking.MAX_BOOK_TITLE_LENGTH)
|
||||
blockEntity.setTitle(value);
|
||||
}
|
||||
|
||||
public void assemble(ServerPlayer player, String title) {
|
||||
if (!stillValid(player) || title.isBlank() || title.length() > PortBookNetworking.MAX_BOOK_TITLE_LENGTH
|
||||
|| !binder.getItem(PortBookBinderBlockEntity.OUTPUT).isEmpty()
|
||||
|| !PortAuthoredWriting.supports(blockEntity.symbolPages())
|
||||
|| !getCarried().isEmpty()) return;
|
||||
ItemStack cover = binder.getItem(PortBookBinderBlockEntity.COVER);
|
||||
ItemStack panel = binder.getItem(PortBookBinderBlockEntity.PANEL);
|
||||
if (!cover.is(Items.LEATHER) || !PageItem.isLinkPanel(panel)) return;
|
||||
blockEntity.setTitle(title);
|
||||
ItemStack book = new ItemStack(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
var id = PortBookNetworking.createAge(player, title, blockEntity.symbolPages());
|
||||
if (id == null) return;
|
||||
book.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
book.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
if (blockEntity.hasSymbolPages())
|
||||
book.set(Rivencraft.BOOK_SYMBOLS.get(), blockEntity.symbolPages());
|
||||
var modifiers = panel.get(Rivencraft.LINK_PROPERTIES.get());
|
||||
if (modifiers != null) book.set(Rivencraft.LINK_PROPERTIES.get(), modifiers);
|
||||
cover.shrink(1);
|
||||
panel.shrink(1);
|
||||
binder.setItem(PortBookBinderBlockEntity.COVER, cover);
|
||||
binder.setItem(PortBookBinderBlockEntity.PANEL, panel);
|
||||
blockEntity.clearSymbolPages();
|
||||
setCarried(book);
|
||||
broadcastChanges();
|
||||
}
|
||||
|
||||
@Override public ItemStack quickMoveStack(Player player, int index) {
|
||||
if (!stillValid(player) || index < 0 || index >= slots.size()) return ItemStack.EMPTY;
|
||||
Slot slot = slots.get(index);
|
||||
if (!slot.hasItem()) return ItemStack.EMPTY;
|
||||
if (index >= PortBookBinderBlockEntity.FIRST_PAGE && index < PortBookBinderBlockEntity.SIZE) {
|
||||
ItemStack page = slot.getItem().copy();
|
||||
ItemStack remainder = page.copy();
|
||||
if (!moveItemStackTo(remainder, PortBookBinderBlockEntity.SIZE, slots.size(), true)
|
||||
|| !remainder.isEmpty()) return ItemStack.EMPTY;
|
||||
blockEntity.takePage(index - PortBookBinderBlockEntity.FIRST_PAGE);
|
||||
broadcastChanges();
|
||||
return page;
|
||||
}
|
||||
ItemStack stack = slot.getItem();
|
||||
ItemStack original = stack.copy();
|
||||
boolean moved;
|
||||
if (index < PortBookBinderBlockEntity.SIZE) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.SIZE, slots.size(), true);
|
||||
} else if (stack.is(Items.LEATHER)) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.COVER,
|
||||
PortBookBinderBlockEntity.COVER + 1, false);
|
||||
} else if (PageItem.isLinkPanel(stack)) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.PANEL,
|
||||
PortBookBinderBlockEntity.PANEL + 1, false);
|
||||
} else if (stack.is(Rivencraft.PAGE.get()) && stack.has(Rivencraft.SYMBOL_PAGE_ID.get())) {
|
||||
moved = moveItemStackTo(stack, PortBookBinderBlockEntity.FIRST_PAGE,
|
||||
PortBookBinderBlockEntity.SIZE, false);
|
||||
} else return ItemStack.EMPTY;
|
||||
if (!moved) return ItemStack.EMPTY;
|
||||
if (stack.isEmpty()) slot.setByPlayer(ItemStack.EMPTY);
|
||||
else slot.setChanged();
|
||||
slot.onTake(player, stack);
|
||||
return original;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import com.llamalad7.mixinextras.sugar.Local;
|
||||
import net.dnorberg.rivencraft.age.PortCarverPages;
|
||||
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.chunk.CarvingMask;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.NoiseChunk;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.blending.Blender;
|
||||
import net.minecraft.world.level.levelgen.material.rule.MaterialRule;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
|
||||
/** Merge authored geometry before vanilla consumes its mask with aquifer/surface rules. */
|
||||
@Mixin(NoiseBasedChunkGenerator.class)
|
||||
public abstract class AgeCarversMixin {
|
||||
@Inject(method = "generateCarvers", at = @At(value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/level/chunk/CarvingMask;isEmpty()Z"))
|
||||
private void rivencraft$authoredCarvers(ChunkAccess chunk, Blender blender, NoiseChunk noise,
|
||||
RandomState random, BiomeManager biomes, WorldGenRegion region, MaterialRule material,
|
||||
CallbackInfo ci, @Local CarvingMask mask) {
|
||||
if (region == null) return;
|
||||
var level = region.getLevel();
|
||||
if (level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator generator
|
||||
&& generator.base() == (Object) this && generator.terrainFeatures() != null) {
|
||||
PortCarverPages.carve((NoiseBasedChunkGenerator) (Object) this, chunk, random,
|
||||
region.registryAccess(), generator.terrainFeatures().carvers(), level.getSeed(), mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||
import net.dnorberg.rivencraft.age.PortStructurePages;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.LevelHeightAccessor;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.minecraft.world.level.biome.Climate;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.RandomState;
|
||||
import net.minecraft.world.level.levelgen.structure.Structure;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.ModifyArg;
|
||||
|
||||
/** Matches 1.21.1's explicit-page biome override, scoped only to authored Age structures. */
|
||||
@Mixin(Structure.class)
|
||||
public abstract class AgeStructureBiomeMixin {
|
||||
// 26.3 jigsaw placement checks biome columns inside findGenerationPoint,
|
||||
// before the final isValidBiome test. Set the predicate when constructing
|
||||
// the context so both checks see the explicit-page override.
|
||||
@ModifyArg(method = "generate", at = @At(value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/level/levelgen/structure/Structure$GenerationContext;<init>(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/world/level/biome/BiomeSource;Lnet/minecraft/world/level/biome/Climate$Sampler;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager;JLnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/LevelHeightAccessor;Ljava/util/function/Predicate;)V"),
|
||||
index = 9)
|
||||
private Predicate<Holder<Biome>> rivencraft$authoredBiome(
|
||||
RegistryAccess registries, ChunkGenerator chunkGenerator, BiomeSource biomes,
|
||||
Climate.Sampler sampler, RandomState random, StructureTemplateManager templates,
|
||||
long seed, ChunkPos chunk, LevelHeightAccessor height, Predicate<Holder<Biome>> original) {
|
||||
if (chunkGenerator instanceof PortFeatureChunkGenerator generator
|
||||
&& generator.terrainFeatures() != null
|
||||
&& PortStructurePages.authored((Structure) (Object) this, registries,
|
||||
generator.terrainFeatures().structures())) {
|
||||
return biome -> true;
|
||||
}
|
||||
return original;
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import java.util.Optional;
|
||||
import net.minecraft.world.level.levelgen.structure.placement.AbstractSpreadingStructurePlacement;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.gen.Accessor;
|
||||
|
||||
/** Read-only placement settings for Age-local copies, never edits global placements. */
|
||||
@Mixin(AbstractSpreadingStructurePlacement.class)
|
||||
public interface StructurePlacementAccessor {
|
||||
@Accessor("frequencyReductionMethod")
|
||||
AbstractSpreadingStructurePlacement.FrequencyReductionMethod rivencraft$frequencyReductionMethod();
|
||||
@Accessor("frequency") float rivencraft$frequency();
|
||||
@Accessor("salt") int rivencraft$salt();
|
||||
@Accessor("exclusionZone")
|
||||
Optional<AbstractSpreadingStructurePlacement.ExclusionZone> rivencraft$exclusionZone();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
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;
|
||||
|
||||
public record AssembleBinderPayload(String title) implements CustomPacketPayload {
|
||||
public static final Type<AssembleBinderPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "assemble_binder"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, AssembleBinderPayload> STREAM_CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.stringUtf8(32), AssembleBinderPayload::title,
|
||||
AssembleBinderPayload::new);
|
||||
@Override public Type<AssembleBinderPayload> type() { return TYPE; }
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
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;
|
||||
|
||||
public record BindDescriptiveBookPayload(boolean offhand, String title) implements CustomPacketPayload {
|
||||
public static final Type<BindDescriptiveBookPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "bind_descriptive_book"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, BindDescriptiveBookPayload> STREAM_CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.BOOL, BindDescriptiveBookPayload::offhand,
|
||||
ByteBufCodecs.stringUtf8(32), BindDescriptiveBookPayload::title,
|
||||
BindDescriptiveBookPayload::new);
|
||||
|
||||
@Override
|
||||
public Type<BindDescriptiveBookPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
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;
|
||||
|
||||
public record BindLinkingBookPayload(boolean offhand, String title) implements CustomPacketPayload {
|
||||
public static final Type<BindLinkingBookPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "bind_linking_book"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, BindLinkingBookPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.BOOL, BindLinkingBookPayload::offhand,
|
||||
ByteBufCodecs.stringUtf8(32), BindLinkingBookPayload::title,
|
||||
BindLinkingBookPayload::new);
|
||||
|
||||
@Override
|
||||
public Type<BindLinkingBookPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTravel;
|
||||
import net.dnorberg.rivencraft.age.PortAgeTerrain;
|
||||
import net.dnorberg.rivencraft.age.RuntimeAgeBackend;
|
||||
import net.dnorberg.rivencraft.age.PortRandomAgeSelection;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.item.LinkingBookItem;
|
||||
import net.dnorberg.rivencraft.menu.PortBookBinderMenu;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.sounds.SoundEvents;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.world.InteractionHand;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
|
||||
import java.util.Locale;
|
||||
import java.util.List;
|
||||
|
||||
/** Server-authoritative handheld Linking Book binding and travel. */
|
||||
public final class PortBookNetworking {
|
||||
public static final int MAX_BOOK_TITLE_LENGTH = 32;
|
||||
|
||||
private PortBookNetworking() {
|
||||
}
|
||||
|
||||
public static void registerPayloads(RegisterPayloadHandlersEvent event) {
|
||||
var registrar = event.registrar("1");
|
||||
registrar.playToServer(BindLinkingBookPayload.TYPE, BindLinkingBookPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) bind(player, payload);
|
||||
});
|
||||
registrar.playToServer(UseLinkingBookPayload.TYPE, UseLinkingBookPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) use(player, payload);
|
||||
});
|
||||
registrar.playToServer(BindDescriptiveBookPayload.TYPE, BindDescriptiveBookPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) bindAge(player, payload);
|
||||
});
|
||||
registrar.playToServer(UseDescriptiveBookPayload.TYPE, UseDescriptiveBookPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player) enterAge(player, payload);
|
||||
});
|
||||
registrar.playToServer(AssembleBinderPayload.TYPE, AssembleBinderPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player
|
||||
&& player.containerMenu instanceof PortBookBinderMenu menu) {
|
||||
menu.assemble(player, payload.title().strip());
|
||||
}
|
||||
});
|
||||
registrar.playToServer(SetBinderTitlePayload.TYPE, SetBinderTitlePayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
if (context.player() instanceof ServerPlayer player
|
||||
&& player.containerMenu instanceof PortBookBinderMenu menu
|
||||
&& menu.containerId == payload.containerId()) {
|
||||
menu.setTitle(player, payload.title());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void bind(ServerPlayer player, BindLinkingBookPayload payload) {
|
||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||
if (!stack.is(Rivencraft.LINKING_BOOK.get()) || stack.has(Rivencraft.LINK_BOOK_TARGET.get())) return;
|
||||
String title = payload.title().strip();
|
||||
if (title.isEmpty() || title.length() > MAX_BOOK_TITLE_LENGTH) return;
|
||||
stack.set(Rivencraft.LINK_BOOK_TARGET.get(), LinkBookTarget.at(player));
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
player.sendOverlayMessage(Component.literal("Bound Linking Book: " + title));
|
||||
player.containerMenu.broadcastChanges();
|
||||
}
|
||||
|
||||
private static void use(ServerPlayer player, UseLinkingBookPayload payload) {
|
||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||
if (!stack.is(Rivencraft.LINKING_BOOK.get())) return;
|
||||
String failure = LinkingBookItem.travel(player, stack);
|
||||
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
|
||||
}
|
||||
|
||||
private static void bindAge(ServerPlayer player, BindDescriptiveBookPayload payload) {
|
||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||
if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get()) || stack.has(Rivencraft.AGE_BOOK_ID.get())) return;
|
||||
String title = payload.title().strip();
|
||||
if (title.isEmpty() || title.length() > MAX_BOOK_TITLE_LENGTH) return;
|
||||
Identifier id = createAge(player, title);
|
||||
if (id == null) return;
|
||||
stack.set(Rivencraft.AGE_BOOK_ID.get(), id);
|
||||
stack.set(DataComponents.CUSTOM_NAME, Component.literal(title));
|
||||
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN,
|
||||
SoundSource.PLAYERS, 1.0F, 1.0F);
|
||||
player.sendOverlayMessage(Component.literal("Bound Descriptive Book: " + title));
|
||||
player.containerMenu.broadcastChanges();
|
||||
}
|
||||
|
||||
/** Shared name resolution and Age creation for handheld books and the Binder. */
|
||||
public static Identifier createAge(ServerPlayer player, String title) {
|
||||
return createAge(player, title, List.of());
|
||||
}
|
||||
|
||||
public static Identifier createAge(ServerPlayer player, String title, List<Identifier> pages) {
|
||||
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
|
||||
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
|
||||
try {
|
||||
biomePlan = PortAuthoredWriting.biomePlan(pages, player.registryAccess());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
|
||||
return null;
|
||||
}
|
||||
String base = title.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9._-]+", "_")
|
||||
.replaceAll("^[_\\.-]+|[_\\.-]+$", "");
|
||||
if (base.isEmpty()) base = "age";
|
||||
var server = player.level().getServer();
|
||||
String name = base;
|
||||
int suffix = 2;
|
||||
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
while (RuntimeAgeBackend.isNameTaken(server, id)) {
|
||||
name = base + "_" + suffix++;
|
||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
}
|
||||
try {
|
||||
long seed = player.getRandom().nextLong();
|
||||
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
|
||||
if (pages.isEmpty()) {
|
||||
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
|
||||
terrain = random.terrain();
|
||||
biomePlan = random.biomes();
|
||||
}
|
||||
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||
PortAuthoredWriting.rockPlan(pages, seed),
|
||||
PortAuthoredWriting.vegetationPlan(pages),
|
||||
PortAuthoredWriting.dungeonCount(pages),
|
||||
PortAuthoredWriting.lakePlan(pages, seed),
|
||||
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
|
||||
net.dnorberg.rivencraft.age.PortWeatherMode.fromPages(pages));
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||
return null;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
private static void enterAge(ServerPlayer player, UseDescriptiveBookPayload payload) {
|
||||
ItemStack stack = player.getItemInHand(hand(payload.offhand()));
|
||||
if (!stack.is(Rivencraft.DESCRIPTIVE_BOOK.get())) return;
|
||||
Identifier id = stack.get(Rivencraft.AGE_BOOK_ID.get());
|
||||
if (id == null) return;
|
||||
ServerLevel source = player.level();
|
||||
Vec3 departure = player.position();
|
||||
Vec3 incoming = player.getDeltaMovement();
|
||||
String failure = PortAgeTravel.enter(player, id);
|
||||
if (failure != null) player.sendOverlayMessage(Component.literal(failure));
|
||||
else LinkingBookItem.applySuccessfulLink(player,
|
||||
stack.getOrDefault(Rivencraft.LINK_PROPERTIES.get(), LinkProperties.NONE),
|
||||
source, departure, incoming, player.getYRot());
|
||||
}
|
||||
|
||||
private static InteractionHand hand(boolean offhand) {
|
||||
return offhand ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
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;
|
||||
|
||||
public record SetBinderTitlePayload(int containerId, String title) implements CustomPacketPayload {
|
||||
public static final Type<SetBinderTitlePayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "set_binder_title"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, SetBinderTitlePayload> STREAM_CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.VAR_INT, SetBinderTitlePayload::containerId,
|
||||
ByteBufCodecs.stringUtf8(32), SetBinderTitlePayload::title,
|
||||
SetBinderTitlePayload::new);
|
||||
@Override public Type<SetBinderTitlePayload> type() { return TYPE; }
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
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;
|
||||
|
||||
public record UseDescriptiveBookPayload(boolean offhand) implements CustomPacketPayload {
|
||||
public static final Type<UseDescriptiveBookPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "use_descriptive_book"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, UseDescriptiveBookPayload> STREAM_CODEC =
|
||||
StreamCodec.composite(ByteBufCodecs.BOOL, UseDescriptiveBookPayload::offhand,
|
||||
UseDescriptiveBookPayload::new);
|
||||
|
||||
@Override
|
||||
public Type<UseDescriptiveBookPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package net.dnorberg.rivencraft.network;
|
||||
|
||||
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;
|
||||
|
||||
public record UseLinkingBookPayload(boolean offhand) implements CustomPacketPayload {
|
||||
public static final Type<UseLinkingBookPayload> TYPE = new Type<>(
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "use_linking_book"));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, UseLinkingBookPayload> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.BOOL, UseLinkingBookPayload::offhand, UseLinkingBookPayload::new);
|
||||
|
||||
@Override
|
||||
public Type<UseLinkingBookPayload> type() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.dnorberg.rivencraft.writing.PortSymbolPages;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.world.item.CreativeModeTab;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
/** Only items already ported to 26.3 appear here. */
|
||||
public final class ModCreativeTabs {
|
||||
private static final DeferredRegister<CreativeModeTab> TABS =
|
||||
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, Rivencraft.MOD_ID);
|
||||
|
||||
static {
|
||||
TABS.register("rivencraft", () -> CreativeModeTab.builder()
|
||||
.title(Component.translatable("itemGroup.rivencraft"))
|
||||
.icon(() -> Rivencraft.DESCRIPTIVE_BOOK.get().getDefaultInstance())
|
||||
.displayItems((parameters, output) -> {
|
||||
output.accept(Rivencraft.DESCRIPTIVE_BOOK.get());
|
||||
output.accept(Rivencraft.LINKING_BOOK.get());
|
||||
output.accept(Rivencraft.CRYSTAL_BLOCK_ITEM.get());
|
||||
output.accept(Rivencraft.BUDDING_CRYSTAL_ITEM.get());
|
||||
output.accept(Rivencraft.SMALL_CRYSTAL_BUD_ITEM.get());
|
||||
output.accept(Rivencraft.MEDIUM_CRYSTAL_BUD_ITEM.get());
|
||||
output.accept(Rivencraft.LARGE_CRYSTAL_BUD_ITEM.get());
|
||||
output.accept(Rivencraft.CRYSTAL_CLUSTER_ITEM.get());
|
||||
output.accept(Rivencraft.CRYSTAL_SHARD.get());
|
||||
output.accept(Rivencraft.INK_VIAL.get());
|
||||
output.accept(Rivencraft.BOOK_BINDER_ITEM.get());
|
||||
output.accept(Rivencraft.INK_MIXER_ITEM.get());
|
||||
})
|
||||
.build());
|
||||
TABS.register("rivencraft_link_pages", () -> CreativeModeTab.builder()
|
||||
.title(Component.translatable("itemGroup.rivencraft.pages.link"))
|
||||
.icon(PageItem::createLinkPanelPage)
|
||||
.displayItems((parameters, output) -> {
|
||||
int[] flags = {LinkProperties.DISARM, LinkProperties.MAINTAIN_MOMENTUM,
|
||||
LinkProperties.INTRA_LINKING};
|
||||
for (int combination = 0; combination < 8; combination++) {
|
||||
int selected = 0;
|
||||
for (int bit = 0; bit < flags.length; bit++)
|
||||
if ((combination & (1 << bit)) != 0) selected |= flags[bit];
|
||||
output.accept(PageItem.createLinkPanelPage(selected));
|
||||
}
|
||||
})
|
||||
.build());
|
||||
for (String category : PortSymbolPages.categories()) {
|
||||
String path = category.substring(category.indexOf(':') + 1);
|
||||
TABS.register("rivencraft_pages_" + path, () -> CreativeModeTab.builder()
|
||||
.title(Component.translatableWithFallback("itemGroup.rivencraft.pages." + path,
|
||||
"Rivencraft " + PortSymbolPages.fallbackName(
|
||||
net.minecraft.resources.Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path))
|
||||
+ " Pages"))
|
||||
.icon(() -> PortSymbolPages.icon(category))
|
||||
.displayItems((parameters, output) ->
|
||||
PortSymbolPages.pages(category, parameters.holders()).forEach(output::accept))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
private ModCreativeTabs() {
|
||||
}
|
||||
|
||||
public static void register(IEventBus modEventBus) {
|
||||
TABS.register(modEventBus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.PortScaledBiomeSource;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
/** Serializes the custom biome resolver in saved 26.3 Age level stems. */
|
||||
public final class PortBiomeSources {
|
||||
private static final DeferredRegister<MapCodec<? extends BiomeSource>> SOURCES =
|
||||
DeferredRegister.create(Registries.BIOME_SOURCE, Rivencraft.MOD_ID);
|
||||
|
||||
static {
|
||||
SOURCES.register("scaled", () -> PortScaledBiomeSource.CODEC);
|
||||
}
|
||||
|
||||
private PortBiomeSources() {}
|
||||
|
||||
public static void register(IEventBus bus) {
|
||||
SOURCES.register(bus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.dnorberg.rivencraft.registry;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.age.PortFlatChunkGenerator;
|
||||
import net.dnorberg.rivencraft.age.PortFeatureChunkGenerator;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
/** Codec for the authored Flat/Void biome source and layer combination. */
|
||||
public final class PortChunkGenerators {
|
||||
private static final DeferredRegister<MapCodec<? extends ChunkGenerator>> GENERATORS =
|
||||
DeferredRegister.create(Registries.CHUNK_GENERATOR, Rivencraft.MOD_ID);
|
||||
|
||||
static {
|
||||
GENERATORS.register("age_flat", () -> PortFlatChunkGenerator.CODEC);
|
||||
// Keep the beta codec ID so previously saved Rocks Ages remain readable.
|
||||
GENERATORS.register("age_rocks", () -> PortFeatureChunkGenerator.CODEC);
|
||||
}
|
||||
|
||||
private PortChunkGenerators() {}
|
||||
|
||||
public static void register(IEventBus bus) {
|
||||
GENERATORS.register(bus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.item.LinkProperties;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
|
||||
/** Built-in Legacy ink additives, carried forward from the 1.21.1 mixer. */
|
||||
public final class InkModifierIngredients {
|
||||
public record Effect(int flag, double chance) {}
|
||||
public record Ingredient(ItemStack item, List<Effect> effects) {}
|
||||
|
||||
private static final List<Ingredient> BUILT_IN = List.of(
|
||||
new Ingredient(new ItemStack(Items.GUNPOWDER), List.of(new Effect(LinkProperties.DISARM, 0.20))),
|
||||
new Ingredient(new ItemStack(Items.MUSHROOM_STEW), List.of(new Effect(LinkProperties.DISARM, 0.05))),
|
||||
new Ingredient(new ItemStack(Items.EXPERIENCE_BOTTLE), List.of(new Effect(LinkProperties.INTRA_LINKING, 0.15))),
|
||||
new Ingredient(new ItemStack(Items.ENDER_PEARL), List.of(
|
||||
new Effect(LinkProperties.INTRA_LINKING, 0.15),
|
||||
new Effect(LinkProperties.DISARM, 0.15))),
|
||||
new Ingredient(new ItemStack(Items.FEATHER), List.of(new Effect(LinkProperties.MAINTAIN_MOMENTUM, 0.15))),
|
||||
new Ingredient(new ItemStack(Items.FIRE_CHARGE), List.of(new Effect(LinkProperties.DISARM, 0.25)))
|
||||
);
|
||||
|
||||
private InkModifierIngredients() {}
|
||||
|
||||
public static List<Ingredient> builtIn() { return BUILT_IN; }
|
||||
|
||||
public static Ingredient find(ItemStack stack) {
|
||||
return BUILT_IN.stream().filter(ingredient -> stack.is(ingredient.item().getItem())).findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 172 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user