diff --git a/docs/PORTING.md b/docs/PORTING.md index ca32a2b..98fb28c 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -329,6 +329,12 @@ Age-seeded biomes in the legacy diagonal-checkerboard layout. Each square is uses Minecraft's serializable checkerboard biome source so the layout remains stable across server reloads. +Version 0.2.22.1 includes the server-owned Age traits in the validated +Descriptive Book screen response and displays the terrain, biome distribution, +lighting, weather, day length, celestial angle, and celestial phase on the +book's right-hand description page. This works for held books and Book Stands +without copying mutable Age-generation data onto the item. + Version 0.2.14.0 restores the legacy Linking and Descriptive Book artwork for inventory items and gives each book its matching open-book texture when placed on a Book Stand. Return Linking Books created inside another Age also retain and diff --git a/gradle.properties b/gradle.properties index 9b52c20..11d09c4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,7 @@ mod_name=Rivencraft # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=LGPL-3.0-or-later # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. -mod_version=0.2.22.0 +mod_version=0.2.22.1 immersive_portals_version=6.0.7 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. diff --git a/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java b/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java index d727f0d..f1bd602 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java +++ b/src/main/java/net/dnorberg/rivencraft/client/ClientBookInteractionHandler.java @@ -92,8 +92,8 @@ public final class ClientBookInteractionHandler { return; } minecraft.setScreen(payload.fromStand() - ? new DescriptiveBookScreen(payload.source().pos(), payload.book()) - : new DescriptiveBookScreen(hand, payload.book())); + ? new DescriptiveBookScreen(payload.source().pos(), payload.book(), payload.traits()) + : new DescriptiveBookScreen(hand, payload.book(), payload.traits())); }); } } diff --git a/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java b/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java index e66223d..62496c3 100644 --- a/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java +++ b/src/main/java/net/dnorberg/rivencraft/client/screen/DescriptiveBookScreen.java @@ -1,6 +1,7 @@ package net.dnorberg.rivencraft.client.screen; import net.dnorberg.rivencraft.book.AgeReference; +import net.dnorberg.rivencraft.book.AgeTraits; import net.dnorberg.rivencraft.client.ClientBookExploreOverlay; import net.dnorberg.rivencraft.network.UseDescriptiveBookPayload; import net.dnorberg.rivencraft.network.BindDescriptiveBookPayload; @@ -22,6 +23,9 @@ import net.minecraft.util.FormattedCharSequence; import net.minecraft.world.item.ItemStack; import net.minecraft.world.InteractionHand; import net.neoforged.neoforge.network.PacketDistributor; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; public final class DescriptiveBookScreen extends Screen implements ExplorableBookScreen { private static final int BOOK_TEXT_COLOR = 0xFF3B281B; @@ -40,6 +44,7 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo private final AgeReference age; private final ResourceLocation generationProfile; + private final AgeTraits traits; private final InteractionHand hand; private final BlockPos bookStandPos; private DestinationPreviewRenderer preview; @@ -47,18 +52,20 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo private Button exploreButton; private boolean movingToExploreOverlay; - public DescriptiveBookScreen(InteractionHand hand, ItemStack book) { + public DescriptiveBookScreen(InteractionHand hand, ItemStack book, @Nullable AgeTraits traits) { super(book.getHoverName()); age = book.get(ModDataComponents.AGE_REFERENCE.get()); generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get()); + this.traits = traits; this.hand = hand; bookStandPos = null; } - public DescriptiveBookScreen(BlockPos bookStandPos, ItemStack book) { + public DescriptiveBookScreen(BlockPos bookStandPos, ItemStack book, @Nullable AgeTraits traits) { super(book.getHoverName()); age = book.get(ModDataComponents.AGE_REFERENCE.get()); generationProfile = book.get(ModDataComponents.AGE_GENERATION_PROFILE.get()); + this.traits = traits; hand = InteractionHand.MAIN_HAND; this.bookStandPos = bookStandPos.immutable(); } @@ -138,14 +145,34 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo left + 177, top + 45, 124); } 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); + int detailsY = drawWrappedWithoutShadow( + graphics, Component.literal(age.name()), left + 177, top + 43, 124) + 3; + detailsY = Math.max(detailsY, top + 72); if (generationProfile != null) { Component profileName = generationProfile.getNamespace().equals("rivencraft") ? Component.translatable("generator.rivencraft." + generationProfile.getPath()) : Component.literal(generationProfile.toString()); - drawWrappedWithoutShadow(graphics, + detailsY = drawWrappedWithoutShadow(graphics, Component.translatable("screen.rivencraft.age.generator", profileName), - left + 177, top + 72, 124); + left + 177, detailsY, 124) + 2; + } + if (traits != null) { + detailsY = drawTrait(graphics, "biomes", traitName("biome_distribution", traits.biomeDistribution()), + left + 177, detailsY); + detailsY = drawTrait(graphics, "lighting", traitName("lighting", traits.lighting()), + left + 177, detailsY); + detailsY = drawTrait(graphics, "weather", traitName("weather", traits.weather()), + left + 177, detailsY); + Component dayLength = traits.hasStoppedCelestialCycle() + ? Component.translatable("trait.rivencraft.day_length.stopped") + : Component.literal(String.format(Locale.ROOT, "%.2fx", traits.celestialPeriod())); + detailsY = drawTrait(graphics, "day_length", dayLength, left + 177, detailsY); + detailsY = drawTrait(graphics, "celestial_angle", + Component.literal(String.format(Locale.ROOT, "%.0f°", traits.celestialAngle())), + left + 177, detailsY); + drawTrait(graphics, "celestial_phase", + Component.literal(String.format(Locale.ROOT, "%.0f%%", traits.celestialPhase() * 100.0F)), + left + 177, detailsY); } } @@ -288,11 +315,22 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo onClose(); } - private void drawWrappedWithoutShadow(GuiGraphics graphics, Component text, int x, int y, int maxWidth) { + private int drawTrait(GuiGraphics graphics, String key, Component value, int x, int y) { + return drawWrappedWithoutShadow(graphics, + Component.translatable("screen.rivencraft.age.trait." + key, value), x, y, 124) + 2; + } + + private static Component traitName(String group, Enum value) { + return Component.translatable("trait.rivencraft." + group + "." + + value.name().toLowerCase(Locale.ROOT)); + } + + private int drawWrappedWithoutShadow(GuiGraphics graphics, Component text, int x, int y, int maxWidth) { for (FormattedCharSequence line : font.split(text, maxWidth)) { graphics.drawString(font, line, x, y, BOOK_TEXT_COLOR, false); y += font.lineHeight; } + return y; } } diff --git a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java index 025361f..a4bdefa 100644 --- a/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java +++ b/src/main/java/net/dnorberg/rivencraft/network/ModNetworking.java @@ -75,18 +75,20 @@ public final class ModNetworking { 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 -> { + var definition = reference == null + ? null + : AgeRegistrySavedData.get(player.serverLevel()).find(reference.id()).orElse(null); + if (definition != null && !access.stack().has(ModDataComponents.AGE_GENERATION_PROFILE.get())) { 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())); + GlobalPos.of(player.level().dimension(), payload.standPos()), access.stack().copy(), + definition == null ? null : definition.traits())); } private static void bindDescriptiveBook(BindDescriptiveBookPayload payload, IPayloadContext context) { diff --git a/src/main/java/net/dnorberg/rivencraft/network/OpenDescriptiveBookScreenPayload.java b/src/main/java/net/dnorberg/rivencraft/network/OpenDescriptiveBookScreenPayload.java index 19d73d9..db9cfb8 100644 --- a/src/main/java/net/dnorberg/rivencraft/network/OpenDescriptiveBookScreenPayload.java +++ b/src/main/java/net/dnorberg/rivencraft/network/OpenDescriptiveBookScreenPayload.java @@ -1,27 +1,64 @@ package net.dnorberg.rivencraft.network; import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.book.AgeTraits; 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; +import org.jetbrains.annotations.Nullable; /** Server-approved book contents, sent only when the book is available to open. */ -public record OpenDescriptiveBookScreenPayload(boolean fromStand, boolean offhand, GlobalPos source, ItemStack book) +public record OpenDescriptiveBookScreenPayload( + boolean fromStand, + boolean offhand, + GlobalPos source, + ItemStack book, + @Nullable AgeTraits traits +) implements CustomPacketPayload { public static final Type TYPE = new Type<>( ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "open_descriptive_book_screen") ); - public static final StreamCodec STREAM_CODEC = StreamCodec.composite( - ByteBufCodecs.BOOL, OpenDescriptiveBookScreenPayload::fromStand, - ByteBufCodecs.BOOL, OpenDescriptiveBookScreenPayload::offhand, - GlobalPos.STREAM_CODEC, OpenDescriptiveBookScreenPayload::source, - ItemStack.STREAM_CODEC, OpenDescriptiveBookScreenPayload::book, - OpenDescriptiveBookScreenPayload::new - ); + public static final StreamCodec STREAM_CODEC = + StreamCodec.of(OpenDescriptiveBookScreenPayload::encode, OpenDescriptiveBookScreenPayload::decode); + + private static void encode(RegistryFriendlyByteBuf buffer, OpenDescriptiveBookScreenPayload payload) { + buffer.writeBoolean(payload.fromStand()); + buffer.writeBoolean(payload.offhand()); + GlobalPos.STREAM_CODEC.encode(buffer, payload.source()); + ItemStack.STREAM_CODEC.encode(buffer, payload.book()); + buffer.writeBoolean(payload.traits() != null); + if (payload.traits() != null) { + AgeTraits traits = payload.traits(); + buffer.writeEnum(traits.biomeDistribution()); + buffer.writeEnum(traits.lighting()); + buffer.writeEnum(traits.weather()); + buffer.writeDouble(traits.celestialPeriod()); + buffer.writeFloat(traits.celestialAngle()); + buffer.writeFloat(traits.celestialPhase()); + } + } + + private static OpenDescriptiveBookScreenPayload decode(RegistryFriendlyByteBuf buffer) { + boolean fromStand = buffer.readBoolean(); + boolean offhand = buffer.readBoolean(); + GlobalPos source = GlobalPos.STREAM_CODEC.decode(buffer); + ItemStack book = ItemStack.STREAM_CODEC.decode(buffer); + AgeTraits traits = buffer.readBoolean() + ? new AgeTraits( + buffer.readEnum(AgeTraits.BiomeDistribution.class), + buffer.readEnum(AgeTraits.Lighting.class), + buffer.readEnum(AgeTraits.Weather.class), + buffer.readDouble(), + buffer.readFloat(), + buffer.readFloat() + ) + : null; + return new OpenDescriptiveBookScreenPayload(fromStand, offhand, source, book, traits); + } @Override public Type type() { diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index bab7d2b..fa79589 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -29,6 +29,34 @@ "screen.rivencraft.age.name_prompt": "Name this Age before writing the Descriptive Book", "screen.rivencraft.age.create": "Create", "screen.rivencraft.age.generator": "Terrain: %s", + "screen.rivencraft.age.trait.biomes": "Biomes: %s", + "screen.rivencraft.age.trait.lighting": "Lighting: %s", + "screen.rivencraft.age.trait.weather": "Weather: %s", + "screen.rivencraft.age.trait.day_length": "Day length: %s", + "screen.rivencraft.age.trait.celestial_angle": "Angle: %s", + "screen.rivencraft.age.trait.celestial_phase": "Phase: %s", + "trait.rivencraft.biome_distribution.native": "Native", + "trait.rivencraft.biome_distribution.single": "Single", + "trait.rivencraft.biome_distribution.tiled": "Tiled", + "trait.rivencraft.biome_distribution.grid": "Grid", + "trait.rivencraft.biome_distribution.tiny": "Tiny", + "trait.rivencraft.biome_distribution.small": "Small", + "trait.rivencraft.biome_distribution.medium": "Medium", + "trait.rivencraft.biome_distribution.large": "Large", + "trait.rivencraft.biome_distribution.huge": "Huge", + "trait.rivencraft.lighting.normal": "Normal", + "trait.rivencraft.lighting.dark": "Dark", + "trait.rivencraft.lighting.bright": "Bright", + "trait.rivencraft.weather.normal": "Normal", + "trait.rivencraft.weather.fast": "Fast", + "trait.rivencraft.weather.slow": "Slow", + "trait.rivencraft.weather.off": "Off", + "trait.rivencraft.weather.always": "Always", + "trait.rivencraft.weather.cloudy": "Cloudy", + "trait.rivencraft.weather.rain": "Rain", + "trait.rivencraft.weather.snow": "Snow", + "trait.rivencraft.weather.storm": "Storm", + "trait.rivencraft.day_length.stopped": "Stopped", "generator.rivencraft.standard": "Standard", "generator.rivencraft.amplified": "Amplified", "generator.rivencraft.island": "Island",