Commit c80b0fee by Jonathan Thomas

More refactoring around PlayerData and player-based friendship. Also, adding in…

More refactoring around PlayerData and player-based friendship. Also, adding in migration of data from entity-based friendship to player-based.
parent 802d4d67
Pipeline #12734 passed with stages
in 1 minute 57 seconds
......@@ -4,6 +4,7 @@ import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.ui.BubbleRenderer;
import com.owlmaddie.ui.PlayerMessageManager;
import com.owlmaddie.utils.ClientEntityFinder;
......@@ -100,7 +101,7 @@ public class ClientPackets {
ClientPlayNetworking.registerGlobalReceiver(ServerPackets.PACKET_S2C_MESSAGE, (client, handler, buffer, responseSender) -> {
// Read the data from the server packet
UUID entityId = UUID.fromString(buffer.readString());
String playerIdStr = buffer.readString();
UUID playerId = UUID.fromString(buffer.readString());
String message = buffer.readString(32767);
int line = buffer.readInt();
String status_name = buffer.readString(32767);
......@@ -113,18 +114,18 @@ public class ClientPackets {
if (entity != null) {
ChatDataManager chatDataManager = ChatDataManager.getClientInstance();
EntityChatData chatData = chatDataManager.getOrCreateChatData(entity.getUuidAsString());
chatData.playerId = playerIdStr;
PlayerData playerData = chatData.getPlayerData(playerId);
if (!message.isEmpty()) {
chatData.currentMessage = message;
}
chatData.currentLineNumber = line;
chatData.status = ChatDataManager.ChatStatus.valueOf(status_name);
chatData.sender = ChatDataManager.ChatSender.valueOf(sender_name);
chatData.friendship = friendship;
playerData.friendship = friendship;
if (chatData.sender == ChatDataManager.ChatSender.USER && !playerIdStr.isEmpty()) {
if (chatData.sender == ChatDataManager.ChatSender.USER) {
// Add player message to queue for rendering
PlayerMessageManager.addMessage(UUID.fromString(chatData.playerId), chatData.currentMessage, ChatDataManager.TICKS_TO_DISPLAY_USER_MESSAGE);
PlayerMessageManager.addMessage(playerId, chatData.currentMessage, ChatDataManager.TICKS_TO_DISPLAY_USER_MESSAGE);
}
// Play sound with volume based on distance (from player or entity)
......
......@@ -3,6 +3,7 @@ package com.owlmaddie.ui;
import com.mojang.blaze3d.systems.RenderSystem;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.utils.EntityHeights;
import com.owlmaddie.utils.EntityRendererAccessor;
import com.owlmaddie.utils.TextureLoader;
......@@ -10,6 +11,7 @@ import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
import net.minecraft.client.font.TextRenderer.TextLayerType;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.render.*;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.util.math.MatrixStack;
......@@ -470,10 +472,15 @@ public class BubbleRenderer {
// Get position matrix
Matrix4f matrix = matrices.peek().getPositionMatrix();
// Get the player
ClientPlayerEntity player = MinecraftClient.getInstance().player;
// Look-up greeting (if any)
EntityChatData chatData = null;
PlayerData playerData = null;
if (entity instanceof MobEntity) {
chatData = ChatDataManager.getClientInstance().getOrCreateChatData(entity.getUuidAsString());
playerData = chatData.getPlayerData(player.getUuid());
} else if (entity instanceof PlayerEntity) {
chatData = PlayerMessageManager.getMessage(entity.getUuid());
}
......@@ -519,13 +526,13 @@ public class BubbleRenderer {
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
// Draw text background (no smaller than 50F tall)
drawTextBubbleBackground("text-top", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship);
drawTextBubbleBackground("text-top", matrices, -64, 0, 128, scaledTextHeight, playerData.friendship);
// Draw face icon of entity
drawEntityIcon(matrices, entity, -82, 7, 32, 32);
// Draw Friendship status
drawFriendshipStatus(matrices, 51, 18, 31, 21, chatData.friendship);
drawFriendshipStatus(matrices, 51, 18, 31, 21, playerData.friendship);
// Draw 'arrows' & 'keyboard' buttons
if (chatData.currentLineNumber > 0) {
......@@ -545,10 +552,10 @@ public class BubbleRenderer {
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, false);
// Draw 'resume chat' button
if (chatData.friendship == 3) {
if (playerData.friendship == 3) {
// Friend chat bubble
drawIcon("button-chat-friend", matrices, -16, textHeaderHeight, 32, 17);
} else if (chatData.friendship == -3) {
} else if (playerData.friendship == -3) {
// Enemy chat bubble
drawIcon("button-chat-enemy", matrices, -16, textHeaderHeight, 32, 17);
} else {
......@@ -561,7 +568,7 @@ public class BubbleRenderer {
drawEntityName(entity, matrix, immediate, fullBright, 24F + DISPLAY_PADDING, true);
// Draw text background
drawTextBubbleBackground("text-top-player", matrices, -64, 0, 128, scaledTextHeight, chatData.friendship);
drawTextBubbleBackground("text-top-player", matrices, -64, 0, 128, scaledTextHeight, playerData.friendship);
// Draw face icon of player
drawPlayerIcon(matrices, entity, -75, 14, 18, 18);
......
......@@ -15,10 +15,7 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
......@@ -137,11 +134,11 @@ public class ChatDataManager {
}
// Save chat data to file
public String GetLightChatData() {
public String GetLightChatData(UUID playerId) {
try {
// Create "light" version of entire chat data HashMap
HashMap<String, EntityChatDataLight> lightVersionMap = new HashMap<>();
this.entityChatDataMap.forEach((id, entityChatData) -> lightVersionMap.put(id, entityChatData.toLightVersion()));
this.entityChatDataMap.forEach((id, entityChatData) -> lightVersionMap.put(id, entityChatData.toLightVersion(playerId)));
// Convert light chat data to JSON string
return GSON.toJson(lightVersionMap).toString();
......@@ -174,6 +171,11 @@ public class ChatDataManager {
try (InputStreamReader reader = new InputStreamReader(new FileInputStream(loadFile), StandardCharsets.UTF_8)) {
Type type = new TypeToken<ConcurrentHashMap<String, EntityChatData>>(){}.getType();
this.entityChatDataMap = GSON.fromJson(reader, type);
// Post-process each EntityChatData object
for (EntityChatData entityChatData : entityChatDataMap.values()) {
entityChatData.postDeserializeInitialization();
}
} catch (Exception e) {
LOGGER.error("Error loading chat data", e);
this.entityChatDataMap = new ConcurrentHashMap<>();
......
......@@ -26,6 +26,12 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The {@code EntityChatData} class represents a conversation between an
* entity and one or more players, including friendship, character sheets,
......@@ -34,38 +40,99 @@ import java.util.stream.Collectors;
public class EntityChatData {
public static final Logger LOGGER = LoggerFactory.getLogger("creaturechat");
public String entityId;
public String playerId;
public String currentMessage;
public int currentLineNumber;
public ChatDataManager.ChatStatus status;
public List<ChatMessage> previousMessages;
public String characterSheet;
public ChatDataManager.ChatSender sender;
public int friendship; // -3 to 3 (0 = neutral)
public int auto_generated;
@SerializedName("playerId")
private String legacyPlayerId;
@SerializedName("previousMessages")
public List<ChatMessage> legacyMessages;
@SerializedName("friendship")
public int legacyFriendship;
public String birthDate;
public String deathDate;
// The map to store data for each player interacting with this entity
public Map<String, PlayerData> players;
public EntityChatData(String entityId, String playerId) {
this.entityId = entityId;
this.playerId = playerId;
this.players = new HashMap<>();
this.players.put("", new PlayerData()); // Add default blank player
this.currentMessage = "";
this.currentLineNumber = 0;
this.previousMessages = new ArrayList<>();
this.characterSheet = "";
this.status = ChatDataManager.ChatStatus.NONE;
this.sender = ChatDataManager.ChatSender.USER;
this.friendship = 0;
this.auto_generated = 0;
this.legacyFriendship = 0;
this.legacyMessages = new ArrayList<>();
}
// Post-deserialization initialization
public void postDeserializeInitialization() {
if (this.players == null) {
this.players = new HashMap<>(); // Ensure players map is initialized
}
this.players.computeIfAbsent("", k -> new PlayerData());
if (this.legacyPlayerId != null && !this.legacyPlayerId.isEmpty()) {
this.migrateData(this.legacyPlayerId);
}
}
// Migrate old data into the new structure
private void migrateData(String oldPlayerId) {
// Ensure the blank player data entry exists
PlayerData blankPlayerData = this.players.computeIfAbsent("", k -> new PlayerData());
// Migrate the old data to the blank player
if (this.legacyMessages != null) {
blankPlayerData.messages.addAll(this.legacyMessages);
}
blankPlayerData.friendship = this.legacyFriendship;
// Clean up old player data
this.legacyPlayerId = null;
this.legacyMessages = null;
this.legacyFriendship = 0;
}
// Get the player data (or fallback to the blank player)
public PlayerData getPlayerData(UUID playerId) {
// Check if the playerId exists in the players map
String playerIdStr = playerId.toString();
if (this.players.containsKey(playerIdStr)) {
return this.players.get(playerIdStr);
} else if (this.players.containsKey("")) {
// If the specific player ID is not found, fall back to the "" blank player
return this.players.get("");
}
return null;
}
// Generate light version of chat data (no previous messages)
public EntityChatDataLight toLightVersion() {
public EntityChatDataLight toLightVersion(UUID playerId) {
EntityChatDataLight light = new EntityChatDataLight();
light.entityId = this.entityId;
light.currentMessage = this.currentMessage;
light.currentLineNumber = this.currentLineNumber;
light.status = this.status;
light.sender = this.sender;
light.friendship = this.friendship;
PlayerData playerData = this.getPlayerData(playerId);
if (playerData != null) {
light.friendship = playerData.friendship;
} else {
light.friendship = 0;
}
return light;
}
......@@ -152,7 +219,13 @@ public class EntityChatData {
contextData.put("entity_class", getCharacterProp("Class"));
contextData.put("entity_skills", getCharacterProp("Skills"));
contextData.put("entity_background", getCharacterProp("Background"));
contextData.put("entity_friendship", String.valueOf(friendship));
PlayerData playerData = this.getPlayerData(player.getUuid());
if (playerData != null) {
contextData.put("entity_friendship", String.valueOf(playerData.friendship));
} else {
contextData.put("entity_friendship", String.valueOf(0));
}
return contextData;
}
......@@ -169,11 +242,11 @@ public class EntityChatData {
}
// Add USER Message
if (systemPrompt == "system-character") {
if (systemPrompt.equals("system-character")) {
// Add message without playerId (so it does not display)
this.addMessage(userMessage, ChatDataManager.ChatSender.USER, "");
} else if (systemPrompt == "system-chat") {
this.addMessage(userMessage, ChatDataManager.ChatSender.USER, player.getUuidAsString());
this.addMessage(userMessage, ChatDataManager.ChatSender.USER, player.getUuid());
} else if (systemPrompt.equals("system-chat")) {
this.addMessage(userMessage, ChatDataManager.ChatSender.USER, player.getUuid());
}
// Add PLAYER context information
......@@ -183,11 +256,14 @@ public class EntityChatData {
ConfigurationHandler.Config config = new ConfigurationHandler(ServerPackets.serverInstance).loadConfig();
String promptText = ChatPrompt.loadPromptFromResource(ServerPackets.serverInstance.getResourceManager(), systemPrompt);
// Get messages for player
PlayerData playerData = this.getPlayerData(player.getUuid());
// fetch HTTP response from ChatGPT
ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, previousMessages, false).thenAccept(output_message -> {
if (output_message != null && systemPrompt == "system-character") {
ChatGPTRequest.fetchMessageFromChatGPT(config, promptText, contextData, playerData.messages, false).thenAccept(output_message -> {
if (output_message != null && systemPrompt.equals("system-character")) {
// Character Sheet: Remove system-character message from previous messages
previousMessages.clear();
playerData.messages.clear();
// Add NEW CHARACTER sheet & greeting
this.characterSheet = output_message;
......@@ -195,9 +271,9 @@ public class EntityChatData {
if (shortGreeting.isEmpty()) {
shortGreeting = Randomizer.getRandomMessage(Randomizer.RandomType.NO_RESPONSE);
}
this.addMessage(shortGreeting.replace("\n", " "), ChatDataManager.ChatSender.ASSISTANT, player.getUuidAsString());
this.addMessage(shortGreeting.replace("\n", " "), ChatDataManager.ChatSender.ASSISTANT, player.getUuid());
} else if (output_message != null && systemPrompt == "system-chat") {
} else if (output_message != null && systemPrompt.equals("system-chat")) {
// Chat Message: Parse message for behaviors
ParsedMessage result = MessageParser.parseMessage(output_message.replace("\n", " "));
MobEntity entity = (MobEntity)ServerEntityFinder.getEntityByUUID(player.getServerWorld(), UUID.fromString(entityId));
......@@ -271,7 +347,7 @@ public class EntityChatData {
int new_friendship = Math.max(-3, Math.min(3, behavior.getArgument()));
// Does friendship improve?
if (new_friendship > this.friendship) {
if (new_friendship > playerData.friendship) {
// Stop any attack/flee if friendship improves
EntityBehaviorManager.removeGoal(entity, FleePlayerGoal.class);
EntityBehaviorManager.removeGoal(entity, AttackPlayerGoal.class);
......@@ -284,7 +360,7 @@ public class EntityChatData {
}
// Merchant deals (if friendship changes with a Villager
if (entity instanceof VillagerEntity && this.friendship != new_friendship) {
if (entity instanceof VillagerEntity && playerData.friendship != new_friendship) {
VillagerEntityAccessor villager = (VillagerEntityAccessor) entity;
switch (new_friendship) {
case 3:
......@@ -311,7 +387,7 @@ public class EntityChatData {
}
// Tame best friends and un-tame worst enemies
if (entity instanceof TameableEntity && this.friendship != new_friendship) {
if (entity instanceof TameableEntity && playerData.friendship != new_friendship) {
TameableEntity tamableEntity = (TameableEntity) entity;
if (new_friendship == 3 && !tamableEntity.isTamed()) {
tamableEntity.setOwner(player);
......@@ -321,12 +397,12 @@ public class EntityChatData {
}
}
this.friendship = new_friendship;
playerData.friendship = new_friendship;
}
}
// Add ASSISTANT message to history
this.addMessage(result.getOriginalMessage(), ChatDataManager.ChatSender.ASSISTANT, player.getUuidAsString());
this.addMessage(result.getOriginalMessage(), ChatDataManager.ChatSender.ASSISTANT, player.getUuid());
// Get cleaned message (i.e. no <BEHAVIOR> strings)
String cleanedMessage = result.getCleanedMessage();
......@@ -339,7 +415,7 @@ public class EntityChatData {
} else {
// Error / No Chat Message (Failure)
String randomErrorMessage = Randomizer.getRandomMessage(Randomizer.RandomType.ERROR);
this.addMessage(randomErrorMessage, ChatDataManager.ChatSender.ASSISTANT, player.getUuidAsString());
this.addMessage(randomErrorMessage, ChatDataManager.ChatSender.ASSISTANT, player.getUuid());
// Determine error message to display
String errorMessage = "Help is available at discord.creaturechat.com";
......@@ -353,7 +429,7 @@ public class EntityChatData {
// Clear history (if no character sheet was generated)
if (characterSheet.isEmpty()) {
previousMessages.clear();
playerData.messages.clear();
}
}
......@@ -367,12 +443,15 @@ public class EntityChatData {
}
// Add a message to the history and update the current message
public void addMessage(String message, ChatDataManager.ChatSender messageSender, String playerId) {
public void addMessage(String message, ChatDataManager.ChatSender messageSender, UUID playerId) {
// Truncate message (prevent crazy long messages... just in case)
String truncatedMessage = message.substring(0, Math.min(message.length(), ChatDataManager.MAX_CHAR_IN_USER_MESSAGE));
// Get or create player data
PlayerData playerData = getPlayerData(playerId);
// Add message to history
previousMessages.add(new ChatMessage(truncatedMessage, messageSender));
playerData.messages.add(new ChatMessage(truncatedMessage, messageSender));
// Set new message and reset line number of displayed text
currentMessage = truncatedMessage;
......@@ -385,7 +464,6 @@ public class EntityChatData {
status = ChatDataManager.ChatStatus.PENDING;
}
sender = messageSender;
this.playerId = playerId;
// Broadcast to all players
ServerPackets.BroadcastPacketMessage(this);
......
package com.owlmaddie.chat;
import java.util.ArrayList;
import java.util.List;
public class PlayerData {
public List<ChatMessage> messages;
public int friendship;
public PlayerData() {
this.messages = new ArrayList<>();
this.friendship = 0;
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.network.ServerPackets;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
......@@ -31,8 +32,9 @@ public class MixinLivingEntity {
private void modifyCanTarget(LivingEntity target, CallbackInfoReturnable<Boolean> cir) {
if (target instanceof PlayerEntity) {
LivingEntity thisEntity = (LivingEntity) (Object) this;
EntityChatData chatData = getChatData(thisEntity);
if (chatData.friendship > 0) {
EntityChatData entityData = getChatData(thisEntity);
PlayerData playerData = entityData.getPlayerData(target.getUuid());
if (playerData.friendship > 0) {
// Friendly creatures can't target a player
cir.setReturnValue(false);
}
......
......@@ -2,6 +2,7 @@ package com.owlmaddie.mixin;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.network.ServerPackets;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.TameableEntity;
......@@ -53,7 +54,8 @@ public class MixinMobEntity {
// Get chat data for entity
ChatDataManager chatDataManager = ChatDataManager.getServerInstance();
EntityChatData chatData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString());
EntityChatData entityData = chatDataManager.getOrCreateChatData(thisEntity.getUuidAsString());
PlayerData playerData = entityData.getPlayerData(player.getUuid());
// Check if the player successfully interacts with an item
if (player instanceof ServerPlayerEntity) {
......@@ -73,11 +75,11 @@ public class MixinMobEntity {
String giveItemMessage = "<" + serverPlayer.getName().getString() +
action_verb + "you " + itemCount + " " + itemName + ">";
if (!chatData.characterSheet.isEmpty() && chatData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", chatData, serverPlayer, thisEntity, giveItemMessage, true);
if (!entityData.characterSheet.isEmpty() && entityData.auto_generated < chatDataManager.MAX_AUTOGENERATE_RESPONSES) {
ServerPackets.generate_chat("N/A", entityData, serverPlayer, thisEntity, giveItemMessage, true);
}
} else if (itemStack.isEmpty() && chatData.friendship == 3) {
} else if (itemStack.isEmpty() && playerData.friendship == 3) {
// Player's hand is empty, Ride your best friend!
player.startRiding(thisEntity, true);
}
......
......@@ -3,6 +3,7 @@ package com.owlmaddie.network;
import com.owlmaddie.chat.ChatDataManager;
import com.owlmaddie.chat.EntityChatData;
import com.owlmaddie.chat.ChatDataSaverScheduler;
import com.owlmaddie.chat.PlayerData;
import com.owlmaddie.commands.ConfigurationHandler;
import com.owlmaddie.goals.EntityBehaviorManager;
import com.owlmaddie.goals.GoalPriority;
......@@ -169,7 +170,7 @@ public class ServerPackets {
LOGGER.info("Server send compressed, chunked login message packets to player: " + player.getName().getString());
// Get lite JSON data & compress to byte array
String chatDataJSON = ChatDataManager.getServerInstance().GetLightChatData();
String chatDataJSON = ChatDataManager.getServerInstance().GetLightChatData(player.getUuid());
byte[] compressedData = Compression.compressString(chatDataJSON);
if (compressedData == null) {
LOGGER.error("Failed to compress chat data.");
......@@ -314,19 +315,21 @@ public class ServerPackets {
entity.setPersistent();
}
PacketByteBuf buffer = new PacketByteBuf(Unpooled.buffer());
// Write the entity's chat updated data
buffer.writeString(chatData.entityId);
buffer.writeString(chatData.playerId);
buffer.writeString(chatData.currentMessage);
buffer.writeInt(chatData.currentLineNumber);
buffer.writeString(chatData.status.toString());
buffer.writeString(chatData.sender.toString());
buffer.writeInt(chatData.friendship);
// Iterate over all players and send the packet
for (ServerPlayerEntity player : serverInstance.getPlayerManager().getPlayerList()) {
PlayerData playerData = chatData.getPlayerData(player.getUuid());
PacketByteBuf buffer = new PacketByteBuf(Unpooled.buffer());
// Write the entity's chat updated data
buffer.writeString(chatData.entityId);
buffer.writeString(player.getUuidAsString());
buffer.writeString(chatData.currentMessage);
buffer.writeInt(chatData.currentLineNumber);
buffer.writeString(chatData.status.toString());
buffer.writeString(chatData.sender.toString());
buffer.writeInt(playerData.friendship);
LOGGER.debug("Server broadcast message to client: " + player.getName().getString() + " | Message: " + chatData.currentMessage);
ServerPlayNetworking.send(player, PACKET_S2C_MESSAGE, buffer);
}
......
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