Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/owlmaddie/inventory/MobInventoryMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Item, Integer> map) {
List<String> parts = new ArrayList<>();
for (Map.Entry<Item, Integer> entry : map.entrySet()) {
Expand Down
39 changes: 24 additions & 15 deletions src/main/java/com/owlmaddie/mixin/MixinMobEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
91 changes: 91 additions & 0 deletions src/main/java/com/owlmaddie/utils/BookHelper.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
39 changes: 24 additions & 15 deletions src/vs/v1_20_5/main/java/com/owlmaddie/mixin/MixinMobEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
100 changes: 100 additions & 0 deletions src/vs/v1_20_5/main/java/com/owlmaddie/utils/BookHelper.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
39 changes: 24 additions & 15 deletions src/vs/v1_21_0/main/java/com/owlmaddie/mixin/MixinMobEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading