diff --git a/CHANGELOG.md b/CHANGELOG.md index b94a5582..0703d0ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ All notable changes to **CreatureChat™** are documented in this file. The form ## Unreleased ### Added +- New Book Item + - Load client entity chat data and display friends and enemies by most recent + - Using Fabric DataGen to create book item + - New packet types for sending / receiving all entity data (by UUID - per page) + - Dynamic Next/Previous buttons with hover (hidden when no page to turn) + - Summary pages are displayed first, then Detail pages with more info + - Improvements to page contents, layout, pagination - and page turn sounds + - Integrating entityType into chat data, and incorporating death into broadcast, login, and book pages + - Adding entityName into chat data, for dead mobs + - Remember book screen state when exiting (so book resumes exactly where you left it) + - Adding top buttons + hover states - integrated hover assets and new arrow buttons + - Adding deterministic random stickers to pages (based on UUID and page #) + - Clean recent messages (remove behaviors) - Document SPDX header and changelog requirements in AGENTS.md for contributors ### Changed diff --git a/build.gradle b/build.gradle index 6861a589..47ce4f88 100644 --- a/build.gradle +++ b/build.gradle @@ -134,6 +134,7 @@ loom { runs { datagen { server() + client() vmArg "-Dfabric-api.datagen" vmArg "-Dfabric-api.datagen.output-dir=${file('src/main/generated').absolutePath}" vmArg "-Dfabric-api.datagen.modid=creaturechat" diff --git a/src/client/java/com/owlmaddie/ClientInit.java b/src/client/java/com/owlmaddie/ClientInit.java index 70b66275..0fcd4ecf 100644 --- a/src/client/java/com/owlmaddie/ClientInit.java +++ b/src/client/java/com/owlmaddie/ClientInit.java @@ -13,14 +13,20 @@ import com.owlmaddie.ui.InventoryKeyHandler; import com.owlmaddie.ui.PlayerMessageManager; import com.owlmaddie.utils.TickDelta; +import com.owlmaddie.utils.UseItemCallbackHelper; import com.owlmaddie.inventory.ModMenus; import com.owlmaddie.inventory.MobInventoryScreen; +import com.owlmaddie.items.ModItems; +import com.owlmaddie.ui.BookScreen; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.fabricmc.fabric.api.event.player.UseItemCallback; import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.Minecraft; +import net.minecraft.world.InteractionResultHolder; /** * The {@code ClientInit} class initializes this mod in the client and defines all hooks into the @@ -56,6 +62,16 @@ public void onInitializeClient() { ClientPackets.register(); MenuScreens.register(ModMenus.MOB_INVENTORY, MobInventoryScreen::new); + UseItemCallback.EVENT.register((player, world, hand) -> { + if (player.getItemInHand(hand).is(ModItems.BOOK)) { + if (world.isClientSide) { + Minecraft.getInstance().setScreen(new BookScreen()); + } + return InteractionResultHolder.success(player.getItemInHand(hand)); + } + return UseItemCallbackHelper.handleUseItemAction(player, world, hand); + }); + // Register an event callback to render text bubbles WorldRenderEvents.BEFORE_DEBUG_RENDER.register(ctx -> { float delta = TickDelta.get(ctx); diff --git a/src/client/java/com/owlmaddie/datagen/CreatureChatModelProvider.java b/src/client/java/com/owlmaddie/datagen/CreatureChatModelProvider.java new file mode 100644 index 00000000..71743ddc --- /dev/null +++ b/src/client/java/com/owlmaddie/datagen/CreatureChatModelProvider.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.datagen; + +import com.owlmaddie.items.ModItems; +import net.fabricmc.fabric.api.datagen.v1.provider.FabricModelProvider; +import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; +import net.minecraft.data.models.BlockModelGenerators; +import net.minecraft.data.models.ItemModelGenerators; +import net.minecraft.data.models.model.ModelTemplates; + +/** + * Generates item model JSON files using Minecraft's model data generators + * instead of manually constructing JSON. + */ +public class CreatureChatModelProvider extends FabricModelProvider { + public CreatureChatModelProvider(FabricDataOutput output) { + super(output); + } + + @Override + public void generateBlockStateModels(BlockModelGenerators blockStateModelGenerators) { + // No block models are generated for this mod. + } + + @Override + public void generateItemModels(ItemModelGenerators itemModelGenerators) { + // Generates the "minecraft:item/generated" style model for the book item. + itemModelGenerators.generateFlatItem(ModItems.BOOK, ModelTemplates.FLAT_ITEM); + } +} + diff --git a/src/client/java/com/owlmaddie/network/ClientPackets.java b/src/client/java/com/owlmaddie/network/ClientPackets.java index 6984069b..3fd52621 100644 --- a/src/client/java/com/owlmaddie/network/ClientPackets.java +++ b/src/client/java/com/owlmaddie/network/ClientPackets.java @@ -94,6 +94,12 @@ public static void sendChat(Entity entity, String message) { ClientPacketHelper.send(ServerPackets.PACKET_C2S_SEND_CHAT, buf); } + public static void requestEntityData(UUID entityId) { + FriendlyByteBuf buf = ClientBufferHelper.create(); + buf.writeUtf(entityId.toString()); + ClientPacketHelper.send(ServerPackets.PACKET_C2S_REQUEST_ENTITY_DATA, buf); + } + // Reading a Map from the buffer public static Map readPlayerDataMap(FriendlyByteBuf buffer) { int size = buffer.readInt(); // Read the size of the map @@ -119,6 +125,8 @@ public static void register() { String sender_name = buffer.readUtf(32767); ChatDataManager.ChatSender sender = ChatDataManager.ChatSender.valueOf(sender_name); Map players = readPlayerDataMap(buffer); + long lastTs = buffer.readLong(); + long deathTs = buffer.readLong(); // Update the chat data manager on the client-side client.execute(() -> { // Make sure to run on the client thread @@ -140,6 +148,8 @@ public static void register() { chatData.status = status; chatData.sender = sender; chatData.players = players; + chatData.lastMessage = lastTs; + chatData.death = deathTs != 0L ? deathTs : null; // Play sound with volume based on distance (from player or entity) Mob entity = ClientEntityFinder.getEntityByUUID(client.level, entityId); @@ -207,6 +217,40 @@ public static void register() { }); }); + ClientPacketHelper.registerReceiver(ServerPackets.PACKET_S2C_ENTITY_DATA, (client, handler, buffer, responseSender) -> { + String entityId = buffer.readUtf(); + byte[] compressed = buffer.readByteArray(); + client.execute(() -> { + String json = Decompression.decompressString(compressed); + if (json == null || json.isEmpty()) { + return; + } + Gson gson = new Gson(); + EntityChatData data = gson.fromJson(json, EntityChatData.class); + data.postDeserializeInitialization(); + LOGGER.info("Client received full data for entity {}", entityId); + ChatDataManager mgr = ChatDataManager.getClientInstance(); + EntityChatData existing = mgr.entityChatDataMap.get(entityId); + if (existing != null) { + existing.currentMessage = data.currentMessage; + existing.currentLineNumber = data.currentLineNumber; + existing.status = data.status; + existing.sender = data.sender; + existing.players = data.players; + existing.characterSheet = data.characterSheet; + existing.auto_generated = data.auto_generated; + existing.previousMessages = data.previousMessages; + existing.born = data.born; + existing.death = data.death; + existing.lastMessage = data.lastMessage; + existing.entityType = data.entityType; + existing.entityName = data.entityName; + } else { + mgr.entityChatDataMap.put(entityId, data); + } + }); + }); + // Client-side packet handler, receive entire whitelist / blacklist, and update BubbleRenderer ClientPacketHelper.registerReceiver(ServerPackets.PACKET_S2C_WHITELIST, (client, handler, buffer, responseSender) -> { // Read the whitelist data from the buffer diff --git a/src/client/java/com/owlmaddie/render/PoseHelper.java b/src/client/java/com/owlmaddie/render/PoseHelper.java new file mode 100644 index 00000000..5d1a19b8 --- /dev/null +++ b/src/client/java/com/owlmaddie/render/PoseHelper.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.render; + +import com.mojang.blaze3d.vertex.PoseStack; + +/** + * Cross-version wrapper for simple pose transformations. + */ +public final class PoseHelper { + private PoseHelper() {} + + public static void push(PoseStack stack) { + stack.pushPose(); + } + + public static void pop(PoseStack stack) { + stack.popPose(); + } + + public static void translate(PoseStack stack, float x, float y) { + stack.translate(x, y, 0); + } + + public static void scale(PoseStack stack, float sx, float sy) { + stack.scale(sx, sy, 1.0f); + } +} diff --git a/src/client/java/com/owlmaddie/render/RenderPipelineHelper.java b/src/client/java/com/owlmaddie/render/RenderPipelineHelper.java new file mode 100644 index 00000000..c97a1385 --- /dev/null +++ b/src/client/java/com/owlmaddie/render/RenderPipelineHelper.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.render; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.resources.ResourceLocation; + +/** + * Helper to blit GUI textures across Minecraft versions. + */ +public final class RenderPipelineHelper { + private RenderPipelineHelper() {} + + public static void blitGuiTexture( + GuiGraphics ctx, + ResourceLocation tex, + int x, int y, + int u, int v, + int width, int height, + int texWidth, int texHeight + ) { + ctx.blit(tex, x, y, u, v, width, height, texWidth, texHeight); + } +} diff --git a/src/client/java/com/owlmaddie/ui/BookScreen.java b/src/client/java/com/owlmaddie/ui/BookScreen.java new file mode 100644 index 00000000..162a9aa0 --- /dev/null +++ b/src/client/java/com/owlmaddie/ui/BookScreen.java @@ -0,0 +1,1101 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.ui; + +import com.owlmaddie.chat.ChatDataManager; +import com.owlmaddie.chat.ChatMessage; +import com.owlmaddie.chat.EntityChatData; +import com.owlmaddie.chat.PlayerData; +import com.owlmaddie.network.ClientPackets; +import com.owlmaddie.render.EntityTextureHelper; +import com.owlmaddie.render.PoseHelper; +import com.owlmaddie.render.RenderPipelineHelper; +import com.owlmaddie.utils.ClientEntityFinder; +import com.owlmaddie.utils.EntityCreationHelper; +import com.owlmaddie.message.MessageParser; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Player; +import java.time.Instant; +import java.time.ZoneId; +import com.mojang.blaze3d.platform.NativeImage; +import net.minecraft.server.packs.resources.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Screen that displays a two-page log of recently chatted entities. + */ +public class BookScreen extends ScreenHelper { + private static final int BOOK_WIDTH = 300; + private static final int BOOK_HEIGHT = 200; + + private static final Logger LOGGER = LoggerFactory.getLogger("creaturechat"); + + private enum Mode { SUMMARY, DETAIL } + private Mode mode = Mode.SUMMARY; + + // remember last opened state + private static Mode lastMode = Mode.SUMMARY; + private static int lastSummaryIndex = 0; + private static int lastDetailPage = 0; + private static String lastDetailEntityId = null; + private static String lastSearchQuery = ""; + private static boolean lastSearchVisible = false; + + // detail state restoration + private int pendingDetailIndex = -1; + private int pendingDetailPage; + + // summary state + private int summaryIndex; + private int hoveredSummary = -1; + + // detail state + private EntityChatData detailEntity; + private int detailPage; // left page index + private int detailTotalPages; // total single pages for detailEntity + private List detailSections = Collections.emptyList(); // character sheet sections + private List> sectionPages; + private List detailMessages; + private List> messagePages; + private int sectionRemainingSpace; + private boolean messagesOnSectionPage; + private int lastPrevMsgCount; + private String lastCurrentMsg; + + private final List all; + private List ordered; + private EditBox dummyField; + private EditBox searchField; + private boolean searchVisible; + private Button prevButton; + private Button nextButton; + private static final int PREV_X = 20, PREV_Y = 164, PREV_W = 32, PREV_H = 22; + private static final int NEXT_X = 244, NEXT_Y = 164, NEXT_W = 32, NEXT_H = 22; + private static final int INDEX_BTN_X = 10, INDEX_BTN_Y = 8, INDEX_BTN_W = 32, INDEX_BTN_H = 22; + private static final int SEARCH_BTN_X = 46, SEARCH_BTN_Y = 8, SEARCH_BTN_W = 32, SEARCH_BTN_H = 22; + private static final int EXIT_BTN_X = 258, EXIT_BTN_Y = 8, EXIT_BTN_W = 32, EXIT_BTN_H = 22; + private static final int SEARCH_FIELD_X = SEARCH_BTN_X + SEARCH_BTN_W + 5; + private static final int SEARCH_FIELD_Y = 9; + private static final int SEARCH_FIELD_W = EXIT_BTN_X - 5 - SEARCH_FIELD_X; + private static final int SEARCH_FIELD_H = 21; + private static final int PAGE_CONTENT_W = 106; // width of text block per page + private static final int PAGE_CONTENT_H = 122; // height of text block (for scissor) + private static final int PAGE1_X = 32, PAGE1_Y = 51; // left page top-left + private static final int PAGE2_X = 162, PAGE2_Y = 51; // right page top-left + private static final int LABEL_COLOR = 0xFF6B4A3B; // warm brown, matches book UI + private static final int BODY_COLOR = 0xFF2A2A2A; // dark text + private static final Random RNG = new Random(); + + private static final int SUMMARY_ROWS_PER_PAGE = 4; // per single page + private static final int SUMMARY_ROW_H = 30; + + private static class Pair { + final String label; + final String value; + Pair(String l, String v) { this.label = l; this.value = v; } + } + + private static class MessageChunk { + final ChatMessage msg; + final List lines; + final boolean showSpeaker; + MessageChunk(ChatMessage m, List lines, boolean showSpeaker) { + this.msg = m; + this.lines = lines; + this.showSpeaker = showSpeaker; + } + } + + private static class Sticker { + final ResourceLocation tex; + final int w; + final int h; + Sticker(ResourceLocation t, int w, int h) { + this.tex = t; + this.w = w; + this.h = h; + } + } + + private static class Rect { + final int x, y, w, h; + Rect(int x, int y, int w, int h) { + this.x = x; + this.y = y; + this.w = w; + this.h = h; + } + boolean intersects(Rect o, int pad) { + return x < o.x + o.w + pad && x + w + pad > o.x && y < o.y + o.h + pad && y + h + pad > o.y; + } + } + + private static final Rect LEFT_TOP_AREA_REL = new Rect(117, 43, 30, 24); + private static final Rect LEFT_BOTTOM_AREA_REL = new Rect(44, 154, 100, 27); + private static final Rect RIGHT_TOP_AREA_REL = new Rect(240, 42, 30, 24); + private static final Rect RIGHT_BOTTOM_AREA_REL = new Rect(156, 154, 100, 27); + + private static final List STICKERS = new ArrayList<>(); + + private static void loadStickers() { + if (!STICKERS.isEmpty()) return; + var rm = Minecraft.getInstance().getResourceManager(); + try { + rm.listResources("textures/ui/book/stickers", loc -> loc.getPath().endsWith(".png")) + .forEach((loc, res) -> { + try (var in = res.open()) { + NativeImage img = NativeImage.read(in); + STICKERS.add(new Sticker(loc, img.getWidth(), img.getHeight())); + } catch (IOException ignored) { + } + }); + } catch (Exception ignored) { + } + } + + public BookScreen() { + super(Component.literal("Creature Log")); + ChatDataManager mgr = ChatDataManager.getClientInstance(); + all = mgr.entityChatDataMap.values().stream() + .filter(data -> { + String nameText = resolveName(data); + boolean hasName = nameText != null && !nameText.isBlank(); + boolean hasMsg = data.currentMessage != null && !data.currentMessage.isBlank(); + return hasName && (hasMsg || data.death != null); + }) + .sorted(Comparator.comparingLong(BookScreen::getLastInteraction).reversed()) + .collect(Collectors.toList()); + ordered = new ArrayList<>(all); + sortOrdered(); + + // restore prior search filtering + summaryIndex = lastSummaryIndex; + mode = lastMode; + if (lastSearchQuery != null && !lastSearchQuery.isEmpty()) { + String q = lastSearchQuery.toLowerCase(Locale.ROOT); + ordered = ordered.stream() + .filter(d -> { + String n = resolveName(d); + return n != null && n.toLowerCase(Locale.ROOT).contains(q); + }) + .collect(Collectors.toList()); + sortOrdered(); + } + + // attempt to restore detail view after initialization + if (mode == Mode.DETAIL && lastDetailEntityId != null) { + pendingDetailPage = lastDetailPage; + for (int i = 0; i < ordered.size(); i++) { + if (ordered.get(i).entityId.equals(lastDetailEntityId)) { + pendingDetailIndex = i; + break; + } + } + if (pendingDetailIndex < 0) { + mode = Mode.SUMMARY; + } + } + } + + private String resolveName(EntityChatData data) { + Entity entity = getEntity(data.entityId); + String nameText = null; + if (entity instanceof Mob) { + if (entity.getCustomName() != null) { + nameText = entity.getCustomName().getString(); + } + } else if (entity instanceof Player) { + nameText = entity.getName().getString(); + } + if (nameText == null || nameText.isBlank()) { + if (data.entityName != null && !data.entityName.isBlank()) { + nameText = data.entityName; + } + } + if (nameText == null || nameText.isBlank()) { + String sheetName = data.getCharacterProp("Name"); + if (sheetName != null && !sheetName.isBlank() && !"N/A".equals(sheetName)) { + nameText = sheetName; + } + } + return nameText; + } + + private static long getLastInteraction(EntityChatData data) { + if (data.lastMessage != null) return data.lastMessage; + if (data.death != null) return data.death; + return 0L; + } + + private String friendlyTime(long millis) { + long minutes = millis / 60000L; + if (minutes < 60) return minutes + "min"; + long hours = minutes / 60; + long mins = minutes % 60; + if (hours < 24) { + String res = hours + "Hr"; + if (hours < 4 && mins > 0) res += " " + mins + "min"; + return res; + } + long days = hours / 24; + if (days < 7) return days + (days == 1 ? " day" : " days"); + long weeks = days / 7; + if (weeks < 4) return weeks + (weeks == 1 ? " week" : " weeks"); + long months = days / 30; + return months + (months == 1 ? " month" : " months"); + } + + @Override + protected void init() { + super.init(); + + BG_WIDTH = BOOK_WIDTH; + BG_HEIGHT = BOOK_HEIGHT; + TITLE_OFFSET = 0; + + bgX = (this.width - BG_WIDTH) / 2; + bgY = (this.height - BG_HEIGHT) / 2; + + dummyField = new EditBox(font, bgX, bgY, 0, 0, Component.empty()); + + searchField = new EditBox(font, + bgX + SEARCH_FIELD_X, + bgY + SEARCH_FIELD_Y, + SEARCH_FIELD_W, + SEARCH_FIELD_H, + Component.empty()); + searchField.visible = lastSearchVisible; + searchField.active = lastSearchVisible; + searchVisible = lastSearchVisible; + searchField.setValue(lastSearchQuery); + searchField.setResponder(text -> { + if (searchVisible) onSearchChanged(text); + }); + + addRenderableWidget(searchField); + if (searchVisible) { + setFocused(searchField); + setInitialFocus(searchField); + searchField.setCursorPosition(searchField.getValue().length()); + } + + prevButton = createPageButton( + bgX + PREV_X, bgY + PREV_Y, + PREV_W, PREV_H, + textures.GetUI("book/previous"), + textures.GetUI("book/previous-hover"), + w -> { + if (mode == Mode.SUMMARY) { + summaryIndex = Math.max(0, summaryIndex - SUMMARY_ROWS_PER_PAGE * 2); + } else { + if (detailPage == 0) { + mode = Mode.SUMMARY; + detailEntity = null; + } else { + detailPage = Math.max(0, detailPage - 2); + } + } + updateButtons(); + requestDataForCurrentPages(); + LOGGER.info("BookScreen: previous clicked mode={} summaryIndex={} detailPage={}", mode, summaryIndex, detailPage); + } + ); + addRenderableWidget(prevButton); + + nextButton = createPageButton( + bgX + NEXT_X, bgY + NEXT_Y, + NEXT_W, NEXT_H, + textures.GetUI("book/next"), + textures.GetUI("book/next-hover"), + w -> { + if (mode == Mode.SUMMARY) { + int spread = SUMMARY_ROWS_PER_PAGE * 2; + int maxIndex = Math.max(0, ((ordered.size() - 1) / spread) * spread); + summaryIndex = Math.min(summaryIndex + spread, maxIndex); + } else { + int maxLeft = ((detailTotalPages - 1) / 2) * 2; + detailPage = Math.min(detailPage + 2, maxLeft); + } + updateButtons(); + requestDataForCurrentPages(); + LOGGER.info("BookScreen: next clicked mode={} summaryIndex={} detailPage={}", mode, summaryIndex, detailPage); + } + ); + addRenderableWidget(nextButton); + + updateButtons(); + requestDataForCurrentPages(); + + if (pendingDetailIndex >= 0) { + openDetail(pendingDetailIndex); + detailPage = Math.min(pendingDetailPage, detailTotalPages - 1); + updateButtons(); + requestDataForCurrentPages(); + pendingDetailIndex = -1; + } + } + + private void updateButtons() { + boolean prevActive; + boolean nextActive; + if (mode == Mode.SUMMARY) { + prevActive = summaryIndex > 0; + nextActive = summaryIndex + SUMMARY_ROWS_PER_PAGE * 2 < ordered.size(); + } else { + prevActive = true; // always allow returning to summary + nextActive = detailPage + 2 < detailTotalPages; + } + if (prevButton != null) { + prevButton.active = prevActive; + prevButton.visible = prevActive; + } + if (nextButton != null) { + nextButton.active = nextActive; + nextButton.visible = nextActive; + } + } + + private Button createPageButton(int x, int y, int width, int height, + ResourceLocation normalTex, ResourceLocation hoverTex, + Button.OnPress onPress) { + return new Button(x, y, width, height, Component.empty(), onPress, w -> Component.empty()) { + @Override + public void renderWidget(net.minecraft.client.gui.GuiGraphics ctx, int mouseX, int mouseY, float delta) { + ResourceLocation tex = isHovered() ? hoverTex : normalTex; + RenderPipelineHelper.blitGuiTexture(ctx, tex, getX(), getY(), 0, 0, width, height, width, height); + } + + @Override + public void playDownSound(net.minecraft.client.sounds.SoundManager mgr) { + mgr.play(SimpleSoundInstance.forUI(SoundEvents.BOOK_PAGE_TURN, 1.0F)); + } + }; + } + + private void sortOrdered() { + ordered.sort(Comparator.comparingLong(BookScreen::getLastInteraction).reversed()); + } + + @Override + public void tick() { + super.tick(); + sortOrdered(); + if (mode == Mode.DETAIL && detailEntity != null) { + int count = detailEntity.previousMessages == null ? 0 : detailEntity.previousMessages.size(); + String current = detailEntity.currentMessage; + if (count != lastPrevMsgCount || !Objects.equals(current, lastCurrentMsg)) { + int prevPage = detailPage; + buildDetailContent(); + detailPage = Math.min(prevPage, detailTotalPages - 1); + } + } + } + + private boolean inside(double mx, double my, int x, int y, int w, int h) { + return mx >= x && mx <= x + w && my >= y && my <= y + h; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == 0) { + if (inside(mouseX, mouseY, bgX + INDEX_BTN_X, bgY + INDEX_BTN_Y, INDEX_BTN_W, INDEX_BTN_H)) { + mode = Mode.SUMMARY; + detailEntity = null; + detailPage = 0; + summaryIndex = 0; + updateButtons(); + requestDataForCurrentPages(); + LOGGER.info("BookScreen: index button pressed"); + return true; + } + if (inside(mouseX, mouseY, bgX + SEARCH_BTN_X, bgY + SEARCH_BTN_Y, SEARCH_BTN_W, SEARCH_BTN_H)) { + toggleSearch(); + return true; + } + if (inside(mouseX, mouseY, bgX + EXIT_BTN_X, bgY + EXIT_BTN_Y, EXIT_BTN_W, EXIT_BTN_H)) { + LOGGER.info("BookScreen: exit button pressed"); + onClose(); + return true; + } + if (mode == Mode.SUMMARY && hoveredSummary >= 0) { + openDetail(hoveredSummary); + return true; + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + private void toggleSearch() { + searchVisible = !searchVisible; + searchField.visible = searchVisible; + searchField.active = searchVisible; + searchField.setFocused(searchVisible); + if (searchVisible) { + setFocused(searchField); + setInitialFocus(searchField); + searchField.setCursorPosition(searchField.getValue().length()); + LOGGER.info("BookScreen: search opened and focused"); + } else { + setFocused(null); + searchField.setValue(""); + ordered = new ArrayList<>(all); + sortOrdered(); + summaryIndex = 0; + mode = Mode.SUMMARY; + updateButtons(); + requestDataForCurrentPages(); + LOGGER.info("BookScreen: search closed"); + } + } + + private void onSearchChanged(String text) { + String q = text.toLowerCase(Locale.ROOT); + ordered = all.stream() + .filter(d -> { + String n = resolveName(d); + return n != null && n.toLowerCase(Locale.ROOT).contains(q); + }) + .collect(Collectors.toList()); + sortOrdered(); + summaryIndex = 0; + mode = Mode.SUMMARY; + updateButtons(); + requestDataForCurrentPages(); + LOGGER.info("BookScreen: search '{}' results={}", q, ordered.size()); + } + + private void openDetail(int idx) { + if (idx < 0 || idx >= ordered.size()) return; + detailEntity = ordered.get(idx); + detailPage = 0; + buildDetailContent(); + mode = Mode.DETAIL; + updateButtons(); + requestDataForCurrentPages(); + } + + private void buildDetailContent() { + detailSections = new ArrayList<>(); + if (detailEntity.death != null) { + String deathDate = Instant.ofEpochMilli(detailEntity.death) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .toString(); + detailSections.add(new Pair("Death Date", deathDate)); + } + + detailSections.add(new Pair("Personality", orLoremSeeded(detailEntity.getCharacterProp("Personality"), detailEntity.entityId, "Personality", 20, 100))); + detailSections.add(new Pair("Speaking Style / Tone", orLoremSeeded(firstNonNull( + detailEntity.getCharacterProp("Speaking Style / Tone"), + detailEntity.getCharacterProp("Speaking Style"), + detailEntity.getCharacterProp("Tone") + ), detailEntity.entityId, "SpeakingStyle", 20, 100))); + detailSections.add(new Pair("Skills", orLoremSeeded(detailEntity.getCharacterProp("Skills"), detailEntity.entityId, "Skills", 20, 100))); + detailSections.add(new Pair("Likes", orLoremSeeded(detailEntity.getCharacterProp("Likes"), detailEntity.entityId, "Likes", 20, 100))); + detailSections.add(new Pair("Dislikes", orLoremSeeded(detailEntity.getCharacterProp("Dislikes"), detailEntity.entityId, "Dislikes", 20, 100))); + detailSections.add(new Pair("Background", orLoremSeeded(detailEntity.getCharacterProp("Background"), detailEntity.entityId, "Background", 20, 100))); + + detailMessages = new ArrayList<>(); + if (detailEntity.previousMessages != null) { + Player player = Minecraft.getInstance().player; + String pName = player != null ? player.getDisplayName().getString() : ""; + List filtered = detailEntity.previousMessages.stream() + .filter(m -> !(m.sender == ChatDataManager.ChatSender.USER && !pName.equals(m.name))) + .collect(Collectors.toList()); + if (detailEntity.death != null) { + for (int i = filtered.size() - 1; i >= 0; i--) { + ChatMessage m = filtered.get(i); + if (m.sender == ChatDataManager.ChatSender.ASSISTANT) { + detailMessages.add(m); + break; + } + } + } else { + List last = new ArrayList<>(); + ChatDataManager.ChatSender expected = null; + for (int i = filtered.size() - 1; i >= 0 && last.size() < 4; i--) { + ChatMessage m = filtered.get(i); + if (last.isEmpty()) { + if (m.sender != ChatDataManager.ChatSender.ASSISTANT) continue; + expected = ChatDataManager.ChatSender.USER; + last.add(m); + } else { + if (m.sender != expected) continue; + last.add(m); + expected = expected == ChatDataManager.ChatSender.USER + ? ChatDataManager.ChatSender.ASSISTANT + : ChatDataManager.ChatSender.USER; + } + } + detailMessages.addAll(last); + } + } + + paginateSections(); + paginateMessages(); + detailTotalPages = sectionPages.size() + messagePages.size() - (messagesOnSectionPage ? 1 : 0); + lastPrevMsgCount = detailEntity.previousMessages == null ? 0 : detailEntity.previousMessages.size(); + lastCurrentMsg = detailEntity.currentMessage; + } + + private void paginateSections() { + if (detailSections == null) { + detailSections = Collections.emptyList(); + } + sectionPages = new ArrayList<>(); + sectionRemainingSpace = 0; + List page = new ArrayList<>(); + int headerHeight = Math.round(this.font.lineHeight * 1.12f) + 6 + 35; + int available = PAGE_CONTENT_H - headerHeight; + int used = 0; + for (Pair orig : detailSections) { + List lines = wrapLines(orig.value, PAGE_CONTENT_W, 0.92f); + int lineH = Math.round(this.font.lineHeight * 0.92f); + int idx = 0; + boolean first = true; + while (idx < lines.size()) { + if (used >= available) { + sectionPages.add(page); + page = new ArrayList<>(); + available = PAGE_CONTENT_H; + used = 0; + } + int labelH = first ? this.font.lineHeight : 0; + int maxLines = (available - used - labelH - 4) / lineH; + if (maxLines <= 0) { + sectionPages.add(page); + page = new ArrayList<>(); + available = PAGE_CONTENT_H; + used = 0; + continue; + } + int end = Math.min(idx + maxLines, lines.size()); + String valChunk = String.join(" ", lines.subList(idx, end)); + page.add(new Pair(first ? orig.label : null, valChunk)); + used += labelH + (end - idx) * lineH + 4; + idx = end; + first = false; + } + } + if (!page.isEmpty()) { + sectionPages.add(page); + } + if (sectionPages.isEmpty()) { + sectionPages.add(Collections.emptyList()); + sectionRemainingSpace = PAGE_CONTENT_H - headerHeight; + } else { + int availLast = sectionPages.size() == 1 ? PAGE_CONTENT_H - headerHeight : PAGE_CONTENT_H; + sectionRemainingSpace = Math.max(0, availLast - used); + } + } + + private void paginateMessages() { + messagePages = new ArrayList<>(); + int headerH = this.font.lineHeight + 2; + int lineH = Math.round(this.font.lineHeight * 0.9f); + int minMsgH = this.font.lineHeight + 1 + lineH + 4; + messagesOnSectionPage = sectionRemainingSpace >= headerH + minMsgH && !detailMessages.isEmpty(); + int available = messagesOnSectionPage ? sectionRemainingSpace - headerH : PAGE_CONTENT_H - headerH; + if (available < 0) available = 0; + List page = new ArrayList<>(); + int remaining = available; + for (ChatMessage m : detailMessages) { + String cleaned = MessageParser.parseMessage(safe(m.message)).getCleanedMessage(); + List lines = wrapLines(cleaned, PAGE_CONTENT_W - 4, 0.9f); + boolean first = true; + int idx = 0; + while (idx < lines.size()) { + if (remaining <= 0) { + messagePages.add(page); + page = new ArrayList<>(); + remaining = PAGE_CONTENT_H - headerH; + } + int metaH = first ? this.font.lineHeight + 1 : 0; + int maxLines = (remaining - metaH - 4) / lineH; + if (maxLines <= 0) { + messagePages.add(page); + page = new ArrayList<>(); + remaining = PAGE_CONTENT_H - headerH; + continue; + } + int end = Math.min(idx + maxLines, lines.size()); + List chunkLines = new ArrayList<>(lines.subList(idx, end)); + page.add(new MessageChunk(m, chunkLines, first)); + remaining -= metaH + (end - idx) * lineH + 4; + idx = end; + first = false; + } + } + if (!page.isEmpty()) { + messagePages.add(page); + } + if (messagePages.isEmpty()) { + messagePages.add(Collections.emptyList()); + } + } + + @Override + protected void renderContent(net.minecraft.client.gui.GuiGraphics ctx, int mouseX, int mouseY, float delta) { + if (mode == Mode.SUMMARY) { + hoveredSummary = -1; + renderSummaryPage(ctx, bgX + PAGE1_X, bgY + PAGE1_Y, summaryIndex, mouseX, mouseY); + renderSummaryPage(ctx, bgX + PAGE2_X, bgY + PAGE2_Y, summaryIndex + SUMMARY_ROWS_PER_PAGE, mouseX, mouseY); + } else if (detailEntity != null) { + Set used = new HashSet<>(); + renderDetailPage(ctx, bgX + PAGE1_X, bgY + PAGE1_Y, detailPage, used); + renderDetailPage(ctx, bgX + PAGE2_X, bgY + PAGE2_Y, detailPage + 1, used); + } + + renderTopButtons(ctx, mouseX, mouseY); + } + + private void renderTopButtons(net.minecraft.client.gui.GuiGraphics ctx, int mouseX, int mouseY) { + blitButton(ctx, mouseX, mouseY, INDEX_BTN_X, INDEX_BTN_Y, INDEX_BTN_W, INDEX_BTN_H, + "book/index", "book/index-hover"); + blitButton(ctx, mouseX, mouseY, SEARCH_BTN_X, SEARCH_BTN_Y, SEARCH_BTN_W, SEARCH_BTN_H, + "book/search", "book/search-hover"); + blitButton(ctx, mouseX, mouseY, EXIT_BTN_X, EXIT_BTN_Y, EXIT_BTN_W, EXIT_BTN_H, + "book/exit", "book/exit-hover"); + } + + private void blitButton(net.minecraft.client.gui.GuiGraphics ctx, int mouseX, int mouseY, + int x, int y, int w, int h, String normal, String hover) { + boolean hov = inside(mouseX, mouseY, bgX + x, bgY + y, w, h); + ResourceLocation tex = textures.GetUI(hov ? hover : normal); + RenderPipelineHelper.blitGuiTexture(ctx, tex, bgX + x, bgY + y, 0, 0, w, h, w, h); + } + + private void renderSummaryPage(net.minecraft.client.gui.GuiGraphics ctx, int x, int y, int startIndex, int mouseX, int mouseY) { + ctx.enableScissor(x, y, x + PAGE_CONTENT_W, y + PAGE_CONTENT_H); + for (int i = 0; i < SUMMARY_ROWS_PER_PAGE; i++) { + int idx = startIndex + i; + if (idx >= ordered.size()) break; + int rowY = y + i * SUMMARY_ROW_H; + boolean hover = mouseX >= x && mouseX <= x + PAGE_CONTENT_W && mouseY >= rowY && mouseY <= rowY + SUMMARY_ROW_H; + if (hover) { + ctx.fill(x, rowY, x + PAGE_CONTENT_W, rowY + SUMMARY_ROW_H, 0x406B4A3B); + hoveredSummary = idx; + } + EntityChatData data = ordered.get(idx); + Entity entity = getEntity(data.entityId); + if (entity == null && data.entityType != null) { + EntityType type = EntityType.byString(data.entityType).orElse(null); + if (type != null) { + entity = EntityCreationHelper.create(type); + } + } + if (entity != null) { + PoseHelper.push(ctx.pose()); + PoseHelper.scale(ctx.pose(), 0.6f, 0.6f); + drawEntityIcon(ctx, entity, (int) (x / 0.6f), (int) (rowY / 0.6f)); + PoseHelper.pop(ctx.pose()); + } + + String name = resolveName(data); + if (name == null) name = "Unknown"; + int avail = PAGE_CONTENT_W - 22; + if (data.death != null) avail -= this.font.width("RIP: "); + name = this.font.plainSubstrByWidth(name, avail); + if (data.death != null) { + Component comp = Component.literal("RIP: ") + .append(Component.literal(name).withStyle(Style.EMPTY.withStrikethrough(true))); + ctx.drawString(this.font, comp, x + 22, rowY + 2, BODY_COLOR, false); + } else { + ctx.drawString(this.font, name, x + 22, rowY + 2, BODY_COLOR, false); + } + + Player player = Minecraft.getInstance().player; + PlayerData pData = player != null ? data.getPlayerData(player.getDisplayName().getString()) : null; + int friendship = pData != null ? pData.friendship : 0; + ResourceLocation frTex = textures.GetUI("friendship" + friendship); + int iconW = 0; + if (frTex != null) { + PoseHelper.push(ctx.pose()); + PoseHelper.translate(ctx.pose(), x + 22, rowY + 12); + PoseHelper.scale(ctx.pose(), 0.6f, 0.6f); + RenderPipelineHelper.blitGuiTexture(ctx, frTex, 0, 0, 0, 0, 31, 21, 31, 21); + PoseHelper.pop(ctx.pose()); + iconW = Math.round(31 * 0.6f); + } + + long last = getLastInteraction(data); + String time = friendlyTime(System.currentTimeMillis() - last); + ctx.drawString(this.font, time, x + 22 + iconW + 4, rowY + this.font.lineHeight + 4, BODY_COLOR, false); + } + ctx.disableScissor(); + } + + private void renderStickers(net.minecraft.client.gui.GuiGraphics ctx, int x, int y, int pageIndex, Set used) { + loadStickers(); + if (STICKERS.isEmpty() || detailEntity == null) return; + + UUID uuid; + try { + uuid = UUID.fromString(detailEntity.entityId); + } catch (Exception e) { + return; + } + long seed = uuid.getMostSignificantBits() ^ uuid.getLeastSignificantBits() ^ (long) (pageIndex + 1); + Random rand = new Random(seed); + + List placed = new ArrayList<>(); + List blocked = List.of( + new Rect(bgX + PREV_X, bgY + PREV_Y, PREV_W, PREV_H), + new Rect(bgX + NEXT_X, bgY + NEXT_Y, NEXT_W, NEXT_H), + new Rect(bgX + INDEX_BTN_X, bgY + INDEX_BTN_Y, INDEX_BTN_W, INDEX_BTN_H), + new Rect(bgX + SEARCH_BTN_X, bgY + SEARCH_BTN_Y, SEARCH_BTN_W, SEARCH_BTN_H), + new Rect(bgX + EXIT_BTN_X, bgY + EXIT_BTN_Y, EXIT_BTN_W, EXIT_BTN_H) + ); + + Rect content = new Rect(x, y, PAGE_CONTENT_W, PAGE_CONTENT_H); + Rect[] areaRels = (pageIndex % 2 == 0) + ? new Rect[]{LEFT_TOP_AREA_REL, LEFT_BOTTOM_AREA_REL} + : new Rect[]{RIGHT_TOP_AREA_REL, RIGHT_BOTTOM_AREA_REL}; + + for (Rect rel : areaRels) { + Rect area = new Rect(bgX + rel.x, bgY + rel.y, rel.w, rel.h); + int count = rand.nextInt(2); // 0-1 sticker per area + for (int i = 0; i < count; i++) { + Sticker st = null; + for (int attempt = 0; attempt < 20 && st == null; attempt++) { + Sticker cand = STICKERS.get(rand.nextInt(STICKERS.size())); + if (!used.contains(cand.tex)) { + st = cand; + } + } + if (st == null || st.w > area.w || st.h > area.h) continue; + boolean done = false; + for (int attempt = 0; attempt < 20 && !done; attempt++) { + int sx = area.x + rand.nextInt(area.w - st.w + 1); + int sy = area.y + rand.nextInt(area.h - st.h + 1); + Rect r = new Rect(sx, sy, st.w, st.h); + boolean collide = false; + for (Rect p : placed) { + if (r.intersects(p, 4)) { collide = true; break; } + } + if (!collide) { + for (Rect b : blocked) { + if (r.intersects(b, 0)) { collide = true; break; } + } + } + if (!collide && !(r.x >= content.x && r.x + r.w <= content.x + content.w && r.y >= content.y && r.y + r.h <= content.y + content.h)) { + RenderPipelineHelper.blitGuiTexture(ctx, st.tex, sx, sy, 0, 0, st.w, st.h, st.w, st.h); + placed.add(r); + used.add(st.tex); + done = true; + } + } + } + } + } + + private void renderDetailPage(net.minecraft.client.gui.GuiGraphics ctx, int x, int y, int pageIndex, Set used) { + if (pageIndex >= detailTotalPages) return; + if (detailEntity == null) return; + + renderStickers(ctx, x, y, pageIndex, used); + + Player player = Minecraft.getInstance().player; + String playerName = player != null ? player.getDisplayName().getString() : ""; + PlayerData pData = detailEntity.getPlayerData(playerName); + int friendship = pData != null ? pData.friendship : 0; + Entity entity = getEntity(detailEntity.entityId); + if (entity == null && detailEntity.entityType != null) { + EntityType type = EntityType.byString(detailEntity.entityType).orElse(null); + if (type != null) { + entity = EntityCreationHelper.create(type); + } + } + + int sectionPageCount = sectionPages.size(); + + if (pageIndex < sectionPageCount) { // character sheet pages + List page = sectionPages.get(pageIndex); + int lineY = y; + if (pageIndex == 0) { + String displayName = resolveName(detailEntity); + if (displayName == null || displayName.isBlank()) { + displayName = entity != null ? entity.getName().getString() : "Unknown"; + } + int titleColor = 0xFF000000; + if (friendship < 0) titleColor = 0xFFFF3A3A; + else if (friendship > 0) titleColor = 0xFF2ECC40; + int availNameW = (int)Math.floor(PAGE_CONTENT_W / 1.12f); + if (detailEntity.death != null) availNameW -= this.font.width("RIP: "); + displayName = this.font.plainSubstrByWidth(displayName, availNameW); + Component comp = detailEntity.death != null + ? Component.literal("RIP: ").append(Component.literal(displayName).withStyle(Style.EMPTY.withStrikethrough(true))) + : Component.literal(displayName); + PoseHelper.push(ctx.pose()); + PoseHelper.translate(ctx.pose(), (float)x, (float)y); + PoseHelper.scale(ctx.pose(), 1.12f, 1.12f); + ctx.drawString(this.font, comp, 1, 1, 0x66000000, false); + ctx.drawString(this.font, comp, 0, 0, titleColor, false); + PoseHelper.pop(ctx.pose()); + lineY = y + Math.round(this.font.lineHeight * 1.12f) + 6; + if (entity != null) drawEntityIcon(ctx, entity, x, lineY); + ResourceLocation frTex = textures.GetUI("friendship" + friendship); + if (frTex != null) { + RenderPipelineHelper.blitGuiTexture(ctx, frTex, x + 34, lineY, 0, 0, 31, 21, 31, 21); + } + lineY += 35; + } + ctx.enableScissor(x, y, x + PAGE_CONTENT_W, y + PAGE_CONTENT_H); + for (Pair p : page) { + lineY = drawPair(ctx, p.label, p.value, x, lineY, PAGE_CONTENT_W); + } + // append messages on same page if space was reserved + if (messagesOnSectionPage && pageIndex == sectionPageCount - 1) { + lineY = renderMessagesPage(ctx, x, lineY, messagePages.get(0), true); + } + ctx.disableScissor(); + } else { // messages pages + int msgPage = pageIndex - sectionPageCount + (messagesOnSectionPage ? 1 : 0); + List msgs = messagePages.get(msgPage); + ctx.enableScissor(x, y, x + PAGE_CONTENT_W, y + PAGE_CONTENT_H); + renderMessagesPage(ctx, x, y, msgs, msgPage == 0 && !messagesOnSectionPage); + ctx.disableScissor(); + } + + } + + private int renderMessagesPage(net.minecraft.client.gui.GuiGraphics ctx, int x, int startY, List chunks, boolean showHeader) { + int lineY = startY; + if (showHeader) { + String header = detailEntity != null && detailEntity.death != null ? "Last Words" : "Recent Messages"; + ctx.drawString(this.font, header, x, lineY, LABEL_COLOR, false); + lineY += this.font.lineHeight + 2; + } + for (MessageChunk mc : chunks) { + if (mc.showSpeaker) { + String speaker = mc.msg.sender == ChatDataManager.ChatSender.USER ? "You" : resolveName(detailEntity); + if (speaker == null || speaker.isBlank()) speaker = "Unknown"; + long ts = mc.msg.timestamp == null ? 0L : mc.msg.timestamp; + String ago = friendlyTime(System.currentTimeMillis() - ts) + " ago"; + float metaScale = 0.8f; + int timeW = Math.round(this.font.width(ago) * metaScale); + int availSpeaker = (int)Math.floor((PAGE_CONTENT_W - timeW - 2) / metaScale); + speaker = this.font.plainSubstrByWidth(speaker, availSpeaker); + PoseHelper.push(ctx.pose()); + PoseHelper.translate(ctx.pose(), x, lineY); + PoseHelper.scale(ctx.pose(), metaScale, metaScale); + ctx.drawString(this.font, speaker, 0, 0, LABEL_COLOR, false); + ctx.drawString(this.font, ago, Math.round(PAGE_CONTENT_W / metaScale) - this.font.width(ago), 0, LABEL_COLOR, false); + PoseHelper.pop(ctx.pose()); + lineY += Math.round(this.font.lineHeight * metaScale) + 1; + } + lineY = drawLines(ctx, mc.lines, x + 4, lineY, 0.9f, BODY_COLOR); + lineY += 4; + } + return lineY; + } + + private void requestDataForCurrentPages() { + sortOrdered(); + if (mode == Mode.DETAIL && detailEntity != null) { + UUID id = UUID.fromString(detailEntity.entityId); + ClientPackets.requestEntityData(id); + } + } + +// ---------- helpers ---------- + + private static String firstNonNull(String... vals) { + for (String v : vals) if (v != null && !v.isEmpty()) return v; + return null; + } + private static String orLorem(String v) { + if (v == null || v.isEmpty() || "N/A".equalsIgnoreCase(v)) return lorem(20, 100); + return v; + } + private static String lorem(int min, int max) { + String[] words = ("lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut " + + "labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut " + + "aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + + "dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt in culpa qui officia " + + "deserunt mollit anim id est laborum").split(" "); + int target = min + RNG.nextInt(Math.max(1, max - min + 1)); + StringBuilder sb = new StringBuilder(); + while (sb.length() < target) { + if (sb.length() > 0) sb.append(' '); + sb.append(words[RNG.nextInt(words.length)]); + } + String s = sb.toString(); + // Trim to nearest word under target and capitalize first letter + if (s.length() > target) s = s.substring(0, Math.max(0, s.lastIndexOf(' ', target))); + if (!s.isEmpty()) s = Character.toUpperCase(s.charAt(0)) + s.substring(1); + return s + "."; + } + + /** Draw a label then a wrapped value; returns next y. */ + private int drawPair(net.minecraft.client.gui.GuiGraphics ctx, String label, String value, int x, int y, int widthPx) { + if (label != null) { + ctx.drawString(this.font, label, x, y, LABEL_COLOR, false); + y += this.font.lineHeight; + } + return drawWrapped(ctx, value, x, y, widthPx, 0.92f, BODY_COLOR) + 4; + } + + /** Wrapped text with scaling. Returns next y. */ + private int drawWrapped(net.minecraft.client.gui.GuiGraphics ctx, String text, int x, int y, + int maxWidthPx, float scale, int color) { + List lines = wrapLines(text, maxWidthPx, scale); + return drawLines(ctx, lines, x, y, scale, color); + } + + private int drawLines(net.minecraft.client.gui.GuiGraphics ctx, List lines, int x, int y, + float scale, int color) { + PoseHelper.push(ctx.pose()); + PoseHelper.translate(ctx.pose(), (float)x, (float)y); + PoseHelper.scale(ctx.pose(), scale, scale); + int drawnPx = 0; + for (String line : lines) { + ctx.drawString(this.font, line, 0, drawnPx, color, false); + drawnPx += this.font.lineHeight; + } + PoseHelper.pop(ctx.pose()); + return y + Math.round(drawnPx * scale); + } + + private int measureWrappedHeight(String text, int maxWidthPx, float scale) { + List lines = wrapLines(text, maxWidthPx, scale); + return (int)Math.ceil(lines.size() * this.font.lineHeight * scale); + } + + private List wrapLines(String text, int maxWidthPx, float scale) { + int avail = (int)Math.floor(maxWidthPx / scale); + List lines = new ArrayList<>(); + String rest = text == null ? "" : text.trim(); + while (!rest.isEmpty()) { + String piece = this.font.plainSubstrByWidth(rest, avail); + int cut = piece.length(); + if (cut <= 0) break; + if (cut < rest.length()) { + int space = piece.lastIndexOf(' '); + if (space > 0) { + cut = space; + piece = piece.substring(0, space); + } + } + lines.add(piece.trim()); + rest = rest.substring(Math.min(rest.length(), cut)).trim(); + } + if (lines.isEmpty()) lines.add(""); + return lines; + } + + private String safe(String s) { + return s == null ? "" : s; + } + + private void drawEntityIcon(net.minecraft.client.gui.GuiGraphics ctx, Entity entity, int x, int y) { + var dispatcher = Minecraft.getInstance().getEntityRenderDispatcher(); + var renderer = dispatcher.getRenderer(entity); + ResourceLocation skinId = EntityTextureHelper.getTexture(renderer, entity); + if (skinId == null) return; + ResourceLocation icon = textures.GetEntity(skinId.getPath()); + if (icon == null) return; + RenderPipelineHelper.blitGuiTexture(ctx, icon, + x, y, + 0, 0, + 30, 30, + 30, 30); + } + + private Entity getEntity(String id) { + try { + UUID uuid = UUID.fromString(id); + var level = Minecraft.getInstance().level; + if (level == null) return null; + return ClientEntityFinder.getEntityByUUID(level, uuid); + } catch (Exception e) { + return null; + } + } + + @Override + public void onClose() { + lastMode = mode; + lastSummaryIndex = summaryIndex; + lastDetailPage = detailPage; + lastDetailEntityId = detailEntity != null ? detailEntity.entityId : null; + lastSearchQuery = searchField != null ? searchField.getValue() : ""; + lastSearchVisible = searchVisible; + super.onClose(); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + protected EditBox getTextField() { + return this.dummyField; + } + + @Override + protected Component getLabelText() { + return Component.empty(); + } + + @Override + protected String getBackgroundTextureId() { + return "book/book"; + } + + // Deterministic seed from entityId + field key (FNV-1a 64-bit) + private static long seedFrom(String id, String key) { + long h = 0xcbf29ce484222325L; + for (int i = 0; i < id.length(); i++) { h ^= id.charAt(i); h *= 0x100000001b3L; } + h ^= 0x9e3779b97f4a7c15L; // scramble between id and key + for (int i = 0; i < key.length(); i++) { h ^= key.charAt(i); h *= 0x100000001b3L; } + return h; + } + + // Lorem with a provided seed (so it's stable per entity/field) + private static String loremSeeded(int min, int max, long seed) { + java.util.Random r = new java.util.Random(seed); + String[] words = ("lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut " + + "labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut " + + "aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in voluptate velit esse cillum " + + "dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt in culpa qui officia " + + "deserunt mollit anim id est laborum").split(" "); + int target = min + r.nextInt(Math.max(1, max - min + 1)); + StringBuilder sb = new StringBuilder(); + while (sb.length() < target) { + if (sb.length() > 0) sb.append(' '); + sb.append(words[r.nextInt(words.length)]); + } + String s = sb.toString(); + if (s.length() > target) s = s.substring(0, Math.max(0, s.lastIndexOf(' ', target))); + if (!s.isEmpty()) s = Character.toUpperCase(s.charAt(0)) + s.substring(1); + return s + "."; + } + + // If v is null/empty/"N/A", return stable lorem seeded by entityId+field + private static String orLoremSeeded(String v, String entityId, String fieldKey, int min, int max) { + if (v == null || v.isEmpty() || "N/A".equalsIgnoreCase(v)) { + return loremSeeded(min, max, seedFrom(entityId, fieldKey)); + } + return v; + } + +} + diff --git a/src/client/java/com/owlmaddie/ui/ButtonHelper.java b/src/client/java/com/owlmaddie/ui/ButtonHelper.java index f2c0e434..d1b36a2d 100644 --- a/src/client/java/com/owlmaddie/ui/ButtonHelper.java +++ b/src/client/java/com/owlmaddie/ui/ButtonHelper.java @@ -7,8 +7,12 @@ import net.minecraft.client.gui.components.Button; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ButtonHelper { + private static final Logger LOGGER = LoggerFactory.getLogger("creaturechat"); + /** * Create an image‐only button that swaps between normal/hover textures. * Version‐specific subclasses just override the rendering hook. @@ -21,9 +25,19 @@ public static Button createImageButton( Button.OnPress onPress, Button.CreateNarration narrate ) { + final boolean missing = normalTex == null || hoverTex == null; + if (missing) { + LOGGER.warn("ButtonHelper: missing texture for button at ({}, {})", x, y); + } + return new Button(x, y, width, height, Component.empty(), onPress, narrate) { @Override public void renderWidget(GuiGraphics ctx, int mouseX, int mouseY, float delta) { + if (missing) { + // fall back to default rendering when textures are unavailable + super.renderWidget(ctx, mouseX, mouseY, delta); + return; + } ResourceLocation tex = isHovered() ? hoverTex : normalTex; ctx.blit(tex, getX(), getY(), 0, 0, width, height, width, height); } diff --git a/src/client/java/com/owlmaddie/ui/ChatScreen.java b/src/client/java/com/owlmaddie/ui/ChatScreen.java index 824bf68a..1f4ef394 100644 --- a/src/client/java/com/owlmaddie/ui/ChatScreen.java +++ b/src/client/java/com/owlmaddie/ui/ChatScreen.java @@ -158,4 +158,9 @@ protected EditBox getTextField() { protected Component getLabelText() { return this.labelText; } + + @Override + protected String getBackgroundTextureId() { + return "chat-background"; + } } diff --git a/src/client/java/com/owlmaddie/ui/ScreenHelper.java b/src/client/java/com/owlmaddie/ui/ScreenHelper.java index 4d5ee240..c7d6ef97 100644 --- a/src/client/java/com/owlmaddie/ui/ScreenHelper.java +++ b/src/client/java/com/owlmaddie/ui/ScreenHelper.java @@ -28,13 +28,19 @@ protected ScreenHelper(Component title) { /** Subclass must return its label Text here */ protected abstract Component getLabelText(); + /** Subclass must supply the UI texture id for its background */ + protected abstract String getBackgroundTextureId(); + + /** Hook for subclasses to render additional content above the background */ + protected void renderContent(GuiGraphics context, int mouseX, int mouseY, float delta) {} + @Override public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { // Full-screen vanilla gradient renderBackground(context); - // Chat-box texture - ResourceLocation bgTex = textures.GetUI("chat-background"); + // Background texture + ResourceLocation bgTex = textures.GetUI(getBackgroundTextureId()); if (bgTex != null) { context.blit( bgTex, @@ -45,6 +51,9 @@ public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { ); } + // Allow subclass to render content on top of background but before widgets + renderContent(context, mouseX, mouseY, delta); + // Render all children (textField, buttons) super.render(context, mouseX, mouseY, delta); diff --git a/src/client/java/com/owlmaddie/utils/EntityCreationHelper.java b/src/client/java/com/owlmaddie/utils/EntityCreationHelper.java new file mode 100644 index 00000000..4543a147 --- /dev/null +++ b/src/client/java/com/owlmaddie/utils/EntityCreationHelper.java @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.utils; + +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; + +/** + * Utility for creating client-side entity instances across versions. + */ +public class EntityCreationHelper { + public static Entity create(EntityType type) { + Level level = Minecraft.getInstance().level; + return level == null ? null : type.create(level); + } +} diff --git a/src/client/java/com/owlmaddie/utils/UseItemCallbackHelper.java b/src/client/java/com/owlmaddie/utils/UseItemCallbackHelper.java index 667e72a3..d46f0313 100644 --- a/src/client/java/com/owlmaddie/utils/UseItemCallbackHelper.java +++ b/src/client/java/com/owlmaddie/utils/UseItemCallbackHelper.java @@ -17,7 +17,7 @@ public final class UseItemCallbackHelper { private UseItemCallbackHelper() {} /** - * Fabric 1.20.x & 1.21.2 handler using TypedActionResult<ItemStack>. + * Fabric 1.20.x–1.21.1 handler using InteractionResultHolder. */ public static InteractionResultHolder handleUseItemAction( Player player, diff --git a/src/main/java/com/owlmaddie/ModInit.java b/src/main/java/com/owlmaddie/ModInit.java index 332664ec..ef8c4580 100644 --- a/src/main/java/com/owlmaddie/ModInit.java +++ b/src/main/java/com/owlmaddie/ModInit.java @@ -6,6 +6,7 @@ import com.owlmaddie.commands.CreatureChatCommands; import com.owlmaddie.inventory.ModMenus; import com.owlmaddie.network.ServerPackets; +import com.owlmaddie.items.ModItems; import net.fabricmc.api.ModInitializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,8 +29,9 @@ public void onInitialize() { // Register server commands CreatureChatCommands.register(); - // Register menus and events + // Register menus, items, and events ModMenus.register(); + ModItems.register(); ServerPackets.register(); LOGGER.info("CreatureChat MOD Initialized!"); diff --git a/src/main/java/com/owlmaddie/chat/ChatDataManager.java b/src/main/java/com/owlmaddie/chat/ChatDataManager.java index 3e4e94c3..e378aa4c 100644 --- a/src/main/java/com/owlmaddie/chat/ChatDataManager.java +++ b/src/main/java/com/owlmaddie/chat/ChatDataManager.java @@ -33,6 +33,7 @@ public class ChatDataManager { public static int DISPLAY_NUM_LINES = 3; public static int MAX_CHAR_IN_USER_MESSAGE = 512; public static int TICKS_TO_DISPLAY_USER_MESSAGE = 70; + public static int MAX_AUTOGENERATE_RESPONSES = 12; private static final Gson GSON = new Gson(); public enum ChatStatus { @@ -135,11 +136,22 @@ public void updateUUID(String oldUUID, String newUUID) { } // Save chat data to file - public String GetLightChatData(String playerId) { + public String GetLightChatData(String playerName) { try { - // Create "light" version of entire chat data HashMap + // Create light versions for living entities HashMap lightVersionMap = new HashMap<>(); - this.entityChatDataMap.forEach((name, entityChatData) -> lightVersionMap.put(name, entityChatData.toLightVersion(playerId))); + + this.entityChatDataMap.values().stream() + .filter(data -> data.death == null) + .forEach(data -> lightVersionMap.put(data.entityId, data.toLightVersion(playerName))); + + // Include up to 100 most recent dead entities + this.entityChatDataMap.values().stream() + .filter(data -> data.death != null) + .sorted((a, b) -> Long.compare(b.death, a.death)) + .limit(100) + .forEach(data -> lightVersionMap.put(data.entityId, data.toLightVersion(playerName))); + return GSON.toJson(lightVersionMap); } catch (Exception e) { // Handle exceptions diff --git a/src/main/java/com/owlmaddie/chat/EntityChatData.java b/src/main/java/com/owlmaddie/chat/EntityChatData.java index a20fb25b..4fa6fa9c 100644 --- a/src/main/java/com/owlmaddie/chat/EntityChatData.java +++ b/src/main/java/com/owlmaddie/chat/EntityChatData.java @@ -88,6 +88,9 @@ public class EntityChatData { public List previousMessages; public Long born; public Long death; + public Long lastMessage; + public String entityName; + public String entityType; public transient AutoMessageBucket autoBucket; @SerializedName("playerId") @@ -112,6 +115,9 @@ public EntityChatData(String entityId) { this.auto_generated = 0; this.previousMessages = new ArrayList<>(); this.born = System.currentTimeMillis();; + this.lastMessage = null; + this.entityName = ""; + this.entityType = null; this.autoBucket = null; // Old, unused migrated properties @@ -127,6 +133,10 @@ public void postDeserializeInitialization() { if (this.legacyPlayerId != null && !this.legacyPlayerId.isEmpty()) { this.migrateData(); } + if (this.previousMessages != null && !this.previousMessages.isEmpty()) { + ChatMessage last = this.previousMessages.get(this.previousMessages.size() - 1); + this.lastMessage = last.timestamp; + } } // Migrate old data into the new structure @@ -184,6 +194,10 @@ public EntityChatDataLight toLightVersion(String playerName) { } public String getCharacterProp(String propertyName) { + if (characterSheet == null || characterSheet.isEmpty()) { + return "N/A"; + } + // Create a case-insensitive regex pattern to match the property name and capture its value Pattern pattern = Pattern.compile("-?\\s*" + Pattern.quote(propertyName) + ":\\s*(.+)", Pattern.CASE_INSENSITIVE); Matcher matcher = pattern.matcher(characterSheet); @@ -776,8 +790,8 @@ public void addMessage(String message, ChatDataManager.ChatSender sender, Server // Add context-switching logic for USER messages only String playerName = player.getDisplayName().getString(); if (sender == ChatDataManager.ChatSender.USER && previousMessages.size() > 1) { - ChatMessage lastMessage = previousMessages.get(previousMessages.size() - 1); - if (lastMessage.name == null || !lastMessage.name.equals(playerName)) { // Null-safe check + ChatMessage lastMsg = previousMessages.get(previousMessages.size() - 1); + if (lastMsg.name == null || !lastMsg.name.equals(playerName)) { // Null-safe check boolean isReturningPlayer = previousMessages.stream().anyMatch(msg -> playerName.equals(msg.name)); // Avoid NPE here too String note = isReturningPlayer ? "" @@ -791,7 +805,9 @@ public void addMessage(String message, ChatDataManager.ChatSender sender, Server } // Add message to history - previousMessages.add(new ChatMessage(truncatedMessage, sender, playerName)); + ChatMessage chatMsg = new ChatMessage(truncatedMessage, sender, playerName); + previousMessages.add(chatMsg); + this.lastMessage = chatMsg.timestamp; // Log regular message addition LOGGER.info("Message added: status={}, sender={}, message={}, player={}, entity={}", diff --git a/src/main/java/com/owlmaddie/chat/EntityChatDataLight.java b/src/main/java/com/owlmaddie/chat/EntityChatDataLight.java index 5cf617d7..259cc9ee 100644 --- a/src/main/java/com/owlmaddie/chat/EntityChatDataLight.java +++ b/src/main/java/com/owlmaddie/chat/EntityChatDataLight.java @@ -18,6 +18,10 @@ public class EntityChatDataLight { public ChatDataManager.ChatStatus status; public ChatDataManager.ChatSender sender; public Map players; + public Long death; + public Long lastMessage; + public String entityName; + public String entityType; // Constructor to initialize the light version from the full version public EntityChatDataLight(EntityChatData fullData, String playerName) { @@ -26,6 +30,10 @@ public EntityChatDataLight(EntityChatData fullData, String playerName) { this.currentLineNumber = fullData.currentLineNumber; this.status = fullData.status; this.sender = fullData.sender; + this.death = fullData.death; + this.lastMessage = fullData.lastMessage; + this.entityName = fullData.entityName; + this.entityType = fullData.entityType; // Initialize the players map and add only the current player's data this.players = new HashMap<>(); diff --git a/src/main/java/com/owlmaddie/datagen/CreatureChatDataGenerator.java b/src/main/java/com/owlmaddie/datagen/CreatureChatDataGenerator.java index 719f085e..3e0332ad 100644 --- a/src/main/java/com/owlmaddie/datagen/CreatureChatDataGenerator.java +++ b/src/main/java/com/owlmaddie/datagen/CreatureChatDataGenerator.java @@ -5,7 +5,12 @@ import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator; +import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; +import net.minecraft.data.DataProvider; +/** + * Registers all data generation providers for the mod. + */ public class CreatureChatDataGenerator implements DataGeneratorEntrypoint { @Override public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) { @@ -15,3 +20,4 @@ public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) { pack.addProvider(CreatureChatLangProvider::new); } } + diff --git a/src/main/java/com/owlmaddie/i18n/CCText.java b/src/main/java/com/owlmaddie/i18n/CCText.java index 1b386777..526de3fd 100644 --- a/src/main/java/com/owlmaddie/i18n/CCText.java +++ b/src/main/java/com/owlmaddie/i18n/CCText.java @@ -12,9 +12,12 @@ public class CCText { // UI text public static final TR UI_CHAT_TITLE = new TR("ui.chat_title", "CreatureChat"); public static final TR UI_ENTER_MESSAGE = new TR("ui.enter_message", "Enter your message:"); + public static final TR UI_CREATURE_BOOK = new TR("ui.chat_book_item", "Creature Book"); + public static final List UI_TEXT = List.of( UI_CHAT_TITLE, - UI_ENTER_MESSAGE + UI_ENTER_MESSAGE, + UI_CREATURE_BOOK ); // Configuration command text diff --git a/src/main/java/com/owlmaddie/items/ModItems.java b/src/main/java/com/owlmaddie/items/ModItems.java new file mode 100644 index 00000000..d57e8b8e --- /dev/null +++ b/src/main/java/com/owlmaddie/items/ModItems.java @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.items; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; + +/** + * Registers items used by the mod. + */ +public final class ModItems { + private ModItems() {} + + /** The book that opens the creature log UI. */ + private static final ResourceLocation BOOK_ID = new ResourceLocation("creaturechat", "book"); + + public static final Item BOOK = Registry.register( + BuiltInRegistries.ITEM, + BOOK_ID, + new Item(new Item.Properties() + .stacksTo(1) + ) + ); + + /** Placeholder for future item registrations. */ + public static void register() {} +} + diff --git a/src/main/java/com/owlmaddie/network/ServerPackets.java b/src/main/java/com/owlmaddie/network/ServerPackets.java index f6c6ff87..699ab1cc 100644 --- a/src/main/java/com/owlmaddie/network/ServerPackets.java +++ b/src/main/java/com/owlmaddie/network/ServerPackets.java @@ -18,6 +18,7 @@ import com.owlmaddie.utils.Compression; import com.owlmaddie.utils.Randomizer; import com.owlmaddie.utils.ServerEntityFinder; +import com.google.gson.Gson; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; @@ -66,9 +67,11 @@ public class ServerPackets { public static final ResourceLocation PACKET_C2S_OPEN_CHAT = new ResourceLocation("creaturechat", "packet_c2s_open_chat"); public static final ResourceLocation PACKET_C2S_CLOSE_CHAT = new ResourceLocation("creaturechat", "packet_c2s_close_chat"); public static final ResourceLocation PACKET_C2S_SEND_CHAT = new ResourceLocation("creaturechat", "packet_c2s_send_chat"); + public static final ResourceLocation PACKET_C2S_REQUEST_ENTITY_DATA = new ResourceLocation("creaturechat", "packet_c2s_request_entity_data"); public static final ResourceLocation PACKET_S2C_ENTITY_MESSAGE = new ResourceLocation("creaturechat", "packet_s2c_entity_message"); public static final ResourceLocation PACKET_S2C_PLAYER_MESSAGE = new ResourceLocation("creaturechat", "packet_s2c_player_message"); public static final ResourceLocation PACKET_S2C_LOGIN = new ResourceLocation("creaturechat", "packet_s2c_login"); + public static final ResourceLocation PACKET_S2C_ENTITY_DATA = new ResourceLocation("creaturechat", "packet_s2c_entity_data"); public static final ResourceLocation PACKET_S2C_WHITELIST = new ResourceLocation("creaturechat", "packet_s2c_whitelist"); public static final ResourceLocation PACKET_S2C_PLAYER_STATUS = new ResourceLocation("creaturechat", "packet_s2c_player_status"); public static final ParticleType HEART_SMALL_PARTICLE = Particles.HEART_SMALL_PARTICLE; @@ -203,6 +206,41 @@ public static void register() { }); }); + PacketHelper.registerReceiver(PACKET_C2S_REQUEST_ENTITY_DATA, (server, player, buf) -> { + String entityId = buf.readUtf(); + server.execute(() -> { + EntityChatData source = ChatDataManager.getServerInstance().getOrCreateChatData(entityId); + Mob entity = (Mob) ServerEntityFinder.getEntityByUUID((ServerLevel) player.level(), UUID.fromString(entityId)); + if (entity != null) { + source.entityType = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); + source.entityName = entity.getDisplayName().getString(); + } + EntityChatData sendData = new EntityChatData(entityId); + sendData.currentMessage = source.currentMessage; + sendData.currentLineNumber = source.currentLineNumber; + sendData.status = source.status; + sendData.sender = source.sender; + sendData.characterSheet = source.characterSheet; + sendData.auto_generated = source.auto_generated; + sendData.previousMessages = source.previousMessages; + sendData.born = source.born; + sendData.death = source.death; + sendData.entityName = source.entityName; + sendData.entityType = source.entityType; + String pName = player.getDisplayName().getString(); + sendData.players.put(pName, source.getPlayerData(pName)); + Gson gson = new Gson(); + String json = gson.toJson(sendData); + byte[] compressed = Compression.compressString(json); + if (compressed == null) return; + FriendlyByteBuf buffer = BufferHelper.create(); + buffer.writeUtf(entityId); + buffer.writeByteArray(compressed); + PacketHelper.send(player, PACKET_S2C_ENTITY_DATA, buffer); + LOGGER.info("Server sending full data for entity {} to player {}", entityId, pName); + }); + }); + // Send lite chat data JSON to new player (to populate client data) // Data is sent in chunks, to prevent exceeding the 32767 limit per String. ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { @@ -267,7 +305,11 @@ public static void register() { String entityUUID = entity.getStringUUID(); if (entity.getRemovalReason() == Entity.RemovalReason.KILLED && ChatDataManager.getServerInstance().entityChatDataMap.containsKey(entityUUID)) { LOGGER.debug("Entity killed (" + entityUUID + "), updating death time stamp."); - ChatDataManager.getServerInstance().entityChatDataMap.get(entityUUID).death = System.currentTimeMillis(); + EntityChatData data = ChatDataManager.getServerInstance().entityChatDataMap.get(entityUUID); + data.death = System.currentTimeMillis(); + data.entityType = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); + data.entityName = entity.getDisplayName().getString(); + BroadcastEntityMessage(data); } }); } @@ -312,6 +354,9 @@ public static void generate_character(String userLanguage, EntityChatData chatDa TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 3.5F); EntityBehaviorManager.addGoal(entity, talkGoal, GoalPriority.TALK_PLAYER); + chatData.entityType = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); + chatData.entityName = entity.getDisplayName().getString(); + // Grab random adjective String randomAdjective = Randomizer.getRandomMessage(Randomizer.RandomType.ADJECTIVE); String randomClass = Randomizer.getRandomMessage(Randomizer.RandomType.CLASS); @@ -405,6 +450,9 @@ public static void generate_chat(String userLanguage, EntityChatData chatData, S TalkPlayerGoal talkGoal = new TalkPlayerGoal(player, entity, 3.5F); EntityBehaviorManager.addGoal(entity, talkGoal, GoalPriority.TALK_PLAYER); + chatData.entityType = BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()).toString(); + chatData.entityName = entity.getDisplayName().getString(); + // Add new message chatData.generateMessage(userLanguage, player, message, is_auto_message); } @@ -454,6 +502,8 @@ public static void BroadcastEntityMessage(EntityChatData chatData) { buffer.writeUtf(chatData.status.toString()); buffer.writeUtf(chatData.sender.toString()); writePlayerDataMap(buffer, chatData.players); + buffer.writeLong(chatData.lastMessage != null ? chatData.lastMessage : 0L); + buffer.writeLong(chatData.death != null ? chatData.death : 0L); // Send message to player PacketHelper.send(player, PACKET_S2C_ENTITY_MESSAGE, buffer); diff --git a/src/main/resources/assets/creaturechat/lang/de_de.json b/src/main/resources/assets/creaturechat/lang/de_de.json index 7adf6df7..5333f17e 100644 --- a/src/main/resources/assets/creaturechat/lang/de_de.json +++ b/src/main/resources/assets/creaturechat/lang/de_de.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Lösung: Serverfehler, später erneut versuchen", "creaturechat.solution.try_again": "Lösung: Versuche es später erneut", "creaturechat.solution.verify_url": "Lösung: Überprüfe die API-URL", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Gib deine Nachricht ein:" } diff --git a/src/main/resources/assets/creaturechat/lang/es_es.json b/src/main/resources/assets/creaturechat/lang/es_es.json index 6019e1ca..eed3843e 100644 --- a/src/main/resources/assets/creaturechat/lang/es_es.json +++ b/src/main/resources/assets/creaturechat/lang/es_es.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Solución: Error del servidor, intenta de nuevo más tarde", "creaturechat.solution.try_again": "Solución: Intenta de nuevo más tarde", "creaturechat.solution.verify_url": "Solución: Verifica la URL de la API", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Escribe tu mensaje:" } diff --git a/src/main/resources/assets/creaturechat/lang/es_mx.json b/src/main/resources/assets/creaturechat/lang/es_mx.json index 0b1abd91..07946e96 100644 --- a/src/main/resources/assets/creaturechat/lang/es_mx.json +++ b/src/main/resources/assets/creaturechat/lang/es_mx.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Solución: Error del servidor, intenta de nuevo más tarde", "creaturechat.solution.try_again": "Solución: Intenta de nuevo más tarde", "creaturechat.solution.verify_url": "Solución: Verifica la URL de la API", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Escribe tu mensaje:" } diff --git a/src/main/resources/assets/creaturechat/lang/fr_fr.json b/src/main/resources/assets/creaturechat/lang/fr_fr.json index 9b519d67..9f3e1761 100644 --- a/src/main/resources/assets/creaturechat/lang/fr_fr.json +++ b/src/main/resources/assets/creaturechat/lang/fr_fr.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Solution : Erreur serveur, réessayez plus tard", "creaturechat.solution.try_again": "Solution : Réessayez plus tard", "creaturechat.solution.verify_url": "Solution : Vérifiez l’URL de l’API", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Entrez votre message :" } diff --git a/src/main/resources/assets/creaturechat/lang/hi_in.json b/src/main/resources/assets/creaturechat/lang/hi_in.json index 0fbb411d..f47dc4a6 100644 --- a/src/main/resources/assets/creaturechat/lang/hi_in.json +++ b/src/main/resources/assets/creaturechat/lang/hi_in.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "समाधान: सर्वर त्रुटि, बाद में कोशिश करें", "creaturechat.solution.try_again": "समाधान: बाद में फिर कोशिश करें", "creaturechat.solution.verify_url": "समाधान: API URL जांचें", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "अपना संदेश लिखें:" } diff --git a/src/main/resources/assets/creaturechat/lang/id_id.json b/src/main/resources/assets/creaturechat/lang/id_id.json index 659c1be4..c7332bbf 100644 --- a/src/main/resources/assets/creaturechat/lang/id_id.json +++ b/src/main/resources/assets/creaturechat/lang/id_id.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Solusi: Error server, coba lagi nanti", "creaturechat.solution.try_again": "Solusi: Coba lagi nanti", "creaturechat.solution.verify_url": "Solusi: Periksa URL API", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Tulis pesanmu:" } diff --git a/src/main/resources/assets/creaturechat/lang/ja_jp.json b/src/main/resources/assets/creaturechat/lang/ja_jp.json index 2301cfc9..17c0a750 100644 --- a/src/main/resources/assets/creaturechat/lang/ja_jp.json +++ b/src/main/resources/assets/creaturechat/lang/ja_jp.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "解決策: サーバーエラー、後で再試行してください", "creaturechat.solution.try_again": "解決策: 後でもう一度試してください", "creaturechat.solution.verify_url": "解決策: APIのURLを確認してください", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "メッセージを入力してください:" } diff --git a/src/main/resources/assets/creaturechat/lang/ko_kr.json b/src/main/resources/assets/creaturechat/lang/ko_kr.json index 9d55f089..e1dae584 100644 --- a/src/main/resources/assets/creaturechat/lang/ko_kr.json +++ b/src/main/resources/assets/creaturechat/lang/ko_kr.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "해결 방법: 서버 오류, 나중에 다시 시도하세요", "creaturechat.solution.try_again": "해결 방법: 나중에 다시 시도하세요", "creaturechat.solution.verify_url": "해결 방법: API URL을 확인하세요", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "메시지를 입력하세요:" } diff --git a/src/main/resources/assets/creaturechat/lang/nl_nl.json b/src/main/resources/assets/creaturechat/lang/nl_nl.json index 4cba9d5a..54f88d55 100644 --- a/src/main/resources/assets/creaturechat/lang/nl_nl.json +++ b/src/main/resources/assets/creaturechat/lang/nl_nl.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Oplossing: Serverfout, probeer het later opnieuw", "creaturechat.solution.try_again": "Oplossing: Probeer het later opnieuw", "creaturechat.solution.verify_url": "Oplossing: Controleer de API-URL", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Voer je bericht in:" } diff --git a/src/main/resources/assets/creaturechat/lang/pl_pl.json b/src/main/resources/assets/creaturechat/lang/pl_pl.json index 0fdc500e..98b96f25 100644 --- a/src/main/resources/assets/creaturechat/lang/pl_pl.json +++ b/src/main/resources/assets/creaturechat/lang/pl_pl.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Rozwiązanie: Błąd serwera, spróbuj ponownie później", "creaturechat.solution.try_again": "Rozwiązanie: Spróbuj ponownie później", "creaturechat.solution.verify_url": "Rozwiązanie: Sprawdź adres URL API", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Wpisz swoją wiadomość:" } diff --git a/src/main/resources/assets/creaturechat/lang/pt_br.json b/src/main/resources/assets/creaturechat/lang/pt_br.json index faab1082..fada856c 100644 --- a/src/main/resources/assets/creaturechat/lang/pt_br.json +++ b/src/main/resources/assets/creaturechat/lang/pt_br.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Solução: Erro do servidor, tente novamente mais tarde", "creaturechat.solution.try_again": "Solução: Tente novamente mais tarde", "creaturechat.solution.verify_url": "Solução: Verifique a URL da API", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Digite sua mensagem:" } diff --git a/src/main/resources/assets/creaturechat/lang/pt_pt.json b/src/main/resources/assets/creaturechat/lang/pt_pt.json index d1bb1184..6fcfbd6a 100644 --- a/src/main/resources/assets/creaturechat/lang/pt_pt.json +++ b/src/main/resources/assets/creaturechat/lang/pt_pt.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Solução: Erro no servidor, tente novamente mais tarde", "creaturechat.solution.try_again": "Solução: Tente novamente mais tarde", "creaturechat.solution.verify_url": "Solução: Verifique a URL da API", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Escreve a tua mensagem:" } diff --git a/src/main/resources/assets/creaturechat/lang/ru_ru.json b/src/main/resources/assets/creaturechat/lang/ru_ru.json index be2ed2de..bde6abaa 100644 --- a/src/main/resources/assets/creaturechat/lang/ru_ru.json +++ b/src/main/resources/assets/creaturechat/lang/ru_ru.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Решение: Ошибка сервера, попробуйте позже", "creaturechat.solution.try_again": "Решение: Попробуйте позже", "creaturechat.solution.verify_url": "Решение: Проверьте URL API", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Введите сообщение:" } diff --git a/src/main/resources/assets/creaturechat/lang/sv_se.json b/src/main/resources/assets/creaturechat/lang/sv_se.json index 304b7257..d06af1d3 100644 --- a/src/main/resources/assets/creaturechat/lang/sv_se.json +++ b/src/main/resources/assets/creaturechat/lang/sv_se.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Lösning: Serverfel, försök igen senare", "creaturechat.solution.try_again": "Lösning: Försök igen senare", "creaturechat.solution.verify_url": "Lösning: Verifiera API-URL:en", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Skriv ditt meddelande:" } diff --git a/src/main/resources/assets/creaturechat/lang/tr_tr.json b/src/main/resources/assets/creaturechat/lang/tr_tr.json index 244c2b9a..df1500d7 100644 --- a/src/main/resources/assets/creaturechat/lang/tr_tr.json +++ b/src/main/resources/assets/creaturechat/lang/tr_tr.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Çözüm: Sunucu hatası, daha sonra tekrar dene", "creaturechat.solution.try_again": "Çözüm: Daha sonra tekrar dene", "creaturechat.solution.verify_url": "Çözüm: API URL\u0027sini kontrol et", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Mesajını yaz:" } diff --git a/src/main/resources/assets/creaturechat/lang/uk_ua.json b/src/main/resources/assets/creaturechat/lang/uk_ua.json index d37e00b9..c17b04a8 100644 --- a/src/main/resources/assets/creaturechat/lang/uk_ua.json +++ b/src/main/resources/assets/creaturechat/lang/uk_ua.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "Рішення: Помилка сервера, спробуй пізніше", "creaturechat.solution.try_again": "Рішення: Спробуй пізніше", "creaturechat.solution.verify_url": "Рішення: Перевір API-URL", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "Введи своє повідомлення:" } diff --git a/src/main/resources/assets/creaturechat/lang/zh_cn.json b/src/main/resources/assets/creaturechat/lang/zh_cn.json index cd9f9ee8..17c6147f 100644 --- a/src/main/resources/assets/creaturechat/lang/zh_cn.json +++ b/src/main/resources/assets/creaturechat/lang/zh_cn.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "解决方法: 服务器错误,请稍后再试", "creaturechat.solution.try_again": "解决方法: 稍后再试", "creaturechat.solution.verify_url": "解决方法: 验证 API URL", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "输入你的消息:" } diff --git a/src/main/resources/assets/creaturechat/lang/zh_tw.json b/src/main/resources/assets/creaturechat/lang/zh_tw.json index b013edc0..577f9f59 100644 --- a/src/main/resources/assets/creaturechat/lang/zh_tw.json +++ b/src/main/resources/assets/creaturechat/lang/zh_tw.json @@ -109,6 +109,7 @@ "creaturechat.solution.server_error": "解決方法: 伺服器錯誤,稍後再試", "creaturechat.solution.try_again": "解決方法: 稍後再試", "creaturechat.solution.verify_url": "解決方法: 驗證 API URL", + "creaturechat.ui.chat_book_item": "Creature Book", "creaturechat.ui.chat_title": "CreatureChat", "creaturechat.ui.enter_message": "輸入你的訊息:" } diff --git a/src/main/resources/assets/creaturechat/textures/item/book.png b/src/main/resources/assets/creaturechat/textures/item/book.png new file mode 100644 index 00000000..c3daf448 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/item/book.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/book.png b/src/main/resources/assets/creaturechat/textures/ui/book/book.png new file mode 100644 index 00000000..5fe84fb2 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/book.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/exit-hover.png b/src/main/resources/assets/creaturechat/textures/ui/book/exit-hover.png new file mode 100644 index 00000000..19d568cf Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/exit-hover.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/exit.png b/src/main/resources/assets/creaturechat/textures/ui/book/exit.png new file mode 100644 index 00000000..503aad7c Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/exit.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/index-hover.png b/src/main/resources/assets/creaturechat/textures/ui/book/index-hover.png new file mode 100644 index 00000000..1fece0eb Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/index-hover.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/index.png b/src/main/resources/assets/creaturechat/textures/ui/book/index.png new file mode 100644 index 00000000..fc9a310a Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/index.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/next-hover.png b/src/main/resources/assets/creaturechat/textures/ui/book/next-hover.png new file mode 100644 index 00000000..ab659458 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/next-hover.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/next.png b/src/main/resources/assets/creaturechat/textures/ui/book/next.png new file mode 100644 index 00000000..7e1ece5a Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/next.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/previous-hover.png b/src/main/resources/assets/creaturechat/textures/ui/book/previous-hover.png new file mode 100644 index 00000000..4a36b8b6 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/previous-hover.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/previous.png b/src/main/resources/assets/creaturechat/textures/ui/book/previous.png new file mode 100644 index 00000000..cdb5061e Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/previous.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/search-hover.png b/src/main/resources/assets/creaturechat/textures/ui/book/search-hover.png new file mode 100644 index 00000000..693a3c76 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/search-hover.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/search.png b/src/main/resources/assets/creaturechat/textures/ui/book/search.png new file mode 100644 index 00000000..76e09334 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/search.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/apple.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/apple.png new file mode 100644 index 00000000..3eaeb200 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/apple.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/blue flower.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/blue flower.png new file mode 100644 index 00000000..adb954e0 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/blue flower.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/blue grass.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/blue grass.png new file mode 100644 index 00000000..dc179c08 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/blue grass.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/cloud 1.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/cloud 1.png new file mode 100644 index 00000000..05fba5ac Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/cloud 1.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/clouds 1.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/clouds 1.png new file mode 100644 index 00000000..f7527065 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/clouds 1.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/cresent moon 2.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/cresent moon 2.png new file mode 100644 index 00000000..e3776b52 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/cresent moon 2.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/cresent moon.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/cresent moon.png new file mode 100644 index 00000000..588f2845 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/cresent moon.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/crop.py b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/crop.py new file mode 100644 index 00000000..27eb9aed --- /dev/null +++ b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/crop.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +import argparse, os, sys +from PIL import Image + +def iter_pngs(paths, recursive): + for p in paths: + if os.path.isdir(p): + if recursive: + for root, _, files in os.walk(p): + for name in files: + if name.lower().endswith(".png"): + yield os.path.join(root, name) + else: + for name in os.listdir(p): + full = os.path.join(p, name) + if os.path.isfile(full) and name.lower().endswith(".png"): + yield full + else: + if p.lower().endswith(".png"): + yield p + +def expand_box(bbox, w, h, pad): + if not bbox or pad <= 0: + return bbox + l, t, r, b = bbox + return (max(0, l - pad), max(0, t - pad), min(w, r + pad), min(h, b + pad)) + +def crop_png(path, out_path, threshold=0, pad=0, dry_run=False): + try: + with Image.open(path) as im: + # Ensure alpha channel is present (handles P with tRNS too) + if im.mode != "RGBA": + im = im.convert("RGBA") + w, h = im.size + alpha = im.getchannel("A") + # Build a binary mask of "visible" pixels by alpha threshold + if threshold > 0: + mask = alpha.point(lambda p: 255 if p > threshold else 0, mode="L") + else: + mask = alpha + bbox = mask.getbbox() # (left, upper, right, lower) or None + + if bbox is None: + print(f"SKIP (fully transparent): {path}") + return False + + bbox = expand_box(bbox, w, h, pad) + + if bbox == (0, 0, w, h): + print(f"OK (already tight): {path} [{w}x{h}]") + # Still write if out_path differs (e.g., copying to out dir) + if out_path and os.path.abspath(out_path) != os.path.abspath(path): + if not dry_run: + os.makedirs(os.path.dirname(out_path), exist_ok=True) + im.save(out_path, format="PNG", optimize=True) + return False + + cropped = im.crop(bbox) + cw, ch = cropped.size + msg = f"{path} [{w}x{h}] -> [{cw}x{ch}]" + if dry_run: + print("DRY RUN:", msg) + return True + + os.makedirs(os.path.dirname(out_path), exist_ok=True) + # Save atomically when overwriting + tmp = out_path + ".tmp" + cropped.save(tmp, format="PNG", optimize=True) + os.replace(tmp, out_path) + print("TRIM:", msg) + return True + except Exception as e: + print(f"ERROR: {path}: {e}", file=sys.stderr) + return False + +def main(): + ap = argparse.ArgumentParser( + description="Trim transparent margins from PNG files." + ) + ap.add_argument("paths", nargs="+", help="Files or directories to process") + ap.add_argument("-r", "--recursive", action="store_true", + help="Recurse into subdirectories") + ap.add_argument("-o", "--out-dir", default=None, + help="Write results under this directory instead of overwriting files") + ap.add_argument("-t", "--threshold", type=int, default=0, + help="Alpha threshold 0-255 (default 0: any nonzero alpha is kept)") + ap.add_argument("-p", "--pad", type=int, default=0, + help="Extra padding (pixels) to keep around content") + ap.add_argument("-n", "--dry-run", action="store_true", + help="Show what would change without writing files") + args = ap.parse_args() + + # If a single directory is provided and an out-dir is used, preserve structure + base_dir_for_rel = None + if args.out_dir and len(args.paths) == 1 and os.path.isdir(args.paths[0]): + base_dir_for_rel = os.path.abspath(args.paths[0]) + + changed = 0 + total = 0 + for src in iter_pngs(args.paths, args.recursive): + total += 1 + if args.out_dir: + if base_dir_for_rel and os.path.abspath(src).startswith(base_dir_for_rel + os.sep): + rel = os.path.relpath(src, start=base_dir_for_rel) + out_path = os.path.join(args.out_dir, rel) + else: + out_path = os.path.join(args.out_dir, os.path.basename(src)) + else: + out_path = src + + if crop_png(src, out_path, threshold=args.threshold, pad=args.pad, dry_run=args.dry_run): + changed += 1 + + print(f"\nDone. Examined {total} file(s). Trimmed {changed} file(s).") + +if __name__ == "__main__": + main() diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/crystal yellow.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/crystal yellow.png new file mode 100644 index 00000000..81277a9c Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/crystal yellow.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/dandelion.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/dandelion.png new file mode 100644 index 00000000..b7509aa7 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/dandelion.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/dark flower .png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/dark flower .png new file mode 100644 index 00000000..b890c048 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/dark flower .png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diam 2.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diam 2.png new file mode 100644 index 00000000..8abdef1b Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diam 2.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diamond pickaxe.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diamond pickaxe.png new file mode 100644 index 00000000..4b8110ca Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diamond pickaxe.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diamond sword.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diamond sword.png new file mode 100644 index 00000000..cc49e5c1 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diamond sword.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diamonds.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diamonds.png new file mode 100644 index 00000000..8f3ac68e Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/diamonds.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/eclipse.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/eclipse.png new file mode 100644 index 00000000..612eeda7 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/eclipse.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/enchant table.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/enchant table.png new file mode 100644 index 00000000..84050433 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/enchant table.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/feather .png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/feather .png new file mode 100644 index 00000000..16c8030e Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/feather .png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/grass 2.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/grass 2.png new file mode 100644 index 00000000..2019feff Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/grass 2.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/grass block.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/grass block.png new file mode 100644 index 00000000..77fbac65 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/grass block.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/grass flowers.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/grass flowers.png new file mode 100644 index 00000000..1b8739a5 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/grass flowers.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/ink blot.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/ink blot.png new file mode 100644 index 00000000..9422e980 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/ink blot.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/moon.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/moon.png new file mode 100644 index 00000000..3a9f9edc Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/moon.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/moon2.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/moon2.png new file mode 100644 index 00000000..09ad40e0 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/moon2.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/portal swirls.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/portal swirls.png new file mode 100644 index 00000000..26165844 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/portal swirls.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/potion1.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/potion1.png new file mode 100644 index 00000000..3ae4345e Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/potion1.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/potion2.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/potion2.png new file mode 100644 index 00000000..b5aea767 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/potion2.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/pumpkin .png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/pumpkin .png new file mode 100644 index 00000000..fd44b2cf Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/pumpkin .png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/purple blue flower.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/purple blue flower.png new file mode 100644 index 00000000..041ba020 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/purple blue flower.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/red flower.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/red flower.png new file mode 100644 index 00000000..82c7ef8d Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/red flower.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/ruined_portal.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/ruined_portal.png new file mode 100644 index 00000000..dd469780 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/ruined_portal.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/star sparkles.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/star sparkles.png new file mode 100644 index 00000000..133af7be Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/star sparkles.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sun 2.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sun 2.png new file mode 100644 index 00000000..a57f94a4 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sun 2.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sun 3.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sun 3.png new file mode 100644 index 00000000..2acef829 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sun 3.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sun.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sun.png new file mode 100644 index 00000000..f7be93b5 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sun.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sunflower.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sunflower.png new file mode 100644 index 00000000..9118a958 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/sunflower.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/tnt.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/tnt.png new file mode 100644 index 00000000..93e74298 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/tnt.png differ diff --git a/src/main/resources/assets/creaturechat/textures/ui/book/stickers/tnt1.png b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/tnt1.png new file mode 100644 index 00000000..522887b2 Binary files /dev/null and b/src/main/resources/assets/creaturechat/textures/ui/book/stickers/tnt1.png differ diff --git a/src/vs/v1_20_2/client/java/com/owlmaddie/ui/ScreenHelper.java b/src/vs/v1_20_2/client/java/com/owlmaddie/ui/ScreenHelper.java index af3df5d1..50650b50 100644 --- a/src/vs/v1_20_2/client/java/com/owlmaddie/ui/ScreenHelper.java +++ b/src/vs/v1_20_2/client/java/com/owlmaddie/ui/ScreenHelper.java @@ -30,13 +30,19 @@ protected ScreenHelper(Component title) { /** Subclass must return its label Text here */ protected abstract Component getLabelText(); + /** Subclass must supply the UI texture id for its background */ + protected abstract String getBackgroundTextureId(); + + /** Hook for subclasses to render additional content above the background */ + protected void renderContent(GuiGraphics context, int mouseX, int mouseY, float delta) {} + @Override public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { // Draw the vanilla gradient once renderBackground(context, mouseX, mouseY, delta); - // Draw the chat-box texture - ResourceLocation bgTex = textures.GetUI("chat-background"); + // Draw the background texture + ResourceLocation bgTex = textures.GetUI(getBackgroundTextureId()); if (bgTex != null) { context.blit( bgTex, @@ -47,6 +53,9 @@ public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { ); } + // Allow subclass content between background and widgets + renderContent(context, mouseX, mouseY, delta); + // Render children, but suppress their background call skipNextBackground = true; super.render(context, mouseX, mouseY, delta); diff --git a/src/vs/v1_20_2/main/java/com/owlmaddie/items/ModItems.java b/src/vs/v1_20_2/main/java/com/owlmaddie/items/ModItems.java new file mode 100644 index 00000000..fdbbabfb --- /dev/null +++ b/src/vs/v1_20_2/main/java/com/owlmaddie/items/ModItems.java @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.items; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; + +/** + * Registers items used by the mod. + */ +public final class ModItems { + private ModItems() {} + + /** The book that opens the creature log UI. */ + private static final ResourceLocation BOOK_ID = new ResourceLocation("creaturechat", "book"); + + public static final Item BOOK = Registry.register( + BuiltInRegistries.ITEM, + BOOK_ID, + new Item(new Item.Properties().stacksTo(1)) + ); + + /** Placeholder for future item registrations. */ + public static void register() {} +} diff --git a/src/vs/v1_20_5/main/java/com/owlmaddie/datagen/CreatureChatEnglishLangProvider.java b/src/vs/v1_20_5/main/java/com/owlmaddie/datagen/CreatureChatEnglishLangProvider.java new file mode 100644 index 00000000..4e81f83a --- /dev/null +++ b/src/vs/v1_20_5/main/java/com/owlmaddie/datagen/CreatureChatEnglishLangProvider.java @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.datagen; + +import com.owlmaddie.items.ModItems; +import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; +import net.fabricmc.fabric.api.datagen.v1.provider.FabricLanguageProvider; +import net.minecraft.core.HolderLookup; + +import java.util.concurrent.CompletableFuture; + +/** + * Generates the English language translations for the mod. + */ +public class CreatureChatEnglishLangProvider extends FabricLanguageProvider { + public CreatureChatEnglishLangProvider(FabricDataOutput dataOutput, CompletableFuture registries) { + super(dataOutput, "en_us", registries); + } + + @Override + public void generateTranslations(HolderLookup.Provider registries, TranslationBuilder builder) { + builder.add(ModItems.BOOK, "Creature Book"); + } +} diff --git a/src/vs/v1_21_0/client/java/com/owlmaddie/render/PoseHelper.java b/src/vs/v1_21_0/client/java/com/owlmaddie/render/PoseHelper.java new file mode 100644 index 00000000..934f9a94 --- /dev/null +++ b/src/vs/v1_21_0/client/java/com/owlmaddie/render/PoseHelper.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.render; + +import com.mojang.blaze3d.vertex.PoseStack; + +/** + * Cross-version wrapper for simple pose transformations. Modified for Minecraft 1.21. + */ +public final class PoseHelper { + private PoseHelper() {} + + public static void push(PoseStack stack) { + stack.pushPose(); + } + + public static void pop(PoseStack stack) { + stack.popPose(); + } + + public static void translate(PoseStack stack, float x, float y) { + stack.translate(x, y, 0); + } + + public static void scale(PoseStack stack, float sx, float sy) { + stack.scale(sx, sy, 1.0f); + } +} diff --git a/src/vs/v1_21_2/client/java/com/owlmaddie/ClientInit.java b/src/vs/v1_21_2/client/java/com/owlmaddie/ClientInit.java new file mode 100644 index 00000000..1279bded --- /dev/null +++ b/src/vs/v1_21_2/client/java/com/owlmaddie/ClientInit.java @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie; + +import com.owlmaddie.chat.ChatDataManager; +import com.owlmaddie.network.ClientPackets; +import com.owlmaddie.particle.CreatureParticleFactory; +import com.owlmaddie.particle.LeadParticleFactory; +import com.owlmaddie.particle.Particles; +import com.owlmaddie.ui.BubbleRenderer; +import com.owlmaddie.ui.ClickHandler; +import com.owlmaddie.ui.InventoryKeyHandler; +import com.owlmaddie.ui.PlayerMessageManager; +import com.owlmaddie.utils.TickDelta; +import com.owlmaddie.inventory.ModMenus; +import com.owlmaddie.inventory.MobInventoryScreen; +import com.owlmaddie.items.ModItems; +import com.owlmaddie.ui.BookScreen; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.particle.v1.ParticleFactoryRegistry; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.fabricmc.fabric.api.event.player.UseItemCallback; +import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.Minecraft; +import net.minecraft.world.InteractionResult; + +/** + * The {@code ClientInit} class initializes this mod in the client and defines all hooks into the + * render pipeline to draw chat bubbles, text, and entity icons. + */ +public class ClientInit implements ClientModInitializer { + private static long tickCounter = 0; + + @Override + public void onInitializeClient() { + // Register particle factories + ParticleFactoryRegistry.getInstance().register(Particles.HEART_SMALL_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.HEART_BIG_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.FIRE_SMALL_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.FIRE_BIG_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.ATTACK_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.FLEE_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.FOLLOW_FRIEND_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.FOLLOW_ENEMY_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.PROTECT_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.LEAD_FRIEND_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.LEAD_ENEMY_PARTICLE, CreatureParticleFactory::new); + ParticleFactoryRegistry.getInstance().register(Particles.LEAD_PARTICLE, LeadParticleFactory::new); + + ClientTickEvents.END_CLIENT_TICK.register(client -> { + tickCounter++; + PlayerMessageManager.tickUpdate(); + }); + + // Register events + ClickHandler.register(); + InventoryKeyHandler.register(); + ClientPackets.register(); + MenuScreens.register(ModMenus.MOB_INVENTORY, MobInventoryScreen::new); + + UseItemCallback.EVENT.register((player, world, hand) -> { + if (player.getItemInHand(hand).is(ModItems.BOOK)) { + if (world.isClientSide) { + Minecraft.getInstance().setScreen(new BookScreen()); + } + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + }); + + // Register an event callback to render text bubbles + WorldRenderEvents.BEFORE_DEBUG_RENDER.register(ctx -> { + float delta = TickDelta.get(ctx); + BubbleRenderer.drawTextAboveEntities(ctx, tickCounter, delta); + }); + + // Register an event callback for when the client disconnects from a server or changes worlds + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> { + // Clear or reset the ChatDataManager + ChatDataManager.getClientInstance().clearData(); + }); + } +} diff --git a/src/vs/v1_21_2/client/java/com/owlmaddie/render/RenderPipelineHelper.java b/src/vs/v1_21_2/client/java/com/owlmaddie/render/RenderPipelineHelper.java new file mode 100644 index 00000000..c33886ec --- /dev/null +++ b/src/vs/v1_21_2/client/java/com/owlmaddie/render/RenderPipelineHelper.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.render; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderType; +import net.minecraft.resources.ResourceLocation; + +/** + * Helper to blit GUI textures across Minecraft versions. + * Modified for Minecraft 1.21.2 where a RenderType supplier is required. + */ +public final class RenderPipelineHelper { + private RenderPipelineHelper() {} + + public static void blitGuiTexture( + GuiGraphics ctx, + ResourceLocation tex, + int x, int y, + int u, int v, + int width, int height, + int texWidth, int texHeight + ) { + ctx.blit(RenderType::guiTextured, tex, x, y, (float) u, (float) v, width, height, texWidth, texHeight); + } +} diff --git a/src/vs/v1_21_2/client/java/com/owlmaddie/ui/ScreenHelper.java b/src/vs/v1_21_2/client/java/com/owlmaddie/ui/ScreenHelper.java index 54552cc9..055db6e5 100644 --- a/src/vs/v1_21_2/client/java/com/owlmaddie/ui/ScreenHelper.java +++ b/src/vs/v1_21_2/client/java/com/owlmaddie/ui/ScreenHelper.java @@ -35,13 +35,19 @@ protected ScreenHelper(Component title) { */ protected abstract Component getLabelText(); + /** Subclass must supply the UI texture id for its background */ + protected abstract String getBackgroundTextureId(); + + /** Hook for subclasses to render additional content above the background */ + protected void renderContent(GuiGraphics context, int mouseX, int mouseY, float delta) {} + @Override public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { // Draw the vanilla gradient once super.renderBackground(context, mouseX, mouseY, delta); - // Draw the chat-box texture - ResourceLocation bgTex = textures.GetUI("chat-background"); + // Draw the background texture + ResourceLocation bgTex = textures.GetUI(getBackgroundTextureId()); if (bgTex != null) { RenderSystem.enableBlend(); RenderSystem.defaultBlendFunc(); @@ -56,6 +62,9 @@ public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { RenderSystem.disableBlend(); } + // Allow subclass content between background and widgets + renderContent(context, mouseX, mouseY, delta); + // Render children/widgets but suppress their background call skipNextBackground = true; super.render(context, mouseX, mouseY, delta); diff --git a/src/vs/v1_21_2/client/java/com/owlmaddie/utils/EntityCreationHelper.java b/src/vs/v1_21_2/client/java/com/owlmaddie/utils/EntityCreationHelper.java new file mode 100644 index 00000000..674bc2a6 --- /dev/null +++ b/src/vs/v1_21_2/client/java/com/owlmaddie/utils/EntityCreationHelper.java @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.utils; + +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.level.Level; + +/** + * Utility for creating client-side entity instances across versions. + * + *

In 1.21+, {@link EntityType#create(Level, EntitySpawnReason)} replaces the + * older overload. This implementation supplies the {@code TRIGGERED} spawn reason.

+ */ +public class EntityCreationHelper { + public static Entity create(EntityType type) { + Level level = Minecraft.getInstance().level; + return level == null ? null : type.create(level, EntitySpawnReason.TRIGGERED); + } +} diff --git a/src/vs/v1_21_2/client/java/com/owlmaddie/utils/UseItemCallbackHelper.java b/src/vs/v1_21_2/client/java/com/owlmaddie/utils/UseItemCallbackHelper.java index 5dd5b362..0d811b2f 100644 --- a/src/vs/v1_21_2/client/java/com/owlmaddie/utils/UseItemCallbackHelper.java +++ b/src/vs/v1_21_2/client/java/com/owlmaddie/utils/UseItemCallbackHelper.java @@ -4,25 +4,29 @@ package com.owlmaddie.utils; import com.owlmaddie.ui.ClickHandler; -import net.fabricmc.fabric.api.event.player.UseItemCallback; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; -public class UseItemCallbackHelper { +/** + * Helper for UseItemCallback, forwarding to the shared shouldCancelAction logic. + */ +public final class UseItemCallbackHelper { + private UseItemCallbackHelper() {} + /** - * Fabric 1.21.2+ handler using ActionResult + * Fabric 1.21.2+ handler returning InteractionResult. */ public static InteractionResult handleUseItemAction( Player player, Level world, InteractionHand hand ) { - // fully qualified call into your ClickHandler - if (ClickHandler.shouldCancelAction(world)) { - return InteractionResult.FAIL; - } - return InteractionResult.PASS; + return shouldCancelAction(world) ? InteractionResult.FAIL : InteractionResult.PASS; + } + + private static boolean shouldCancelAction(Level world) { + return ClickHandler.shouldCancelAction(world); } } diff --git a/src/vs/v1_21_2/main/java/com/owlmaddie/items/ModItems.java b/src/vs/v1_21_2/main/java/com/owlmaddie/items/ModItems.java new file mode 100644 index 00000000..23c8325b --- /dev/null +++ b/src/vs/v1_21_2/main/java/com/owlmaddie/items/ModItems.java @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.items; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; + +/** + * Registers items used by the mod. + */ +public final class ModItems { + private ModItems() {} + + /** The book that opens the creature log UI. */ + private static final ResourceLocation BOOK_ID = new ResourceLocation("creaturechat", "book"); + + public static final Item BOOK = Registry.register( + BuiltInRegistries.ITEM, + BOOK_ID, + new Item(new Item.Properties() + .stacksTo(1) + .useItemDescriptionPrefix() + .setId(ResourceKey.create(Registries.ITEM, BOOK_ID)) + ) + ); + + /** Placeholder for future item registrations. */ + public static void register() {} +} + diff --git a/src/vs/v1_21_4/client/java/com/owlmaddie/datagen/CreatureChatModelProvider.java b/src/vs/v1_21_4/client/java/com/owlmaddie/datagen/CreatureChatModelProvider.java new file mode 100644 index 00000000..6a9bc2b3 --- /dev/null +++ b/src/vs/v1_21_4/client/java/com/owlmaddie/datagen/CreatureChatModelProvider.java @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.datagen; + +import com.owlmaddie.items.ModItems; +import net.fabricmc.fabric.api.client.datagen.v1.provider.FabricModelProvider; +import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; +import net.minecraft.client.data.models.BlockModelGenerators; +import net.minecraft.client.data.models.ItemModelGenerators; +import net.minecraft.client.data.models.model.ModelTemplates; + +/** + * Generates item model JSON files using Minecraft's model data generators + * instead of manually constructing JSON. + */ +public class CreatureChatModelProvider extends FabricModelProvider { + public CreatureChatModelProvider(FabricDataOutput output) { + super(output); + } + + @Override + public void generateBlockStateModels(BlockModelGenerators blockStateModelGenerators) { + // No block models are generated for this mod. + } + + @Override + public void generateItemModels(ItemModelGenerators itemModelGenerators) { + // Generates the "minecraft:item/generated" style model for the book item. + itemModelGenerators.generateFlatItem(ModItems.BOOK, ModelTemplates.FLAT_ITEM); + } +} + diff --git a/src/vs/v1_21_5/client/java/com/owlmaddie/ui/ScreenHelper.java b/src/vs/v1_21_5/client/java/com/owlmaddie/ui/ScreenHelper.java index 0f66d39d..aea9198f 100644 --- a/src/vs/v1_21_5/client/java/com/owlmaddie/ui/ScreenHelper.java +++ b/src/vs/v1_21_5/client/java/com/owlmaddie/ui/ScreenHelper.java @@ -35,13 +35,19 @@ protected ScreenHelper(Component title) { */ protected abstract Component getLabelText(); + /** Subclass must supply the UI texture id for its background */ + protected abstract String getBackgroundTextureId(); + + /** Hook for subclasses to render additional content above the background */ + protected void renderContent(GuiGraphics context, int mouseX, int mouseY, float delta) {} + @Override public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { // Draw the vanilla gradient once super.renderBackground(context, mouseX, mouseY, delta); - // Draw the chat-box texture - ResourceLocation bgTex = textures.GetUI("chat-background"); + // Draw the background texture + ResourceLocation bgTex = textures.GetUI(getBackgroundTextureId()); if (bgTex != null) { BlendHelper.enableBlend(); BlendHelper.defaultBlendFunc(); @@ -56,6 +62,9 @@ public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { BlendHelper.disableBlend(); } + // Allow subclass content between background and widgets + renderContent(context, mouseX, mouseY, delta); + // Render children/widgets but suppress their background call skipNextBackground = true; super.render(context, mouseX, mouseY, delta); diff --git a/src/vs/v1_21_6/client/java/com/owlmaddie/render/PoseHelper.java b/src/vs/v1_21_6/client/java/com/owlmaddie/render/PoseHelper.java new file mode 100644 index 00000000..86d28329 --- /dev/null +++ b/src/vs/v1_21_6/client/java/com/owlmaddie/render/PoseHelper.java @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.render; + +import org.joml.Matrix3x2fStack; + +/** + * Cross-version wrapper for simple pose transformations. + * Modified for Minecraft 1.21.6+ which uses JOML matrix stacks. + */ +public final class PoseHelper { + private PoseHelper() {} + + public static void push(Matrix3x2fStack stack) { + stack.pushMatrix(); + } + + public static void pop(Matrix3x2fStack stack) { + stack.popMatrix(); + } + + public static void translate(Matrix3x2fStack stack, float x, float y) { + stack.translate(x, y); + } + + public static void scale(Matrix3x2fStack stack, float sx, float sy) { + stack.scale(sx, sy); + } +} diff --git a/src/vs/v1_21_6/client/java/com/owlmaddie/render/RenderPipelineHelper.java b/src/vs/v1_21_6/client/java/com/owlmaddie/render/RenderPipelineHelper.java new file mode 100644 index 00000000..ebfdc94a --- /dev/null +++ b/src/vs/v1_21_6/client/java/com/owlmaddie/render/RenderPipelineHelper.java @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: 2025 owlmaddie LLC +// SPDX-License-Identifier: GPL-3.0-or-later +// Assets CC-BY-NC-SA-4.0; CreatureChat™ trademark © owlmaddie LLC - unauthorized use prohibited +package com.owlmaddie.render; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.resources.ResourceLocation; + +/** + * Helper to blit GUI textures across Minecraft versions. + * Modified for Minecraft 1.21.6 where a pipeline must be specified. + */ +public final class RenderPipelineHelper { + private RenderPipelineHelper() {} + + public static void blitGuiTexture( + GuiGraphics ctx, + ResourceLocation tex, + int x, int y, + int u, int v, + int width, int height, + int texWidth, int texHeight + ) { + ctx.blit(RenderPipelines.GUI_TEXTURED, tex, x, y, (float)u, (float)v, width, height, texWidth, texHeight); + } +} diff --git a/src/vs/v1_21_6/client/java/com/owlmaddie/ui/ScreenHelper.java b/src/vs/v1_21_6/client/java/com/owlmaddie/ui/ScreenHelper.java index b27a5995..84d11169 100644 --- a/src/vs/v1_21_6/client/java/com/owlmaddie/ui/ScreenHelper.java +++ b/src/vs/v1_21_6/client/java/com/owlmaddie/ui/ScreenHelper.java @@ -35,13 +35,19 @@ protected ScreenHelper(Component title) { */ protected abstract Component getLabelText(); + /** Subclass must supply the UI texture id for its background */ + protected abstract String getBackgroundTextureId(); + + /** Hook for subclasses to render additional content above the background */ + protected void renderContent(GuiGraphics context, int mouseX, int mouseY, float delta) {} + @Override public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { // Draw the vanilla gradient once super.renderBackground(context, mouseX, mouseY, delta); - // Draw the chat-box texture - ResourceLocation bgTex = textures.GetUI("chat-background"); + // Draw the background texture + ResourceLocation bgTex = textures.GetUI(getBackgroundTextureId()); if (bgTex != null) { BlendHelper.enableBlend(); BlendHelper.defaultBlendFunc(); @@ -56,6 +62,9 @@ public void render(GuiGraphics context, int mouseX, int mouseY, float delta) { BlendHelper.disableBlend(); } + // Allow subclass content between background and widgets + renderContent(context, mouseX, mouseY, delta); + // Render children/widgets but suppress their background call skipNextBackground = true; super.render(context, mouseX, mouseY, delta);