Commit 0c75b901 by Jonathan Thomas

Merge branch 'bees-and-wither' into 'develop'

Bees, wither, wandering trade, and error handling

See merge request !28
parents a9c7b69f 1e55dcba
Pipeline #13345 passed with stages
in 2 minutes 21 seconds
......@@ -4,6 +4,21 @@ All notable changes to **CreatureChat** are documented in this file. The format
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Added
- Wither now drops a Nether Star at max friendship (for pacifists)
### Changed
- Broadcasting and receiving chat messages now ignores if the UUID is valid (to keep data synced)
- Improved error handling to prevent broken "..." pending chat status. (HTTP and message processing is more protected)
### Fixed
- Bees no longer forget their chat data when entering/leaving hives (writeNbt & readNbt modified)
- Vexes no longer take damage when chat data exists
- Wandering Trader no longer despawns if it has chat data
- Removed randomized error messages from chat history (so it doesn't break the chat history when an error is shown)
## [1.3.0] - 2025-01-14
### Added
......
......@@ -127,16 +127,9 @@ public class ClientPackets {
return;
}
// Update the chat data manager on the client-side
MobEntity entity = ClientEntityFinder.getEntityByUUID(client.world, entityId);
if (entity == null) {
LOGGER.warn("Entity with ID '{}' not found. Skipping message processing.", entityId);
return;
}
// Get entity chat data for current entity & player
ChatDataManager chatDataManager = ChatDataManager.getClientInstance();
EntityChatData chatData = chatDataManager.getOrCreateChatData(entity.getUuidAsString());
EntityChatData chatData = chatDataManager.getOrCreateChatData(entityId.toString());
// Add entity message
if (!message.isEmpty()) {
......@@ -148,7 +141,10 @@ public class ClientPackets {
chatData.players = players;
// Play sound with volume based on distance (from player or entity)
MobEntity entity = ClientEntityFinder.getEntityByUUID(client.world, entityId);
if (entity != null) {
playNearbyUISound(client, entity, 0.2f);
}
});
});
......
......@@ -196,6 +196,7 @@ public class ChatGPTRequest {
lastErrorMessage = cleanError;
} catch (Exception e) {
LOGGER.error("Failed to read error response", e);
lastErrorMessage = "Failed to read error response: " + e.getMessage();
}
return null;
} else {
......@@ -214,12 +215,16 @@ public class ChatGPTRequest {
if (chatGPTResponse != null && chatGPTResponse.choices != null && !chatGPTResponse.choices.isEmpty()) {
String content = chatGPTResponse.choices.get(0).message.content;
return content;
} else {
lastErrorMessage = "Failed to parse response from LLM";
return null;
}
}
} catch (IOException e) {
LOGGER.error("Failed to fetch message from ChatGPT", e);
} catch (Exception e) {
LOGGER.error("Failed to request message from LLM", e);
lastErrorMessage = "Failed to request message from LLM: " + e.getMessage();
return null;
}
return null; // If there was an error or no response, return null
});
}
}
......
......@@ -13,7 +13,9 @@ import com.owlmaddie.particle.ParticleEmitter;
import com.owlmaddie.utils.Randomizer;
import com.owlmaddie.utils.ServerEntityFinder;
import com.owlmaddie.utils.VillagerEntityAccessor;
import com.owlmaddie.utils.WitherEntityAccessor;
import net.minecraft.entity.ExperienceOrbEntity;
import net.minecraft.entity.boss.WitherEntity;
import net.minecraft.entity.boss.dragon.EnderDragonEntity;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.TameableEntity;
......@@ -271,6 +273,7 @@ public class EntityChatData {
// fetch HTTP response from ChatGPT
ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> {
try {
if (output_message != null) {
// Character Sheet: Remove system-character message from previous messages
previousMessages.clear();
......@@ -281,24 +284,30 @@ public class EntityChatData {
this.addMessage(shortGreeting, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
} else {
// No valid LLM response
throw new RuntimeException(ChatGPTRequest.lastErrorMessage);
}
} catch (Exception e) {
// Log the exception for debugging
LOGGER.error("Error processing LLM response", e);
// Error / No Chat Message (Failure)
String randomErrorMessage = Randomizer.getRandomMessage(Randomizer.RandomType.ERROR);
this.addMessage(randomErrorMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
// Determine error message to display
String errorMessage = "Help is available at discord.creaturechat.com";
if (!ChatGPTRequest.lastErrorMessage.isEmpty()) {
errorMessage = "Error: " + truncateString(ChatGPTRequest.lastErrorMessage, 55) + "\n" + errorMessage;
// Remove the error message from history to prevent it from affecting future ChatGPT requests
if (!previousMessages.isEmpty()) {
previousMessages.remove(previousMessages.size() - 1);
}
// Send clickable error message
ServerPackets.SendClickableError(player,
errorMessage, "http://discord.creaturechat.com");
// Clear history (if no character sheet was generated)
if (characterSheet.isEmpty()) {
previousMessages.clear();
String errorMessage = "Error: ";
if (e.getMessage() != null && !e.getMessage().isEmpty()) {
errorMessage += truncateString(e.getMessage(), 55) + "\n";
}
errorMessage += "Help is available at discord.creaturechat.com";
ServerPackets.SendClickableError(player, errorMessage, "http://discord.creaturechat.com");
}
});
}
......@@ -334,10 +343,11 @@ public class EntityChatData {
// fetch HTTP response from ChatGPT
ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> {
try {
if (output_message != null) {
// Chat Message: Parse message for behaviors
ParsedMessage result = MessageParser.parseMessage(output_message.replace("\n", " "));
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), UUID.fromString(entityId));
MobEntity entity = (MobEntity) ServerEntityFinder.getEntityByUUID(player.getServerWorld(), UUID.fromString(entityId));
// Determine entity's default speed
// Some Entities (i.e. Axolotl) set this incorrectly... so adjusting in the SpeedControls class
......@@ -429,6 +439,13 @@ public class EntityChatData {
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
if (entity instanceof WitherEntity && new_friendship == 3) {
// Best friend a Nether and get a NETHER_STAR
WitherEntity wither = (WitherEntity) entity;
((WitherEntityAccessor) wither).callDropEquipment(entity.getWorld().getDamageSources().generic(), 1, true);
entity.getWorld().playSound(entity, entity.getBlockPos(), SoundEvents.ENTITY_WITHER_DEATH, SoundCategory.PLAYERS, 0.3F, 1.0F);
}
if (entity instanceof EnderDragonEntity && new_friendship == 3) {
// Trigger end of game (friendship always wins!)
EnderDragonEntity dragon = (EnderDragonEntity) entity;
......@@ -452,8 +469,8 @@ public class EntityChatData {
// Loop to spawn XP orbs
for (int j = 1; j <= 11; j++) {
float xpFraction = (j == 11) ? 0.2F : 0.08F;
int xpAmount = MathHelper.floor((float)baseXP * xpFraction);
ExperienceOrbEntity.spawn((ServerWorld)entity.getWorld(), entity.getPos(), xpAmount);
int xpAmount = MathHelper.floor((float) baseXP * xpFraction);
ExperienceOrbEntity.spawn((ServerWorld) entity.getWorld(), entity.getPos(), xpAmount);
}
}
......@@ -539,24 +556,29 @@ public class EntityChatData {
new ChatMessage(result.getOriginalMessage(), ChatDataManager.ChatSender.ASSISTANT, player.getDisplayName().getString()));
} else {
// No valid LLM response
throw new RuntimeException(ChatGPTRequest.lastErrorMessage);
}
} catch (Exception e) {
// Log the exception for debugging
LOGGER.error("Error processing LLM response", e);
// Error / No Chat Message (Failure)
String randomErrorMessage = Randomizer.getRandomMessage(Randomizer.RandomType.ERROR);
this.addMessage(randomErrorMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
// Determine error message to display
String errorMessage = "Help is available at discord.creaturechat.com";
if (!ChatGPTRequest.lastErrorMessage.isEmpty()) {
errorMessage = "Error: " + truncateString(ChatGPTRequest.lastErrorMessage, 55) + "\n" + errorMessage;
// Remove the error message from history to prevent it from affecting future ChatGPT requests
if (!previousMessages.isEmpty()) {
previousMessages.remove(previousMessages.size() - 1);
}
// Send clickable error message
ServerPackets.SendClickableError(player,
errorMessage, "http://discord.creaturechat.com");
// Clear history (if no character sheet was generated)
if (characterSheet.isEmpty()) {
previousMessages.clear();
String errorMessage = "Error: ";
if (e.getMessage() != null && !e.getMessage().isEmpty()) {
errorMessage += truncateString(e.getMessage(), 55) + "\n";
}
errorMessage += "Help is available at discord.creaturechat.com";
ServerPackets.SendClickableError(player, errorMessage, "http://discord.creaturechat.com");
}
});
}
......
package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import net.minecraft.entity.Entity;
import net.minecraft.nbt.NbtCompound;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.UUID;
@Mixin(Entity.class)
public abstract class MixinEntityChatData {
@Shadow
public abstract UUID getUuid();
/**
* When writing NBT data, if the entity has chat data then store its UUID under "CCUUID".
*/
@Inject(method = "writeNbt", at = @At("TAIL"))
private void writeChatData(NbtCompound nbt, CallbackInfoReturnable<NbtCompound> cir) {
UUID currentUUID = this.getUuid();
// Retrieve or create the chat data for this entity.
EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(currentUUID.toString());
// If the entity actually has chat data (for example, if its character sheet is non-empty), add CCUUID.
if (!chatData.characterSheet.isEmpty()) {
// Note: cir.getReturnValue() returns the NBT compound the method is about to return.
cir.getReturnValue().putUuid("CCUUID", currentUUID);
}
}
/**
* When reading NBT data, if there is a "CCUUID" entry and it does not match the entity’s current UUID,
* update our chat data key to reflect the change.
*/
@Inject(method = "readNbt", at = @At("TAIL"))
private void readChatData(NbtCompound nbt, CallbackInfo ci) {
UUID currentUUID = this.getUuid();
if (nbt.contains("CCUUID")) {
UUID originalUUID = nbt.getUuid("CCUUID");
if (!originalUUID.equals(currentUUID)) {
ChatDataManager.getServerInstance().updateUUID(originalUUID.toString(), currentUUID.toString());
}
}
}
}
package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import net.minecraft.entity.mob.VexEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Mixin to modify Vex behavior by setting `alive = false` if chat data exists.
*/
@Mixin(VexEntity.class)
public abstract class MixinVexEntity {
@Shadow
private boolean alive;
@Inject(method = "tick", at = @At("HEAD"))
private void disableVexIfChatData(CallbackInfo ci) {
VexEntity vex = (VexEntity) (Object) this;
// Get chat data for this Vex
EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(vex.getUuidAsString());
if (this.alive && !chatData.characterSheet.isEmpty()) {
this.alive = false; // Prevents the Vex from ticking and taking damage
}
}
}
package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import net.minecraft.entity.passive.WanderingTraderEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Prevents WanderingTraderEntity from despawning if it has chat data or a character sheet.
*/
@Mixin(WanderingTraderEntity.class)
public abstract class MixinWanderingTrader {
private static final Logger LOGGER = LoggerFactory.getLogger("creaturechat");
@Inject(method = "tickDespawnDelay", at = @At("HEAD"), cancellable = true)
private void preventTraderDespawn(CallbackInfo ci) {
WanderingTraderEntity trader = (WanderingTraderEntity) (Object) this;
// Get chat data for this trader
EntityChatData chatData = ChatDataManager.getServerInstance().getOrCreateChatData(trader.getUuidAsString());
// If the character sheet is not empty, cancel the function to prevent despawning
if (!chatData.characterSheet.isEmpty()) {
ci.cancel();
}
}
}
package com.owlmaddie.mixin;
import com.owlmaddie.utils.WitherEntityAccessor;
import net.minecraft.entity.boss.WitherEntity;
import net.minecraft.entity.damage.DamageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
/**
* Mixin to expose the protected dropEquipment method from WitherEntity.
*/
@Mixin(WitherEntity.class)
public abstract class MixinWitherEntity implements WitherEntityAccessor {
@Shadow
protected abstract void dropEquipment(DamageSource source, int lootingMultiplier, boolean allowDrops);
@Override
public void callDropEquipment(DamageSource source, int lootingMultiplier, boolean allowDrops) {
dropEquipment(source, lootingMultiplier, allowDrops);
}
}
......@@ -351,10 +351,10 @@ public class ServerPackets {
chatData.currentLineNumber, chatData.sender);
for (ServerWorld world : serverInstance.getWorlds()) {
// Find Entity by UUID and update custom name
UUID entityId = UUID.fromString(chatData.entityId);
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(world, entityId);
if (entity != null) {
// Set custom name (if null)
String characterName = chatData.getCharacterProp("name");
if (!characterName.isEmpty() && !characterName.equals("N/A") && entity.getCustomName() == null) {
LOGGER.debug("Setting entity name to " + characterName + " for " + chatData.entityId);
......@@ -362,6 +362,7 @@ public class ServerPackets {
entity.setCustomNameVisible(true);
entity.setPersistent();
}
}
// Make auto-generated message appear as a pending icon (attack, show/give, arrival)
if (chatData.sender == ChatDataManager.ChatSender.USER && chatData.auto_generated > 0) {
......@@ -384,7 +385,6 @@ public class ServerPackets {
break;
}
}
}
// Send new message to all connected players
public static void BroadcastPlayerMessage(EntityChatData chatData, ServerPlayerEntity sender) {
......
package com.owlmaddie.utils;
import net.minecraft.entity.damage.DamageSource;
/**
* Accessor interface for WitherEntity to allow calling dropEquipment externally.
*/
public interface WitherEntityAccessor {
void callDropEquipment(DamageSource source, int lootingMultiplier, boolean allowDrops);
}
......@@ -7,6 +7,10 @@
"MixinMobEntityAccessor",
"MixinLivingEntity",
"MixinBucketable",
"MixinEntityChatData",
"MixinWitherEntity",
"MixinVexEntity",
"MixinWanderingTrader",
"MixinVillagerEntity",
"MixinOnChat"
],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment