Complete milestone 3 book previews and Age generators
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-09-22 05:39:52 -04:00
1 parent 3242eb8192
commit 16208c02a4
40 files changed
+1131 -64

No files matched your search

+22 -8
View File
@@ -28,21 +28,22 @@ right-clicking with an empty hand. Sneak-right-click with an empty hand to take
the book back and restore the vanilla lectern. Breaking a Book Stand drops both
the stored book and a vanilla lectern.
Bound Linking Books request a server-validated 32-by-32-by-32 snapshot around
their destination. The book keeps its target-centered close-up framing as a
rotating Ponder scene on the left page, backed by the
destination biome's sky color. The snapshot is visual only; the server
independently validates every link.
Bound Linking Books show a rotating Ponder snapshot when Create is installed.
Without Create, they use a built-in animated linking panel tinted by the
destination sky. The server independently validates every link.
## Descriptive Book
Open a page-free Descriptive Book, name its Age, and press Create. Rivencraft
assigns the book a permanent Age UUID, creates a dedicated vanilla-like runtime
dimension, and prepares a five-by-five cobblestone arrival platform at the
assigns the book a permanent Age UUID, selects a deterministic base terrain,
creates a dedicated runtime dimension, and prepares a five-by-five cobblestone
arrival platform at the
highest suitable nearby terrain. Solid blocks and tree leaves are valid
footing. Candidate columns are scanned from the top down so cave floors are not
selected, and fluid-covered columns are excluded. If no suitable surface is
available, the platform is generated at Y=63. Reopen the book and click its preview to travel.
The book cannot reopen while its Age is being generated, whether held or on a
Book Stand. Once creation finishes, reopening shows the written book.
Copies of the written book continue to resolve to the same persistent Age
across server restarts.
@@ -50,6 +51,10 @@ New Age dimension folders use a Minecraft-safe lowercase form of the assigned
name. Conflicts receive incrementing suffixes such as `_2` and `_3`; existing
Age folders are never renamed.
Page-free Ages deterministically select one of six base terrains inspired by
Mystcraft Legacy: Standard, Amplified, Island, Cave, Flat, or Void. The chosen
profile is saved with the Age and does not change when the server restarts.
Linking creates a bound Return Link to the exact source position and orientation
according to the configured Return Link policy.
@@ -61,9 +66,18 @@ server config to `ON_CREATION` (default), `ALWAYS`, or `DISABLED`.
- A Java 21 JDK
- Minecraft 1.21.1
- NeoForge 21.1.251 or newer in the 21.1 line
- Ponder 1.0.82 for Minecraft 1.21.1 (client)
- Dynamic Dimensions AR Edition 0.9.1 and Bad Packets 0.8.2 (client and server)
Create is optional. When installed, Rivencraft uses its Ponder renderer for
book previews when Immersive Portals is absent. Immersive Portals is also
optional and takes priority in the default `AUTO` mode, providing a live
rotating destination view. Its Explore button enables WASD and jump/sneak
camera movement plus left-drag camera look; right-click returns to the rotating
view. Without either integration, Rivencraft uses its built-in animated linking
panel. Clients can set `book_preview.mode` in
`rivencraft-client.toml` to `AUTO`, `IMMERSIVE_PORTALS`, `PONDER`, or `LEGACY`.
Unavailable optional renderers safely fall back to `LEGACY`.
## Build and run
```shell
+8 -1
View File
@@ -33,6 +33,10 @@ repositories {
content { includeGroup 'curse.maven' }
}
maven { url = 'https://maven.bai.lol' }
maven {
url = 'https://api.modrinth.com/maven'
content { includeGroup 'maven.modrinth' }
}
}
base {
@@ -121,7 +125,10 @@ configurations {
}
dependencies {
implementation "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
// Create bundles the Ponder APIs used by the optional snapshot preview.
// Keep them on the compile classpath without forcing either mod at runtime.
compileOnly "net.createmod.ponder:ponder-neoforge:${ponder_version}+mc${minecraft_version}"
compileOnly "maven.modrinth:immersive-portals-neoforge:${immersive_portals_version}"
implementation "curse.maven:dynamicdimension-ar-edition-1469680:${dynamic_dimensions_file_id}"
implementation "lol.bai:badpackets:neo-${badpackets_version}"
// Example optional mod dependency with JEI
Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

+86
View File
@@ -215,6 +215,92 @@ 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.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.
The preview code is separated behind a renderer interface and uses an isolated
Ponder level populated from server-authoritative destination snapshots.
+2 -1
View File
@@ -35,7 +35,8 @@ 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.10.5
mod_version=0.2.16.6
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.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
@@ -10,6 +10,7 @@ import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.event.BookStandConversionHandler;
import net.dnorberg.rivencraft.network.ModNetworking;
import net.dnorberg.rivencraft.config.RivencraftConfig;
import net.dnorberg.rivencraft.config.RivencraftClientConfig;
import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModContainer;
@@ -26,6 +27,7 @@ public final class Rivencraft {
public Rivencraft(IEventBus modEventBus, ModContainer modContainer) {
AgeDimensionService.register();
modContainer.registerConfig(ModConfig.Type.SERVER, RivencraftConfig.SPEC);
modContainer.registerConfig(ModConfig.Type.CLIENT, RivencraftClientConfig.SPEC);
modEventBus.addListener(ModNetworking::register);
ModDataComponents.register(modEventBus);
ModBlocks.register(modEventBus);
@@ -20,7 +20,7 @@ public record AgeDefinition(
public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath(
Rivencraft.MOD_ID, "random_vanilla"
);
public static final int CURRENT_GENERATION_VERSION = 1;
public static final int CURRENT_GENERATION_VERSION = 2;
public AgeDefinition {
Objects.requireNonNull(id, "id");
@@ -4,10 +4,14 @@ import dev.galacticraft.dynamicdimensions.api.DynamicDimensionRegistry;
import dev.galacticraft.dynamicdimensions.api.event.DynamicDimensionLoadCallback;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
@@ -15,9 +19,19 @@ import net.minecraft.tags.BlockTags;
import net.minecraft.util.valueproviders.UniformInt;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.biome.Biomes;
import net.minecraft.world.level.biome.MultiNoiseBiomeSource;
import net.minecraft.world.level.biome.MultiNoiseBiomeSourceParameterLists;
import net.minecraft.world.level.biome.TheEndBiomeSource;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.levelgen.FlatLevelSource;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
import net.minecraft.world.level.levelgen.flat.FlatLevelGeneratorSettings;
import net.neoforged.fml.ModList;
/** Creates, reloads, and resolves Rivencraft's runtime Age dimensions. */
public final class AgeDimensionService {
@@ -34,10 +48,11 @@ public final class AgeDimensionService {
for (AgeDefinition age : AgeRegistrySavedData.get(overworld).definitions()) {
loader.loadDynamicDimension(
age.dimensionId(),
overworld.getChunkSource().getGenerator(),
createDimensionType()
createGenerator(server, age),
createDimensionType(server, age)
);
}
synchronizeImmersivePortals(server);
});
}
@@ -70,15 +85,23 @@ public final class AgeDimensionService {
ServerLevel overworld = server.overworld();
ServerLevel loaded = DynamicDimensionRegistry.from(server).loadDynamicDimension(
definition.dimensionId(),
overworld.getChunkSource().getGenerator(),
createDimensionType()
createGenerator(server, definition),
createDimensionType(server, definition)
);
if (loaded == null) {
Rivencraft.LOGGER.error("Failed to load Age {} as {}", definition.id(), definition.dimensionId());
} else {
synchronizeImmersivePortals(server);
}
return loaded;
}
private static void synchronizeImmersivePortals(MinecraftServer server) {
if (ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.dimensionsChanged(server);
}
}
public static ResourceKey<Level> dimensionKey(AgeDefinition definition) {
return ResourceKey.create(net.minecraft.core.registries.Registries.DIMENSION, definition.dimensionId());
}
@@ -166,7 +189,60 @@ public final class AgeDimensionService {
return new BlockPos(0, floorY, 0);
}
private static DimensionType createDimensionType() {
private static ChunkGenerator createGenerator(MinecraftServer server, AgeDefinition age) {
ResourceLocation profile = age.generationProfile();
if (profile.equals(AgeGenerationProfiles.FLAT)) {
return new FlatLevelSource(FlatLevelGeneratorSettings.getDefault(
server.registryAccess().lookupOrThrow(Registries.BIOME),
server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET),
server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
));
}
if (profile.equals(AgeGenerationProfiles.VOID)) {
FlatLevelGeneratorSettings settings = new FlatLevelGeneratorSettings(
Optional.empty(),
server.registryAccess().lookupOrThrow(Registries.BIOME).getOrThrow(Biomes.THE_VOID),
List.of()
);
settings.updateLayers();
return new FlatLevelSource(settings);
}
var biomes = server.registryAccess().lookupOrThrow(Registries.BIOME);
var biomeParameters = server.registryAccess()
.lookupOrThrow(Registries.MULTI_NOISE_BIOME_SOURCE_PARAMETER_LIST);
var noiseSettings = server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS);
if (profile.equals(AgeGenerationProfiles.CAVE)) {
return new NoiseBasedChunkGenerator(
MultiNoiseBiomeSource.createFromPreset(
biomeParameters.getOrThrow(MultiNoiseBiomeSourceParameterLists.NETHER)),
noiseSettings.getOrThrow(NoiseGeneratorSettings.NETHER)
);
}
if (profile.equals(AgeGenerationProfiles.ISLAND)) {
return new NoiseBasedChunkGenerator(
TheEndBiomeSource.create(biomes),
noiseSettings.getOrThrow(NoiseGeneratorSettings.END)
);
}
ResourceKey<NoiseGeneratorSettings> settingsKey = profile.equals(AgeGenerationProfiles.AMPLIFIED)
? NoiseGeneratorSettings.AMPLIFIED
: NoiseGeneratorSettings.OVERWORLD;
// random_vanilla is retained as a migration alias for existing Ages.
return new NoiseBasedChunkGenerator(
MultiNoiseBiomeSource.createFromPreset(
biomeParameters.getOrThrow(MultiNoiseBiomeSourceParameterLists.OVERWORLD)),
noiseSettings.getOrThrow(settingsKey)
);
}
private static DimensionType createDimensionType(MinecraftServer server, AgeDefinition age) {
if (age.generationProfile().equals(AgeGenerationProfiles.CAVE)) {
return copyDimensionType(server.getLevel(Level.NETHER).dimensionType());
}
if (age.generationProfile().equals(AgeGenerationProfiles.ISLAND)) {
return copyDimensionType(server.getLevel(Level.END).dimensionType());
}
return new DimensionType(
OptionalLong.empty(),
true,
@@ -185,4 +261,34 @@ public final class AgeDimensionService {
new DimensionType.MonsterSettings(false, true, UniformInt.of(0, 7), 0)
);
}
/** Dynamic Dimensions registers one dimension-type value per Age, so each needs a distinct instance graph. */
private static DimensionType copyDimensionType(DimensionType type) {
DimensionType.MonsterSettings monsters = type.monsterSettings();
return new DimensionType(
type.fixedTime(),
type.hasSkyLight(),
type.hasCeiling(),
type.ultraWarm(),
type.natural(),
type.coordinateScale(),
type.bedWorks(),
type.respawnAnchorWorks(),
type.minY(),
type.height(),
type.logicalHeight(),
type.infiniburn(),
type.effectsLocation(),
type.ambientLight(),
new DimensionType.MonsterSettings(
monsters.piglinSafe(),
monsters.hasRaids(),
UniformInt.of(
monsters.monsterSpawnLightTest().getMinValue(),
monsters.monsterSpawnLightTest().getMaxValue()
),
monsters.monsterSpawnBlockLightLimit()
)
);
}
}
@@ -0,0 +1,39 @@
package net.dnorberg.rivencraft.book;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.ResourceLocation;
/** Stable base-terrain families inspired by Mystcraft Legacy's terrain symbols. */
public final class AgeGenerationProfiles {
public static final ResourceLocation STANDARD = id("standard");
public static final ResourceLocation AMPLIFIED = id("amplified");
public static final ResourceLocation ISLAND = id("island");
public static final ResourceLocation CAVE = id("cave");
public static final ResourceLocation FLAT = id("flat");
public static final ResourceLocation VOID = id("void");
private static final List<ResourceLocation> RANDOM_PROFILES = List.of(
STANDARD, AMPLIFIED, ISLAND, CAVE, FLAT, VOID
);
private AgeGenerationProfiles() {
}
/** Selects one stable base terrain from the Age's already-persisted seed. */
public static ResourceLocation select(long seed) {
return RANDOM_PROFILES.get(Math.floorMod(Long.hashCode(seed), RANDOM_PROFILES.size()));
}
public static boolean isSupported(ResourceLocation profile) {
return RANDOM_PROFILES.contains(profile);
}
public static List<ResourceLocation> all() {
return RANDOM_PROFILES;
}
private static ResourceLocation id(String path) {
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
}
}
@@ -50,6 +50,12 @@ public final class AgeRegistrySavedData extends SavedData {
return Optional.ofNullable(ages.get(ageId));
}
public Optional<AgeDefinition> findByDimension(ResourceLocation dimensionId) {
return ages.values().stream()
.filter(age -> age.dimensionId().equals(dimensionId))
.findFirst();
}
public List<AgeDefinition> definitions() {
return List.copyOf(ages.values());
}
@@ -73,11 +79,12 @@ public final class AgeRegistrySavedData extends SavedData {
String name = normalizedName(reference.name());
ResourceLocation dimensionId = availableDimensionId(name);
long seed = ageSeed(worldSeed, dimensionId);
AgeDefinition created = new AgeDefinition(
reference.id(),
name,
ageSeed(worldSeed, dimensionId),
AgeDefinition.RANDOM_VANILLA_PROFILE,
seed,
AgeGenerationProfiles.select(seed),
AgeDefinition.CURRENT_GENERATION_VERSION,
createdGameTime,
dimensionId,
@@ -4,6 +4,7 @@ import java.util.Comparator;
import java.util.Optional;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
@@ -12,6 +13,7 @@ import net.minecraft.tags.BlockTags;
import net.minecraft.world.level.border.WorldBorder;
import net.minecraft.world.level.levelgen.Heightmap;
import net.minecraft.world.phys.Vec3;
import net.neoforged.fml.ModList;
/** Server-side validation and execution for book travel. */
public final class LinkingService {
@@ -39,7 +41,7 @@ public final class LinkingService {
}
Vec3 position = destination.get();
player.teleportTo(destinationLevel, position.x, position.y, position.z, target.yaw(), target.pitch());
teleport(player, destinationLevel, position, target);
player.resetFallDistance();
return LinkResult.SUCCESS;
}
@@ -56,18 +58,32 @@ public final class LinkingService {
}
Vec3 position = destination.get();
player.teleportTo(
destinationLevel,
position.x,
position.y,
position.z,
target.yaw(),
target.pitch()
);
teleport(player, destinationLevel, position, target);
player.resetFallDistance();
return LinkResult.SUCCESS;
}
private static void teleport(
ServerPlayer player,
ServerLevel destinationLevel,
Vec3 position,
LinkTarget target
) {
if (ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.teleport(
player, destinationLevel, position, target.yaw(), target.pitch());
} else {
player.teleportTo(
destinationLevel,
position.x,
position.y,
position.z,
target.yaw(),
target.pitch()
);
}
}
static Optional<Vec3> findSafeDestination(ServerLevel level, BlockPos origin) {
return findSafeDestination(level, origin, false);
}
@@ -5,6 +5,7 @@ import com.mojang.blaze3d.vertex.VertexConsumer;
import com.mojang.math.Axis;
import net.dnorberg.rivencraft.block.BookStandBlock;
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.model.BookModel;
import net.minecraft.client.model.geom.ModelLayers;
import net.minecraft.client.Minecraft;
@@ -13,13 +14,17 @@ import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import net.minecraft.client.renderer.blockentity.EnchantTableRenderer;
import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.state.BlockState;
import org.joml.Matrix4f;
public final class BookStandRenderer implements BlockEntityRenderer<BookStandBlockEntity> {
private static final ResourceLocation LINKING_BOOK_TEXTURE = ResourceLocation.fromNamespaceAndPath(
"rivencraft", "textures/entity/linking_book.png");
private static final ResourceLocation DESCRIPTIVE_BOOK_TEXTURE = ResourceLocation.fromNamespaceAndPath(
"rivencraft", "textures/entity/descriptive_book.png");
private final BookModel bookModel;
private final Font font;
private final EntityRenderDispatcher entityRenderer;
@@ -51,7 +56,10 @@ public final class BookStandRenderer implements BlockEntityRenderer<BookStandBlo
poseStack.mulPose(Axis.ZP.rotationDegrees(67.5F));
poseStack.translate(0.0F, -0.125F, 0.0F);
bookModel.setupAnim(0.0F, 0.1F, 0.9F, 1.2F);
VertexConsumer consumer = EnchantTableRenderer.BOOK_LOCATION.buffer(bufferSource, RenderType::entitySolid);
ResourceLocation bookTexture = blockEntity.getBook().is(ModItems.DESCRIPTIVE_BOOK.get())
? DESCRIPTIVE_BOOK_TEXTURE
: LINKING_BOOK_TEXTURE;
VertexConsumer consumer = bufferSource.getBuffer(RenderType.entitySolid(bookTexture));
bookModel.render(poseStack, consumer, packedLight, packedOverlay, -1);
poseStack.popPose();
@@ -6,6 +6,13 @@ import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.dnorberg.rivencraft.registry.ModBlocks;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.phys.Vec3;
import net.dnorberg.rivencraft.network.RequestDescriptiveBookScreenPayload;
import net.dnorberg.rivencraft.network.OpenDescriptiveBookScreenPayload;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
@@ -32,7 +39,8 @@ public final class ClientBookInteractionHandler {
if (linkingBook) {
Minecraft.getInstance().setScreen(new LinkingBookScreen(event.getHand(), event.getItemStack().copy()));
} else {
Minecraft.getInstance().setScreen(new DescriptiveBookScreen(event.getHand(), event.getItemStack().copy()));
PacketDistributor.sendToServer(new RequestDescriptiveBookScreenPayload(
false, event.getHand() == InteractionHand.OFF_HAND, BlockPos.ZERO));
}
}
@@ -55,7 +63,37 @@ public final class ClientBookInteractionHandler {
if (book.is(ModItems.LINKING_BOOK.get())) {
Minecraft.getInstance().setScreen(new LinkingBookScreen(event.getPos(), book.copy()));
} else if (book.is(ModItems.DESCRIPTIVE_BOOK.get())) {
Minecraft.getInstance().setScreen(new DescriptiveBookScreen(event.getPos(), book.copy()));
PacketDistributor.sendToServer(new RequestDescriptiveBookScreenPayload(true, false, event.getPos()));
}
}
public static void openDescriptiveBook(OpenDescriptiveBookScreenPayload payload, IPayloadContext context) {
context.enqueueWork(() -> {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player == null || minecraft.level == null || minecraft.screen != null
|| !minecraft.level.dimension().equals(payload.source().dimension())) {
return;
}
InteractionHand hand = payload.offhand() ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
ItemStack currentBook;
if (payload.fromStand()) {
if (minecraft.player.distanceToSqr(Vec3.atCenterOf(payload.source().pos())) > 64.0D
|| !(minecraft.level.getBlockEntity(payload.source().pos()) instanceof BookStandBlockEntity stand)) {
return;
}
currentBook = stand.getBook();
} else {
currentBook = minecraft.player.getItemInHand(hand);
}
// Ignore delayed replies if the player has moved or replaced the book.
if (!currentBook.is(ModItems.DESCRIPTIVE_BOOK.get())
|| !ItemStack.isSameItemSameComponents(currentBook, payload.book())) {
return;
}
minecraft.setScreen(payload.fromStand()
? new DescriptiveBookScreen(payload.source().pos(), payload.book())
: new DescriptiveBookScreen(hand, payload.book()));
});
}
}
@@ -10,6 +10,12 @@ public interface DestinationPreviewRenderer extends AutoCloseable {
void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick);
default boolean supportsExploration() { return false; }
default boolean isExploring() { return false; }
default void setExploring(boolean exploring) { }
default void rotate(double deltaX, double deltaY) { }
default void resetView() { }
@Override
default void close() {
}
@@ -0,0 +1,44 @@
package net.dnorberg.rivencraft.client.preview;
import net.dnorberg.rivencraft.config.RivencraftClientConfig;
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(
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;
};
}
}
@@ -0,0 +1,152 @@
package net.dnorberg.rivencraft.client.preview;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.blaze3d.pipeline.TextureTarget;
import net.dnorberg.rivencraft.network.LinkPreviewPayload;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import qouteall.imm_ptl.core.ClientWorldLoader;
import qouteall.imm_ptl.core.render.GuiPortalRendering;
import qouteall.imm_ptl.core.render.MyRenderHelper;
import qouteall.imm_ptl.core.render.context_management.WorldRenderInfo;
/** Live remote-world preview rendered through Immersive Portals' GUI API. */
public final class ImmersivePortalsDestinationPreviewRenderer implements DestinationPreviewRenderer {
private static final double ORBIT_RADIUS = 11.0D;
// Immersive Portals keeps GUI render tasks until the end of the frame. A
// shared target follows its API example and cannot be destroyed while a
// queued task may still reference it.
private static final RenderTarget FRAMEBUFFER = new TextureTarget(2, 2, true, true);
private final Minecraft minecraft = Minecraft.getInstance();
private LinkPreviewPayload destination;
private Vec3 cameraPosition;
private float yaw;
private float pitch;
private int ticks;
private boolean exploring;
private boolean hasCompletedFrame;
@Override
public void accept(LinkPreviewPayload payload) {
destination = payload;
resetView();
}
@Override
public void tick() {
ticks++;
if (!exploring || cameraPosition == null) {
return;
}
double forward = (minecraft.options.keyUp.isDown() ? 1.0D : 0.0D)
- (minecraft.options.keyDown.isDown() ? 1.0D : 0.0D);
double strafe = (minecraft.options.keyRight.isDown() ? 1.0D : 0.0D)
- (minecraft.options.keyLeft.isDown() ? 1.0D : 0.0D);
double vertical = (minecraft.options.keyJump.isDown() ? 1.0D : 0.0D)
- (minecraft.options.keyShift.isDown() ? 1.0D : 0.0D);
double radians = Math.toRadians(yaw);
Vec3 motion = new Vec3(
-Math.sin(radians) * forward + Math.cos(radians) * strafe,
vertical,
Math.cos(radians) * forward + Math.sin(radians) * strafe
);
if (motion.lengthSqr() > 0.0D) {
cameraPosition = cameraPosition.add(motion.normalize().scale(0.22D));
}
}
@Override
public void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick) {
graphics.fill(left, top, left + width, top + height, 0xFF080C18);
if (destination == null || !ClientWorldLoader.getIsInitialized()) {
return;
}
ClientLevel world = ClientWorldLoader.getWorld(destination.dimension());
if (world == null) {
return;
}
Vec3 renderPosition = cameraPosition;
float renderYaw = yaw;
float renderPitch = pitch;
if (!exploring) {
double angle = (ticks + partialTick) * 0.012D;
Vec3 center = Vec3.atCenterOf(destination.origin()).add(0.0D, 2.0D, 0.0D);
renderPosition = center.add(Math.sin(angle) * ORBIT_RADIUS, 5.0D, Math.cos(angle) * ORBIT_RADIUS);
renderYaw = (float)Math.toDegrees(angle) + 180.0F;
renderPitch = 20.0F;
}
Matrix4f cameraTransform = new Matrix4f()
.rotateX((float)Math.toRadians(renderPitch))
.rotateY((float)Math.toRadians(renderYaw + 180.0F));
WorldRenderInfo info = new WorldRenderInfo.Builder()
.setWorld(world)
.setCameraPos(renderPosition)
.setCameraTransformation(cameraTransform)
.setOverwriteCameraTransformation(true)
.setRenderDistance(Math.min(8, minecraft.options.getEffectiveRenderDistance()))
.setDoRenderHand(false)
.setEnableViewBobbing(false)
.setDoRenderSky(true)
.setHasFog(true)
.build();
if (hasCompletedFrame) {
double scale = minecraft.getWindow().getGuiScale();
MyRenderHelper.drawFramebufferWithBounds(FRAMEBUFFER, false, false,
(int)(left * scale), (int)((left + width) * scale),
(int)(top * scale), (int)((top + height) * scale));
// Immersive Portals draws by changing the raw OpenGL viewport. Restore
// the window viewport before the screen renders its text and widgets.
MyRenderHelper.restoreViewPort();
}
// The API does not clear targets when sky rendering is enabled. Clear
// color and depth after displaying the completed frame, then render the
// next view into a clean target at the end of this frame.
int sky = destination.skyColor();
FRAMEBUFFER.setClearColor(
((sky >> 16) & 0xFF) / 255.0F,
((sky >> 8) & 0xFF) / 255.0F,
(sky & 0xFF) / 255.0F,
1.0F
);
FRAMEBUFFER.clear(true);
minecraft.getMainRenderTarget().bindWrite(true);
MyRenderHelper.restoreViewPort();
GuiPortalRendering.submitNextFrameRendering(info, FRAMEBUFFER);
hasCompletedFrame = true;
graphics.renderOutline(left, top, width, height, exploring ? 0xFFE0C36A : 0xFF7B6AA8);
}
@Override public boolean supportsExploration() { return true; }
@Override public boolean isExploring() { return exploring; }
@Override public void setExploring(boolean exploring) { this.exploring = exploring; }
@Override
public void rotate(double deltaX, double deltaY) {
if (exploring) {
yaw += (float)deltaX * 0.35F;
pitch = Mth.clamp(pitch + (float)deltaY * 0.35F, -89.0F, 89.0F);
}
}
@Override
public void resetView() {
exploring = false;
if (destination != null) {
cameraPosition = Vec3.atCenterOf(destination.origin()).add(0.0D, 2.0D, ORBIT_RADIUS);
yaw = destination.yaw();
pitch = destination.pitch();
}
}
@Override
public void close() {
hasCompletedFrame = false;
}
}
@@ -0,0 +1,75 @@
package net.dnorberg.rivencraft.client.preview;
import net.dnorberg.rivencraft.network.LinkPreviewPayload;
import net.minecraft.client.gui.GuiGraphics;
/**
* Built-in linking-panel animation inspired by Mystcraft Legacy's portal bloom.
*
* <p>The original effect used a GLSL noise shader over an optional live world
* view. This renderer keeps its two expanding, noisy spirals and destination
* tint, but draws them with the normal GUI API so it needs no renderer mod.</p>
*/
public final class LegacyDestinationPreviewRenderer implements DestinationPreviewRenderer {
private static final int CELL_SIZE = 3;
private static final int BORDER_COLOR = 0xFF7B6AA8;
private int ticks;
private int linkColor = 0xFF006666;
@Override
public void accept(LinkPreviewPayload payload) {
linkColor = 0xFF000000 | payload.skyColor() & 0x00FFFFFF;
}
@Override
public void tick() {
ticks++;
}
@Override
public void render(GuiGraphics graphics, int left, int top, int width, int height, float partialTick) {
graphics.fill(left, top, left + width, top + height, 0xFF000011);
float time = (ticks + partialTick) * 0.035F;
float reveal = Math.min(1.0F, time / 1.8F);
for (int y = 1; y < height - 1; y += CELL_SIZE) {
for (int x = 1; x < width - 1; x += CELL_SIZE) {
float normalizedX = (x - width * 0.5F) / height;
float normalizedY = (y - height * 0.5F) / height;
float first = spiral(normalizedX, normalizedY, time, 0.0F);
float second = spiral(normalizedX, normalizedY, time, 2.35F);
float intensity = Math.max(first, second) * reveal;
if (intensity <= 0.035F) {
continue;
}
int color = scaledColor(linkColor, Math.min(1.0F, 0.12F + intensity));
graphics.fill(left + x, top + y,
left + Math.min(width - 1, x + CELL_SIZE),
top + Math.min(height - 1, y + CELL_SIZE), color);
}
}
graphics.renderOutline(left, top, width, height, BORDER_COLOR);
}
private static float spiral(float x, float y, float time, float phase) {
float distance = (float)Math.sqrt(x * x + y * y);
float angle = (float)Math.atan2(y, x);
float arm = 0.5F + 0.5F * (float)Math.cos(angle * 3.0F - distance * 25.0F + time * 4.0F + phase);
float pulse = 0.72F + 0.28F * noise(x * 31.0F, y * 31.0F, time + phase);
float core = Math.max(0.0F, 1.0F - distance * 2.15F);
return core * (0.28F + arm * 0.72F) * pulse;
}
private static float noise(float x, float y, float time) {
double value = Math.sin(x * 12.9898D + y * 78.233D + time * 19.19D) * 43758.5453D;
return (float)(value - Math.floor(value));
}
private static int scaledColor(int color, float scale) {
int red = Math.min(255, Math.round(((color >> 16) & 0xFF) * scale + 18.0F * scale));
int green = Math.min(255, Math.round(((color >> 8) & 0xFF) * scale + 24.0F * scale));
int blue = Math.min(255, Math.round((color & 0xFF) * scale + 42.0F * scale));
return 0xFF000000 | red << 16 | green << 8 | blue;
}
}
@@ -6,9 +6,10 @@ import net.dnorberg.rivencraft.network.BindDescriptiveBookPayload;
import net.dnorberg.rivencraft.network.ModNetworking;
import net.dnorberg.rivencraft.network.RequestLinkPreviewPayload;
import net.dnorberg.rivencraft.network.LinkPreviewPayload;
import net.dnorberg.rivencraft.network.ReleaseLinkPreviewPayload;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderer;
import net.dnorberg.rivencraft.client.preview.PonderDestinationPreviewRenderer;
import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderers;
import net.minecraft.core.BlockPos;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
@@ -36,14 +37,17 @@ public final class DescriptiveBookScreen extends Screen {
private static final int LINK_PANEL_HEIGHT = 86;
private final AgeReference age;
private final ResourceLocation generationProfile;
private final InteractionHand hand;
private final BlockPos bookStandPos;
private DestinationPreviewRenderer preview;
private EditBox titleInput;
private Button exploreButton;
public DescriptiveBookScreen(InteractionHand hand, ItemStack book) {
super(book.getHoverName());
age = book.get(ModDataComponents.AGE_REFERENCE.get());
generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get());
this.hand = hand;
bookStandPos = null;
}
@@ -51,6 +55,7 @@ public final class DescriptiveBookScreen extends Screen {
public DescriptiveBookScreen(BlockPos bookStandPos, ItemStack book) {
super(book.getHoverName());
age = book.get(ModDataComponents.AGE_REFERENCE.get());
generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get());
hand = InteractionHand.MAIN_HAND;
this.bookStandPos = bookStandPos.immutable();
}
@@ -74,12 +79,18 @@ public final class DescriptiveBookScreen extends Screen {
.bounds(rightPage + 65, top + 100, 59, 20)
.build());
} else {
preview = new PonderDestinationPreviewRenderer();
preview = DestinationPreviewRenderers.create();
if (preview.supportsExploration()) {
exploreButton = addRenderableWidget(Button.builder(
Component.translatable("screen.rivencraft.book.explore"), button -> toggleExploration())
.bounds(left + 61, top + 145, 60, 20).build());
}
PacketDistributor.sendToServer(new RequestLinkPreviewPayload(
true,
bookStandPos != null,
bookStandPos == null && hand == InteractionHand.OFF_HAND,
bookStandPos == null ? BlockPos.ZERO : bookStandPos
bookStandPos == null ? BlockPos.ZERO : bookStandPos,
preview.supportsExploration()
));
}
}
@@ -96,6 +107,11 @@ public final class DescriptiveBookScreen extends Screen {
// Like Linking Books, the parchment is the full screen background.
}
@Override
public boolean isPauseScreen() {
return false;
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
int left = (width - BOOK_WIDTH) / 2;
@@ -109,6 +125,14 @@ public final class DescriptiveBookScreen extends Screen {
} else {
preview.render(graphics, left + LINK_PANEL_X, top + LINK_PANEL_Y, LINK_PANEL_WIDTH, LINK_PANEL_HEIGHT, partialTick);
drawWrappedWithoutShadow(graphics, Component.literal(age.name()), left + 177, top + 43, 124);
if (generationProfile != null) {
Component profileName = generationProfile.getNamespace().equals("rivencraft")
? Component.translatable("generator.rivencraft." + generationProfile.getPath())
: Component.literal(generationProfile.toString());
drawWrappedWithoutShadow(graphics,
Component.translatable("screen.rivencraft.age.generator", profileName),
left + 177, top + 72, 124);
}
}
super.render(graphics, mouseX, mouseY, partialTick);
@@ -117,6 +141,9 @@ public final class DescriptiveBookScreen extends Screen {
@Override
public void removed() {
if (preview != null) {
if (preview.supportsExploration()) {
PacketDistributor.sendToServer(new ReleaseLinkPreviewPayload());
}
preview.close();
}
}
@@ -129,6 +156,10 @@ public final class DescriptiveBookScreen extends Screen {
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (preview != null && preview.isExploring() && button == 1) {
resetExploration();
return true;
}
if (age != null && button == 0) {
int panelLeft = (width - BOOK_WIDTH) / 2 + LINK_PANEL_X;
int panelTop = (height - BOOK_HEIGHT) / 2 + LINK_PANEL_Y;
@@ -141,6 +172,26 @@ public final class DescriptiveBookScreen extends Screen {
return super.mouseClicked(mouseX, mouseY, button);
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
if (preview != null && preview.isExploring() && button == 0) {
preview.rotate(dragX, dragY);
return true;
}
return super.mouseDragged(mouseX, mouseY, button, dragX, dragY);
}
private void toggleExploration() {
preview.setExploring(!preview.isExploring());
exploreButton.setMessage(Component.translatable(preview.isExploring()
? "screen.rivencraft.book.exploring" : "screen.rivencraft.book.explore"));
}
private void resetExploration() {
preview.resetView();
exploreButton.setMessage(Component.translatable("screen.rivencraft.book.explore"));
}
private void link() {
PacketDistributor.sendToServer(new UseDescriptiveBookPayload(
bookStandPos != null,
@@ -2,14 +2,16 @@ package net.dnorberg.rivencraft.client.screen;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderer;
import net.dnorberg.rivencraft.client.preview.PonderDestinationPreviewRenderer;
import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderers;
import net.dnorberg.rivencraft.network.BindLinkingBookPayload;
import net.dnorberg.rivencraft.network.ModNetworking;
import net.dnorberg.rivencraft.network.UseLinkingBookPayload;
import net.dnorberg.rivencraft.network.UseBookStandPayload;
import net.dnorberg.rivencraft.network.RequestLinkPreviewPayload;
import net.dnorberg.rivencraft.network.LinkPreviewPayload;
import net.dnorberg.rivencraft.network.ReleaseLinkPreviewPayload;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
@@ -42,6 +44,7 @@ public final class LinkingBookScreen extends Screen {
private final BlockPos bookStandPos;
private DestinationPreviewRenderer preview;
private EditBox titleInput;
private Button exploreButton;
public LinkingBookScreen(InteractionHand hand, ItemStack book) {
super(book.getHoverName());
@@ -78,12 +81,18 @@ public final class LinkingBookScreen extends Screen {
.bounds(rightPage + 65, top + 100, 59, 20)
.build());
} else {
preview = new PonderDestinationPreviewRenderer();
preview = DestinationPreviewRenderers.create();
if (preview.supportsExploration()) {
exploreButton = addRenderableWidget(Button.builder(
Component.translatable("screen.rivencraft.book.explore"), button -> toggleExploration())
.bounds(left + 61, top + 145, 60, 20).build());
}
PacketDistributor.sendToServer(new RequestLinkPreviewPayload(
false,
bookStandPos != null,
bookStandPos == null && hand == InteractionHand.OFF_HAND,
bookStandPos == null ? BlockPos.ZERO : bookStandPos
bookStandPos == null ? BlockPos.ZERO : bookStandPos,
preview.supportsExploration()
));
}
}
@@ -101,6 +110,11 @@ public final class LinkingBookScreen extends Screen {
// default full-screen world blur behind it.
}
@Override
public boolean isPauseScreen() {
return false;
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
int left = (width - BOOK_WIDTH) / 2;
@@ -120,7 +134,7 @@ public final class LinkingBookScreen extends Screen {
drawWrappedWithoutShadow(graphics, title, left + 177, top + 43, 124);
BlockPos pos = target.location().pos();
drawCenteredWithoutShadow(graphics,
Component.literal(target.location().dimension().location().toString()), rightCenter, top + 76);
LinkingBookItem.destinationName(book, target), rightCenter, top + 76);
drawCenteredWithoutShadow(graphics,
Component.literal(pos.getX() + ", " + pos.getY() + ", " + pos.getZ()), rightCenter, top + 89);
drawCenteredWithoutShadow(graphics,
@@ -132,6 +146,10 @@ public final class LinkingBookScreen extends Screen {
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
if (preview != null && preview.isExploring() && button == 1) {
resetExploration();
return true;
}
if (target != null && button == 0) {
int left = (width - BOOK_WIDTH) / 2 + PREVIEW_X;
int top = (height - BOOK_HEIGHT) / 2 + PREVIEW_Y;
@@ -147,10 +165,33 @@ public final class LinkingBookScreen extends Screen {
@Override
public void removed() {
if (preview != null) {
if (preview.supportsExploration()) {
PacketDistributor.sendToServer(new ReleaseLinkPreviewPayload());
}
preview.close();
}
}
@Override
public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) {
if (preview != null && preview.isExploring() && button == 0) {
preview.rotate(dragX, dragY);
return true;
}
return super.mouseDragged(mouseX, mouseY, button, dragX, dragY);
}
private void toggleExploration() {
preview.setExploring(!preview.isExploring());
exploreButton.setMessage(Component.translatable(preview.isExploring()
? "screen.rivencraft.book.exploring" : "screen.rivencraft.book.explore"));
}
private void resetExploration() {
preview.resetView();
exploreButton.setMessage(Component.translatable("screen.rivencraft.book.explore"));
}
public void acceptPreview(LinkPreviewPayload payload) {
if (preview != null) {
preview.accept(payload);
@@ -0,0 +1,65 @@
package net.dnorberg.rivencraft.compat;
import java.util.Map;
import java.util.WeakHashMap;
import net.minecraft.core.BlockPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.phys.Vec3;
import qouteall.imm_ptl.core.api.PortalAPI;
import qouteall.imm_ptl.core.chunk_loading.ChunkLoader;
import qouteall.imm_ptl.core.chunk_loading.ImmPtlChunkTracking;
import qouteall.q_misc_util.dimension.DimensionIntId;
/** Isolated server bridge; this class is loaded only when Immersive Portals is installed. */
public final class ImmersivePortalsChunkLoaders {
private static final Map<ServerPlayer, ChunkLoader> LOADERS = new WeakHashMap<>();
private ImmersivePortalsChunkLoaders() {
}
public static void open(ServerPlayer player, ServerLevel level, BlockPos center) {
close(player);
int centerChunkX = center.getX() >> 4;
int centerChunkZ = center.getZ() >> 4;
// Ensure the camera has useful terrain immediately while the wider
// Immersive Portals loader continues generating and streaming outward.
for (int x = centerChunkX - 1; x <= centerChunkX + 1; x++) {
for (int z = centerChunkZ - 1; z <= centerChunkZ + 1; z++) {
level.getChunk(x, z);
}
}
ChunkLoader loader = new ChunkLoader(level.dimension(), centerChunkX, centerChunkZ, 8);
PortalAPI.addChunkLoaderForPlayer(player, loader);
LOADERS.put(player, loader);
ImmPtlChunkTracking.immediatelyUpdateForPlayer(player);
}
public static void close(ServerPlayer player) {
ChunkLoader loader = LOADERS.remove(player);
if (loader != null) {
PortalAPI.removeChunkLoaderForPlayer(player, loader);
}
}
/** Registers runtime dimensions and sends the revised ID map to connected clients. */
public static void dimensionsChanged(MinecraftServer server) {
DimensionIntId.onServerDimensionChanged(server);
}
/** Uses Immersive Portals' seamless transfer so its client-world registry stays active. */
public static void teleport(
ServerPlayer player,
ServerLevel destination,
Vec3 position,
float yaw,
float pitch
) {
player.setYRot(yaw);
player.setYHeadRot(yaw);
player.setXRot(pitch);
PortalAPI.teleportEntity(player, destination, position);
}
}
@@ -0,0 +1,40 @@
package net.dnorberg.rivencraft.config;
import net.neoforged.neoforge.common.ModConfigSpec;
/** Client-only presentation preferences. */
public final class RivencraftClientConfig {
public enum PreviewMode {
/** Prefer Immersive Portals, then Ponder, then Rivencraft's built-in animation. */
AUTO,
/** Request the optional Immersive Portals destination renderer. */
IMMERSIVE_PORTALS,
/** Request Create's Ponder destination renderer. */
PONDER,
/** Always use Rivencraft's built-in legacy-style animation. */
LEGACY
}
public static final ModConfigSpec SPEC;
private static final ModConfigSpec.EnumValue<PreviewMode> PREVIEW_MODE;
static {
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
builder.push("book_preview");
PREVIEW_MODE = builder.comment(
"Selects the renderer used by Linking and Descriptive Book destination panels.",
"AUTO prefers Immersive Portals, then Create/Ponder, then LEGACY.",
"Requesting an unavailable optional renderer falls back to LEGACY."
)
.defineEnum("mode", PreviewMode.AUTO);
builder.pop();
SPEC = builder.build();
}
private RivencraftClientConfig() {
}
public static PreviewMode previewMode() {
return PREVIEW_MODE.get();
}
}
@@ -3,12 +3,14 @@ package net.dnorberg.rivencraft.gametest;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.book.AgeDefinition;
import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.LinkingService;
import net.dnorberg.rivencraft.book.AgeVisitSavedData;
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.BlockPos;
@@ -60,12 +62,21 @@ public final class LinkingGameTests {
-12.0F
);
ItemStack returnBook = DescriptiveBookItem.createReturnBook(source);
ItemStack returnBook = DescriptiveBookItem.createReturnBook(source, "Atrus's Study");
helper.assertTrue(returnBook.is(ModItems.LINKING_BOOK.get()),
"Descriptive Book travel must create a Linking Book");
helper.assertValueEqual(returnBook.get(ModDataComponents.LINK_TARGET.get()), source,
"The Return Link must retain the source dimension, location, and orientation");
helper.assertValueEqual(returnBook.get(ModDataComponents.LINK_DESTINATION_NAME.get()), "Atrus's Study",
"The Return Link must retain the source Age's display name");
helper.succeed();
}
@GameTest(template = "empty")
public static void vanillaDimensionUsesFriendlyLinkName(GameTestHelper helper) {
helper.assertValueEqual(LinkingBookItem.destinationName(helper.getLevel()), "Overworld",
"Standard Linking Books must use the friendly vanilla dimension name");
helper.succeed();
}
@@ -103,8 +114,8 @@ public final class LinkingGameTests {
"A new Age dimension folder must derive from the player-assigned name");
helper.assertValueEqual(conflicting.dimensionId().getPath(), first.dimensionId().getPath() + "_2",
"A conflicting Age dimension folder must receive an incrementing suffix");
helper.assertValueEqual(first.generationProfile(), AgeDefinition.RANDOM_VANILLA_PROFILE,
"A page-free book must use the random vanilla generation profile");
helper.assertTrue(AgeGenerationProfiles.isSupported(first.generationProfile()),
"A page-free book must select a supported legacy-inspired terrain profile");
helper.assertValueEqual(first.generationVersion(), AgeDefinition.CURRENT_GENERATION_VERSION,
"New Ages must record the current generation algorithm version");
helper.assertTrue(first.seed() != another.seed(), "Distinct Age UUIDs must receive distinct random seeds");
@@ -121,6 +132,9 @@ public final class LinkingGameTests {
helper.runAfterDelay(1, () -> {
var ageLevel = player.getServer().getLevel(target.location().dimension());
helper.assertTrue(ageLevel != null, "The generated Age must be registered with the server");
AgeDefinition definition = AgeRegistrySavedData.get(helper.getLevel()).find(reference.id()).orElseThrow();
helper.assertValueEqual(ageLevel.getSeed(), definition.seed(),
"The runtime dimension seed must match the persisted Age definition");
BlockPos spawn = target.location().pos();
helper.assertTrue(ageLevel.getBlockState(spawn.below()).is(Blocks.COBBLESTONE),
"The generated Age must have a cobblestone arrival platform");
@@ -130,6 +144,34 @@ public final class LinkingGameTests {
});
}
@GameTest(template = "empty", timeoutTicks = 200)
public static void legacyBaseTerrainProfilesLoadRuntimeDimensions(GameTestHelper helper) {
for (var profile : AgeGenerationProfiles.all()) {
UUID id = UUID.randomUUID();
AgeDefinition definition = new AgeDefinition(
id,
"Profile " + profile.getPath(),
id.getMostSignificantBits(),
profile,
AgeDefinition.CURRENT_GENERATION_VERSION,
helper.getLevel().getGameTime(),
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(
Rivencraft.MOD_ID, "gametest/" + profile.getPath() + "_" + id
),
null
);
var level = AgeDimensionService.ensureLoaded(helper.getLevel().getServer(), definition);
helper.assertTrue(level != null, "The " + profile + " profile must load a runtime dimension");
if (level == null) {
continue;
}
helper.assertTrue(level.dimension().location().equals(definition.dimensionId()),
"The " + profile + " profile must retain its assigned dimension id");
}
helper.succeed();
}
@GameTest(template = "empty")
public static void travelUsesLeafyDestinationWithoutFloor(GameTestHelper helper) {
helper.setBlock(ORIGIN.below(), Blocks.AIR);
@@ -67,7 +67,7 @@ public final class DescriptiveBookItem extends Item {
case DISABLED -> false;
};
if (createReturnLink) {
ItemStack returnBook = createReturnBook(returnTarget);
ItemStack returnBook = createReturnBook(returnTarget, LinkingBookItem.destinationName(sourceLevel));
if (!player.getInventory().add(returnBook)) {
player.drop(returnBook, false);
}
@@ -84,8 +84,15 @@ public final class DescriptiveBookItem extends Item {
}
public static ItemStack createReturnBook(LinkTarget returnTarget) {
return createReturnBook(returnTarget, null);
}
public static ItemStack createReturnBook(LinkTarget returnTarget, String destinationName) {
ItemStack returnBook = new ItemStack(ModItems.LINKING_BOOK.get());
returnBook.set(ModDataComponents.LINK_TARGET.get(), returnTarget);
if (destinationName != null && !destinationName.isBlank()) {
returnBook.set(ModDataComponents.LINK_DESTINATION_NAME.get(), destinationName);
}
returnBook.set(DataComponents.CUSTOM_NAME, Component.translatable("item.rivencraft.linking_book.return"));
return returnBook;
}
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.item;
import java.util.List;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
import net.dnorberg.rivencraft.book.LinkingService;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModSounds;
@@ -10,6 +11,7 @@ import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
@@ -93,11 +95,49 @@ public final class LinkingBookItem extends Item {
BlockPos position = target.location().pos();
tooltipComponents.add(Component.translatable(
"item.rivencraft.linking_book.destination",
dimension.toString(),
destinationName(stack, target).getString(),
position.getX(),
position.getY(),
position.getZ()
)
.withStyle(ChatFormatting.GRAY));
}
public static String destinationName(ServerLevel level) {
return AgeRegistrySavedData.get(level)
.findByDimension(level.dimension().location())
.map(definition -> definition.name())
.orElseGet(() -> vanillaDimensionName(level.dimension().location()));
}
public static Component destinationName(ItemStack stack, LinkTarget target) {
String savedName = stack.get(ModDataComponents.LINK_DESTINATION_NAME.get());
if (savedName != null && !savedName.isBlank()) {
return Component.literal(savedName);
}
ResourceLocation dimension = target.location().dimension().location();
if (dimension.equals(Level.OVERWORLD.location())) {
return Component.translatable("dimension.minecraft.overworld");
}
if (dimension.equals(Level.NETHER.location())) {
return Component.translatable("dimension.minecraft.the_nether");
}
if (dimension.equals(Level.END.location())) {
return Component.translatable("dimension.minecraft.the_end");
}
return Component.literal(dimension.toString());
}
private static String vanillaDimensionName(ResourceLocation dimension) {
if (dimension.equals(Level.OVERWORLD.location())) {
return "Overworld";
}
if (dimension.equals(Level.NETHER.location())) {
return "The Nether";
}
if (dimension.equals(Level.END.location())) {
return "The End";
}
return dimension.toString();
}
}
@@ -8,10 +8,14 @@ import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.resources.ResourceKey;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
public record LinkPreviewPayload(int skyColor, List<SnapshotBlock> blocks) implements CustomPacketPayload {
public record LinkPreviewPayload(ResourceKey<Level> dimension, BlockPos origin, float yaw, float pitch,
int skyColor, List<SnapshotBlock> blocks) implements CustomPacketPayload {
/** Each axis of the destination volume captured for the book preview. */
public static final int SNAPSHOT_SIZE = 32;
/** A square footprint keeps every side visible during an orbit. */
@@ -21,6 +25,10 @@ public record LinkPreviewPayload(int skyColor, List<SnapshotBlock> blocks) imple
);
public static final StreamCodec<RegistryFriendlyByteBuf, LinkPreviewPayload> STREAM_CODEC = StreamCodec.of(
(buffer, payload) -> {
buffer.writeResourceKey(payload.dimension);
buffer.writeBlockPos(payload.origin);
buffer.writeFloat(payload.yaw);
buffer.writeFloat(payload.pitch);
buffer.writeInt(payload.skyColor);
buffer.writeVarInt(payload.blocks.size());
for (SnapshotBlock block : payload.blocks) {
@@ -29,6 +37,10 @@ public record LinkPreviewPayload(int skyColor, List<SnapshotBlock> blocks) imple
}
},
buffer -> {
ResourceKey<Level> dimension = buffer.readResourceKey(Registries.DIMENSION);
BlockPos origin = buffer.readBlockPos();
float yaw = buffer.readFloat();
float pitch = buffer.readFloat();
int skyColor = buffer.readInt();
int size = buffer.readVarInt();
if (size < 0 || size > MAX_BLOCKS) {
@@ -38,7 +50,7 @@ public record LinkPreviewPayload(int skyColor, List<SnapshotBlock> blocks) imple
for (int i = 0; i < size; i++) {
blocks.add(new SnapshotBlock(buffer.readBlockPos(), Block.stateById(buffer.readVarInt())));
}
return new LinkPreviewPayload(skyColor, List.copyOf(blocks));
return new LinkPreviewPayload(dimension, origin, yaw, pitch, skyColor, List.copyOf(blocks));
}
);
@@ -2,15 +2,18 @@ package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.AgeReference;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.dnorberg.rivencraft.block.entity.BookStandBlockEntity;
import net.dnorberg.rivencraft.client.ClientLinkPreviewHandler;
import net.dnorberg.rivencraft.client.ClientBookInteractionHandler;
import net.dnorberg.rivencraft.item.DescriptiveBookItem;
import net.dnorberg.rivencraft.item.LinkingBookItem;
import net.dnorberg.rivencraft.registry.ModDataComponents;
import net.dnorberg.rivencraft.registry.ModItems;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerLevel;
@@ -24,18 +27,29 @@ import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.fml.ModList;
import net.dnorberg.rivencraft.compat.ImmersivePortalsChunkLoaders;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
import java.util.List;
import java.util.UUID;
public final class ModNetworking {
public static final int MAX_BOOK_TITLE_LENGTH = 32;
// Only accessed on the server thread; identity keeps identical blank books independent.
private static final Set<ItemStack> WRITING_BOOKS = Collections.newSetFromMap(new IdentityHashMap<>());
private ModNetworking() {
}
public static void register(RegisterPayloadHandlersEvent event) {
PayloadRegistrar registrar = event.registrar("6");
PayloadRegistrar registrar = event.registrar("8");
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
ModNetworking::requestDescriptiveBookScreen);
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
(payload, context) -> ClientBookInteractionHandler.openDescriptiveBook(payload, context));
registrar.playToServer(BindLinkingBookPayload.TYPE, BindLinkingBookPayload.STREAM_CODEC, ModNetworking::bindBook);
registrar.playToServer(BindDescriptiveBookPayload.TYPE, BindDescriptiveBookPayload.STREAM_CODEC,
ModNetworking::bindDescriptiveBook);
@@ -45,17 +59,43 @@ public final class ModNetworking {
registrar.playToServer(UseBookStandPayload.TYPE, UseBookStandPayload.STREAM_CODEC, ModNetworking::useBookStand);
registrar.playToServer(RequestLinkPreviewPayload.TYPE, RequestLinkPreviewPayload.STREAM_CODEC,
ModNetworking::requestLinkPreview);
registrar.playToServer(ReleaseLinkPreviewPayload.TYPE, ReleaseLinkPreviewPayload.STREAM_CODEC,
(payload, context) -> releaseLinkPreview(context));
registrar.playToClient(LinkPreviewPayload.TYPE, LinkPreviewPayload.STREAM_CODEC,
ClientLinkPreviewHandler::handle);
}
private static void requestDescriptiveBookScreen(RequestDescriptiveBookScreenPayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer player)) {
return;
}
BookAccess access = descriptiveBookAccess(player, payload.fromStand(), payload.offhand(), payload.standPos());
if (access == null || WRITING_BOOKS.contains(access.stack())) {
return;
}
AgeReference reference = access.stack().get(ModDataComponents.AGE_REFERENCE.get());
if (reference != null && !access.stack().has(ModDataComponents.AGE_GENERATION_PROFILE.get())) {
AgeRegistrySavedData.get(player.serverLevel()).find(reference.id()).ifPresent(definition -> {
access.stack().set(ModDataComponents.AGE_GENERATION_PROFILE.get(), definition.generationProfile());
access.sync().run();
});
}
// Requests run on the same server thread as Age creation. A request queued
// during generation can only open the finished book, never a stale client copy.
player.inventoryMenu.broadcastChanges();
PacketDistributor.sendToPlayer(player, new OpenDescriptiveBookScreenPayload(
payload.fromStand(), payload.offhand(),
GlobalPos.of(player.level().dimension(), payload.standPos()), access.stack().copy()));
}
private static void bindDescriptiveBook(BindDescriptiveBookPayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer player)) {
return;
}
BookAccess access = descriptiveBookAccess(player, payload.fromStand(), payload.offhand(), payload.standPos());
if (access == null || access.stack().has(ModDataComponents.AGE_REFERENCE.get())) {
if (access == null || WRITING_BOOKS.contains(access.stack())
|| access.stack().has(ModDataComponents.AGE_REFERENCE.get())) {
return;
}
String title = normalizeTitle(payload.title());
@@ -63,17 +103,24 @@ public final class ModNetworking {
return;
}
AgeReference reference = new AgeReference(UUID.randomUUID(), title);
if (AgeDimensionService.resolve(player, reference).isEmpty()) {
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
return;
}
WRITING_BOOKS.add(access.stack());
try {
AgeReference reference = new AgeReference(UUID.randomUUID(), title);
if (AgeDimensionService.resolve(player, reference).isEmpty()) {
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.unavailable"), true);
return;
}
access.stack().set(ModDataComponents.AGE_REFERENCE.get(), reference);
access.stack().set(DataComponents.CUSTOM_NAME, Component.literal(title));
access.sync().run();
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, SoundSource.PLAYERS, 1.0F, 1.0F);
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.created", title), true);
access.stack().set(ModDataComponents.AGE_REFERENCE.get(), reference);
AgeRegistrySavedData.get(player.serverLevel()).find(reference.id()).ifPresent(definition ->
access.stack().set(ModDataComponents.AGE_GENERATION_PROFILE.get(), definition.generationProfile()));
access.stack().set(DataComponents.CUSTOM_NAME, Component.literal(title));
access.sync().run();
player.level().playSound(null, player.blockPosition(), SoundEvents.BOOK_PAGE_TURN, SoundSource.PLAYERS, 1.0F, 1.0F);
player.displayClientMessage(Component.translatable("message.rivencraft.descriptive_book.created", title), true);
} finally {
WRITING_BOOKS.remove(access.stack());
}
}
private static void bindBook(BindLinkingBookPayload payload, IPayloadContext context) {
@@ -92,6 +139,7 @@ public final class ModNetworking {
}
stack.set(ModDataComponents.LINK_TARGET.get(), LinkTarget.from(player));
stack.set(ModDataComponents.LINK_DESTINATION_NAME.get(), LinkingBookItem.destinationName(player.serverLevel()));
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.displayClientMessage(Component.translatable("message.rivencraft.linking_book.bound", title), true);
@@ -192,7 +240,17 @@ public final class ModNetworking {
}
}
int skyColor = previewSkyColor(level, origin);
PacketDistributor.sendToPlayer(player, new LinkPreviewPayload(skyColor, List.copyOf(blocks)));
if (payload.immersivePortal() && ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.open(player, level, origin);
}
PacketDistributor.sendToPlayer(player, new LinkPreviewPayload(
target.location().dimension(), origin, target.yaw(), target.pitch(), skyColor, List.copyOf(blocks)));
}
private static void releaseLinkPreview(IPayloadContext context) {
if (context.player() instanceof ServerPlayer player && ModList.get().isLoaded("immersive_portals_core")) {
ImmersivePortalsChunkLoaders.close(player);
}
}
/** Vanilla Nether and End renderers do not use their biomes' blue Overworld sky tint. */
@@ -0,0 +1,30 @@
package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.GlobalPos;
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.ResourceLocation;
import net.minecraft.world.item.ItemStack;
/** Server-approved book contents, sent only when the book is available to open. */
public record OpenDescriptiveBookScreenPayload(boolean fromStand, boolean offhand, GlobalPos source, ItemStack book)
implements CustomPacketPayload {
public static final Type<OpenDescriptiveBookScreenPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "open_descriptive_book_screen")
);
public static final StreamCodec<RegistryFriendlyByteBuf, OpenDescriptiveBookScreenPayload> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL, OpenDescriptiveBookScreenPayload::fromStand,
ByteBufCodecs.BOOL, OpenDescriptiveBookScreenPayload::offhand,
GlobalPos.STREAM_CODEC, OpenDescriptiveBookScreenPayload::source,
ItemStack.STREAM_CODEC, OpenDescriptiveBookScreenPayload::book,
OpenDescriptiveBookScreenPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -0,0 +1,19 @@
package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record ReleaseLinkPreviewPayload() implements CustomPacketPayload {
public static final Type<ReleaseLinkPreviewPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "release_link_preview"));
public static final StreamCodec<RegistryFriendlyByteBuf, ReleaseLinkPreviewPayload> STREAM_CODEC =
StreamCodec.unit(new ReleaseLinkPreviewPayload());
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -0,0 +1,28 @@
package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.BlockPos;
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.ResourceLocation;
/** Requests the current Descriptive Book screen after server-side writing has finished. */
public record RequestDescriptiveBookScreenPayload(boolean fromStand, boolean offhand, BlockPos standPos)
implements CustomPacketPayload {
public static final Type<RequestDescriptiveBookScreenPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "request_descriptive_book_screen")
);
public static final StreamCodec<RegistryFriendlyByteBuf, RequestDescriptiveBookScreenPayload> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL, RequestDescriptiveBookScreenPayload::fromStand,
ByteBufCodecs.BOOL, RequestDescriptiveBookScreenPayload::offhand,
BlockPos.STREAM_CODEC, RequestDescriptiveBookScreenPayload::standPos,
RequestDescriptiveBookScreenPayload::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -8,7 +8,8 @@ import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
public record RequestLinkPreviewPayload(boolean descriptiveBook, boolean fromStand, boolean offhand, BlockPos standPos)
public record RequestLinkPreviewPayload(boolean descriptiveBook, boolean fromStand, boolean offhand,
BlockPos standPos, boolean immersivePortal)
implements CustomPacketPayload {
public static final Type<RequestLinkPreviewPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "request_link_preview")
@@ -19,6 +20,7 @@ public record RequestLinkPreviewPayload(boolean descriptiveBook, boolean fromSta
ByteBufCodecs.BOOL, RequestLinkPreviewPayload::fromStand,
ByteBufCodecs.BOOL, RequestLinkPreviewPayload::offhand,
BlockPos.STREAM_CODEC, RequestLinkPreviewPayload::standPos,
ByteBufCodecs.BOOL, RequestLinkPreviewPayload::immersivePortal,
RequestLinkPreviewPayload::new
);
@@ -5,6 +5,8 @@ import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.AgeReference;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.network.codec.ByteBufCodecs;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
@@ -25,6 +27,18 @@ public final class ModDataComponents {
.networkSynchronized(AgeReference.STREAM_CODEC)
.cacheEncoding()
.build());
public static final DeferredHolder<DataComponentType<?>, DataComponentType<ResourceLocation>> AGE_GENERATION_PROFILE =
DATA_COMPONENTS.register("age_generation_profile", () -> DataComponentType.<ResourceLocation>builder()
.persistent(ResourceLocation.CODEC)
.networkSynchronized(ResourceLocation.STREAM_CODEC)
.cacheEncoding()
.build());
public static final DeferredHolder<DataComponentType<?>, DataComponentType<String>> LINK_DESTINATION_NAME =
DATA_COMPONENTS.register("link_destination_name", () -> DataComponentType.<String>builder()
.persistent(com.mojang.serialization.Codec.STRING)
.networkSynchronized(ByteBufCodecs.STRING_UTF8)
.cacheEncoding()
.build());
private ModDataComponents() {
}
@@ -28,7 +28,17 @@
"screen.rivencraft.age.title_hint": "Name this Age",
"screen.rivencraft.age.name_prompt": "Name this Age before writing the Descriptive Book",
"screen.rivencraft.age.create": "Create",
"screen.rivencraft.age.generator": "Terrain: %s",
"generator.rivencraft.standard": "Standard",
"generator.rivencraft.amplified": "Amplified",
"generator.rivencraft.island": "Island",
"generator.rivencraft.cave": "Cave",
"generator.rivencraft.flat": "Flat",
"generator.rivencraft.void": "Void",
"generator.rivencraft.random_vanilla": "Standard",
"screen.rivencraft.book.preview_pending": "Loading destination...",
"screen.rivencraft.book.click_to_link": "Click the panel to link",
"screen.rivencraft.book.explore": "Explore",
"screen.rivencraft.book.exploring": "Exploring",
"itemGroup.rivencraft": "Rivencraft"
}
@@ -1,6 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "minecraft:item/book"
"layer0": "rivencraft:item/descriptive_book"
}
}
@@ -1,6 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "minecraft:item/enchanted_book"
"layer0": "rivencraft:item/linking_book"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

+10 -3
View File
@@ -87,12 +87,19 @@ A spiritual successor and modern port of Mystcraft for Minecraft 1.21.1 on NeoFo
side="BOTH"
[[dependencies.${mod_id}]]
modId="ponder"
type="required"
versionRange="[1.0.82,2.0.0)"
modId="create"
type="optional"
versionRange="[0,)"
ordering="AFTER"
side="CLIENT"
[[dependencies.${mod_id}]]
modId="immersive_portals_core"
type="optional"
versionRange="[6.0.7,7.0.0)"
ordering="AFTER"
side="BOTH"
[[dependencies.${mod_id}]]
modId="dynamicdimensions"
type="required"