diff --git a/CHANGELOG.md b/CHANGELOG.md index b94a5582..359d5eb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ All notable changes to **CreatureChat™** are documented in this file. The form ## Unreleased +### Added +- Mobs can now read books! Right-click to share the book or add it to their inventory. + ### Added - Document SPDX header and changelog requirements in AGENTS.md for contributors diff --git a/src/main/java/com/owlmaddie/inventory/MobInventoryMenu.java b/src/main/java/com/owlmaddie/inventory/MobInventoryMenu.java index 94881a38..768e01ba 100644 --- a/src/main/java/com/owlmaddie/inventory/MobInventoryMenu.java +++ b/src/main/java/com/owlmaddie/inventory/MobInventoryMenu.java @@ -233,6 +233,17 @@ public void removed(Player player) { AdvancementHelper.potatoWar(serverPlayer); } } + if (added.size() == 1 && removed.isEmpty() && disarmedToInventory.isEmpty() && disarmedTaken.isEmpty() && !swapped && !handChanged) { + Item item = added.keySet().iterator().next(); + if (com.owlmaddie.utils.BookHelper.isBook(item)) { + ItemStack stack = findBookStack(item); + String contents = com.owlmaddie.utils.BookHelper.summarizeBook(stack); + String msg = "<" + player.getDisplayName().getString() + + " shared a book with you> - Book contents: \"" + contents + "\""; + ServerPackets.generate_chat("N/A", chatData, serverPlayer, mob, msg, true); + return; + } + } StringBuilder msg = new StringBuilder("<" + player.getDisplayName().getString()); boolean first = true; if (swapped) { @@ -340,6 +351,16 @@ private void collectDisarmed(ItemStack initial, ItemStack finalMain, ItemStack f }); } + private ItemStack findBookStack(Item item) { + for (int i = 0; i < mobInvSize; i++) { + ItemStack stack = inventory.getItem(i); + if (!stack.isEmpty() && stack.getItem() == item) { + return stack; + } + } + return ItemStack.EMPTY; + } + private static String joinCounts(Map map) { List parts = new ArrayList<>(); for (Map.Entry entry : map.entrySet()) { diff --git a/src/main/java/com/owlmaddie/mixin/MixinMobEntity.java b/src/main/java/com/owlmaddie/mixin/MixinMobEntity.java index f7cb6066..d57a0d34 100644 --- a/src/main/java/com/owlmaddie/mixin/MixinMobEntity.java +++ b/src/main/java/com/owlmaddie/mixin/MixinMobEntity.java @@ -208,21 +208,30 @@ private void onItemGiven(Player player, InteractionHand hand, CallbackInfoReturn // Player has item in hand if (!itemStack.isEmpty()) { ServerPlayer serverPlayer = (ServerPlayer) player; - String itemName = itemStack.getItem().getName(itemStack).getString(); - int itemCount = itemStack.getCount(); - - // Decide verb - String action_verb = " shows "; - if (cir.getReturnValue().consumesAction()) { - action_verb = " gives "; - } - - // Prepare a message about the interaction - String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + - action_verb + "you " + itemCount + " " + itemName + ">"; - - if (!entityData.characterSheet.isEmpty()) { - ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + if (com.owlmaddie.utils.BookHelper.isBook(itemStack.getItem())) { + String contents = com.owlmaddie.utils.BookHelper.summarizeBook(itemStack); + String msg = "<" + serverPlayer.getDisplayName().getString() + + " shared a book with you> - Book contents: \"" + contents + "\""; + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, msg, true); + } + } else { + String itemName = itemStack.getItem().getName(itemStack).getString(); + int itemCount = itemStack.getCount(); + + // Decide verb + String action_verb = " shows "; + if (cir.getReturnValue().consumesAction()) { + action_verb = " gives "; + } + + // Prepare a message about the interaction + String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + + action_verb + "you " + itemCount + " " + itemName + ">"; + + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + } } } else if (itemStack.isEmpty() && playerData.friendship == 3) { diff --git a/src/main/java/com/owlmaddie/utils/BookHelper.java b/src/main/java/com/owlmaddie/utils/BookHelper.java new file mode 100644 index 00000000..f601e378 --- /dev/null +++ b/src/main/java/com/owlmaddie/utils/BookHelper.java @@ -0,0 +1,91 @@ +// 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 com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +public class BookHelper { + public static boolean isBook(Item item) { + return item == Items.WRITTEN_BOOK || item == Items.WRITABLE_BOOK || item == Items.BOOK; + } + + private static String extractText(String json) { + try { + JsonElement element = JsonParser.parseString(json); + return collectText(element); + } catch (Exception e) { + return json; + } + } + + private static String collectText(JsonElement element) { + if (element == null) { + return ""; + } + if (element.isJsonPrimitive()) { + return element.getAsString(); + } + StringBuilder sb = new StringBuilder(); + if (element.isJsonObject()) { + JsonObject obj = element.getAsJsonObject(); + if (obj.has("text")) { + sb.append(obj.get("text").getAsString()); + } + if (obj.has("extra")) { + JsonArray arr = obj.getAsJsonArray("extra"); + for (JsonElement e : arr) { + sb.append(collectText(e)); + } + } + } else if (element.isJsonArray()) { + for (JsonElement e : element.getAsJsonArray()) { + sb.append(collectText(e)); + } + } + return sb.toString(); + } + + private static String readBook(ItemStack stack) { + if (!stack.hasTag()) { + return ""; + } + CompoundTag tag = stack.getTag(); + ListTag pages = tag.getList("pages", 8); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pages.size(); i++) { + String page = pages.getString(i); + if (stack.is(Items.WRITTEN_BOOK)) { + sb.append(extractText(page)); + } else { + sb.append(page); + } + } + return sb.toString(); + } + + public static String summarizeBook(ItemStack stack) { + if (!isBook(stack.getItem())) { + return "N/A"; + } + String text = readBook(stack); + if (text.isEmpty()) { + return "N/A"; + } + if (text.length() <= 2048) { + return text; + } + int skip = text.length() - 2048; + String start = text.substring(0, 1024); + String end = text.substring(text.length() - 1024); + return start + "... skipped " + skip + " chars ..." + end; + } +} diff --git a/src/vs/v1_20_5/main/java/com/owlmaddie/mixin/MixinMobEntity.java b/src/vs/v1_20_5/main/java/com/owlmaddie/mixin/MixinMobEntity.java index 1fb089f4..96b4b820 100644 --- a/src/vs/v1_20_5/main/java/com/owlmaddie/mixin/MixinMobEntity.java +++ b/src/vs/v1_20_5/main/java/com/owlmaddie/mixin/MixinMobEntity.java @@ -210,21 +210,30 @@ private void onItemGiven(Player player, InteractionHand hand, CallbackInfoReturn // Player has item in hand if (!itemStack.isEmpty()) { ServerPlayer serverPlayer = (ServerPlayer) player; - String itemName = itemStack.getItem().getName(itemStack).getString(); - int itemCount = itemStack.getCount(); - - // Decide verb - String action_verb = " shows "; - if (cir.getReturnValue().consumesAction()) { - action_verb = " gives "; - } - - // Prepare a message about the interaction - String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + - action_verb + "you " + itemCount + " " + itemName + ">"; - - if (!entityData.characterSheet.isEmpty()) { - ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + if (com.owlmaddie.utils.BookHelper.isBook(itemStack.getItem())) { + String contents = com.owlmaddie.utils.BookHelper.summarizeBook(itemStack); + String msg = "<" + serverPlayer.getDisplayName().getString() + + " shared a book with you> - Book contents: \"" + contents + "\""; + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, msg, true); + } + } else { + String itemName = itemStack.getItem().getName(itemStack).getString(); + int itemCount = itemStack.getCount(); + + // Decide verb + String action_verb = " shows "; + if (cir.getReturnValue().consumesAction()) { + action_verb = " gives "; + } + + // Prepare a message about the interaction + String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + + action_verb + "you " + itemCount + " " + itemName + ">"; + + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + } } } else if (itemStack.isEmpty() && playerData.friendship == 3) { diff --git a/src/vs/v1_20_5/main/java/com/owlmaddie/utils/BookHelper.java b/src/vs/v1_20_5/main/java/com/owlmaddie/utils/BookHelper.java new file mode 100644 index 00000000..da053f53 --- /dev/null +++ b/src/vs/v1_20_5/main/java/com/owlmaddie/utils/BookHelper.java @@ -0,0 +1,100 @@ +// 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 com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.WrittenBookContent; +import net.minecraft.world.item.component.WritableBookContent; + +public class BookHelper { + public static boolean isBook(Item item) { + return item == Items.WRITTEN_BOOK || item == Items.WRITABLE_BOOK || item == Items.BOOK; + } + + private static String extractText(String json) { + try { + JsonElement element = JsonParser.parseString(json); + return collectText(element); + } catch (Exception e) { + return json; + } + } + + private static String collectText(JsonElement element) { + if (element == null) { + return ""; + } + if (element.isJsonPrimitive()) { + return element.getAsString(); + } + StringBuilder sb = new StringBuilder(); + if (element.isJsonObject()) { + JsonObject obj = element.getAsJsonObject(); + if (obj.has("text")) { + sb.append(obj.get("text").getAsString()); + } + if (obj.has("extra")) { + JsonArray arr = obj.getAsJsonArray("extra"); + for (JsonElement e : arr) { + sb.append(collectText(e)); + } + } + } else if (element.isJsonArray()) { + for (JsonElement e : element.getAsJsonArray()) { + sb.append(collectText(e)); + } + } + return sb.toString(); + } + + private static String readBook(ItemStack stack) { + StringBuilder sb = new StringBuilder(); + if (stack.is(Items.WRITTEN_BOOK)) { + WrittenBookContent content = stack.get(DataComponents.WRITTEN_BOOK_CONTENT); + if (content == null) { + return ""; + } + for (var page : content.pages()) { + Component c = page.raw(); + sb.append(c.getString()); + } + } else { + WritableBookContent content = stack.get(DataComponents.WRITABLE_BOOK_CONTENT); + if (content == null) { + return ""; + } + for (var page : content.pages()) { + String raw = page.raw(); + // Writable books store raw JSON strings; attempt to extract plain text + sb.append(extractText(raw)); + } + } + return sb.toString(); + } + + public static String summarizeBook(ItemStack stack) { + if (!isBook(stack.getItem())) { + return "N/A"; + } + String text = readBook(stack); + if (text.isEmpty()) { + return "N/A"; + } + if (text.length() <= 2048) { + return text; + } + int skip = text.length() - 2048; + String start = text.substring(0, 1024); + String end = text.substring(text.length() - 1024); + return start + "... skipped " + skip + " chars ..." + end; + } +} diff --git a/src/vs/v1_21_0/main/java/com/owlmaddie/mixin/MixinMobEntity.java b/src/vs/v1_21_0/main/java/com/owlmaddie/mixin/MixinMobEntity.java index 474866f8..f79028ac 100644 --- a/src/vs/v1_21_0/main/java/com/owlmaddie/mixin/MixinMobEntity.java +++ b/src/vs/v1_21_0/main/java/com/owlmaddie/mixin/MixinMobEntity.java @@ -213,21 +213,30 @@ private void onItemGiven(Player player, InteractionHand hand, CallbackInfoReturn // Player has item in hand if (!itemStack.isEmpty()) { ServerPlayer serverPlayer = (ServerPlayer) player; - String itemName = itemStack.getItem().getName(itemStack).getString(); - int itemCount = itemStack.getCount(); - - // Decide verb - String action_verb = " shows "; - if (cir.getReturnValue().consumesAction()) { - action_verb = " gives "; - } - - // Prepare a message about the interaction - String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + - action_verb + "you " + itemCount + " " + itemName + ">"; - - if (!entityData.characterSheet.isEmpty()) { - ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + if (com.owlmaddie.utils.BookHelper.isBook(itemStack.getItem())) { + String contents = com.owlmaddie.utils.BookHelper.summarizeBook(itemStack); + String msg = "<" + serverPlayer.getDisplayName().getString() + + " shared a book with you> - Book contents: \"" + contents + "\""; + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, msg, true); + } + } else { + String itemName = itemStack.getItem().getName(itemStack).getString(); + int itemCount = itemStack.getCount(); + + // Decide verb + String action_verb = " shows "; + if (cir.getReturnValue().consumesAction()) { + action_verb = " gives "; + } + + // Prepare a message about the interaction + String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + + action_verb + "you " + itemCount + " " + itemName + ">"; + + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + } } } else if (itemStack.isEmpty() && playerData.friendship == 3) { diff --git a/src/vs/v1_21_5/main/java/com/owlmaddie/mixin/MixinMobEntity.java b/src/vs/v1_21_5/main/java/com/owlmaddie/mixin/MixinMobEntity.java index bcdfc034..4b809732 100644 --- a/src/vs/v1_21_5/main/java/com/owlmaddie/mixin/MixinMobEntity.java +++ b/src/vs/v1_21_5/main/java/com/owlmaddie/mixin/MixinMobEntity.java @@ -214,21 +214,30 @@ private void onItemGiven(Player player, InteractionHand hand, CallbackInfoReturn // Player has item in hand if (!itemStack.isEmpty()) { ServerPlayer serverPlayer = (ServerPlayer) player; - String itemName = itemStack.getItem().getName(itemStack).getString(); - int itemCount = itemStack.getCount(); - - // Decide verb - String action_verb = " shows "; - if (cir.getReturnValue().consumesAction()) { - action_verb = " gives "; - } + if (com.owlmaddie.utils.BookHelper.isBook(itemStack.getItem())) { + String contents = com.owlmaddie.utils.BookHelper.summarizeBook(itemStack); + String msg = "<" + serverPlayer.getDisplayName().getString() + + " shared a book with you> - Book contents: \"" + contents + "\""; + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, msg, true); + } + } else { + String itemName = itemStack.getItem().getName(itemStack).getString(); + int itemCount = itemStack.getCount(); + + // Decide verb + String action_verb = " shows "; + if (cir.getReturnValue().consumesAction()) { + action_verb = " gives "; + } - // Prepare a message about the interaction - String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + - action_verb + "you " + itemCount + " " + itemName + ">"; + // Prepare a message about the interaction + String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + + action_verb + "you " + itemCount + " " + itemName + ">"; - if (!entityData.characterSheet.isEmpty()) { - ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + } } } else if (itemStack.isEmpty() && playerData.friendship == 3) { diff --git a/src/vs/v1_21_6/main/java/com/owlmaddie/mixin/MixinMobEntity.java b/src/vs/v1_21_6/main/java/com/owlmaddie/mixin/MixinMobEntity.java index d5f0c1b6..b652f57c 100644 --- a/src/vs/v1_21_6/main/java/com/owlmaddie/mixin/MixinMobEntity.java +++ b/src/vs/v1_21_6/main/java/com/owlmaddie/mixin/MixinMobEntity.java @@ -194,21 +194,30 @@ private void onItemGiven(Player player, InteractionHand hand, CallbackInfoReturn // Player has item in hand if (!itemStack.isEmpty()) { ServerPlayer serverPlayer = (ServerPlayer) player; - String itemName = itemStack.getItem().getName(itemStack).getString(); - int itemCount = itemStack.getCount(); - - // Decide verb - String action_verb = " shows "; - if (cir.getReturnValue().consumesAction()) { - action_verb = " gives "; - } + if (com.owlmaddie.utils.BookHelper.isBook(itemStack.getItem())) { + String contents = com.owlmaddie.utils.BookHelper.summarizeBook(itemStack); + String msg = "<" + serverPlayer.getDisplayName().getString() + + " shared a book with you> - Book contents: \"" + contents + "\""; + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, msg, true); + } + } else { + String itemName = itemStack.getItem().getName(itemStack).getString(); + int itemCount = itemStack.getCount(); + + // Decide verb + String action_verb = " shows "; + if (cir.getReturnValue().consumesAction()) { + action_verb = " gives "; + } - // Prepare a message about the interaction - String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + - action_verb + "you " + itemCount + " " + itemName + ">"; + // Prepare a message about the interaction + String giveItemMessage = "<" + serverPlayer.getDisplayName().getString() + + action_verb + "you " + itemCount + " " + itemName + ">"; - if (!entityData.characterSheet.isEmpty()) { - ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + if (!entityData.characterSheet.isEmpty()) { + ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true); + } } } else if (itemStack.isEmpty() && playerData.friendship == 3) {