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)
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 {
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,34 +273,41 @@ public class EntityChatData {
// fetch HTTP response from ChatGPT
ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> {
if (output_message != null) {
// Character Sheet: Remove system-character message from previous messages
previousMessages.clear();
try {
if (output_message != null) {
// Character Sheet: Remove system-character message from previous messages
previousMessages.clear();
// Add NEW CHARACTER sheet & greeting
this.characterSheet = output_message;
String shortGreeting = Optional.ofNullable(getCharacterProp("short greeting")).filter(s -> !s.isEmpty()).orElse(Randomizer.getRandomMessage(Randomizer.RandomType.NO_RESPONSE)).replace("\n", " ");
this.addMessage(shortGreeting, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
} else {
// No valid LLM response
throw new RuntimeException(ChatGPTRequest.lastErrorMessage);
}
// Add NEW CHARACTER sheet & greeting
this.characterSheet = output_message;
String shortGreeting = Optional.ofNullable(getCharacterProp("short greeting")).filter(s -> !s.isEmpty()).orElse(Randomizer.getRandomMessage(Randomizer.RandomType.NO_RESPONSE)).replace("\n", " ");
this.addMessage(shortGreeting, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
} catch (Exception e) {
// Log the exception for debugging
LOGGER.error("Error processing LLM response", e);
} else {
// 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,229 +343,242 @@ public class EntityChatData {
// fetch HTTP response from ChatGPT
ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> {
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));
// Determine entity's default speed
// Some Entities (i.e. Axolotl) set this incorrectly... so adjusting in the SpeedControls class
float entitySpeed = SpeedControls.getMaxSpeed(entity);
float entitySpeedMedium = MathHelper.clamp(entitySpeed * 1.15F, 0.5f, 1.15f);
float entitySpeedFast = MathHelper.clamp(entitySpeed * 1.3F, 0.5f, 1.3f);
// Apply behaviors (if any)
for (Behavior behavior : result.getBehaviors()) {
LOGGER.info("Behavior: " + behavior.getName() + (behavior.getArgument() != null ?
", Argument: " + behavior.getArgument() : ""));
// Apply behaviors to entity
if (behavior.getName().equals("FOLLOW")) {
FollowPlayerGoal followGoal = new FollowPlayerGoal(player, entity, entitySpeedMedium);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, followGoal, GoalPriority.FOLLOW_PLAYER);
if (playerData.friendship >= 0) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FOLLOW_FRIEND_PARTICLE, 0.5, 1);
} else {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FOLLOW_ENEMY_PARTICLE, 0.5, 1);
}
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));
// Determine entity's default speed
// Some Entities (i.e. Axolotl) set this incorrectly... so adjusting in the SpeedControls class
float entitySpeed = SpeedControls.getMaxSpeed(entity);
float entitySpeedMedium = MathHelper.clamp(entitySpeed * 1.15F, 0.5f, 1.15f);
float entitySpeedFast = MathHelper.clamp(entitySpeed * 1.3F, 0.5f, 1.3f);
// Apply behaviors (if any)
for (Behavior behavior : result.getBehaviors()) {
LOGGER.info("Behavior: " + behavior.getName() + (behavior.getArgument() != null ?
", Argument: " + behavior.getArgument() : ""));
// Apply behaviors to entity
if (behavior.getName().equals("FOLLOW")) {
FollowPlayerGoal followGoal = new FollowPlayerGoal(player, entity, entitySpeedMedium);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, followGoal, GoalPriority.FOLLOW_PLAYER);
if (playerData.friendship >= 0) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FOLLOW_FRIEND_PARTICLE, 0.5, 1);
} else {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FOLLOW_ENEMY_PARTICLE, 0.5, 1);
}
} else if (behavior.getName().equals("UNFOLLOW")) {
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
} else if (behavior.getName().equals("FLEE")) {
float fleeDistance = 40F;
FleePlayerGoal fleeGoal = new FleePlayerGoal(player, entity, entitySpeedFast, fleeDistance);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, fleeGoal, GoalPriority.FLEE_PLAYER);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FLEE_PARTICLE, 0.5, 1);
} else if (behavior.getName().equals("UNFLEE")) {
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
} else if (behavior.getName().equals("ATTACK")) {
AttackPlayerGoal attackGoal = new AttackPlayerGoal(player, entity, entitySpeedFast);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, attackGoal, GoalPriority.ATTACK_PLAYER);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FLEE_PARTICLE, 0.5, 1);
} else if (behavior.getName().equals("PROTECT")) {
if (playerData.friendship <= 0) {
// force friendship to prevent entity from attacking player when protecting
playerData.friendship = 1;
}
ProtectPlayerGoal protectGoal = new ProtectPlayerGoal(player, entity, 1.0);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, protectGoal, GoalPriority.PROTECT_PLAYER);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, PROTECT_PARTICLE, 0.5, 1);
} else if (behavior.getName().equals("UNPROTECT")) {
EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class);
} else if (behavior.getName().equals("LEAD")) {
LeadPlayerGoal leadGoal = new LeadPlayerGoal(player, entity, entitySpeedMedium);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, leadGoal, GoalPriority.LEAD_PLAYER);
if (playerData.friendship >= 0) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, LEAD_FRIEND_PARTICLE, 0.5, 1);
} else {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, LEAD_ENEMY_PARTICLE, 0.5, 1);
}
} else if (behavior.getName().equals("UNLEAD")) {
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
} else if (behavior.getName().equals("UNFOLLOW")) {
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
} else if (behavior.getName().equals("FLEE")) {
float fleeDistance = 40F;
FleePlayerGoal fleeGoal = new FleePlayerGoal(player, entity, entitySpeedFast, fleeDistance);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, fleeGoal, GoalPriority.FLEE_PLAYER);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FLEE_PARTICLE, 0.5, 1);
} else if (behavior.getName().equals("FRIENDSHIP")) {
int new_friendship = Math.max(-3, Math.min(3, behavior.getArgument()));
} else if (behavior.getName().equals("UNFLEE")) {
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
// Does friendship improve?
if (new_friendship > playerData.friendship) {
// Stop any attack/flee if friendship improves
} else if (behavior.getName().equals("ATTACK")) {
AttackPlayerGoal attackGoal = new AttackPlayerGoal(player, entity, entitySpeedFast);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, attackGoal, GoalPriority.ATTACK_PLAYER);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FLEE_PARTICLE, 0.5, 1);
} else if (behavior.getName().equals("PROTECT")) {
if (playerData.friendship <= 0) {
// force friendship to prevent entity from attacking player when protecting
playerData.friendship = 1;
}
ProtectPlayerGoal protectGoal = new ProtectPlayerGoal(player, entity, 1.0);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, protectGoal, GoalPriority.PROTECT_PLAYER);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, PROTECT_PARTICLE, 0.5, 1);
if (entity instanceof EnderDragonEntity && new_friendship == 3) {
// Trigger end of game (friendship always wins!)
EnderDragonEntity dragon = (EnderDragonEntity) entity;
} else if (behavior.getName().equals("UNPROTECT")) {
EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class);
// Emit particles & sound
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_BIG_PARTICLE, 3, 200);
entity.getWorld().playSound(entity, entity.getBlockPos(), SoundEvents.ENTITY_ENDER_DRAGON_DEATH, SoundCategory.PLAYERS, 0.3F, 1.0F);
entity.getWorld().playSound(entity, entity.getBlockPos(), SoundEvents.UI_TOAST_CHALLENGE_COMPLETE, SoundCategory.PLAYERS, 0.5F, 1.0F);
} else if (behavior.getName().equals("LEAD")) {
LeadPlayerGoal leadGoal = new LeadPlayerGoal(player, entity, entitySpeedMedium);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, leadGoal, GoalPriority.LEAD_PLAYER);
if (playerData.friendship >= 0) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, LEAD_FRIEND_PARTICLE, 0.5, 1);
} else {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, LEAD_ENEMY_PARTICLE, 0.5, 1);
}
} else if (behavior.getName().equals("UNLEAD")) {
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
} else if (behavior.getName().equals("FRIENDSHIP")) {
int new_friendship = Math.max(-3, Math.min(3, behavior.getArgument()));
// Does friendship improve?
if (new_friendship > playerData.friendship) {
// Stop any attack/flee if friendship improves
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);
}
// Check if the game rule for mob loot is enabled
boolean doMobLoot = entity.getWorld().getGameRules().getBoolean(GameRules.DO_MOB_LOOT);
if (entity instanceof EnderDragonEntity && new_friendship == 3) {
// Trigger end of game (friendship always wins!)
EnderDragonEntity dragon = (EnderDragonEntity) entity;
// If this is the first time the dragon is 'befriended', adjust the XP
int baseXP = 500;
if (dragon.getFight() != null && !dragon.getFight().hasPreviouslyKilled()) {
baseXP = 12000;
}
// Emit particles & sound
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_BIG_PARTICLE, 3, 200);
entity.getWorld().playSound(entity, entity.getBlockPos(), SoundEvents.ENTITY_ENDER_DRAGON_DEATH, SoundCategory.PLAYERS, 0.3F, 1.0F);
entity.getWorld().playSound(entity, entity.getBlockPos(), SoundEvents.UI_TOAST_CHALLENGE_COMPLETE, SoundCategory.PLAYERS, 0.5F, 1.0F);
// If the world is a server world and mob loot is enabled, spawn XP orbs
if (entity.getWorld() instanceof ServerWorld && doMobLoot) {
// 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);
// Check if the game rule for mob loot is enabled
boolean doMobLoot = entity.getWorld().getGameRules().getBoolean(GameRules.DO_MOB_LOOT);
// If this is the first time the dragon is 'befriended', adjust the XP
int baseXP = 500;
if (dragon.getFight() != null && !dragon.getFight().hasPreviouslyKilled()) {
baseXP = 12000;
}
}
// Mark fight as over
dragon.getFight().dragonKilled(dragon);
}
}
// If the world is a server world and mob loot is enabled, spawn XP orbs
if (entity.getWorld() instanceof ServerWorld && doMobLoot) {
// 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);
}
}
// Merchant deals (if friendship changes with a Villager
if (entity instanceof VillagerEntity && playerData.friendship != new_friendship) {
VillagerEntityAccessor villager = (VillagerEntityAccessor) entity;
switch (new_friendship) {
case 3:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MAJOR_POSITIVE, 20);
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_POSITIVE, 25);
break;
case 2:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_POSITIVE, 25);
break;
case 1:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_POSITIVE, 10);
break;
case -1:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_NEGATIVE, 10);
break;
case -2:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_NEGATIVE, 25);
break;
case -3:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MAJOR_NEGATIVE, 20);
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_NEGATIVE, 25);
break;
// Mark fight as over
dragon.getFight().dragonKilled(dragon);
}
}
}
// Tame best friends and un-tame worst enemies
if (entity instanceof TameableEntity && playerData.friendship != new_friendship) {
TameableEntity tamableEntity = (TameableEntity) entity;
if (new_friendship == 3 && !tamableEntity.isTamed()) {
tamableEntity.setOwner(player);
} else if (new_friendship == -3 && tamableEntity.isTamed()) {
tamableEntity.setTamed(false);
tamableEntity.setOwnerUuid(null);
// Merchant deals (if friendship changes with a Villager
if (entity instanceof VillagerEntity && playerData.friendship != new_friendship) {
VillagerEntityAccessor villager = (VillagerEntityAccessor) entity;
switch (new_friendship) {
case 3:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MAJOR_POSITIVE, 20);
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_POSITIVE, 25);
break;
case 2:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_POSITIVE, 25);
break;
case 1:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_POSITIVE, 10);
break;
case -1:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_NEGATIVE, 10);
break;
case -2:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_NEGATIVE, 25);
break;
case -3:
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MAJOR_NEGATIVE, 20);
villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_NEGATIVE, 25);
break;
}
}
}
// Emit friendship particles
if (playerData.friendship != new_friendship) {
int friendDiff = new_friendship - playerData.friendship;
if (friendDiff > 0) {
// Heart particles
if (new_friendship == 3) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_BIG_PARTICLE, 0.5, 10);
} else {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_SMALL_PARTICLE, 0.1, 1);
// Tame best friends and un-tame worst enemies
if (entity instanceof TameableEntity && playerData.friendship != new_friendship) {
TameableEntity tamableEntity = (TameableEntity) entity;
if (new_friendship == 3 && !tamableEntity.isTamed()) {
tamableEntity.setOwner(player);
} else if (new_friendship == -3 && tamableEntity.isTamed()) {
tamableEntity.setTamed(false);
tamableEntity.setOwnerUuid(null);
}
}
// Emit friendship particles
if (playerData.friendship != new_friendship) {
int friendDiff = new_friendship - playerData.friendship;
if (friendDiff > 0) {
// Heart particles
if (new_friendship == 3) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_BIG_PARTICLE, 0.5, 10);
} else {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_SMALL_PARTICLE, 0.1, 1);
}
} else if (friendDiff < 0) {
// Fire particles
if (new_friendship == -3) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FIRE_BIG_PARTICLE, 0.5, 10);
} else {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FIRE_SMALL_PARTICLE, 0.1, 1);
} else if (friendDiff < 0) {
// Fire particles
if (new_friendship == -3) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FIRE_BIG_PARTICLE, 0.5, 10);
} else {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FIRE_SMALL_PARTICLE, 0.1, 1);
}
}
}
playerData.friendship = new_friendship;
}
}
playerData.friendship = new_friendship;
// Get cleaned message (i.e. no <BEHAVIOR> strings)
String cleanedMessage = result.getCleanedMessage();
if (cleanedMessage.isEmpty()) {
cleanedMessage = Randomizer.getRandomMessage(Randomizer.RandomType.NO_RESPONSE);
}
}
// Get cleaned message (i.e. no <BEHAVIOR> strings)
String cleanedMessage = result.getCleanedMessage();
if (cleanedMessage.isEmpty()) {
cleanedMessage = Randomizer.getRandomMessage(Randomizer.RandomType.NO_RESPONSE);
}
// Add ASSISTANT message to history
this.addMessage(cleanedMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
// Add ASSISTANT message to history
this.addMessage(cleanedMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
// Update the last entry in previousMessages to use the original message
this.previousMessages.set(this.previousMessages.size() - 1,
new ChatMessage(result.getOriginalMessage(), ChatDataManager.ChatSender.ASSISTANT, player.getDisplayName().getString()));
// Update the last entry in previousMessages to use the original message
this.previousMessages.set(this.previousMessages.size() - 1,
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);
} else {
// 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,27 +362,27 @@ 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) {
chatData.status = ChatDataManager.ChatStatus.PENDING;
}
// Make auto-generated message appear as a pending icon (attack, show/give, arrival)
if (chatData.sender == ChatDataManager.ChatSender.USER && chatData.auto_generated > 0) {
chatData.status = ChatDataManager.ChatStatus.PENDING;
}
// Iterate over all players and send the packet
for (ServerPlayerEntity player : serverInstance.getPlayerManager().getPlayerList()) {
PacketByteBuf buffer = new PacketByteBuf(Unpooled.buffer());
buffer.writeString(chatData.entityId);
buffer.writeString(chatData.currentMessage);
buffer.writeInt(chatData.currentLineNumber);
buffer.writeString(chatData.status.toString());
buffer.writeString(chatData.sender.toString());
writePlayerDataMap(buffer, chatData.players);
// Send message to player
ServerPlayNetworking.send(player, PACKET_S2C_ENTITY_MESSAGE, buffer);
}
break;
// Iterate over all players and send the packet
for (ServerPlayerEntity player : serverInstance.getPlayerManager().getPlayerList()) {
PacketByteBuf buffer = new PacketByteBuf(Unpooled.buffer());
buffer.writeString(chatData.entityId);
buffer.writeString(chatData.currentMessage);
buffer.writeInt(chatData.currentLineNumber);
buffer.writeString(chatData.status.toString());
buffer.writeString(chatData.sender.toString());
writePlayerDataMap(buffer, chatData.players);
// Send message to player
ServerPlayNetworking.send(player, PACKET_S2C_ENTITY_MESSAGE, buffer);
}
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 @@
"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