Compare commits

...
2 Commits
Author SHA1 Message Date
dnorberg b06fbda461 Complete Age lifecycle management (#8)
Build / build (push) Canceled after 0s
2026-09-23 00:55:26 -04:00
dnorberg c0bc91858e Organize project tracking on Gitea (#13)
Build / build (push) Canceled after 0s
2026-09-23 00:43:40 -04:00
13 changed files with 349 additions and 444 deletions

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
---
name: Bug report
about: Incorrect behavior or a regression in Rivencraft
title: ""
labels: type/bug
assignees: ""
---
## Observed behavior
Describe what happened, including the dimension, book type, preview mode, and
installed optional mods when relevant.
## Expected behavior
Describe what should have happened.
## Reproduction
1. List the minimum steps needed to reproduce the problem.
2. Include the Rivencraft, NeoForge, and optional-mod versions.
3. Attach the relevant log or crash report.
## Acceptance criteria
- [ ] The reproduction no longer fails.
- [ ] A focused regression check covers the corrected behavior when practical.
+21
View File
@@ -0,0 +1,21 @@
---
name: Implementation task
about: A scoped feature, refactor, compatibility, or documentation task
title: ""
labels: type/feature
assignees: ""
---
## Goal
Describe the behavior or outcome this task should deliver.
## Acceptance criteria
- [ ] Add observable acceptance criteria.
- [ ] Cover persistence, migration, or compatibility when applicable.
- [ ] Record the validation required before closing the issue.
## Context
Link related issues, upstream references, and relevant design constraints.
+2 -2
View File
@@ -107,8 +107,8 @@ Mystcraft Legacy targets Forge for Minecraft 1.12.2. Rivencraft therefore
ports one subsystem at a time onto current registries, codecs, data packs, and ports one subsystem at a time onto current registries, codecs, data packs, and
NeoForge APIs instead of importing the legacy source tree wholesale. NeoForge APIs instead of importing the legacy source tree wholesale.
See [docs/PORTING.md](docs/PORTING.md) for the initial roadmap and provenance See [docs/PORTING.md](docs/PORTING.md) for the roadmap, live Gitea tracking
rules. links, architectural direction, and provenance rules.
## License ## License
+39
View File
@@ -0,0 +1,39 @@
# Age administration
Rivencraft owns each Age definition for the lifetime of a world. A definition
contains the Age UUID, canonical name, seed, generation version, runtime
dimension identifier, spawn, and deterministic traits. Definitions are stored
in the Overworld saved data and are never rerolled during ordinary loading.
## Lifecycle
An Age definition is created before its runtime dimension. If dimension
creation is interrupted, the saved definition remains authoritative and the
same Age is reconstructed the next time its Descriptive Book is used.
Registered Ages load when the server starts. A book also loads its destination
on demand when an administrator previously unloaded it. Unloading retains the
definition and world folder, so existing books continue to reference the same
Age and later reload the same terrain.
Rivencraft does not automatically delete Ages. Safe permanent deletion needs a
complete reference index for books in inventories, containers, Book Stands,
and unloaded chunks. Until that exists, retaining the definition prevents an
administrator action from silently orphaning books.
Destination-preview chunk loaders are temporary. Rivencraft releases them when
the book closes, the player travels, the player disconnects, the destination
Age is unloaded, or the server stops.
## Commands
The commands require permission level 2.
- `/rivencraft age list` lists every registered Age and whether its runtime
dimension is currently loaded.
- `/rivencraft age unload <dimension>` safely unloads one Age. Players in that
Age are handled by Dynamic Dimensions, and its definition and world data are
retained. Use the command suggestions to select a registered dimension.
Opening a Descriptive Book for an unloaded Age loads it again from its saved
definition.
+59 -437
View File
@@ -1,454 +1,76 @@
# Porting plan # Porting roadmap
Mystcraft Legacy contains more than 700 Java source files and targets Minecraft Rivencraft ports Mystcraft Legacy from Minecraft 1.12.2 to NeoForge 1.21.1.
1.12.2. The Minecraft world-generation, dimension, registry, networking, and The world-generation, dimension, registry, networking, and rendering APIs have
rendering APIs have all changed substantially since that release. The port is changed substantially, so the work is divided into independently testable
split into independently testable milestones. milestones.
## Project tracking
Gitea is the source of truth for project status:
- [Issues](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/issues) contain
scoped implementation tasks, bugs, acceptance criteria, and technical
discussion.
- [Milestones](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestones)
group issues into the porting phases below and show their live completion
state.
- [Releases](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/releases)
contain user-facing changelogs for shipped versions.
New work should have an issue before implementation begins. Commits should
reference the issue number when practical. A milestone closes only after its
acceptance issue and all required implementation issues are complete.
## Milestones ## Milestones
1. **Bootstrap** — NeoForge 1.21.1 build, mod metadata, item registration, and 1. **Bootstrap** — NeoForge 1.21.1 build, mod metadata, item registration, and
CI. **Complete.** CI. **Complete.**
2. **Books and linking** — descriptive/linking book data components, lectern-like 2. **Books and linking** — Descriptive and Linking Book data, Book Stands,
interaction, safe teleportation, and return links. **Complete.** safe teleportation, return links, book screens, and destination previews.
3. **Ages** — dimension definitions backed by data packs, deterministic seeds, **Complete.**
lifecycle management, and persistence. Make the legacy-style animated 3. **Ages** — persistent generated dimensions, deterministic traits,
linking panel the built-in destination preview. Provide optional lifecycle management, data-pack integration, and optional destination
Create/Ponder and Immersive Portals integrations as alternative rotating- renderer compatibility. **Active.** See
snapshot renderers. The configurable preview modes are `AUTO`, [Milestone 3](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestone/8).
`IMMERSIVE_PORTALS`, `PONDER`, and `LEGACY`; `AUTO` prefers Immersive
Portals when it is installed, then Create/Ponder, and finally the built-in
legacy animation.
4. **Writing system** — pages, symbols, notebooks, desks, and book assembly. 4. **Writing system** — pages, symbols, notebooks, desks, and book assembly.
5. **Age grammar** — translate symbol combinations into modern biome, noise, 5. **Age grammar** — translate symbol combinations into modern biome, noise,
dimension-type, and worldgen configuration. Optional integrations with dimension-type, and world-generation configuration. Optional integrations
TerraBlender provide composable biome-region generation, while Serene may add composable biome regions and seasonal behavior.
Seasons and Ecliptic Seasons allow Age generation to participate in their 6. **Instability** — deterministic validation and server-safe effects.
respective seasonal systems. True Always Snow weather will be introduced
with these season integrations; until then, its persisted trait uses
biome-dependent precipitation. None of these integrations are required for
Rivencraft's core world generation.
6. **Instability** — deterministic validation and effects with server-safe
behavior.
7. **Structures and polish** — libraries, crystals, portals, recipes, models, 7. **Structures and polish** — libraries, crystals, portals, recipes, models,
sounds, advancements, compatibility, and migration tests. sounds, advancements, compatibility, and migration tests.
## Architectural direction
Age references use UUIDs rather than dimension keys. The server-owned Age
registry maps each stable identity to its immutable definition and runtime
dimension, keeping saved books valid if an Age is migrated or regenerated.
Operational lifecycle and unload behavior are documented in
[AGES.md](AGES.md).
The core mod must remain usable without Create, Ponder, or Immersive Portals.
The built-in legacy animation is the dependency-free destination preview.
Optional preview modes may use Create/Ponder snapshots or Immersive Portals
live rendering when their dependencies are installed.
The writing system and Age grammar will build on the existing page-free Age
path. Page-free books remain a useful deterministic fallback and test fixture
after written symbol combinations become available.
## Provenance rules ## Provenance rules
- New code uses the `net.dnorberg.rivencraft` package. - New code uses the `net.dnorberg.rivencraft` package.
- Adapted Mystcraft code must retain its upstream copyright notice and be - Adapted Mystcraft code retains its upstream copyright notice and is tracked
tracked in this document or a nearby source comment. in a nearby source comment or commit.
- Do not copy generated, binary, or third-party dependency content from the - Generated, binary, and third-party dependency content is not copied from the
legacy repository. legacy repository.
- Preserve LGPL-3.0-or-later compatibility for the complete distributed mod. - The complete distributed mod remains compatible with LGPL-3.0-or-later.
## First vertical slice ## Release policy
The first functional target is: create a Descriptive Book, bind it to one The version scheme is documented in the project README. Every published
server-managed Age definition, travel to that Age, and use a Linking Book to version receives a Gitea release containing its changelog, built mod JAR, and
return. This exercises persistence, networking, dimensions, and safety before SHA-256 checksum. Release notes describe user-visible behavior, compatibility
the symbol grammar is introduced. changes, migrations, and validation performed for that version; ongoing task
notes stay in issues.
### Milestone 2 progress
- [x] Persistent, network-synchronized link-target data component.
- [x] Linking Book binding with dimension, block position, yaw, and pitch.
- [x] Server-authoritative cross-dimension travel with world-border, collision,
and fluid checks.
- [x] Nearby landing search and clear failure messages.
- [x] Descriptive Book identity and Age reference components.
- [x] Temporary vanilla-dimension Descriptive Book travel with return-link creation.
- [x] Lectern-to-Book-Stand conversion, displayed book, activation, removal,
persistence, and drops.
- [x] Book-screen foundation with server-authoritative naming, binding, and
explicit link activation.
- [x] Ponder dependency and isolated destination-preview level boundary.
- [x] Server-provided destination snapshots rendered as rotating Ponder scenes.
- [x] Book Stand floating names synchronized from the stored book stack.
- [x] Open the stored book screen from a Book Stand with server-validated linking.
- [x] Automated in-game tests for binding, persistence, and travel.
The temporary shapeless Linking Book recipe makes the linking subsystem easy
to test. It can move into the writing/bookbinder progression when that system
is implemented.
Age references use UUIDs rather than dimension keys. Milestone 3 will own the
mapping from that stable identity to server data and a runtime dimension. This
keeps saved books valid if an Age is migrated or regenerated.
Vanilla lecterns accept extensible item tags when inserting books, but their
block entity still validates only vanilla writable and written books. To avoid
an invalid lectern state, inserting a Rivencraft book converts the lectern into
a visually compatible Rivencraft Book Stand. Removing the book converts it
back.
Mystcraft Legacy did not impose an item cooldown when linking. Rivencraft's
initial cooldown used Minecraft's item-type cooldown system, which delayed all
Linking Book stacks at once. Version 0.1.3 removes that cooldown to match the
legacy behavior.
Version 0.1.4 moves Linking Book binding and travel behind a book screen. The
client requests an action, while the server revalidates the held book and owns
all changes to its destination and custom name. Descriptive Book Age references
now carry a display name alongside their stable UUID, with backward-compatible
decoding for existing UUID-only data.
Version 0.1.5 ensures the book interaction listener opens screens only for the
logical-client event. This avoids calling the render system from the integrated
server thread when a book is used in singleplayer.
Version 0.1.6 gives the Linking Book screen an open-book layout inspired by the
legacy interface. The destination preview occupies the left page, the book name
and destination details occupy the right page, and the preview itself is the
link action; the separate Link and Done buttons have been removed.
Version 0.1.7 suppresses Minecraft's default full-screen menu blur for the
Linking Book screen. The open-book texture supplies the GUI background while
the world remains sharp behind it.
Version 0.1.8 renders all Linking Book destination details in the same dark ink
color as the book name for consistent contrast against the parchment.
Version 0.1.9 synchronizes the Book Stand's stored stack to clients and renders
its display name as a camera-facing label above the book.
Version 0.1.10 corrects the nameplate transform and matches Minecraft's normal
plus see-through text passes so angled stand geometry cannot hide the label.
Version 0.1.11 opens the stored book interface when a Book Stand is used with
an empty hand. Linking Books travel through the preview panel, and the server
revalidates the stand position, interaction distance, stored item, and target.
Version 0.1.12 replaces the Linking Book screen's convenience text draws with
explicit no-shadow rendering for clearer dark ink on the parchment texture.
Version 0.1.13 requests a bounded, server-validated 9-by-7-by-9 block snapshot
from the held book or nearby Book Stand and renders it as a clipped, rotating
Ponder world section on the Linking Book's left page.
Version 0.1.14 expands the destination snapshot to 9-by-7-by-17 blocks along
the Z axis, rescales the rotating scene to expose that depth, and colors the
panel background with the destination biome's sky color.
Version 0.1.15 adds three dedicated-server GameTests covering Linking Book
component serialization, successful safe travel with restored orientation, and
rejection of fully blocked destinations. The test suite uses a minimal bundled
structure template and runs with `./gradlew runGameTestServer`.
Version 0.1.16 makes destination previews sample a centered 17-by-7-by-17
volume. The Ponder scene uses a scale that keeps its full horizontal diagonal
inside the panel, so the complete snapshot remains available during its orbit.
Version 0.1.17 changes destination previews to a 9-by-128-by-9 volume: a
square horizontal footprint with a vertical slice spanning 64 blocks below and
63 blocks above the recorded linking location.
Version 0.1.18 changes the preview cache to 9-by-64-by-9, spanning 32 blocks
below and 31 above the link point. Its camera is centered on a 9-by-7-by-9
window around the link point so the book retains a readable close-up.
Version 0.1.19 renders the complete 9-by-64-by-9 snapshot in that Ponder
scene while retaining the target-centered camera framing.
Version 0.1.20 uses a cubic 18-by-18-by-18 snapshot. The renderer remaps the
link point within that cube so its existing target-centered camera framing is
unchanged.
Version 0.1.21 treats air and leaf blocks as passable linking destinations;
links no longer require a solid floor beneath the player.
Version 0.2.0 completes Milestone 2. Until Milestone 3 provides generated Age
dimensions, Descriptive Book UUIDs deterministically resolve to vanilla
dimensions and create a bound Return Link to the source on successful travel.
Version 0.2.1 gives Descriptive Books the same open-parchment layout and
left-page link interaction as Linking Books.
Version 0.2.2 adds the same server-provided Ponder destination preview to
Descriptive Books. If no snapshot is available, the left page shows an
animated linking-panel fallback.
Version 0.2.3 requires Descriptive Book destinations to have a sturdy floor,
while Linking Books continue to permit passable floorless destinations.
Version 0.2.4 expands Descriptive Book arrival validation with an outward,
terrain-aware search for an alternate grounded location near an unsafe target.
Version 0.2.5 uses dimension-appropriate preview skies for vanilla Nether and
End destinations instead of their inherited Overworld biome sky tint.
Version 0.2.6 expands destination snapshots to a 32-by-32-by-32 cube while
keeping the existing target-centered Ponder camera position.
Version 0.2.7 persists each Age's first successful visit and creates its
Return Link only for that initial arrival.
Version 0.2.8 makes Return Link creation a server setting: `ON_CREATION`
(default), `ALWAYS`, or `DISABLED`.
Version 0.2.9.0 begins Milestone 3 with a server-owned, world-persistent Age
registry. Each Age UUID resolves exactly once to an immutable random seed,
generation profile, generation-version marker, canonical name, and creation
time. The temporary vanilla-dimension adapter remains in place until runtime
Age dimensions are introduced by a later release slice.
Version 0.2.10.0 makes page-free Descriptive Books playable. Naming an
unwritten book creates a dedicated, persistent vanilla-like Age through the
Dynamic Dimensions API. Each Age reloads with its server, retains its terrain,
and receives a five-by-five cobblestone platform with cleared arrival headroom.
Version 0.2.10.1 improves Age arrival placement. Non-Nether Ages select the
highest sturdy surface near the origin and fall back to a platform at Y=63 when
no suitable natural footing exists.
Version 0.2.10.2 allows leaf blocks to count as proper natural footing when
selecting the highest arrival surface.
Version 0.2.10.3 scans each candidate column downward from the dimension's top
before choosing footing, preventing cave floors from being selected as the Age
arrival surface. Columns covered by fluid remain ineligible.
Version 0.2.10.4 restores Mystcraft Legacy's standard linking sound for both
Linking and Descriptive Books. Successful travel plays the sound at its source
and destination with the legacy volume and pitch variation.
Version 0.2.10.5 derives each new Age's saved dimension folder from its
player-assigned name. Names are converted to safe lowercase resource paths,
and folder conflicts receive `_2`, `_3`, and later numeric suffixes. Existing
UUID-based dimension folders remain unchanged for save compatibility.
Version 0.2.10.6 prevents a Descriptive Book from reopening while its Age is
being generated. Screen opening now waits for the server's current copy of the
held or displayed book, so a delayed interaction opens only the completed Age.
Version 0.2.11.0 adds a dependency-free, legacy-style portal bloom as the
fallback destination preview for Linking and Descriptive Books. Clients with
Create installed retain the rotating Ponder snapshot; clients without Create
use the destination-tinted animation with the original effect's paired noisy
spirals.
Version 0.2.11.1 makes Create genuinely optional. Ponder remains a compile-time
API for the Create integration, while generated mod metadata no longer requires
Ponder and marks Create as an optional client dependency.
Version 0.2.12.0 introduces the first legacy-inspired Age generator profiles.
New page-free Ages deterministically select and persist Standard, Amplified,
Island, Cave, Flat, or Void base terrain. Existing `random_vanilla` Ages remain
mapped to their original Overworld generator for save compatibility. Terrain
alterations such as Skylands, Tendrils, Spheres, and Floating Islands remain
future Age-grammar work layered above these base profiles.
Version 0.2.12.1 displays the persisted terrain generator on the description
side of written Descriptive Books. Newly written books receive the synchronized
profile immediately, while older books are backfilled from the server-owned Age
registry when opened.
Version 0.2.13.0 constructs Standard, Amplified, Cave, and Island terrain from
Minecraft's worldgen registries instead of borrowing generator instances from
the vanilla levels. Noise settings and biome-source parameter lists therefore
honor the server's loaded data packs. Automated coverage also verifies that a
live Age's seed equals its immutable saved definition; Dynamic Dimensions
derives both from the Overworld seed and the Age dimension identifier.
Version 0.2.16.13 gives Cave and Island/Skylands Ages Overworld biomes, sky,
weather, lighting, and day/night behavior while retaining distinct terrain.
Caves remain enclosed. Skylands use Minecraft's uniform floating-island noise
and Overworld-like surfaces without the End's central dragon island.
Version 0.2.16.14 replaces the Explore text control with a compact eye symbol.
Books opened from a Book Stand display the control disabled, preventing
exploration while the book is mounted.
Version 0.2.16.15 restores Immersive Portals chunk loading for snapshots opened
from Book Stands. Snapshot chunk loading is independent from whether the Explore
control is enabled, so mounted books retain live terrain without allowing the
player to enter Explore mode.
Version 0.2.17.0 begins persistent random Age traits. New page-free Ages select
and save a legacy-inspired biome distribution, lighting mode, weather mode, and
celestial period, angle, and phase. The celestial ranges follow Legacy's random
symbol fallbacks: period 0.2–2.0, angle 0–360 degrees, and phase 0–1. Existing
Ages load with normal Legacy defaults instead of being rerolled. This immutable
trait layer is the foundation for applying each choice to modern worldgen.
Version 0.2.18.0 applies each Age's persisted lighting trait to its runtime
dimension. Normal Lighting retains Overworld skylight and ambient brightness;
Bright Lighting uses Legacy's 0.25 ambient-light floor; Dark Lighting disables
dimension skylight while retaining local block light. The latter is the closest
server-safe modern equivalent to Legacy's removed per-dimension light-table
scaling hook.
Version 0.2.18.1 prevents Cave Ages from choosing a spawn on their upper roof.
Their grounded arrival search now begins with the floor below player Y=64 and
works downward. If it finds no suitable footing, the existing floor-Y=63
platform fallback places the player at Y=64.
Version 0.2.19.0 applies persisted weather traits to loaded Ages. Normal uses
Minecraft's normal weather cycle. Fast and Slow use Legacy's original duration
and cooldown ranges at half and double normal speed for Minecraft's combined
rain/thunder weather timer. Off keeps an Age clear;
Always, Cloudy, Rain, and Snow maintain non-thundering precipitation; Storm
maintains rain and thunder. Minecraft still chooses rain versus snow from biome
temperature, so dedicated Snow/Rain biome overrides remain future work.
Version 0.2.20.0 applies persisted celestial timing to each Age. The saved
period controls NeoForge's per-dimension clock rate, with a period of 1 matching
a vanilla 20-minute day, 0.2 running five times faster, and 2 running at half
speed. The saved phase sets the clock only during first-spawn initialization;
later loads continue from saved world time. Celestial angle remains persisted
for a future custom sky renderer because vanilla dimension data cannot rotate
the sun's render plane.
Version 0.2.20.1 restores Legacy's Zero Length celestial modifier as a rarer
random outcome using its effective one-in-seven grammar weight. Zero Length
freezes an Age at its generated phase without changing the global daylight
cycle, while nonzero Ages retain their independently scaled clocks.
Version 0.2.20.2 gives Zero Length Ages a stationary per-dimension clock rate
instead of advancing and correcting their time once per tick. This keeps the
integer day time fixed while preventing the client skybox from visually
bouncing between adjacent celestial positions.
Version 0.2.21.0 begins applying persisted biome-distribution traits. Native
Distribution retains Minecraft's data-pack-aware Overworld climate source.
Single Biome Distribution deterministically selects one registered biome from
the Age seed and uses it throughout compatible noise-based terrain. Flat and
Void profiles retain their profile-specific biome sources. Legacy's scaled,
tiled, and grid layouts require dedicated modern biome sources and remain the
next distribution work.
Version 0.2.21.1 changes Cave Ages from Nether noise settings to Minecraft's
cave noise settings. Caves remain enclosed, but use stone, water, and
Overworld-like surfaces together with the persisted Native or Single biome
distribution. This prevents Nether terrain generation from leaking into newly
generated Cave Age chunks.
Version 0.2.22.0 applies Tiled Biome Distribution using two deterministic
Age-seeded biomes in the legacy diagonal-checkerboard layout. Each square is
16 chunks (256 blocks) wide, matching Mystcraft Legacy's coordinate shift, and
uses Minecraft's serializable checkerboard biome source so the layout remains
stable across server reloads.
Version 0.2.22.1 includes the server-owned Age traits in the validated
Descriptive Book screen response and displays the terrain, biome distribution,
lighting, weather, day length, celestial angle, and celestial phase on the
book's right-hand description page. This works for held books and Book Stands
without copying mutable Age-generation data onto the item.
Version 0.2.23.0 applies Grid Biome Distribution using two deterministic
Age-seeded biomes in Legacy's diagonal checkerboard layout. Each cell is one
16-by-16-block chunk, and the built-in serializable biome source reconstructs
the same layout whenever the Age reloads.
Version 0.2.24.0 applies Tiny Biome Distribution using three deterministic
Age-seeded biomes and softly bounded seeded regions. Tiny begins at Legacy's
smallest 16-block nominal scale; the serializable biome source retains its
seed, selected biomes, and scale across Age reloads.
Version 0.2.25.0 completes the scaled biome distributions. Small, Medium,
Large, and Huge reuse Tiny's seeded, softly bounded regions at nominal widths
of 32, 64, 128, and 256 blocks. Every size selects three deterministic biomes
and serializes the selection, Age seed, and scale with the runtime dimension.
Version 0.2.25.1 applies biome-specific material rules without replacing an
Age's selected terrain profile. End biome columns generate from End stone;
Nether biome columns use Minecraft's Nether surface rules for netherrack,
soul sand, basalt, and nylium instead of inheriting Overworld blocks.
Version 0.2.26.0 applies each Age's persisted celestial angle to the client
sky. The sun, moon, and stars follow a plane rotated by the saved Legacy-style
angle in normal gameplay and Immersive Portals previews, while vanilla
dimensions retain their standard celestial path.
Version 0.2.26.1 rotates the celestial path around the world's vertical axis,
allowing the saved angle to change each Age's sunrise and sunset compass
direction instead of merely tilting the rendered path.
Version 0.2.14.0 restores the legacy Linking and Descriptive Book artwork for
inventory items and gives each book its matching open-book texture when placed
on a Book Stand. Return Linking Books created inside another Age also retain and
display that Age's saved name instead of exposing its dimension identifier.
Version 0.2.14.1 gives every newly bound Linking Book a friendly destination
label. Standard books and Return Links use saved Age names inside Rivencraft
Ages and the familiar Overworld, The Nether, and The End names in vanilla
dimensions. Existing vanilla links receive the same presentation at display
time without requiring item migration.
Version 0.2.15.0 adds the client-side book-preview mode configuration and the
selection policy for `AUTO`, `IMMERSIVE_PORTALS`, `PONDER`, and `LEGACY`.
`AUTO` uses Ponder when Create is present and otherwise uses the built-in
legacy animation. Explicit optional modes fall back to `LEGACY` when their
renderer is unavailable.
Version 0.2.16.0 implements the optional Immersive Portals GUI renderer. Its
default view orbits the live destination like the Ponder snapshot. The Explore
control switches to a movable remote camera using movement keys and mouse drag;
right-click resets the camera and restores the passive book GUI. A validated
per-player chunk loader exists only while the screen is open and is released
on close. `AUTO` now prefers Immersive Portals, then Ponder, then `LEGACY`.
Version 0.2.16.1 corrects Immersive Portals detection to use its NeoForge mod
ID, `immersive_portals_core`. Rivencraft now also refreshes Immersive Portals'
integer dimension map whenever saved or newly created Ages are loaded, before
their chunks can be synchronized to a player.
Version 0.2.16.2 routes book travel through Immersive Portals' seamless
teleport API when that mod is installed, preserving its client-world registry
during links between Ages and vanilla dimensions. The Explore control is also
positioned fully below the live snapshot window on both book screens.
Version 0.2.16.3 restores the full window viewport immediately after drawing
an Immersive Portals snapshot. Book titles, destination details, and controls
therefore render in their normal page positions instead of being compressed
inside the snapshot rectangle.
Version 0.2.16.4 clears the Immersive Portals GUI framebuffer between remote
world renders, preventing moving camera frames from accumulating into a sky
smear. The framebuffer now follows Immersive Portals' shared-target lifecycle
so a queued end-of-frame render cannot target a texture destroyed on GUI close.
Version 0.2.16.5 expands live-preview chunk loading to Immersive Portals'
reference eight-chunk radius. Rivencraft prepares the central 3x3 destination
chunks and requests an immediate tracking update when the GUI opens, providing
terrain for the first useful snapshot while the remaining chunks stream in.
Version 0.2.16.6 makes Linking and Descriptive Book screens non-pausing. The
integrated server now continues ticking in single-player while a book is open,
allowing live preview chunks to generate and stream into the snapshot.
Version 0.2.16.7 handles configured movement bindings directly in both book
screens while Explore mode is active. Movement, jump, and sneak now move the
remote camera even though Minecraft suspends normal gameplay key states while
a GUI is open.
Version 0.2.16.8 links Explore mode to real player movement in the current
dimension. Entering Explore anchors the source player and destination camera;
walking, jumping, falling, or being moved shifts the snapshot camera by the
same displacement while the book remains open and normal collision still
applies.
The preview code is separated behind a renderer interface and uses an isolated
Ponder level populated from server-authoritative destination snapshots.
## Version policy
Versions follow `x.y.z.b`:
- `x`: major release point.
- `y`: number of completed milestones.
- `z`: release slice within the active milestone.
- `b`: bug-fix and additional-work iteration within that release slice.
Starting a release slice increments `z` and resets `b` to zero. Completing a
milestone increments `y` and resets both `z` and `b` to zero. The first
milestone-2 slice is therefore `0.1.1.0`; a fix on that slice is `0.1.1.1`;
and milestone 2 completion is `0.2.0.0`.
## Milestone 1 acceptance record
- The project builds on a Java 21 JDK with `./gradlew build`.
- Mod identity and dependencies are declared in generated NeoForge metadata.
- Registrations are isolated by registry type and use deferred registration.
- The Descriptive Book loads with a model, name, tooltip, and creative tab.
- CI builds every pushed commit and pull request.
+1 -1
View File
@@ -35,7 +35,7 @@ mod_name=Rivencraft
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. # 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 mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.2.26.1 mod_version=0.2.27.0
immersive_portals_version=6.0.7 immersive_portals_version=6.0.7
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources. # This should match the base package used for the mod sources.
@@ -10,6 +10,8 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.registry.ModBiomeSources; import net.dnorberg.rivencraft.registry.ModBiomeSources;
import net.dnorberg.rivencraft.event.BookStandConversionHandler; import net.dnorberg.rivencraft.event.BookStandConversionHandler;
import net.dnorberg.rivencraft.event.AgeCelestialAngleSync; import net.dnorberg.rivencraft.event.AgeCelestialAngleSync;
import net.dnorberg.rivencraft.event.AgeLifecycleEvents;
import net.dnorberg.rivencraft.command.RivencraftCommands;
import net.dnorberg.rivencraft.network.ModNetworking; import net.dnorberg.rivencraft.network.ModNetworking;
import net.dnorberg.rivencraft.config.RivencraftConfig; import net.dnorberg.rivencraft.config.RivencraftConfig;
import net.dnorberg.rivencraft.config.RivencraftClientConfig; import net.dnorberg.rivencraft.config.RivencraftClientConfig;
@@ -43,6 +45,9 @@ public final class Rivencraft {
NeoForge.EVENT_BUS.addListener(AgeWeatherService::onLevelTickPost); NeoForge.EVENT_BUS.addListener(AgeWeatherService::onLevelTickPost);
NeoForge.EVENT_BUS.addListener(AgeCelestialAngleSync::onPlayerLoggedIn); NeoForge.EVENT_BUS.addListener(AgeCelestialAngleSync::onPlayerLoggedIn);
NeoForge.EVENT_BUS.addListener(AgeCelestialAngleSync::onPlayerChangedDimension); NeoForge.EVENT_BUS.addListener(AgeCelestialAngleSync::onPlayerChangedDimension);
NeoForge.EVENT_BUS.addListener(AgeLifecycleEvents::onPlayerLoggedOut);
NeoForge.EVENT_BUS.addListener(AgeLifecycleEvents::onServerStopping);
NeoForge.EVENT_BUS.addListener(RivencraftCommands::register);
LOGGER.info("Initializing Rivencraft"); LOGGER.info("Initializing Rivencraft");
} }
} }
@@ -1,6 +1,8 @@
package net.dnorberg.rivencraft.book; package net.dnorberg.rivencraft.book;
import dev.galacticraft.dynamicdimensions.api.DynamicDimensionRegistry; import dev.galacticraft.dynamicdimensions.api.DynamicDimensionRegistry;
import dev.galacticraft.dynamicdimensions.api.PlayerRemover;
import dev.galacticraft.dynamicdimensions.api.event.DimensionRemovedCallback;
import dev.galacticraft.dynamicdimensions.api.event.DynamicDimensionLoadCallback; import dev.galacticraft.dynamicdimensions.api.event.DynamicDimensionLoadCallback;
import java.util.Optional; import java.util.Optional;
import java.util.OptionalLong; import java.util.OptionalLong;
@@ -56,6 +58,7 @@ public final class AgeDimensionService {
} }
public static void register() { public static void register() {
DimensionRemovedCallback.register((dimension, level) -> synchronizeImmersivePortals(level.getServer()));
DynamicDimensionLoadCallback.register((server, loader) -> { DynamicDimensionLoadCallback.register((server, loader) -> {
ServerLevel overworld = server.overworld(); ServerLevel overworld = server.overworld();
for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) { for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) {
@@ -115,6 +118,27 @@ public final class AgeDimensionService {
return loaded; return loaded;
} }
/**
* Unloads an Age while retaining its saved definition and dimension data.
* A later book access reconstructs the same runtime level from that data.
*/
public static UnloadResult unload(MinecraftServer server, AgeDefinition definition) {
ResourceKey<Level> key = dimensionKey(definition);
if (server.getLevel(key) == null) {
return UnloadResult.ALREADY_UNLOADED;
}
if (ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.closeForDimension(key);
}
boolean unloaded = DynamicDimensionRegistry.from(server)
.unloadDynamicDimension(definition.dimensionId(), PlayerRemover.DEFAULT);
if (!unloaded) {
return UnloadResult.FAILED;
}
return UnloadResult.UNLOADED;
}
private static void applyCelestialClock(ServerLevel level, AgeDefinition age) { private static void applyCelestialClock(ServerLevel level, AgeDefinition age) {
if (age.traits().hasStoppedCelestialCycle()) { if (age.traits().hasStoppedCelestialCycle()) {
// NeoForge rejects an exact zero rate. The smallest positive float // NeoForge rejects an exact zero rate. The smallest positive float
@@ -396,4 +420,10 @@ public final class AgeDimensionService {
); );
} }
public enum UnloadResult {
UNLOADED,
ALREADY_UNLOADED,
FAILED
}
} }
@@ -76,6 +76,7 @@ public final class LinkingService {
LinkTarget target LinkTarget target
) { ) {
if (ModList.get().isLoaded("immersive_portals_core")) { if (ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.close(player);
ImmersivePortalsChunkLoaders.teleport( ImmersivePortalsChunkLoaders.teleport(
player, destinationLevel, position, target.yaw(), target.pitch()); player, destinationLevel, position, target.yaw(), target.pitch());
} else { } else {
@@ -0,0 +1,81 @@
package net.dnorberg.rivencraft.command;
import com.mojang.brigadier.arguments.StringArgumentType;
import net.dnorberg.rivencraft.book.AgeDefinition;
import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
/** Administrative commands for inspecting and safely unloading persistent Ages. */
public final class RivencraftCommands {
private RivencraftCommands() {
}
public static void register(RegisterCommandsEvent event) {
event.getDispatcher().register(Commands.literal("rivencraft")
.requires(source -> source.hasPermission(2))
.then(Commands.literal("age")
.then(Commands.literal("list")
.executes(context -> listAges(context.getSource())))
.then(Commands.literal("unload")
.then(Commands.argument("dimension", StringArgumentType.string())
.suggests((context, builder) -> {
AgeRegistrySavedData.get(context.getSource().getLevel()).definitions()
.forEach(age -> builder.suggest(age.dimensionId().toString()));
return builder.buildFuture();
})
.executes(context -> unloadAge(
context.getSource(),
StringArgumentType.getString(context, "dimension")
))))));
}
private static int listAges(net.minecraft.commands.CommandSourceStack source) {
var ages = AgeRegistrySavedData.get(source.getLevel()).definitions().stream()
.sorted(java.util.Comparator.comparing(AgeDefinition::name))
.toList();
source.sendSuccess(() -> Component.literal("Rivencraft Ages: " + ages.size()), false);
for (AgeDefinition age : ages) {
boolean loaded = source.getServer().getLevel(AgeDimensionService.dimensionKey(age)) != null;
source.sendSuccess(() -> Component.literal(
(loaded ? "[loaded] " : "[unloaded] ")
+ age.name() + " — " + age.dimensionId()
), false);
}
return ages.size();
}
private static int unloadAge(net.minecraft.commands.CommandSourceStack source, String requestedDimension) {
ResourceLocation dimension = ResourceLocation.tryParse(requestedDimension);
if (dimension == null) {
source.sendFailure(Component.literal("Invalid dimension id: " + requestedDimension));
return 0;
}
AgeDefinition age = AgeRegistrySavedData.get(source.getLevel()).findByDimension(dimension).orElse(null);
if (age == null) {
source.sendFailure(Component.literal("Unknown Rivencraft Age: " + dimension));
return 0;
}
AgeDimensionService.UnloadResult result = AgeDimensionService.unload(source.getServer(), age);
return switch (result) {
case UNLOADED -> {
source.sendSuccess(() -> Component.literal(
"Unloaded " + age.name() + ". Its definition and world data were retained."
), true);
yield 1;
}
case ALREADY_UNLOADED -> {
source.sendFailure(Component.literal(age.name() + " is already unloaded."));
yield 0;
}
case FAILED -> {
source.sendFailure(Component.literal("Dynamic Dimensions refused to unload " + age.name() + "."));
yield 0;
}
};
}
}
@@ -1,11 +1,13 @@
package net.dnorberg.rivencraft.compat; package net.dnorberg.rivencraft.compat;
import java.util.IdentityHashMap;
import java.util.Map; import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.core.BlockPos; import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import qouteall.imm_ptl.core.api.PortalAPI; import qouteall.imm_ptl.core.api.PortalAPI;
import qouteall.imm_ptl.core.chunk_loading.ChunkLoader; import qouteall.imm_ptl.core.chunk_loading.ChunkLoader;
@@ -14,12 +16,12 @@ import qouteall.q_misc_util.dimension.DimensionIntId;
/** Isolated server bridge; this class is loaded only when Immersive Portals is installed. */ /** Isolated server bridge; this class is loaded only when Immersive Portals is installed. */
public final class ImmersivePortalsChunkLoaders { public final class ImmersivePortalsChunkLoaders {
private static final Map<ServerPlayer, ChunkLoader> LOADERS = new WeakHashMap<>(); private static final Map<ServerPlayer, ChunkLoader> LOADERS = new IdentityHashMap<>();
private ImmersivePortalsChunkLoaders() { private ImmersivePortalsChunkLoaders() {
} }
public static void open(ServerPlayer player, ServerLevel level, BlockPos center) { public static synchronized void open(ServerPlayer player, ServerLevel level, BlockPos center) {
close(player); close(player);
int centerChunkX = center.getX() >> 4; int centerChunkX = center.getX() >> 4;
int centerChunkZ = center.getZ() >> 4; int centerChunkZ = center.getZ() >> 4;
@@ -37,13 +39,28 @@ public final class ImmersivePortalsChunkLoaders {
ImmPtlChunkTracking.immediatelyUpdateForPlayer(player); ImmPtlChunkTracking.immediatelyUpdateForPlayer(player);
} }
public static void close(ServerPlayer player) { public static synchronized void close(ServerPlayer player) {
ChunkLoader loader = LOADERS.remove(player); ChunkLoader loader = LOADERS.remove(player);
if (loader != null) { if (loader != null) {
PortalAPI.removeChunkLoaderForPlayer(player, loader); PortalAPI.removeChunkLoaderForPlayer(player, loader);
} }
} }
public static synchronized void closeForDimension(ResourceKey<Level> dimension) {
LOADERS.entrySet().removeIf(entry -> {
if (!entry.getValue().dimension().equals(dimension)) {
return false;
}
PortalAPI.removeChunkLoaderForPlayer(entry.getKey(), entry.getValue());
return true;
});
}
public static synchronized void closeAll() {
LOADERS.forEach(PortalAPI::removeChunkLoaderForPlayer);
LOADERS.clear();
}
/** Registers runtime dimensions and sends the revised ID map to connected clients. */ /** Registers runtime dimensions and sends the revised ID map to connected clients. */
public static void dimensionsChanged(MinecraftServer server) { public static void dimensionsChanged(MinecraftServer server) {
DimensionIntId.onServerDimensionChanged(server); DimensionIntId.onServerDimensionChanged(server);
@@ -0,0 +1,26 @@
package net.dnorberg.rivencraft.event;
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.fml.ModList;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
/** Server-authoritative cleanup for resources that exist only while a preview is active. */
public final class AgeLifecycleEvents {
private AgeLifecycleEvents() {
}
public static void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) {
if (event.getEntity() instanceof ServerPlayer player
&& ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.close(player);
}
}
public static void onServerStopping(ServerStoppingEvent event) {
if (ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.closeAll();
}
}
}
@@ -152,6 +152,42 @@ public final class LinkingGameTests {
}); });
} }
@GameTest(template = "empty", timeoutTicks = 100)
public static void unloadedAgeReloadsWithTheSameDefinition(GameTestHelper helper) {
UUID id = UUID.randomUUID();
AgeDefinition definition = AgeRegistrySavedData.resolveOrCreate(
helper.getLevel(),
new AgeReference(id, "Lifecycle Test Age " + id)
);
var server = helper.getLevel().getServer();
var first = AgeDimensionService.ensureLoaded(server, definition);
helper.assertTrue(first != null, "The lifecycle test Age must load");
helper.runAfterDelay(1, () -> {
helper.assertValueEqual(
AgeDimensionService.unload(server, definition),
AgeDimensionService.UnloadResult.UNLOADED,
"A loaded Age must unload without deleting its definition"
);
helper.runAfterDelay(1, () -> {
helper.assertTrue(server.getLevel(AgeDimensionService.dimensionKey(definition)) == null,
"An unloaded Age must leave the active server level map");
var reloaded = AgeDimensionService.ensureLoaded(server, definition);
helper.assertTrue(reloaded != null, "A book must be able to reload an archived Age");
helper.runAfterDelay(1, () -> {
var registered = server.getLevel(AgeDimensionService.dimensionKey(definition));
helper.assertTrue(registered != null, "The reloaded Age must rejoin the active server level map");
helper.assertValueEqual(registered.getSeed(), definition.seed(),
"Reloading an Age must preserve its immutable seed");
helper.assertValueEqual(registered.dimension().location(), definition.dimensionId(),
"Reloading an Age must preserve its dimension identifier");
helper.succeed();
});
});
});
}
@GameTest(template = "empty", timeoutTicks = 200) @GameTest(template = "empty", timeoutTicks = 200)
public static void legacyBaseTerrainProfilesLoadRuntimeDimensions(GameTestHelper helper) { public static void legacyBaseTerrainProfilesLoadRuntimeDimensions(GameTestHelper helper) {
for (var profile : AgeGenerationProfiles.all()) { for (var profile : AgeGenerationProfiles.all()) {