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

@@ -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"