diff --git a/README.md b/README.md index b81f2b6..b543272 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,9 @@ view. Without either integration, Rivencraft uses its built-in animated linking panel. Clients can set `book_preview.mode` in `rivencraft-client.toml` to `AUTO`, `IMMERSIVE_PORTALS`, `PONDER`, or `LEGACY`. Unavailable optional renderers safely fall back to `LEGACY`. +The complete backend matrix, supported combinations, cleanup behavior, and +known upstream limits are documented in +[docs/PREVIEW_COMPATIBILITY.md](docs/PREVIEW_COMPATIBILITY.md). ## Build and run diff --git a/docs/PORTING.md b/docs/PORTING.md index 97ab773..8ab9213 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -53,7 +53,9 @@ documented in [AGE_PROFILES.md](AGE_PROFILES.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. +live rendering when their dependencies are installed. The tested selection +matrix and integration boundaries are documented in +[PREVIEW_COMPATIBILITY.md](PREVIEW_COMPATIBILITY.md). 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 diff --git a/docs/PREVIEW_COMPATIBILITY.md b/docs/PREVIEW_COMPATIBILITY.md new file mode 100644 index 0000000..a3aa0ca --- /dev/null +++ b/docs/PREVIEW_COMPATIBILITY.md @@ -0,0 +1,78 @@ +# Destination preview compatibility + +Rivencraft has three destination preview backends. The selected backend is a +client presentation detail; the server validates travel and Age generation is +independent of every preview integration. + +## Selection matrix + +Set `book_preview.mode` in `rivencraft-client.toml`. + +| Configured mode | Immersive Portals installed | Create/Ponder installed | Selected backend | +| --- | --- | --- | --- | +| `AUTO` | yes | either | Immersive Portals | +| `AUTO` | no | yes | Ponder snapshot | +| `AUTO` | no | no | built-in legacy animation | +| `IMMERSIVE_PORTALS` | yes | either | Immersive Portals | +| `IMMERSIVE_PORTALS` | no | either | built-in legacy animation | +| `PONDER` | either | yes | Ponder snapshot | +| `PONDER` | either | no | built-in legacy animation | +| `LEGACY` | either | either | built-in legacy animation | + +Explicit optional modes fall back directly to `LEGACY`. They do not silently +select the other optional integration. GameTests exercise every row and every +dependency-presence permutation. + +## Book behavior + +Held Linking and Descriptive Books render the selected backend in the preview +panel. Immersive Portals additionally exposes **Explore**: the player remains +at the opening position, mouse look controls the remote camera, and +right-click returns to the standard book view. + +Books on a Book Stand use the same preview panel, but Explore is disabled so +the player cannot move away and leave a remote preview session owned by the +stand screen. Closing either screen releases its server chunk loader. Linking, +disconnecting, unloading the destination Age, and stopping the server also +release affected loaders. + +The Immersive Portals backend force-loads the initial 3 by 3 destination area +before registering its wider preview chunk loader. The Ponder backend clears +the previous snapshot before applying replacement block data. The live backend +clears its framebuffer on each render and restores the Minecraft viewport +afterward. These rules prevent stale snapshot frames and book UI elements from +being captured in the preview. + +## Supported combinations and upstream limits + +The compatibility baseline is Minecraft 1.21.1, NeoForge 21.1.251, Create +6.0.10, and Immersive Portals 6.0.7. Core Age generation and the `LEGACY` +backend run without Create or Immersive Portals; the headless GameTest server +uses that dependency-free path. + +Immersive Portals 6.0.7 is incompatible with Sodium 0.8.13 for Minecraft +1.21.1: its Sodium compatibility mixin references the removed +`OcclusionCuller.Visitor` class and crashes while entering a world. Sodium +0.6.13 avoids that specific mixin failure. This compatibility boundary belongs +to Immersive Portals and Sodium, before Rivencraft's preview code runs. + +Immersive Portals reports that some Create contraptions can be invisible when +the two mods are installed together. Rivencraft does not replace either mod's +world or contraption renderer, so this remains an upstream rendering limit. +The `PONDER` or `LEGACY` mode is the supported fallback when live rendering is +unstable in a particular graphics or mod configuration. + +## Release verification + +For preview-related releases: + +1. Run `./gradlew runGameTestServer` to verify the complete selection matrix, + core Age creation, persistence, and travel without optional renderers. +2. Open held Linking and Descriptive Books once with each available backend. +3. Open both book types from a Book Stand and confirm Explore is disabled. +4. With Immersive Portals, enter and leave Explore, travel between dimensions, + close the screen, and disconnect; confirm destination chunks render and no + preview loader remains active. +5. With Ponder, reopen different destinations and confirm the previous + snapshot does not bleed into the next one. + diff --git a/gradle.properties b/gradle.properties index f02fde9..d916311 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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. 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.2.28.0 +mod_version=0.2.29.0 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. # This should match the base package used for the mod sources. diff --git a/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderers.java b/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderers.java index 6a0d1ba..0306489 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderers.java +++ b/src/main/java/net/dnorberg/rivencraft/client/preview/DestinationPreviewRenderers.java @@ -1,44 +1,22 @@ package net.dnorberg.rivencraft.client.preview; import net.dnorberg.rivencraft.config.RivencraftClientConfig; +import net.dnorberg.rivencraft.compat.PreviewBackendSelector; import net.neoforged.fml.ModList; /** Selects the configured destination preview while keeping optional integrations optional. */ public final class DestinationPreviewRenderers { - private enum Backend { - IMMERSIVE_PORTALS, - PONDER, - LEGACY - } - private DestinationPreviewRenderers() { } public static DestinationPreviewRenderer create() { boolean ponderAvailable = ModList.get().isLoaded("create"); - // The mode and priority are established before the optional IP renderer - // lands, so selecting it currently follows the documented safe fallback. boolean immersivePortalsAvailable = ModList.get().isLoaded("immersive_portals_core"); - return switch (selectBackend( + return switch (PreviewBackendSelector.select( RivencraftClientConfig.previewMode(), immersivePortalsAvailable, ponderAvailable)) { case IMMERSIVE_PORTALS -> new ImmersivePortalsDestinationPreviewRenderer(); case PONDER -> new PonderDestinationPreviewRenderer(); case LEGACY -> new LegacyDestinationPreviewRenderer(); }; } - - private static Backend selectBackend( - RivencraftClientConfig.PreviewMode mode, - boolean immersivePortalsAvailable, - boolean ponderAvailable - ) { - return switch (mode) { - case AUTO -> immersivePortalsAvailable - ? Backend.IMMERSIVE_PORTALS - : ponderAvailable ? Backend.PONDER : Backend.LEGACY; - case IMMERSIVE_PORTALS -> immersivePortalsAvailable ? Backend.IMMERSIVE_PORTALS : Backend.LEGACY; - case PONDER -> ponderAvailable ? Backend.PONDER : Backend.LEGACY; - case LEGACY -> Backend.LEGACY; - }; - } } diff --git a/src/main/java/net/dnorberg/rivencraft/compat/PreviewBackendSelector.java b/src/main/java/net/dnorberg/rivencraft/compat/PreviewBackendSelector.java new file mode 100644 index 0000000..6542119 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/compat/PreviewBackendSelector.java @@ -0,0 +1,30 @@ +package net.dnorberg.rivencraft.compat; + +import net.dnorberg.rivencraft.config.RivencraftClientConfig; + +/** Pure selection policy for optional destination preview integrations. */ +public final class PreviewBackendSelector { + public enum Backend { + IMMERSIVE_PORTALS, + PONDER, + LEGACY + } + + private PreviewBackendSelector() { + } + + public static Backend select( + RivencraftClientConfig.PreviewMode mode, + boolean immersivePortalsAvailable, + boolean ponderAvailable + ) { + return switch (mode) { + case AUTO -> immersivePortalsAvailable + ? Backend.IMMERSIVE_PORTALS + : ponderAvailable ? Backend.PONDER : Backend.LEGACY; + case IMMERSIVE_PORTALS -> immersivePortalsAvailable ? Backend.IMMERSIVE_PORTALS : Backend.LEGACY; + case PONDER -> ponderAvailable ? Backend.PONDER : Backend.LEGACY; + case LEGACY -> Backend.LEGACY; + }; + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java index 30f4c97..78e9587 100644 --- a/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java +++ b/src/main/java/net/dnorberg/rivencraft/gametest/LinkingGameTests.java @@ -12,6 +12,8 @@ import net.dnorberg.rivencraft.book.LinkTarget; import net.dnorberg.rivencraft.book.LinkingService; import net.dnorberg.rivencraft.book.AgeVisitSavedData; import net.dnorberg.rivencraft.book.AgeTraits; +import net.dnorberg.rivencraft.compat.PreviewBackendSelector; +import net.dnorberg.rivencraft.config.RivencraftClientConfig; import net.dnorberg.rivencraft.item.DescriptiveBookItem; import net.dnorberg.rivencraft.item.LinkingBookItem; import net.dnorberg.rivencraft.registry.ModDataComponents; @@ -40,6 +42,53 @@ public final class LinkingGameTests { private LinkingGameTests() { } + @GameTest(template = "empty") + public static void previewBackendSelectionCoversOptionalDependencyMatrix(GameTestHelper helper) { + assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.AUTO, true, true, + PreviewBackendSelector.Backend.IMMERSIVE_PORTALS); + assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.AUTO, true, false, + PreviewBackendSelector.Backend.IMMERSIVE_PORTALS); + assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.AUTO, false, true, + PreviewBackendSelector.Backend.PONDER); + assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.AUTO, false, false, + PreviewBackendSelector.Backend.LEGACY); + + for (boolean ponderAvailable : new boolean[] {false, true}) { + assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.IMMERSIVE_PORTALS, true, + ponderAvailable, PreviewBackendSelector.Backend.IMMERSIVE_PORTALS); + assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.IMMERSIVE_PORTALS, false, + ponderAvailable, PreviewBackendSelector.Backend.LEGACY); + } + for (boolean immersivePortalsAvailable : new boolean[] {false, true}) { + assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.PONDER, + immersivePortalsAvailable, true, PreviewBackendSelector.Backend.PONDER); + assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.PONDER, + immersivePortalsAvailable, false, PreviewBackendSelector.Backend.LEGACY); + } + for (boolean immersivePortalsAvailable : new boolean[] {false, true}) { + for (boolean ponderAvailable : new boolean[] {false, true}) { + assertPreviewBackend(helper, RivencraftClientConfig.PreviewMode.LEGACY, + immersivePortalsAvailable, ponderAvailable, PreviewBackendSelector.Backend.LEGACY); + } + } + helper.succeed(); + } + + private static void assertPreviewBackend( + GameTestHelper helper, + RivencraftClientConfig.PreviewMode mode, + boolean immersivePortalsAvailable, + boolean ponderAvailable, + PreviewBackendSelector.Backend expected + ) { + helper.assertValueEqual( + PreviewBackendSelector.select(mode, immersivePortalsAvailable, ponderAvailable), + expected, + mode + " selection with Immersive Portals=" + immersivePortalsAvailable + + " and Ponder=" + ponderAvailable + ); + } + @GameTest(template = "empty") public static void dataPackAgeProfilesResolveRegisteredResources(GameTestHelper helper) { var registries = helper.getLevel().registryAccess();