Public Access
This commit is contained in:
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
+152
@@ -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;
|
||||
}
|
||||
}
|
||||
+75
@@ -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;
|
||||
}
|
||||
}
|
||||
+28
@@ -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 |
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user