Commit 0c75b901 by Jonathan Thomas

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

Bees, wither, wandering trade, and error handling

See merge request !28
No related merge requests found
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
}); });
} }
} }
......
...@@ -13,7 +13,9 @@ import com.owlmaddie.particle.ParticleEmitter; ...@@ -13,7 +13,9 @@ import com.owlmaddie.particle.ParticleEmitter;
import com.owlmaddie.utils.Randomizer; import com.owlmaddie.utils.Randomizer;
import com.owlmaddie.utils.ServerEntityFinder; import com.owlmaddie.utils.ServerEntityFinder;
import com.owlmaddie.utils.VillagerEntityAccessor; import com.owlmaddie.utils.VillagerEntityAccessor;
import com.owlmaddie.utils.WitherEntityAccessor;
import net.minecraft.entity.ExperienceOrbEntity; import net.minecraft.entity.ExperienceOrbEntity;
import net.minecraft.entity.boss.WitherEntity;
import net.minecraft.entity.boss.dragon.EnderDragonEntity; import net.minecraft.entity.boss.dragon.EnderDragonEntity;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.TameableEntity; import net.minecraft.entity.passive.TameableEntity;
...@@ -271,34 +273,41 @@ public class EntityChatData { ...@@ -271,34 +273,41 @@ public class EntityChatData {
// fetch HTTP response from ChatGPT // fetch HTTP response from ChatGPT
ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> { ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> {
if (output_message != null) { try {
// Character Sheet: Remove system-character message from previous messages if (output_message != null) {
previousMessages.clear(); // 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 } catch (Exception e) {
this.characterSheet = output_message; // Log the exception for debugging
String shortGreeting = Optional.ofNullable(getCharacterProp("short greeting")).filter(s -> !s.isEmpty()).orElse(Randomizer.getRandomMessage(Randomizer.RandomType.NO_RESPONSE)).replace("\n", " "); LOGGER.error("Error processing LLM response", e);
this.addMessage(shortGreeting, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
} else {
// Error / No Chat Message (Failure) // Error / No Chat Message (Failure)
String randomErrorMessage = Randomizer.getRandomMessage(Randomizer.RandomType.ERROR); String randomErrorMessage = Randomizer.getRandomMessage(Randomizer.RandomType.ERROR);
this.addMessage(randomErrorMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt); this.addMessage(randomErrorMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
// Determine error message to display // Remove the error message from history to prevent it from affecting future ChatGPT requests
String errorMessage = "Help is available at discord.creaturechat.com"; if (!previousMessages.isEmpty()) {
if (!ChatGPTRequest.lastErrorMessage.isEmpty()) { previousMessages.remove(previousMessages.size() - 1);
errorMessage = "Error: " + truncateString(ChatGPTRequest.lastErrorMessage, 55) + "\n" + errorMessage;
} }
// Send clickable error message // Send clickable error message
ServerPackets.SendClickableError(player, String errorMessage = "Error: ";
errorMessage, "http://discord.creaturechat.com"); if (e.getMessage() != null && !e.getMessage().isEmpty()) {
errorMessage += truncateString(e.getMessage(), 55) + "\n";
// Clear history (if no character sheet was generated)
if (characterSheet.isEmpty()) {
previousMessages.clear();
} }
errorMessage += "Help is available at discord.creaturechat.com";
ServerPackets.SendClickableError(player, errorMessage, "http://discord.creaturechat.com");
} }
}); });
} }
...@@ -334,229 +343,242 @@ public class EntityChatData { ...@@ -334,229 +343,242 @@ public class EntityChatData {
// fetch HTTP response from ChatGPT // fetch HTTP response from ChatGPT
ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> { ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> {
if (output_message != null) { try {
// Chat Message: Parse message for behaviors if (output_message != null) {
ParsedMessage result = MessageParser.parseMessage(output_message.replace("\n", " ")); // Chat Message: Parse message for behaviors
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), UUID.fromString(entityId)); 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 // Determine entity's default speed
float entitySpeed = SpeedControls.getMaxSpeed(entity); // Some Entities (i.e. Axolotl) set this incorrectly... so adjusting in the SpeedControls class
float entitySpeedMedium = MathHelper.clamp(entitySpeed * 1.15F, 0.5f, 1.15f); float entitySpeed = SpeedControls.getMaxSpeed(entity);
float entitySpeedFast = MathHelper.clamp(entitySpeed * 1.3F, 0.5f, 1.3f); 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()) { // Apply behaviors (if any)
LOGGER.info("Behavior: " + behavior.getName() + (behavior.getArgument() != null ? for (Behavior behavior : result.getBehaviors()) {
", Argument: " + behavior.getArgument() : "")); LOGGER.info("Behavior: " + behavior.getName() + (behavior.getArgument() != null ?
", Argument: " + behavior.getArgument() : ""));
// Apply behaviors to entity
if (behavior.getName().equals("FOLLOW")) { // Apply behaviors to entity
FollowPlayerGoal followGoal = new FollowPlayerGoal(player, entity, entitySpeedMedium); if (behavior.getName().equals("FOLLOW")) {
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class); FollowPlayerGoal followGoal = new FollowPlayerGoal(player, entity, entitySpeedMedium);
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class); EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, followGoal, GoalPriority.FOLLOW_PLAYER); EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
if (playerData.friendship >= 0) { EntityBehaviorManager.addGoal(entity, followGoal, GoalPriority.FOLLOW_PLAYER);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FOLLOW_FRIEND_PARTICLE, 0.5, 1); if (playerData.friendship >= 0) {
} else { ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FOLLOW_FRIEND_PARTICLE, 0.5, 1);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FOLLOW_ENEMY_PARTICLE, 0.5, 1); } else {
} ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FOLLOW_ENEMY_PARTICLE, 0.5, 1);
}
} else if (behavior.getName().equals("UNFOLLOW")) { } else if (behavior.getName().equals("UNFOLLOW")) {
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
} else if (behavior.getName().equals("FLEE")) { } else if (behavior.getName().equals("FLEE")) {
float fleeDistance = 40F; float fleeDistance = 40F;
FleePlayerGoal fleeGoal = new FleePlayerGoal(player, entity, entitySpeedFast, fleeDistance); FleePlayerGoal fleeGoal = new FleePlayerGoal(player, entity, entitySpeedFast, fleeDistance);
EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, TalkPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class); EntityBehaviorManager.removeGoal(entity, LeadPlayerGoal.class);
EntityBehaviorManager.addGoal(entity, fleeGoal, GoalPriority.FLEE_PLAYER); EntityBehaviorManager.addGoal(entity, fleeGoal, GoalPriority.FLEE_PLAYER);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FLEE_PARTICLE, 0.5, 1); 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("FRIENDSHIP")) { } else if (behavior.getName().equals("UNFLEE")) {
int new_friendship = Math.max(-3, Math.min(3, behavior.getArgument())); EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
// Does friendship improve? } else if (behavior.getName().equals("ATTACK")) {
if (new_friendship > playerData.friendship) { AttackPlayerGoal attackGoal = new AttackPlayerGoal(player, entity, entitySpeedFast);
// Stop any attack/flee if friendship improves 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, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.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) { } else if (behavior.getName().equals("UNPROTECT")) {
// Trigger end of game (friendship always wins!) EntityBehaviorManager.removeGoal(entity, ProtectPlayerGoal.class);
EnderDragonEntity dragon = (EnderDragonEntity) entity;
// Emit particles & sound } else if (behavior.getName().equals("LEAD")) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_BIG_PARTICLE, 3, 200); LeadPlayerGoal leadGoal = new LeadPlayerGoal(player, entity, entitySpeedMedium);
entity.getWorld().playSound(entity, entity.getBlockPos(), SoundEvents.ENTITY_ENDER_DRAGON_DEATH, SoundCategory.PLAYERS, 0.3F, 1.0F); EntityBehaviorManager.removeGoal(entity, FollowPlayerGoal.class);
entity.getWorld().playSound(entity, entity.getBlockPos(), SoundEvents.UI_TOAST_CHALLENGE_COMPLETE, SoundCategory.PLAYERS, 0.5F, 1.0F); 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 if (entity instanceof EnderDragonEntity && new_friendship == 3) {
boolean doMobLoot = entity.getWorld().getGameRules().getBoolean(GameRules.DO_MOB_LOOT); // Trigger end of game (friendship always wins!)
EnderDragonEntity dragon = (EnderDragonEntity) entity;
// If this is the first time the dragon is 'befriended', adjust the XP // Emit particles & sound
int baseXP = 500; ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_BIG_PARTICLE, 3, 200);
if (dragon.getFight() != null && !dragon.getFight().hasPreviouslyKilled()) { entity.getWorld().playSound(entity, entity.getBlockPos(), SoundEvents.ENTITY_ENDER_DRAGON_DEATH, SoundCategory.PLAYERS, 0.3F, 1.0F);
baseXP = 12000; 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 // Check if the game rule for mob loot is enabled
if (entity.getWorld() instanceof ServerWorld && doMobLoot) { boolean doMobLoot = entity.getWorld().getGameRules().getBoolean(GameRules.DO_MOB_LOOT);
// Loop to spawn XP orbs
for (int j = 1; j <= 11; j++) { // If this is the first time the dragon is 'befriended', adjust the XP
float xpFraction = (j == 11) ? 0.2F : 0.08F; int baseXP = 500;
int xpAmount = MathHelper.floor((float)baseXP * xpFraction); if (dragon.getFight() != null && !dragon.getFight().hasPreviouslyKilled()) {
ExperienceOrbEntity.spawn((ServerWorld)entity.getWorld(), entity.getPos(), xpAmount); baseXP = 12000;
} }
}
// Mark fight as over // If the world is a server world and mob loot is enabled, spawn XP orbs
dragon.getFight().dragonKilled(dragon); 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 // Mark fight as over
if (entity instanceof VillagerEntity && playerData.friendship != new_friendship) { dragon.getFight().dragonKilled(dragon);
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;
} }
}
// Tame best friends and un-tame worst enemies // Merchant deals (if friendship changes with a Villager
if (entity instanceof TameableEntity && playerData.friendship != new_friendship) { if (entity instanceof VillagerEntity && playerData.friendship != new_friendship) {
TameableEntity tamableEntity = (TameableEntity) entity; VillagerEntityAccessor villager = (VillagerEntityAccessor) entity;
if (new_friendship == 3 && !tamableEntity.isTamed()) { switch (new_friendship) {
tamableEntity.setOwner(player); case 3:
} else if (new_friendship == -3 && tamableEntity.isTamed()) { villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MAJOR_POSITIVE, 20);
tamableEntity.setTamed(false); villager.getGossip().startGossip(player.getUuid(), VillageGossipType.MINOR_POSITIVE, 25);
tamableEntity.setOwnerUuid(null); 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 // Tame best friends and un-tame worst enemies
if (playerData.friendship != new_friendship) { if (entity instanceof TameableEntity && playerData.friendship != new_friendship) {
int friendDiff = new_friendship - playerData.friendship; TameableEntity tamableEntity = (TameableEntity) entity;
if (friendDiff > 0) { if (new_friendship == 3 && !tamableEntity.isTamed()) {
// Heart particles tamableEntity.setOwner(player);
if (new_friendship == 3) { } else if (new_friendship == -3 && tamableEntity.isTamed()) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_BIG_PARTICLE, 0.5, 10); tamableEntity.setTamed(false);
} else { tamableEntity.setOwnerUuid(null);
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, HEART_SMALL_PARTICLE, 0.1, 1);
} }
}
// 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) { } else if (friendDiff < 0) {
// Fire particles // Fire particles
if (new_friendship == -3) { if (new_friendship == -3) {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FIRE_BIG_PARTICLE, 0.5, 10); ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FIRE_BIG_PARTICLE, 0.5, 10);
} else { } else {
ParticleEmitter.emitCreatureParticle((ServerWorld) entity.getWorld(), entity, FIRE_SMALL_PARTICLE, 0.1, 1); 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) // Add ASSISTANT message to history
String cleanedMessage = result.getCleanedMessage(); this.addMessage(cleanedMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
if (cleanedMessage.isEmpty()) {
cleanedMessage = Randomizer.getRandomMessage(Randomizer.RandomType.NO_RESPONSE);
}
// Add ASSISTANT message to history // Update the last entry in previousMessages to use the original message
this.addMessage(cleanedMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt); 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 } else {
this.previousMessages.set(this.previousMessages.size() - 1, // No valid LLM response
new ChatMessage(result.getOriginalMessage(), ChatDataManager.ChatSender.ASSISTANT, player.getDisplayName().getString())); 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) // Error / No Chat Message (Failure)
String randomErrorMessage = Randomizer.getRandomMessage(Randomizer.RandomType.ERROR); String randomErrorMessage = Randomizer.getRandomMessage(Randomizer.RandomType.ERROR);
this.addMessage(randomErrorMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt); this.addMessage(randomErrorMessage, ChatDataManager.ChatSender.ASSISTANT, player, systemPrompt);
// Determine error message to display // Remove the error message from history to prevent it from affecting future ChatGPT requests
String errorMessage = "Help is available at discord.creaturechat.com"; if (!previousMessages.isEmpty()) {
if (!ChatGPTRequest.lastErrorMessage.isEmpty()) { previousMessages.remove(previousMessages.size() - 1);
errorMessage = "Error: " + truncateString(ChatGPTRequest.lastErrorMessage, 55) + "\n" + errorMessage;
} }
// Send clickable error message // Send clickable error message
ServerPackets.SendClickableError(player, String errorMessage = "Error: ";
errorMessage, "http://discord.creaturechat.com"); if (e.getMessage() != null && !e.getMessage().isEmpty()) {
errorMessage += truncateString(e.getMessage(), 55) + "\n";
// Clear history (if no character sheet was generated)
if (characterSheet.isEmpty()) {
previousMessages.clear();
} }
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 { ...@@ -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