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 ...@@ -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 [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). [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 ## [1.3.0] - 2025-01-14
### Added ### Added
......
...@@ -127,16 +127,9 @@ public class ClientPackets { ...@@ -127,16 +127,9 @@ public class ClientPackets {
return; 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 // Get entity chat data for current entity & player
ChatDataManager chatDataManager = ChatDataManager.getClientInstance(); ChatDataManager chatDataManager = ChatDataManager.getClientInstance();
EntityChatData chatData = chatDataManager.getOrCreateChatData(entity.getUuidAsString()); EntityChatData chatData = chatDataManager.getOrCreateChatData(entityId.toString());
// Add entity message // Add entity message
if (!message.isEmpty()) { if (!message.isEmpty()) {
...@@ -148,7 +141,10 @@ public class ClientPackets { ...@@ -148,7 +141,10 @@ public class ClientPackets {
chatData.players = players; chatData.players = players;
// Play sound with volume based on distance (from player or entity) // Play sound with volume based on distance (from player or entity)
playNearbyUISound(client, entity, 0.2f); MobEntity entity = ClientEntityFinder.getEntityByUUID(client.world, entityId);
if (entity != null) {
playNearbyUISound(client, entity, 0.2f);
}
}); });
}); });
......
...@@ -196,6 +196,7 @@ public class ChatGPTRequest { ...@@ -196,6 +196,7 @@ public class ChatGPTRequest {
lastErrorMessage = cleanError; lastErrorMessage = cleanError;
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Failed to read error response", e); LOGGER.error("Failed to read error response", e);
lastErrorMessage = "Failed to read error response: " + e.getMessage();
} }
return null; return null;
} else { } else {
...@@ -214,12 +215,16 @@ public class ChatGPTRequest { ...@@ -214,12 +215,16 @@ public class ChatGPTRequest {
if (chatGPTResponse != null && chatGPTResponse.choices != null && !chatGPTResponse.choices.isEmpty()) { if (chatGPTResponse != null && chatGPTResponse.choices != null && !chatGPTResponse.choices.isEmpty()) {
String content = chatGPTResponse.choices.get(0).message.content; String content = chatGPTResponse.choices.get(0).message.content;
return content; return content;
} else {
lastErrorMessage = "Failed to parse response from LLM";
return null;
} }
} }
} catch (IOException e) { } catch (Exception e) {
LOGGER.error("Failed to fetch message from ChatGPT", 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
}); });
} }
} }
......
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 { ...@@ -351,10 +351,10 @@ public class ServerPackets {
chatData.currentLineNumber, chatData.sender); chatData.currentLineNumber, chatData.sender);
for (ServerWorld world : serverInstance.getWorlds()) { for (ServerWorld world : serverInstance.getWorlds()) {
// Find Entity by UUID and update custom name
UUID entityId = UUID.fromString(chatData.entityId); UUID entityId = UUID.fromString(chatData.entityId);
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(world, entityId); MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(world, entityId);
if (entity != null) { if (entity != null) {
// Set custom name (if null)
String characterName = chatData.getCharacterProp("name"); String characterName = chatData.getCharacterProp("name");
if (!characterName.isEmpty() && !characterName.equals("N/A") && entity.getCustomName() == null) { if (!characterName.isEmpty() && !characterName.equals("N/A") && entity.getCustomName() == null) {
LOGGER.debug("Setting entity name to " + characterName + " for " + chatData.entityId); LOGGER.debug("Setting entity name to " + characterName + " for " + chatData.entityId);
...@@ -362,27 +362,27 @@ public class ServerPackets { ...@@ -362,27 +362,27 @@ public class ServerPackets {
entity.setCustomNameVisible(true); entity.setCustomNameVisible(true);
entity.setPersistent(); entity.setPersistent();
} }
}
// Make auto-generated message appear as a pending icon (attack, show/give, arrival) // Make auto-generated message appear as a pending icon (attack, show/give, arrival)
if (chatData.sender == ChatDataManager.ChatSender.USER && chatData.auto_generated > 0) { if (chatData.sender == ChatDataManager.ChatSender.USER && chatData.auto_generated > 0) {
chatData.status = ChatDataManager.ChatStatus.PENDING; chatData.status = ChatDataManager.ChatStatus.PENDING;
} }
// Iterate over all players and send the packet // Iterate over all players and send the packet
for (ServerPlayerEntity player : serverInstance.getPlayerManager().getPlayerList()) { for (ServerPlayerEntity player : serverInstance.getPlayerManager().getPlayerList()) {
PacketByteBuf buffer = new PacketByteBuf(Unpooled.buffer()); PacketByteBuf buffer = new PacketByteBuf(Unpooled.buffer());
buffer.writeString(chatData.entityId); buffer.writeString(chatData.entityId);
buffer.writeString(chatData.currentMessage); buffer.writeString(chatData.currentMessage);
buffer.writeInt(chatData.currentLineNumber); buffer.writeInt(chatData.currentLineNumber);
buffer.writeString(chatData.status.toString()); buffer.writeString(chatData.status.toString());
buffer.writeString(chatData.sender.toString()); buffer.writeString(chatData.sender.toString());
writePlayerDataMap(buffer, chatData.players); writePlayerDataMap(buffer, chatData.players);
// Send message to player // Send message to player
ServerPlayNetworking.send(player, PACKET_S2C_ENTITY_MESSAGE, buffer); ServerPlayNetworking.send(player, PACKET_S2C_ENTITY_MESSAGE, buffer);
}
break;
} }
break;
} }
} }
......
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 @@ ...@@ -7,6 +7,10 @@
"MixinMobEntityAccessor", "MixinMobEntityAccessor",
"MixinLivingEntity", "MixinLivingEntity",
"MixinBucketable", "MixinBucketable",
"MixinEntityChatData",
"MixinWitherEntity",
"MixinVexEntity",
"MixinWanderingTrader",
"MixinVillagerEntity", "MixinVillagerEntity",
"MixinOnChat" "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