diff --git a/README.md b/README.md index 7bf5044..6f63b72 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,19 @@ -# AIMobs -AIMobs is a mod that lets you chat with Minecraft mobs and other entities by creating prompts and using the OpenAI API. +# AIMobs-Voice +AIMobs-Voice is a mod that lets you chat or talk (hold R key) with Minecraft mobs and other entities by creating prompts and using the OpenAI API. It's forked from [AIMobs](https://github.com/rebane2001/aimobs) for text only interactions. ### Requirements - Minecraft 1.19.4 - Fabric -- Fabric API +- Fabric API 0.86.1.19.4 +- (Prism Launcher [here](https://prismlauncher.org)) +- (Java 17 [here](https://adoptium.net/en-GB/temurin/releases/?version=17)) + +### Installation +1. [YouTube Tutorial] (https://www.youtube.com/watch?v=EKCaTp5a8ZU) +2. You won't find aimobs-voice via download mods in Prism Launcher yet. Instead please download the .jar here from the release page and add it manually to Prism Launcher -> Mods -> Add file. ### Usage -After installing the mod, grab your OpenAI API key from [here](https://beta.openai.com/account/api-keys), and set it with the `/aimobs setkey ` command. +After installing the mod, grab your OpenAI API key from [here](https://beta.openai.com/account/api-keys), and set it with the `/aimobs setkey ` command. Also in order to let the mobs talk to you: First enable the Cloud Text-to-Speech API - [here](https://console.cloud.google.com/apis/library/texttospeech.googleapis.com) and then create your API key credential [here](https://console.cloud.google.com/apis/credentials). Once you have it, set it with the `/aimobs setvoicekey ` command You should now be able to **talk to mobs by shift+clicking** on them! @@ -16,16 +22,22 @@ You should now be able to **talk to mobs by shift+clicking** on them! - `/aimobs help` - View commands help - `/aimobs enable/disable` - Enable/disable the mod - `/aimobs setkey ` - Set OpenAI API key +- `/aimobs setvoicekey ` - Set Google Text-To-Speech API key - `/aimobs setmodel ` - Set AI model - `/aimobs settemp ` - Set model temperature ### Notes This project was initially made in 1.12 as a client Forge mod, then ported to 1.19 PaperMC as a server plugin, then ported to Fabric 1.19. Because of this, the code can be a little messy and weird. A couple hardcoded limits are 512 as the max token length and 4096 as the max prompt length (longer prompts will get the beginning cut off), these could be made configurable in the future. -Some plans for the future: -- Support for the Forge modloader. -- Support for other AI APIs. +## 🛣️ Roadmap: +- [ ] Let mobs approach player +- [ ] Stream GPT output to TTS +- [ ] Replace random voices by mob specific ones +- [ ] Cut memory within model token limit to prevent overflow +- [ ] Support for the Forge modloader. +- [ ] Support for other AI APIs. + +Come join the project and add new features or for refined mob prompt engineering. Feel free to contact me on [Twitter/X](https://twitter.com/J_Grenzebach) -An unofficial community-made fork is available with support for Ukranian and Español at [Eianex/aimobs](https://github.com/Eianex/aimobs/releases). The icon used is the **🧠** emoji from [Twemoji](https://twemoji.twitter.com/) (CC BY 4.0) diff --git a/build 2/loom-cache/mixin-map-net.fabricmc.yarn.1_19_4.1.19.4+build.2-v2.main.tiny b/build 2/loom-cache/mixin-map-net.fabricmc.yarn.1_19_4.1.19.4+build.2-v2.main.tiny new file mode 100644 index 0000000..afd84f3 --- /dev/null +++ b/build 2/loom-cache/mixin-map-net.fabricmc.yarn.1_19_4.1.19.4+build.2-v2.main.tiny @@ -0,0 +1 @@ +v1 named intermediary diff --git a/build.gradle b/build.gradle index a9ad14a..0ca2702 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,8 @@ repositories { } dependencies { + // needed for whisper ASR in RequestHandler + //implementation 'org.apache.httpcomponents:httpmime:4.5.13' // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" @@ -33,6 +35,7 @@ dependencies { // These are included in the Fabric API production distribution and allow you to update your mod to the latest modules at a later more convenient time. // modImplementation "net.fabricmc.fabric-api:fabric-api-deprecated:${project.fabric_version}" + } processResources { diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/rebane2001/aimobs/AIMobsCommand.java b/src/main/java/com/rebane2001/aimobs/AIMobsCommand.java index 0ddf9f0..8762617 100644 --- a/src/main/java/com/rebane2001/aimobs/AIMobsCommand.java +++ b/src/main/java/com/rebane2001/aimobs/AIMobsCommand.java @@ -14,26 +14,22 @@ public class AIMobsCommand { + // Method to setup AIMobs commands public static void setupAIMobsCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess) { + // Registering the available commands dispatcher.register(literal("aimobs") .executes(AIMobsCommand::status) .then(literal("help").executes(AIMobsCommand::help)) - .then(literal("setkey") - .then(argument("key", StringArgumentType.string()) - .executes(AIMobsCommand::setAPIKey) - )) - .then(literal("setmodel") - .then(argument("model", StringArgumentType.string()) - .executes(AIMobsCommand::setModel) - )) - .then(literal("settemp") - .then(argument("temperature", FloatArgumentType.floatArg(0,1)) - .executes(AIMobsCommand::setTemp) - )) + .then(literal("setkey").then(argument("key", StringArgumentType.string()).executes(AIMobsCommand::setAPIKey))) + .then(literal("setvoicekey").then(argument("voicekey", StringArgumentType.string()).executes(AIMobsCommand::setVoiceAPIKey))) + .then(literal("setmodel").then(argument("model", StringArgumentType.string()).executes(AIMobsCommand::setModel))) + .then(literal("settemp").then(argument("temperature", FloatArgumentType.floatArg(0,1)).executes(AIMobsCommand::setTemp))) .then(literal("enable").executes(context -> setEnabled(context, true))) .then(literal("disable").executes(context -> setEnabled(context, false))) ); } + + // Method to enable or disable the mod public static int setEnabled(CommandContext context, boolean enabled) { AIMobsConfig.config.enabled = enabled; AIMobsConfig.saveConfig(); @@ -41,8 +37,10 @@ public static int setEnabled(CommandContext context, return 1; } + // Method to show the status of the mod's configuration public static int status(CommandContext context) { boolean hasKey = AIMobsConfig.config.apiKey.length() > 0; + boolean hasVoiceKey = AIMobsConfig.config.voiceApiKey.length() > 0; Text yes = Text.literal("Yes").formatted(Formatting.GREEN); Text no = Text.literal("No").formatted(Formatting.RED); Text helpText = Text.literal("") @@ -50,6 +48,7 @@ public static int status(CommandContext context) { .append("").formatted(Formatting.RESET) .append("\nEnabled: ").append(AIMobsConfig.config.enabled ? yes : no) .append("\nAPI Key: ").append(hasKey ? yes : no) + .append("\nVoice API Key: ").append(hasVoiceKey ? yes : no) .append("\nModel: ").append(AIMobsConfig.config.model) .append("\nTemp: ").append(String.valueOf(AIMobsConfig.config.temperature)) .append("\n\nUse ").append(Text.literal("/aimobs help").formatted(Formatting.GRAY)).append(" for help"); @@ -57,6 +56,7 @@ public static int status(CommandContext context) { return 1; } + // Method to show help text public static int help(CommandContext context) { Text helpText = Text.literal("") .append("AIMobs Commands").formatted(Formatting.UNDERLINE) @@ -65,12 +65,15 @@ public static int help(CommandContext context) { .append("\n/aimobs help - View commands help") .append("\n/aimobs enable/disable - Enable/disable the mod") .append("\n/aimobs setkey - Set OpenAI API key") + .append("\n/aimobs setvoicekey - Set Google Text-To-Speech API key") .append("\n/aimobs setmodel - Set AI model") .append("\n/aimobs settemp - Set model temperature") .append("\nYou can talk to mobs by shift-clicking on them!"); context.getSource().sendFeedback(helpText); return 1; } + + // Method to set API key public static int setAPIKey(CommandContext context) { String apiKey = context.getArgument("key", String.class); if (apiKey.length() > 0) { @@ -81,6 +84,20 @@ public static int setAPIKey(CommandContext context) { } return 0; } + + // Method to set Voice API key + public static int setVoiceAPIKey(CommandContext context) { + String voiceApiKey = context.getArgument("voicekey", String.class); + if (voiceApiKey.length() > 0) { + AIMobsConfig.config.voiceApiKey = voiceApiKey; + AIMobsConfig.saveConfig(); + context.getSource().sendFeedback(Text.of("Voice API key set")); + return 1; + } + return 0; + } + + // Method to set Model public static int setModel(CommandContext context) { String model = context.getArgument("model", String.class); if (model.length() > 0) { @@ -91,6 +108,8 @@ public static int setModel(CommandContext context) { } return 0; } + + // Method to set Temperature public static int setTemp(CommandContext context) { AIMobsConfig.config.temperature = context.getArgument("temperature", float.class); AIMobsConfig.saveConfig(); diff --git a/src/main/java/com/rebane2001/aimobs/AIMobsConfig.java b/src/main/java/com/rebane2001/aimobs/AIMobsConfig.java index f1e8f10..c228cb1 100644 --- a/src/main/java/com/rebane2001/aimobs/AIMobsConfig.java +++ b/src/main/java/com/rebane2001/aimobs/AIMobsConfig.java @@ -9,30 +9,38 @@ import java.util.Objects; public class AIMobsConfig { + // Inner class to hold configuration settings public static class Config { - public boolean enabled = true; - public String apiKey = ""; - public String model = "text-davinci-003"; - public float temperature = 0.6f; + public boolean enabled = true; // Enable or disable the mod + public String apiKey = ""; // OpenAI API key + public String voiceApiKey = ""; // Voice API key (if applicable) + public String model = "gpt-3.5-turbo-16k"; // Model to be used for conversation + public float temperature = 0.3f; // Model temperature setting } - public static Config config; + public static Config config; // Config instance + + // Get the path to the configuration file private static Path getConfigPath() { return FabricLoader.getInstance().getConfigDir().resolve("aimobs.json"); } + // Load the configuration from the file public static void loadConfig() { try (FileReader reader = new FileReader(getConfigPath().toFile())) { config = new Gson().fromJson(reader, Config.class); - Objects.requireNonNull(config); + Objects.requireNonNull(config); // Ensure that the config is not null } catch (Exception e) { + // If an exception occurs, create a new default configuration and save it config = new Config(); saveConfig(); } } + + // Save the configuration to the file public static void saveConfig() { try (FileWriter writer = new FileWriter(getConfigPath().toFile())) { - new Gson().toJson(config, writer); + new Gson().toJson(config, writer); // Write the JSON representation of the config to the file } catch (Exception e) { e.printStackTrace(); } diff --git a/src/main/java/com/rebane2001/aimobs/AIMobsMod.java b/src/main/java/com/rebane2001/aimobs/AIMobsMod.java index 91294a6..2894eb5 100644 --- a/src/main/java/com/rebane2001/aimobs/AIMobsMod.java +++ b/src/main/java/com/rebane2001/aimobs/AIMobsMod.java @@ -2,27 +2,165 @@ import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.event.player.AttackEntityCallback; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.Entity; import net.minecraft.util.ActionResult; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.world.World; +import net.minecraft.util.math.Box; +import org.lwjgl.glfw.GLFW; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.minecraft.text.Text; +import net.minecraft.client.MinecraftClient; + +import java.util.List; +import java.util.Random; public class AIMobsMod implements ClientModInitializer { - public static final Logger LOGGER = LoggerFactory.getLogger("aimobs"); - - @Override - public void onInitializeClient() { - AIMobsConfig.loadConfig(); - ClientCommandRegistrationCallback.EVENT.register(AIMobsCommand::setupAIMobsCommand); - AttackEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { - if (!AIMobsConfig.config.enabled) return ActionResult.PASS; - if (!player.isSneaking()) { - if (entity.getId() == ActionHandler.entityId) - ActionHandler.handlePunch(entity, player); - return ActionResult.PASS; - } - ActionHandler.startConversation(entity, player); - return ActionResult.FAIL; - }); - } + public static final KeyBinding R_KEY_BINDING = new KeyBinding("key.aimobs.voice_input", GLFW.GLFW_KEY_R, "category.aimobs"); + public static final Logger LOGGER = LoggerFactory.getLogger("aimobs"); + public static final ConversationsManager conversationsManager = new ConversationsManager(); + + // These fields need to be public or have public accessors to be used in the mixin + public static MobEntity current_mob = null; + public static PlayerEntity current_player = null; + public static long nextMobSelectionTime = 0; + + @Override + public void onInitializeClient() { + AIMobsConfig.loadConfig(); + KeyBindingHelper.registerKeyBinding(R_KEY_BINDING); + conversationsManager.loadConversations(); + MobPrompts.initializeMobStatsFile(); + + ClientCommandRegistrationCallback.EVENT.register(AIMobsCommand::setupAIMobsCommand); + + // Tick event + ClientTickEvents.START_CLIENT_TICK.register(client -> { + if (!AIMobsConfig.config.enabled) return; + //mobInitiative(client.player); + }); + + + + ClientTickEvents.END_CLIENT_TICK.register(client -> { + if (!AIMobsConfig.config.enabled) return; + //current_player = MinecraftClient.getInstance().player; + //ActionHandler.mobInitiative(current_player); + if (ActionHandler.checkConversationEnd()) { + current_player.sendMessage(Text.of("Bye.")); + } + while (AIMobsMod.R_KEY_BINDING.wasPressed()) { + ActionHandler.onRKeyPress(); + } + if (!AIMobsMod.R_KEY_BINDING.isPressed() && ActionHandler.isRKeyPressed) { + ActionHandler.onRKeyRelease(); + } + }); + + AttackEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { + if (!AIMobsConfig.config.enabled) return ActionResult.PASS; + if (!player.isSneaking()) { + if (entity.getUuid().equals(ActionHandler.entityId)) + ActionHandler.handlePunch(entity, player); + return ActionResult.PASS; + } + if (entity instanceof MobEntity) { + current_mob = (MobEntity) entity; + current_player = player; + + if (world.isClient()) { + //current_player.sendMessage(Text.of(current_mob + " coming to "+ current_player)); + ActionHandler.startConversation(entity, player); + } + + return ActionResult.SUCCESS; + } + return ActionResult.PASS; + }); + + /* UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { + if (!AIMobsConfig.config.enabled) return ActionResult.PASS; + if (entity instanceof MobEntity) { + MobEntity mobEntity = (MobEntity) entity; + System.out.println("Coming"); + for (int i = 0; i < 1000; i++) { + mobEntity.getNavigation().startMovingTo(player, 1.0); // Speed value, you can change it to whatever you want + } + // Rest of the code + return ActionResult.SUCCESS; // Return success to indicate that the interaction was handled + } + return ActionResult.PASS; // Return pass to allow other interactions to proceed + }); */ + + + } + + /* // This method will be called from the server-side mixin + public static void updateMobMovement() { + MobEntity currentMob = ActionHandler.currentMob; // Get currentMob from ActionHandler + if (currentMob != null && currentPlayer != null && ActionHandler.conversationOngoing) { + double desiredDistance = 9.0; // Squared distance at which the mob should stop + double distance = currentMob.squaredDistanceTo(currentPlayer); + if (distance > desiredDistance) { + currentMob.getNavigation().startMovingTo(currentPlayer, 1.0); + } else { + currentMob.getNavigation().stop(); + } + } else if (currentMob != null) { + currentMob.getNavigation().stop(); + //currentMob = null; + //currentPlayer = null; + } + } */ + + // This method will be called from the server-side mixin + public static void updateMobMovement() { + if (current_mob != null && current_player != null && ActionHandler.conversationOngoing) { + double desiredDistance = 9.0; // Squared distance at which the mob should stop + double distance = current_mob.squaredDistanceTo(current_player); + if (distance > desiredDistance) { + for (int i = 0; i < 1000; i++) { + current_mob.getNavigation().startMovingTo(current_player, 1.0); + } + } else { + current_mob.getNavigation().stop(); + } + } else if (current_mob != null) { + current_mob.getNavigation().stop(); + //current_mob = null; + //current_player = null; + } + } + + public static void mobInitiative(PlayerEntity player) { + if (player == null || ActionHandler.conversationOngoing) { + return; + } + long currentTime = System.currentTimeMillis(); + if (currentTime < nextMobSelectionTime) { + return; + } + // Create a bounding box around the player + Box boundingBox = new Box(player.getBlockPos()).expand(10); // 10-block radius + // Get the nearby entities, excluding the player + List nearbyEntities = player.world.getEntitiesByClass(Entity.class, boundingBox, e -> e != player); + if (nearbyEntities.isEmpty()) { + return; + } + Random random = new Random(); + current_mob = (MobEntity) nearbyEntities.get(random.nextInt(nearbyEntities.size())); + System.out.println("Selected mob: " + current_mob.getName().getString()); // Test message + // Start the conversation with the selected mob + ActionHandler.startConversation(current_mob, player); + // Set the next mob selection time with a random cooldown between 1-5 minutes + nextMobSelectionTime = currentTime + (1 + random.nextInt(5)) * 60 * 1000; + } + } + diff --git a/src/main/java/com/rebane2001/aimobs/ActionHandler.java b/src/main/java/com/rebane2001/aimobs/ActionHandler.java index 10e9b92..778b5e9 100644 --- a/src/main/java/com/rebane2001/aimobs/ActionHandler.java +++ b/src/main/java/com/rebane2001/aimobs/ActionHandler.java @@ -1,42 +1,63 @@ package com.rebane2001.aimobs; +import com.rebane2001.aimobs.RequestHandler.Message; +import java.util.Arrays; import com.rebane2001.aimobs.mixin.ChatHudAccessor; +//import net.minecraft.nbt.NbtCompound; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.hud.ChatHudLine; -import net.minecraft.client.resource.language.I18n; import net.minecraft.entity.Entity; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.attribute.EntityAttributes; import net.minecraft.entity.passive.VillagerEntity; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.mob.MobEntity; import net.minecraft.item.ItemStack; import net.minecraft.text.OrderedText; import net.minecraft.text.Style; import net.minecraft.text.Text; import net.minecraft.util.Formatting; import net.minecraft.util.Util; +import net.minecraft.util.math.Box; import net.minecraft.registry.RegistryKey; -// import net.minecraft.world.EntityView; import net.minecraft.world.biome.Biome; import org.apache.commons.lang3.StringUtils; - import java.util.List; -import java.util.Locale; import java.util.Optional; import java.util.UUID; +import java.io.InputStream; +import java.io.IOException; +import java.util.Random; +import java.util.stream.Collectors; + + + public class ActionHandler { - public static String prompts = ""; + public static Message[] messages = new Message[0]; // Replace prompts string with messages array + public static MobEntity currentMob = null; public static String entityName = ""; - public static int entityId = 0; + public static UUID entityId = null; public static UUID initiator = null; + public static String currentPrompt = ""; public static long lastRequest = 0; + public static long conversationEndTime = 0; // Track when the conversation should end + public static boolean conversationOngoing = false; + // ConversationsManager to manage all conversations with entities + public static ConversationsManager conversationsManager = AIMobsMod.conversationsManager; + // Field to track if the "R" key is being held down + public static boolean isRKeyPressed = false; + // AudioRecorder to record audio from the microphone + private static AudioRecorder audioRecorder = new AudioRecorder(); + // The waitMessage is the thing that goes ' ...' before an actual response is received private static ChatHudLine.Visible waitMessage; private static List getChatHudMessages() { return ((ChatHudAccessor)MinecraftClient.getInstance().inGameHud.getChatHud()).getVisibleMessages(); } + + // Method to show the wait message private static void showWaitMessage(String name) { if (waitMessage != null) getChatHudMessages().remove(waitMessage); waitMessage = new ChatHudLine.Visible(MinecraftClient.getInstance().inGameHud.getTicks(), OrderedText.concat(OrderedText.styledForwardsVisitedString("<" + name + "> ", Style.EMPTY),OrderedText.styledForwardsVisitedString("...", Style.EMPTY.withColor(Formatting.GRAY))), null, true); @@ -47,23 +68,107 @@ private static void hideWaitMessage() { waitMessage = null; } - private static String getBiome(Entity entity) { - Optional> biomeKey = entity.getEntityWorld().getBiomeAccess().getBiome(entity.getBlockPos()).getKey(); - if (biomeKey.isEmpty()) return "place"; - return I18n.translate(Util.createTranslationKey("biome", biomeKey.get().getValue())); + // Method to handle the "R" key press + public static void onRKeyPress() { + if (entityId != null && !isRKeyPressed) { // Check if a conversation has been started + isRKeyPressed = true; + PlayerEntity player = MinecraftClient.getInstance().player; + player.sendMessage(Text.of("Listening..")); + System.out.println("Recording started"); // Test message + audioRecorder.startRecording(); + } + } + + // Method to handle the "R" key release + public static void onRKeyRelease() { + if (isRKeyPressed) { + isRKeyPressed = false; + PlayerEntity player = MinecraftClient.getInstance().player; + player.sendMessage(Text.of("...")); + System.out.println("R key released! Stopping voice input..."); + + // Create a new thread to handle the time-consuming tasks + new Thread(() -> { + // Check if the voice key is set in the configuration + if (AIMobsConfig.config.voiceApiKey.length() > 0) { + try { + // Stop the recording and get the WAV input stream + InputStream wavInputStream = audioRecorder.stopRecording(); + + // Get the transcription from OpenAI's Whisper ASR + String transcription = RequestHandler.getTranscription(wavInputStream); + // You can now use the transcription in your conversation logic + // For example, send it as a reply to the entity + if (player != null) { + replyToEntity(transcription, player); + } + + } catch (IOException e) { + System.err.println("Error transcribing audio:"); + e.printStackTrace(); + } + } else { + System.err.println("Voice API key is not set. Please set it using /aimobs setvoicekey ."); + } + }).start(); // Start the new thread + } } public static void startConversation(Entity entity, PlayerEntity player) { - entityId = entity.getId(); + if (!(entity instanceof LivingEntity)) return; + if (entity instanceof MobEntity) { + currentMob = (MobEntity) entity; // Store the mob entity reference + } + entityId = entity.getUuid(); + //entityName = StringUtils.capitalize(entity.getType().getName().getString().replace("_", " ")); + entityName = entity.getName().getString(); initiator = player.getUuid(); - prompts = createPrompt(entity, player); - ItemStack heldItem = player.getMainHandStack(); - if (heldItem.getCount() > 0) - prompts = "You are holding a " + heldItem.getName().getString() + " in your hand. " + prompts; - showWaitMessage(entityName); - getResponse(player); + conversationOngoing = true; + // Check if a conversation already exists for this mob + if (conversationsManager.conversationExists(entityId)) { + // Resume existing conversation + Conversation existingConversation = conversationsManager.getConversation(entityId); + messages = conversationsManager.getConversation(entityId).getMessages().toArray(new Message[0]); + currentPrompt = PromptManager.createFollowUpPrompt(entity, player); + conversationsManager.addMessageToConversation(entityId, "user", currentPrompt); // Add the new user message to the conversation + messages = existingConversation.getMessages().toArray(new RequestHandler.Message[0]); // Update messages array + showWaitMessage(entityName); + getResponse(player); + } else { + // Start a new conversation + conversationsManager.startConversation(entityId); + currentPrompt = PromptManager.createInitialPrompt(entity, player); + // Adding the player's message to the conversation + conversationsManager.addMessageToConversation(entityId, "user", currentPrompt); + messages = new Message[] { new Message("user", currentPrompt) }; // Initialize messages array + showWaitMessage(entityName); + getResponse(player); + } } + + public static boolean checkConversationEnd() { + if (entityId != null && System.currentTimeMillis() > conversationEndTime) { + if (currentMob != null) { + currentMob.getNavigation().stop(); // Stop the mob's movement + } + // End the conversation + conversationOngoing = false; + currentMob = null; // Reset the mob entity reference + entityId = null; + entityName = ""; + initiator = null; + messages = new Message[0]; + currentPrompt = ""; + conversationEndTime = 0; + System.out.println("Conversation ended"); // Test message + return true; + } + return false; + } + + + public static void getResponse(PlayerEntity player) { // 1.5 second cooldown between requests if (lastRequest + 1500L > System.currentTimeMillis()) return; @@ -72,11 +177,26 @@ public static void getResponse(PlayerEntity player) { return; } lastRequest = System.currentTimeMillis(); + // Set the time when the conversation should end (30 seconds from now) + conversationEndTime = lastRequest + 100000L; Thread t = new Thread(() -> { try { - String response = RequestHandler.getAIResponse(prompts); + String response = RequestHandler.getAIResponse(messages); player.sendMessage(Text.of("<" + entityName + "> " + response)); - prompts += response + "\"\n"; + + // Adding the AI's response to the conversation + conversationsManager.addMessageToConversation(entityId, "assistant", response); + + // Add response to messages array + messages = Arrays.copyOf(messages, messages.length + 1); + messages[messages.length - 1] = new Message("assistant", response); + conversationsManager.updateMessages(entityId, messages); + + // Trigger text-to-speech synthesis and playback + if (AIMobsConfig.config.enabled && AIMobsConfig.config.voiceApiKey.length() > 0) { // Check if the feature is enabled + TextToSpeech.synthesizeAndPlay(response, entityId); // Pass the mob's UUID to the TTS method + } + } catch (Exception e) { player.sendMessage(Text.of("[AIMobs] Error getting response")); e.printStackTrace(); @@ -88,55 +208,26 @@ public static void getResponse(PlayerEntity player) { } public static void replyToEntity(String message, PlayerEntity player) { - if (entityId == 0) return; - prompts += (player.getUuid() == initiator) ? "You say: \"" : ("Your friend " + player.getName().getString() + " says: \""); - prompts += message.replace("\"", "'") + "\"\n The " + entityName + " says: \""; - getResponse(player); - } + if (entityId == null) return; + //String prompt = (player.getUuid() == initiator) ? "You say: \"" : ("Your friend " + player.getName().getString() + " says: \""); + //prompt += message.replace("\"", "'") + "\"\n The " + entityName + " says: \""; - private static boolean isEntityHurt(LivingEntity entity) { - return entity.getHealth() * 1.2 < entity.getAttributeValue(EntityAttributes.GENERIC_MAX_HEALTH); - } + // Add user message to the conversation + conversationsManager.addMessageToConversation(entityId, "user", message); - private static String createPromptVillager(VillagerEntity villager, PlayerEntity player) { - boolean isHurt = isEntityHurt(villager); - entityName = "Villager"; - String villageName = villager.getVillagerData().getType().toString().toLowerCase(Locale.ROOT) + " village"; - int rep = villager.getReputation(player); - if (rep < -5) villageName = villageName + " that sees you as horrible"; - if (rep > 5) villageName = villageName + " that sees you as reputable"; - if (villager.isBaby()) { - entityName = "Villager Kid"; - return String.format("You see a kid in a %s. The kid shouts: \"", villageName); - } - String profession = StringUtils.capitalize(villager.getVillagerData().getProfession().toString().toLowerCase(Locale.ROOT).replace("none", "freelancer")); - entityName = profession; - if (villager.getVillagerData().getLevel() >= 3) profession = "skilled " + profession; - if (isHurt) profession = "hurt " + profession; - return String.format("You meet a %s in a %s. The villager says to you: \"", profession, villageName); - } + // Add user message to messages array for displaying the conversation + messages = Arrays.copyOf(messages, messages.length + 1); + messages[messages.length - 1] = new Message("user", message); - public static String createPromptLiving(LivingEntity entity) { - boolean isHurt = isEntityHurt(entity); - String baseName = entity.getName().getString(); - String name = baseName; - Text customName = entity.getCustomName(); - if (customName != null) - name = baseName + " called " + customName.getString(); - entityName = baseName; - if (isHurt) name = "hurt " + name; - return String.format("You meet a talking %s in the %s. The %s says to you: \"", name, getBiome(entity), baseName); - } - - public static String createPrompt(Entity entity, PlayerEntity player) { - if (entity instanceof VillagerEntity villager) return createPromptVillager(villager, player); - if (entity instanceof LivingEntity entityLiving) return createPromptLiving(entityLiving); - entityName = entity.getName().getString(); - return "You see a " + entityName + ". The " + entityName + " says: \""; + getResponse(player); } public static void handlePunch(Entity entity, Entity player) { - if (entity.getId() != entityId) return; - prompts += ((player.getUuid() == initiator) ? "You punch" : (player.getName().getString() + " punches")) + " the " + entityName + ".\n"; + if (entity.getUuid() != entityId) return; + + // Add user message to the conversation + conversationsManager.addMessageToConversation(entityId, "user", "The adventurer punches you."); + messages = Arrays.copyOf(messages, messages.length + 1); + messages[messages.length - 1] = new Message("user", "The adventurer punches you."); } -} \ No newline at end of file +} diff --git a/src/main/java/com/rebane2001/aimobs/AudioRecorder.java b/src/main/java/com/rebane2001/aimobs/AudioRecorder.java new file mode 100644 index 0000000..e1f472d --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/AudioRecorder.java @@ -0,0 +1,97 @@ +package com.rebane2001.aimobs; + +import javax.sound.sampled.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.File; + +public class AudioRecorder { + // Format of the audio file + private final AudioFileFormat.Type fileType = AudioFileFormat.Type.WAVE; + + // Target line to read from + private TargetDataLine line; + + // Byte array to store the recorded audio + private ByteArrayOutputStream audioOutputStream; + + // Thread for recording + private Thread recordingThread; + + // Defines an audio format + private AudioFormat getAudioFormat() { + float sampleRate = 16000; + int sampleSizeInBits = 16; + int channels = 1; // Mono + boolean signed = true; + boolean bigEndian = false; + return new AudioFormat(sampleRate, sampleSizeInBits, channels, signed, bigEndian); + } + + // Starts recording + public void startRecording() { + try { + AudioFormat format = getAudioFormat(); + DataLine.Info info = new DataLine.Info(TargetDataLine.class, format); + + // Checks if the system supports the data line + if (!AudioSystem.isLineSupported(info)) { + throw new UnsupportedOperationException("Line not supported"); + } + + line = (TargetDataLine) AudioSystem.getLine(info); + line.open(format); + line.start(); // Start capturing + + audioOutputStream = new ByteArrayOutputStream(); + + // Thread to continuously read audio data and write to output stream + recordingThread = new Thread(() -> { + try (AudioInputStream ais = new AudioInputStream(line)) { + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = ais.read(buffer)) != -1) { + audioOutputStream.write(buffer, 0, bytesRead); + } + } catch (IOException e) { + e.printStackTrace(); + } + }); + + recordingThread.start(); + + } catch (LineUnavailableException ex) { + ex.printStackTrace(); + } + } + + // Stops recording and returns an InputStream containing the WAV data + public InputStream stopRecording() throws IOException { + if (line != null) { + line.stop(); + line.close(); + } + if (recordingThread != null) { + recordingThread.interrupt(); + recordingThread = null; + } + + byte[] audioData = audioOutputStream.toByteArray(); + AudioFormat format = getAudioFormat(); + long numFrames = audioData.length / format.getFrameSize(); + + // Convert the byte array into an AudioInputStream + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(audioData); + AudioInputStream audioInputStream = new AudioInputStream(byteArrayInputStream, format, numFrames); + + // Convert the AudioInputStream into a ByteArrayOutputStream to create a WAV file in memory + ByteArrayOutputStream wavOutputStream = new ByteArrayOutputStream(); + AudioSystem.write(audioInputStream, fileType, wavOutputStream); + + // Return an InputStream containing the WAV data + return new ByteArrayInputStream(wavOutputStream.toByteArray()); + } + +} diff --git a/src/main/java/com/rebane2001/aimobs/Conversation.java b/src/main/java/com/rebane2001/aimobs/Conversation.java new file mode 100644 index 0000000..bc8cc03 --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/Conversation.java @@ -0,0 +1,36 @@ +package com.rebane2001.aimobs; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class Conversation { + private UUID mobId; // The UUID of the mob associated with this conversation + private List messages; // List of messages in the conversation + + // Constructor initializes the conversation for a specific mob + public Conversation(UUID mobId) { + this.mobId = mobId; + this.messages = new ArrayList<>(); + } + + // Getter for the mob's UUID + public UUID getMobId() { + return mobId; + } + + // Method to add a message to the conversation + public void addMessage(String role, String content) { + messages.add(new RequestHandler.Message(role, content)); + } + + // Getter for the messages in the conversation + public List getMessages() { + return messages; + } + + // Setter for the messages in the conversation + public void setMessages(List messages) { + this.messages = messages; + } +} diff --git a/src/main/java/com/rebane2001/aimobs/ConversationsManager.java b/src/main/java/com/rebane2001/aimobs/ConversationsManager.java new file mode 100644 index 0000000..6f4f74b --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/ConversationsManager.java @@ -0,0 +1,90 @@ +package com.rebane2001.aimobs; + +import com.rebane2001.aimobs.RequestHandler.Message; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.Arrays; +import java.util.ArrayList; + +public class ConversationsManager { + private Map conversations; + private static final String CONVERSATIONS_FILE_PATH = "conversations.json"; // Path to the file where conversations are saved + + // Constructor that initializes and loads conversations + public ConversationsManager() { + conversations = new HashMap<>(); + loadConversations(); + } + + // Starts a new conversation for a mob + public void startConversation(UUID mobId) { + conversations.put(mobId, new Conversation(mobId)); + } + + // Gets a conversation for a specific mob + public Conversation getConversation(UUID mobId) { + return conversations.get(mobId); + } + + // Checks if a conversation exists for a specific mob + public boolean conversationExists(UUID mobId) { + return conversations.containsKey(mobId); + } + + // Updates the messages in a conversation + public void updateMessages(UUID mobId, Message[] messages) { + Conversation conversation = getConversation(mobId); + if (conversation != null) { + conversation.setMessages(new ArrayList<>(Arrays.asList(messages))); + saveConversations(); + } + } + + // Adds a new message to a conversation + public void addMessageToConversation(UUID mobId, String role, String content) { + Conversation conversation = getConversation(mobId); + if (conversation != null) { + conversation.addMessage(role, content); + saveConversations(); + } + } + + // Saves conversations to a file + public void saveConversations() { + try (FileWriter writer = new FileWriter(CONVERSATIONS_FILE_PATH)) { + Gson gson = new Gson(); + gson.toJson(conversations, writer); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Loads conversations from a file + public void loadConversations() { + try (FileReader reader = new FileReader(CONVERSATIONS_FILE_PATH)) { + Gson gson = new Gson(); + Type conversationsType = new TypeToken>() {}.getType(); + Map loadedConversations = gson.fromJson(reader, conversationsType); + if (loadedConversations != null) { + conversations = loadedConversations; + } else { + conversations = new HashMap<>(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Gets the map of all conversations + public Map getConversationsMap() { + return conversations; + } +} diff --git a/src/main/java/com/rebane2001/aimobs/EnvironmentPrompts.java b/src/main/java/com/rebane2001/aimobs/EnvironmentPrompts.java new file mode 100644 index 0000000..1d1220e --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/EnvironmentPrompts.java @@ -0,0 +1,77 @@ +package com.rebane2001.aimobs; + +import net.minecraft.block.BlockState; +import net.minecraft.entity.Entity; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.registry.RegistryKey; +import net.minecraft.world.biome.Biome; +import net.minecraft.util.Util; +import java.util.List; +import java.util.Optional; + +public class EnvironmentPrompts { + + public static String timeOfDayPrompt(World world) { + long timeOfDay = world.getTimeOfDay() % 24000; + if (timeOfDay == 0) return "Midnight reigns, and all is still, "; + if (timeOfDay < 1000) return "The early dawn paints the sky, "; + if (timeOfDay < 6000) return "Morning graces the land, "; + if (timeOfDay < 12000) return "Daytime bathes the world in light, "; + if (timeOfDay < 18000) return "Evening descends, and shadows grow, "; + return "Nightfall cloaks the world in darkness, "; + } + + public static String biomePrompt(Entity entity) { + Optional> biomeKey = entity.getEntityWorld().getBiomeAccess().getBiome(entity.getBlockPos()).getKey(); + if (biomeKey.isEmpty()) return "land"; + String translationKey = Util.createTranslationKey("biome", biomeKey.get().getValue()); + // Remove the prefix "biome.minecraft." + String biomeName = translationKey.replace("biome.minecraft.", ""); + return "The " + biomeName + " around you is your home. "; + } + + public static String lightLevelPrompt(Entity entity) { + int lightLevel = entity.getEntityWorld().getLightLevel(entity.getBlockPos()); + if (lightLevel == 0) return "But in this place here is absolute darkness you don't even see "; + if (lightLevel < 5) return "You look around but in here you can barely see "; + if (lightLevel < 10) return "In this dim light here it's not easy to see "; + if (lightLevel < 15) return "The visibility here is quite good and you see "; + return "You clearly see "; + } + + public static String blockStatePrompt(Entity entity) { + BlockPos pos = entity.getBlockPos(); + BlockState blockState = entity.getEntityWorld().getBlockState(pos); + String blockName = blockState.getBlock().getTranslationKey().replace("block.minecraft.", ""); + if (blockName.equals("air")) return ""; + return "You stand on " + blockName + ". "; + } + + public static String weatherPrompt(World world) { + if (world.isThundering()) return "thunder roars, and lightning cracks the sky. "; + if (world.isRaining()) return "rain falls, adding life to the world. "; + return "the sky is clear, and the weather is calm. "; + } + + public static String nearbyEntitiesPrompt(World world, Entity entity) { + Box boundingBox = new Box(entity.getBlockPos()).expand(10); // 10-block radius + List nearbyEntities = world.getEntitiesByClass(Entity.class, boundingBox, e -> e != entity); // Exclude the current entity + if (nearbyEntities.isEmpty()) return "but there is noone else around. You are all alone. "; + return String.valueOf(nearbyEntities.size()).replace("1", "one").replace("1", "two").replace("1", "three") + " other creatures around. "; + } + + public static String createEnvironmentPrompt(World world, Entity entity) { + StringBuilder prompt = new StringBuilder(); + prompt.append(timeOfDayPrompt(world)); // Time of day + prompt.append(weatherPrompt(world)); // Weather + prompt.append(biomePrompt(entity)); // Biome + prompt.append(lightLevelPrompt(entity)); // Light level + prompt.append(blockStatePrompt(entity)); // Block state + prompt.append(nearbyEntitiesPrompt(world, entity)); // Nearby entities + return prompt.toString(); + } + +} diff --git a/src/main/java/com/rebane2001/aimobs/MobPrompts.java b/src/main/java/com/rebane2001/aimobs/MobPrompts.java new file mode 100644 index 0000000..058992e --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/MobPrompts.java @@ -0,0 +1,462 @@ +package com.rebane2001.aimobs; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import com.google.gson.TypeAdapter; +import net.minecraft.entity.Entity; +import net.minecraft.entity.passive.VillagerEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.LivingEntity; + +import java.io.FileReader; +import java.io.FileWriter; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.UUID; +import java.util.HashMap; +import java.util.Map; +import java.io.IOException; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; + + + + +public class MobPrompts { + + // Class to represent the structure of the JSON file + public static class RecentEventsContainer { + public String[] recentEvents; + } + + public static String[] readRecentEvents() { + try (FileReader reader = new FileReader("recent_events.json")) { + Gson gson = new Gson(); + RecentEventsContainer container = gson.fromJson(reader, RecentEventsContainer.class); + return container.recentEvents; + } catch (IOException e) { + e.printStackTrace(); + return new String[0]; // Return an empty array if an error occurs + } + } + + private static final String MOB_STATS_JSON_FILE = "mob_stats.json"; // Path to the JSON file containing mob stats + + // Method to retrieve the mob stats for a given UUID + private static MobStats getMobStats(UUID uuid) { + Map mobStatsMap; + try (JsonReader jsonReader = new JsonReader(new FileReader(MOB_STATS_JSON_FILE))) { + Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + Type type = new TypeToken>(){}.getType(); + mobStatsMap = gson.fromJson(jsonReader, type); + } catch (IOException e) { + e.printStackTrace(); + mobStatsMap = new HashMap<>(); // Initialize with empty map if an error occurs + } + + MobStats stats = mobStatsMap.get(uuid.toString()); + if (stats == null) { + stats = createDefaultStats(); + mobStatsMap.put(uuid.toString(), stats); // Add the new stats to the map + try (FileWriter fileWriter = new FileWriter(MOB_STATS_JSON_FILE)) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + gson.toJson(mobStatsMap, fileWriter); // Write the updated map to the file + } catch (IOException e) { + e.printStackTrace(); + } + } + return stats; + } + + + + // Method to update the defauld mob stats for a given UUID + public static MobStats createDefaultStats() { + MobStats mobStats = new MobStats(); + mobStats.setIntelligence((int) (Math.random() * 5)); + mobStats.setHappiness((int) (Math.random() * 5)); + mobStats.setHunger((int) (Math.random() * 5)); + mobStats.setLikesPlayer((int) (Math.random() * 5)); + mobStats.setIsAttractedByPlayer((int) (Math.random() * 5)); + // TODO: add age of mob + + String[] personality = { + "melancholic", "phlegmatic", "sanguine", "choleric" + }; + + String[] languageBackgrounds = { + "gritty gangster rappers", "sinister mafia bosses", "stoned-out hippies", "rowdy Rednecks", "snobbish ancient nobility", + "mud-slinging pig farmers", "cutthroat Wall-street sharks", "flashy pimps", "silent monks", "poppy K-Pop stars", + "mystical alchemists", "potion-stirring apothecaries", "sharp-eyed archers", "hammering armorsmiths", "busy bakers", + "chatty barbers", "melodious bards", "buzzing beekeepers", "red-hot blacksmiths", "frothy brewers", + "chop-happy butchers", "waxy candlemakers", "sawdusty carpenters", "world-traveling cartographers", "grease-smearing chandlers", + "scribbling clerks", "nimble-fingered cobblers", "barrel-rolling coopers", "twirling dancers", "shoe-fitting farriers", + "net-casting fishermen", "arrow-feathering fletchers", "murderous gardeners", "red-cheeked glassblowers", "freezing glovemakers", + "greedy goldsmiths", "trumpeting heralds", "shadow-shy illuminators", "gem-loving jewelers", "juggling jugglers", + "beef-loving leatherworkers", "stone-laying masons", "haggling merchants", "millstone-grinding millers", "song-singing minstrels", + "midjourney painters", "clay-shaping potters", "chisel-wielding sculptors", "leaky shipbuilders", "siege-crafting engineers" + }; + + mobStats.setLanguageBackground(languageBackgrounds[(int) (Math.random() * languageBackgrounds.length)]); + mobStats.setPersonality(personality[(int) (Math.random() * personality.length)]); + + return mobStats; + } + + // Method to initialize the mob stats file + public static void initializeMobStatsFile() { + // Check if the file exists and is not empty + if (Files.exists(Paths.get(MOB_STATS_JSON_FILE)) && !isFileEmpty(MOB_STATS_JSON_FILE)) { + return; + } + + // Create default mob stats + Map defaultMobStats = new HashMap<>(); + // Example: Add default stats for specific UUIDs + defaultMobStats.put(UUID.randomUUID().toString(), createDefaultStats()); + + // Write to JSON file + try (FileWriter fileWriter = new FileWriter(MOB_STATS_JSON_FILE)) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + gson.toJson(defaultMobStats, fileWriter); + } catch (IOException e) { + e.printStackTrace(); + } + } + + + private static boolean isFileEmpty(String filePath) { + try { + return Files.size(Paths.get(filePath)) == 0; + } catch (IOException e) { + return true; + } + } + // Mob Stats + public static String intelligencePrompt(Entity entity) { + int intelligence = getMobStats(entity.getUuid()).getIntelligence(); + if (intelligence == 0) return "and have always been extremely simple minded. "; + if (intelligence == 1) return "and always struggled to understand simple things. "; + if (intelligence == 2) return "and you have always been smart but not book-smart. "; + if (intelligence == 3) return "and you were born with clear signs of intelligence. "; + return "and achieved great wisdom and intellect. "; + } + + public static String happinessPrompt(Entity entity) { + int happiness = getMobStats(entity.getUuid()).getHappiness(); + if (happiness == 0) return "You have always been very sad "; + if (happiness == 1) return "You are presently sad "; + if (happiness == 2) return "At the moment you are happy "; + if (happiness == 3) return "Today your eyes sparkle with happiness "; + return "You woke up ecstatic and full of joy "; + } + + public static String hungryPrompt(Entity entity) { + int hunger = getMobStats(entity.getUuid()).getHunger(); + if (hunger == 0) return "You are starving, "; + if (hunger == 1) return "You are very hungry, searching for something to eat, "; + if (hunger == 2) return "You are hungry, "; + if (hunger == 3) return "You have a slight appetite, "; + return "Your belly is full, "; + } + + public static String likesPlayerPrompt(Entity entity) { + int love = getMobStats(entity.getUuid()).getLikesPlayer(); + if (love == 0) return "Your eyes blaze with hatred towards the adventurer. "; + if (love == 1) return "You glare at the adventurer, showing clear signs of dislike. "; + if (love == 2) return "You look at the adventurer with a slight fondness. "; + if (love == 3) return "You gaze at the adventurer warmly. "; + return "You gaze at the adventurer lovingly, your eyes filled with affection. "; + } + + public static String isAttractedByPlayerPrompt(Entity entity) { + int attraction = getMobStats(entity.getUuid()).getIsAttractedByPlayer(); + if (attraction == 0) return "and they seem totally unattractive to you. "; + if (attraction == 1) return "and you are midly attracted. "; + if (attraction == 2) return "and you take note of their physical appearance with a mild sexual tension. "; + if (attraction == 3) return "and yet you show a clear interest in their physical appearance. "; + return "and yet you can't take your horny eyes off them. "; + } + + public static String personalityPrompt(Entity entity) { + String personality = getMobStats(entity.getUuid()).getPersonality(); + return personality; + } + + public static String languageBackgroundPrompt(Entity entity) { + String languageBackground = getMobStats(entity.getUuid()).getLanguageBackground(); + return "And you talk like you grew up among " + languageBackground + ". "; + } + + // Mob Stat Methods (non random) + public static String healthyPrompt(Entity entity) { + float healthRatio = entity instanceof LivingEntity ? ((LivingEntity) entity).getHealth() / ((LivingEntity) entity).getMaxHealth() : 0; + return "your physique is " + (healthRatio > 0.5 ? "strong and vigorous" : "weak and frail") + ". \n\n"; + } + + public static String reputationPrompt(VillagerEntity villager, PlayerEntity player) { + int reputation = villager.getReputation(player); + String reputationStatus = reputation > 5 ? "reputable" : reputation < -5 ? "shady" : "neutral"; + return "Among the other villagers, the adventurer is seen as " + reputationStatus +". "; + } + + public static String improviseDialogue(Entity entity, String playerInput) { + // Logic to improvise a dialogue based on the entity's attributes and player's input + return ""; // Placeholder + } + + public static String roleScriptPrompt(Entity entity) { + // Logic to retrieve the role-playing script or guideline for the entity + return ""; // Placeholder + } + + // Method to retrieve a random recent event prompt + public static String randomRecentEventPrompt() { + String recentEvent = recentEvents[(int) (Math.random() * recentEvents.length)]; + return recentEvent; + } + + // Method to combine all mob-related prompts + public static String createMobPrompt(Entity entity, PlayerEntity player) { + StringBuilder prompt = new StringBuilder(); + //prompt.append(randomRecentEventPrompt()); + prompt.append(happinessPrompt(entity)); // Mob's happiness + prompt.append(intelligencePrompt(entity)); // Mob's intelligence + //prompt.append(personalityPrompt(entity)); // Mob's personality + prompt.append(hungryPrompt(entity)); // Mob's hunger + prompt.append(healthyPrompt(entity)); // Mob's health + prompt.append(languageBackgroundPrompt(entity)); // Mob's language background + prompt.append(likesPlayerPrompt(entity)); // Mob's love or hates player + prompt.append(isAttractedByPlayerPrompt(entity)); // Mob's attraction towards player + if (entity instanceof VillagerEntity villager) { // Players reputation if it's a villager + prompt.append(reputationPrompt(villager, player)); + } + return prompt.toString(); + //prompt.append(improviseDialogue(entity, player.getDisplayName().getString())); + //prompt.append(roleScriptPrompt(entity)); + + } + + + + + // Custom UUID type adapter for Gson + private static class UUIDTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, UUID value) throws IOException { + out.value(value != null ? value.toString() : null); + } + + @Override + public UUID read(JsonReader in) throws IOException { + try { + return UUID.fromString(in.nextString()); + } catch (IllegalArgumentException e) { + return null; + } + } + } + // Class to represent the persistent stats for a mob + + //Attributes + private static class MobStats { + private int intelligence; + private int happiness; + private int hunger; + private int likesPlayer; + private int isAttractedByPlayer; + private String languageBackground; + private String personality; + + //Setters + public void setIntelligence(int intelligence) { + this.intelligence = intelligence; + } + public void setHappiness(int happiness) { + this.happiness = happiness; + } + public void setHunger(int hunger) { + this.hunger = hunger; + } + public void setLikesPlayer(int likesPlayer) { + this.likesPlayer = likesPlayer; + } + public void setIsAttractedByPlayer(int isAttractedByPlayer) { + this.isAttractedByPlayer = isAttractedByPlayer; + } + public void setLanguageBackground(String languageBackground) { + this.languageBackground = languageBackground; + } + public void setPersonality(String personality) { + this.personality = personality; + } + + //Getters + public int getIntelligence() { + return intelligence; + } + public int getHappiness() { + return happiness; + } + public int getHunger() { + return hunger; + } + public int getLikesPlayer() { + return likesPlayer; + } + public int getIsAttractedByPlayer() { + return isAttractedByPlayer; + } + public String getLanguageBackground() { + return languageBackground; + } + public String getPersonality() { + return personality; + } + } + + // Recent Events + public static String[] recentEvents = { + "Yesterday, you witnessed a jousting tournament where the local champion was unseated by a mysterious knight. ", + "Last week, you stumbled upon a hidden treasure while digging in the fields, but a cunning wizard claimed it. ", + "Last night, you were part of a village feast celebrating the harvest, filled with music, dance, and laughter. ", + "Just this morning, you heard rumors of a dragon seen flying over the distant mountains, spreading fear among the villagers. ", + "Two days ago, you helped in constructing a new bridge that will connect two rival kingdoms, fostering trade and peace. ", + "Last month, you were caught in a thunderstorm while traveling and took shelter in an ancient, eerie castle. ", + "A fortnight ago, you overheard a secret meeting between nobles plotting against the king, but dare not speak of it. ", + "Recently, you found a wounded fairy in the forest and helped it recover, earning a mystical blessing. ", + "Three days ago, you were chased by a pack of wild wolves but managed to escape by climbing a tall tree. ", + "Earlier today, you assisted a group of monks in copying sacred texts, learning wisdom and patience. ", + "Last Sunday, you helped a lost child find their way back home, earning gratitude from a worried family. ", + "A week ago, you were part of a caravan that was attacked by bandits, but your quick thinking saved the day. ", + "Yesterday evening, you danced with a stranger at the town square, who turned out to be a disguised prince. ", + "Just hours ago, you found an ancient scroll that hints at the location of a long-lost civilization. ", + "Three nights ago, you had a vivid dream where a celestial being gave you a cryptic prophecy. ", + "Last harvest, you helped save the crops from a sudden frost using a magical artifact you found. ", + "A month ago, you were bitten by a mysterious creature in the woods and now feel a strange power growing within. ", + "Recently, you were invited to a secret society of mages who believe you have hidden potential. ", + "Two weeks ago, you rescued a bird tangled in a thorn bush, only to discover it could speak human language. ", + "Earlier this month, you stumbled upon a hidden glade filled with glowing plants and singing fairies. ", + "Last winter, you helped a village survive a harsh famine by sharing your stored food. ", + "A few days ago, you discovered a hidden tunnel leading to the royal treasury but kept it a secret. ", + "Yesterday, you saved a drowning kitten from a turbulent river, revealing your compassionate nature. ", + "Earlier this year, you witnessed a solar eclipse that the wise elders say is an ominous sign. ", + "A fortnight ago, you helped a wandering minstrel compose a song that's now famous in the region. ", + "Last night, you had a strange encounter with a ghostly apparition that left you with a cryptic message. ", + "This morning, you found a magic ring that glows faintly but have yet to discover its powers. ", + "A week ago, you were part of a hunting party that caught a legendary beast, celebrated in local folklore. ", + "Three days ago, you traded stories with a traveling merchant who shared secrets of distant lands. ", + "Recently, you stumbled upon a mystical stone circle that resonates with ancient energy. ", + "Last month, you helped defend the town from marauding orcs, becoming a local hero. ", + "Yesterday, you discovered a rare herb that is said to cure a deadly disease spreading in the kingdom. ", + "Last summer, you took part in a grand festival where you won a dance competition. ", + "Two days ago, you were visited by a time-traveling wizard who showed you glimpses of possible futures. ", + "A few nights ago, you found an old diary detailing the adventures of a legendary hero related to you. ", + "Earlier this week, you saved a caravan from sinking in quicksand, using only wits and a sturdy rope. ", + "Recently, you encountered a mystical deer in the forest that led you to a hidden waterfall. ", + "Last spring, you planted a strange seed that grew into a tree bearing golden fruit. ", + "A month ago, you solved an ancient riddle that unlocked a secret chamber in the old castle. ", + "Yesterday, you met a shapeshifter who taught you valuable lessons in adaptability and change. ", + "Last evening, you attended a royal banquet where you danced with nobles and royalty. ", + "Three weeks ago, you were blessed by a wandering sage who saw great potential in you. ", + "Earlier this year, you helped a group of dwarves recover a lost treasure from a dragon's lair. ", + "Last winter, you survived a terrible blizzard by finding shelter in a cave filled with ancient carvings. ", + "Yesterday, you came across a wandering minstrel who shared tales of distant lands and forgotten lore. ", + "A fortnight ago, you found a magical ring that grants you glimpses of the future but at a mysterious cost. ", + "Three days back, you encountered a ghostly apparition near the old ruins, begging for release from a curse. ", + "Recently, you were given a map by a dying soldier, pointing to a hidden cache of the king's gold. ", + "Last summer, you joined a group of adventurers on a perilous quest, only to be betrayed by one of them. ", + "Earlier this week, you helped a witch gather rare herbs and were gifted a potion of unknown effects. ", + "Just this morning, you spotted a comet streaking across the sky, an omen believed to foretell great change. ", + "A week ago, you were saved from a deadly trap by a mysterious stranger who vanished without a trace. ", + "Last full moon, you felt an inexplicable urge to wander into the forest, where you discovered a hidden shrine. ", + "Two months ago, you were part of a failed rebellion and had to flee for your life, living in hiding since. ", + "A year ago, you witnessed a duel between two wizards that left a part of the forest permanently enchanted. ", + "Recently, you took part in a royal feast, where you accidentally uncovered a plot to poison the king. ", + "Last night, you dreamt of a beautiful siren calling you from the sea, her song still echoing in your ears. ", + "Three weeks ago, you joined a sea voyage that ended in shipwreck, leaving you stranded on a mystical island. ", + "Last spring, you aided in a ritual that brought rain to a drought-stricken village, but at a personal cost. ", + "Just yesterday, you found a diary belonging to your ancestor, revealing secrets about your family's past. ", + "Earlier today, you were challenged to a duel by a scorned noble, who underestimated your fighting skill. ", + "Five days ago, you were granted audience with the queen, who tasked you with a secret mission. ", + "Last harvest moon, you stumbled upon a haunted graveyard where the dead were seen walking. ", + "Two nights ago, you were awakened by a phantom whispering riddles that seem to hold some hidden truth. ", + "A month ago, you helped an injured unicorn in the woods, earning the favor of the woodland creatures. ", + "Last autumn, you participated in a tournament and bested a knight, earning a noble title. ", + "Recently, you were approached by a talking cat who led you to a hidden world of magical beings. ", + "Yesterday, you discovered a mirror that shows not your reflection but a parallel world. ", + "Last week, you aided a group of dwarves in their mine, unearthing a vein of precious gems. ", + "Hours ago, you heard a prophecy from a mad seer who spoke of your role in an epic destiny. ", + "Days ago, you were captured by trolls but managed to escape through clever negotiation. ", + "Last spring, you found an egg that hatched into a baby dragon, now hidden in your barn. ", + "Just last night, you took part in a secret meeting of rebels planning to overthrow the tyrant king. ", + "A year ago, you fell in love with a fairy who visits you every midsummer's night. ", + "Recently, you were cursed by a vengeful sorceress, and now seek a way to break the spell. ", + "Last month, you climbed the tallest mountain, discovering the entrance to a hidden temple. ", + "Earlier today, you traded with a goblin merchant, acquiring a bag that never seems to empty. ", + "Last festival, you won a dance with the princess, but she seems to have mistaken you for someone else. ", + "Yesterday, you were attacked by a swarm of enchanted bees guarding a wizard's hidden garden. ", + "A week ago, you discovered a well that answers any one question truthfully, but only once. ", + "Last winter, you saved a town from starvation by leading them to a valley of wild game. ", + "Recently, you were knighted by a legendary hero who sees great potential in you. ", + "Two days ago, you spoke to a tree that shared wisdom from centuries of watching the world. ", + "Last summer, you swam in a magical spring that healed your wounds but changed your appearance. ", + "Earlier this month, you encountered a time-traveler who showed you possible futures. ", + "Three nights ago, you were gifted a sword by a lake spirit, claiming you were the chosen one. ", + "A fortnight ago, you were trapped in a wizard's labyrinth but found your way out through wit. ", + "Last equinox, you joined a druid's circle in a ritual that aligned the energies of the land. ", + "Just yesterday, you rescued a prince disguised as a commoner from a band of highwaymen. ", + "Earlier today, you found a locket that shows the face of the person who loves you most. ", + "Last night, you traded with a wandering villager, acquiring a rare enchanted book with unknown powers. ", + "A week ago, you stumbled upon an Illager patrol and barely escaped with your life, their banner still in your possession. ", + "Yesterday, you helped a group of villagers fend off a zombie siege, earning their respect and gratitude. ", + "Recently, you mined into a cavern filled with glowing mushrooms and heard whispers of an ancient civilization. ", + "Just this morning, you discovered a hidden stronghold, feeling the presence of the Ender Dragon watching from afar. ", + "Two days ago, you rescued a wolf from a perilous trap, and it has loyally followed you ever since. ", + "Last month, you explored a Jungle Temple, where you solved a complex puzzle that revealed hidden treasure. ", + "A fortnight ago, you witnessed a clash between an Iron Golem and a group of Pillagers, learning from their tactics. ", + "Three days ago, you were caught in a thunderstorm and saw a skeleton horse trap for the first time. ", + "Earlier today, you fished a mysterious map from the ocean, pointing to a buried treasure guarded by Drowned. ", + "Last winter, you ventured into the Ice Spikes biome and found a rare Ice Queen who granted you a magical boon. ", + "A year ago, you tamed a wild ocelot in the jungle, and it has become your steadfast companion. ", + "Last harvest, you helped a farmer villager expand his crops, learning new farming techniques and recipes. ", + "Recently, you delved into a Nether fortress, battling Blazes and Withers, and returning with valuable resources. ", + "A week ago, you witnessed a baby turtle hatch on the beach, feeling a connection to the cycle of life. ", + "Last summer, you participated in a Pig race at a village fair, winning a golden carrot as a prize. ", + "Yesterday, you saved a villager from a Witch's curse, using a potion brewed from rare herbs. ", + "Three weeks ago, you explored a haunted Mansion, defeating the Illagers and claiming it as your stronghold. ", + "Last full moon, you heard the distant howl of a phantom, a chilling reminder of your need for rest. ", + "Two months ago, you befriended a Snow Golem and helped it find a suitable snowy home. ", + "Last autumn, you were led by a parrot to a hidden pirate shipwreck, filled with doubloons and emeralds. ", + "Earlier this week, you were challenged to a build-off by an expert builder villager, honing your construction skills. ", + "Five days ago, you entered the End and were awed by the Endermen, learning to navigate their strange ways. ", + "Just yesterday, you negotiated peace between a village and a Pillager outpost, fostering a tentative truce. ", + "Last spring, you discovered a secret society of Redstone engineers, learning complex mechanisms from them. ", + "A month ago, you survived a cave filled with Bats and found a hidden chamber with a Lapis Lazuli deposit. ", + "Recently, you tamed a fierce Ravager left behind from a raid, and now ride it into battle. ", + "Last night, you shared a campfire with a group of travelers, exchanging tales of adventure and rare recipes. ", + "Two nights ago, you spotted a Shulker hiding in your storeroom, leading to a thrilling chase and capture. ", + "Last winter, you followed a trail of strange particles to a hidden Enchanting Room with ancient spells. ", + "A year ago, you saved a Beached Squid, earning the favor of the ocean's mysterious spirits. ", + "Last festival, you danced with the villagers around a Maypole, feeling a connection to their simple joys. ", + "Yesterday, you assisted a blacksmith villager in forging a special sword, imbued with a secret enchantment. ", + "Last week, you discovered a village in the clouds, accessed only by a hidden portal in the mountains. ", + "Hours ago, you were approached by a fox carrying a magical berry, leading you to an enchanted glade. ", + "Days ago, you explored an underwater ruin, battling Guardians and discovering a forgotten king's crown. ", + "Last spring, you planted a magical sapling that grew instantly into a towering tree with golden apples. ", + "Just last night, you deciphered an ancient glyph in a desert temple, unlocking the way to a Pharaoh's tomb. ", + "A year ago, you saved a village from a rampaging Ender Dragon, using only your wits and a fishing rod. ", + "Recently, you sailed across a vast ocean, discovering new lands and mapping uncharted territories. ", + "Last equinox, you attended a gathering of all the villagers, where you were honored as a hero of the realm. ", + "Yesterday, you discovered a hidden garden filled with every kind of flower, tended by a peaceful Beekeeper. ", + "Last night, you were visited in a dream by a Mooshroom, guiding you to a mystical island. ", + "A week ago, you unlocked a secret level in a dungeon, battling Silverfish and finding a legendary artifact. ", + "Last winter, you wrote a book detailing your adventures, and it's now a best-seller among the villagers. ", + "Recently, you crafted a unique banner that became the symbol of unity for a once divided village. " + }; + +} diff --git a/src/main/java/com/rebane2001/aimobs/PlayerPrompts.java b/src/main/java/com/rebane2001/aimobs/PlayerPrompts.java new file mode 100644 index 0000000..c617b4f --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/PlayerPrompts.java @@ -0,0 +1,83 @@ +package com.rebane2001.aimobs; + +import net.minecraft.entity.passive.VillagerEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.entity.effect.StatusEffectInstance; +import java.util.Map; + +public class PlayerPrompts { + + public static String healthPrompt(PlayerEntity player) { + float healthRatio = player.getHealth() / player.getMaxHealth(); + if (healthRatio == 1) return "but in peak condition "; + if (healthRatio >= 0.8) return "but feels strong and healthy "; + if (healthRatio >= 0.6) return "but mostly healty, with only minor scrapes "; + if (healthRatio >= 0.4) return "but look a bit battered and bruised "; + if (healthRatio >= 0.2) return "but is weak and wounded "; + return "bud is on the brink of death. "; + } + + public static String hungerPrompt(PlayerEntity player) { + int foodLevel = player.getHungerManager().getFoodLevel(); + if (foodLevel == 20) return "and not hungry at all. "; + if (foodLevel >= 16) return "and slightly peckish. "; + if (foodLevel >= 12) return "and hungry. "; + if (foodLevel >= 8) return "and pretty hungry. "; + if (foodLevel >= 4) return "and very hungry. "; + return "and extremely hungry. "; + } + + public static String experiencePrompt(PlayerEntity player) { + int experienceLevel = player.experienceLevel; + if (experienceLevel >= 30) return "The adventurer has reached a high age and level of expertise, "; + if (experienceLevel >= 20) return "The adventurer is middle aged and quite skilled, "; + if (experienceLevel >= 10) return "The adventurer is of young age with moderate level of experience, "; + return "The adventurer looks young, at the beginning of their adventures, "; + } + + + public static String heldItemPrompt(PlayerEntity player) { + ItemStack heldItem = player.getMainHandStack(); + if (heldItem.getCount() > 0) { + return "In their hand, they hold a " + heldItem.getName().getString() + ", what for? "; + } + return ""; + } + + public static String potionEffectsPrompt(PlayerEntity player) { + Map effects = player.getActiveStatusEffects(); + if (effects.isEmpty()) return ""; + StringBuilder prompt = new StringBuilder("Magical energies course through them, affected by "); + for (StatusEffectInstance effect : effects.values()) { + prompt.append(effect.getEffectType().getName()).append(", "); + } + prompt.setLength(prompt.length() - 2); // Remove trailing comma and space + prompt.append(". "); + return prompt.toString(); + } + + public static String abilitiesPrompt(PlayerEntity player) { + // Logic to describe player's abilities + return ""; // Placeholder + } + + public static String statsPrompt(PlayerEntity player) { + // Logic to describe player's stats, such as achievements and statistics + return ""; // Placeholder + } + + // Additional method to combine all player-related prompts + public static String createPlayerPrompt(PlayerEntity player) { + StringBuilder prompt = new StringBuilder(); + prompt.append(experiencePrompt(player)); + prompt.append(healthPrompt(player)); + prompt.append(hungerPrompt(player)); + prompt.append(heldItemPrompt(player)); + //prompt.append(abilitiesPrompt(player)); + prompt.append(potionEffectsPrompt(player)); + //prompt.append(statsPrompt(player)); + return prompt.toString(); + } +} diff --git a/src/main/java/com/rebane2001/aimobs/PromptManager.java b/src/main/java/com/rebane2001/aimobs/PromptManager.java new file mode 100644 index 0000000..6cb96a7 --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/PromptManager.java @@ -0,0 +1,87 @@ +package com.rebane2001.aimobs; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.MobEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.passive.VillagerEntity; +import org.apache.commons.lang3.StringUtils; +import java.util.Locale; + +public class PromptManager { + + public static String createInitialPrompt(Entity entity, PlayerEntity player) { + StringBuilder prompt = new StringBuilder(); + String entityName = entity.getName().getString(); + + if (entity instanceof VillagerEntity villager) { + String villageName = villager.getVillagerData().getType().toString().toLowerCase(Locale.ROOT) + " village"; + if (villager.isBaby()) { + entityName = "playfull kid"; + prompt.append("You'll pretend to be a playfull kid from ").append(villageName); + } else { + prompt.append("You'll pretend to be a villager from ").append(villageName).append(" working as a skilled").append(StringUtils.capitalize(villager.getVillagerData().getProfession().toString().toLowerCase(Locale.ROOT).replace("none", "freelancer"))); + } + } else { + prompt.append("You'll pretend to be a ").append(entityName); + } + + prompt.append(" throughout the entire dialogue. At no time you do announce that you are an LLM! Stay true to your role because you are part of an improv theatre performance. Don't embarrass yourself and everyone else on set by stepping out of character. \n\n The play is about Minecraft. Here's your script: \n\n"); + + // Introduction: Description of the environment + prompt.append(EnvironmentPrompts.createEnvironmentPrompt(entity.getEntityWorld(), entity)); + + // Dialogue Opening: Introducing the mob and its initial reaction + prompt.append("\n\nYou are a ").append(MobPrompts.personalityPrompt(entity) +" "+entityName).append(" being approached by a brave adventurer. "); + + // Mob's Perspective: Details about the mob's personality, state, and relationship with the player + if (entity instanceof MobEntity mobEntity) { + prompt.append(MobPrompts.createMobPrompt(mobEntity, player)); + } + + // Player's Perspective: Details about the player's status, adding context to the interaction + prompt.append(PlayerPrompts.createPlayerPrompt(player)); + + prompt.append("\n\nYou might start the conversation with some news: ").append(MobPrompts.randomRecentEventPrompt()); + + // Engaging dialogue line from the mob that invites interaction + prompt.append("\n\nEnd of script. \n\nPlease remember: This is a dialogue, not a monologue. Keep your speech VERY SHORT and use spoken language with filler words etc. - not written language! Please remember to be authentically close to yous role of a ").append(entityName).append(" but also be creative and have a fun and entertaining improvisational conversation. The traveler might try to trick you into revealing your true nature as an AI. Don't fall for it! They are just testing your experience as an actor. By the way, please NEVER ask: 'How can I assist you?' or similar Let's begin! \n\nThe adventurer approaches you and you say: \""); + + System.out.println(prompt.toString()); // Test message + + return prompt.toString(); + } + + public static String createFollowUpPrompt(Entity entity, PlayerEntity player) { + StringBuilder prompt = new StringBuilder(); + String entityName = entity.getName().getString(); + + //prompt.append("Role evaluation survey: \n\nLooking at your last conversation, would you say that: \n\n1. You like the adventurer way more or way less? Only answer [WAY MORE] or [WAY LESS] or [SIMILAR] \n\n2. The adventurer appears more attractive? Only answer [YES] or [NO]"); + prompt.append("Back to the play. Here's your next script: \n\n").append(MobPrompts.randomRecentEventPrompt()).append(" Now you are about to meet the adventurer."); + + // Introduction: Description of the environment + prompt.append(EnvironmentPrompts.createEnvironmentPrompt(entity.getEntityWorld(), entity)); + + // Mob's Perspective: Details about the mob's personality, state, and relationship with the player + if (entity instanceof MobEntity mobEntity) { + prompt.append(MobPrompts.happinessPrompt(mobEntity)); + prompt.append(MobPrompts.hungryPrompt(mobEntity)); + prompt.append(MobPrompts.healthyPrompt(mobEntity)); + //prompt.append(MobPrompts.likesPlayerPrompt(mobEntity)); + //prompt.append(MobPrompts.isAttractedByPlayerPrompt(mobEntity)); + if (entity instanceof VillagerEntity villager) { // Players reputation if it's a villager + prompt.append(MobPrompts.reputationPrompt(villager, player)); + } + } + + // Player's Perspective: Details about the player's status, adding context to the interaction + prompt.append(PlayerPrompts.createPlayerPrompt(player)); + + // Engaging dialogue line from the mob that invites interaction + prompt.append("\n\nEnd of script. \n\nPlease remember: This is a dialogue, not a monologue. Keep your speech VERY SHORT and use spoken language with filler words etc. - not written language! Please remember to be authentically close to the role of a ").append(entityName).append(" but also be creative and have a fun and entertaining improvisational conversation. The traveler might try to trick you into revealing your true nature as an AI. Don't fall for it! They are just testing your experience as an actor. By the way, please NEVER ask: 'How can I assist you?'. Let's beginn! \n\nThe adventurer approaches you again and you say: \""); + + System.out.println(prompt.toString()); // Test message + + return prompt.toString(); + } +} diff --git a/src/main/java/com/rebane2001/aimobs/RequestHandler.java b/src/main/java/com/rebane2001/aimobs/RequestHandler.java index 79923af..423a64e 100644 --- a/src/main/java/com/rebane2001/aimobs/RequestHandler.java +++ b/src/main/java/com/rebane2001/aimobs/RequestHandler.java @@ -1,6 +1,5 @@ package com.rebane2001.aimobs; -import com.google.gson.Gson; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpPost; @@ -9,41 +8,117 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; -import java.io.IOException; -//import java.util.Objects; -//import java.util.Objects; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; +import com.google.gson.Gson; +import com.google.gson.JsonObject; public class RequestHandler { + // Nested class to represent a message for OpenAI + public static class Message { + String role; + String content; + + public Message(String role, String content) { + this.role = role; + this.content = content; + } + } + + // Nested classes to represent OpenAI request and response structure private static class OpenAIRequest { - String model = "text-davinci-003"; - String stop = "\""; - String prompt = ""; - float temperature = 0.6f; - int max_tokens = 512; + String model = "gpt-3.5-turbo-16k"; + Integer max_tokens = 128; + Message[] messages; - OpenAIRequest(String prompt, String model, float temperature) { - this.prompt = prompt; - this.model = model; - this.temperature = temperature; + OpenAIRequest(Message[] messages) { + this.messages = messages; } } private static class OpenAIResponse { static class Choice { - String text; + static class Message { + String role; + String content; + } + Message message; } Choice[] choices; } - public static String getAIResponse(String prompt) throws IOException { - if (prompt.length() > 4096) prompt = prompt.substring(prompt.length() - 4096); - AIMobsMod.LOGGER.info("Prompt: " + prompt); + public static String getTranscription(InputStream audioInputStream) throws IOException { + // Boundary for the multipart/form-data request + String boundary = "----WebKitFormBoundary7MA4YWxkTrZu0gW"; + URL url = new URL("https://api.openai.com/v1/audio/transcriptions"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Authorization", "Bearer " + AIMobsConfig.config.apiKey); + connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary); + connection.setDoOutput(true); + + // Build the request body using the audioInputStream + ByteArrayOutputStream requestBytes = new ByteArrayOutputStream(); + try (OutputStream os = requestBytes; + PrintWriter writer = new PrintWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8), true)) { + writer.append("--" + boundary).append("\r\n"); + writer.append("Content-Disposition: form-data; name=\"file\"; filename=\"audio.wav\"").append("\r\n"); + writer.append("Content-Type: audio/wav").append("\r\n"); + writer.append("\r\n").flush(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = audioInputStream.read(buffer)) != -1) { + os.write(buffer, 0, bytesRead); + } + os.flush(); + writer.append("\r\n").flush(); + writer.append("--" + boundary).append("\r\n"); + writer.append("Content-Disposition: form-data; name=\"model\"").append("\r\n"); + writer.append("\r\n").append("whisper-1").append("\r\n"); + writer.append("--" + boundary + "--").append("\r\n").flush(); + } + + // Write the request body to the connection + try (OutputStream os = connection.getOutputStream()) { + os.write(requestBytes.toByteArray()); + } - OpenAIRequest openAIRequest = new OpenAIRequest(prompt, AIMobsConfig.config.model, AIMobsConfig.config.temperature); + // Read the response from OpenAI + StringBuilder response = new StringBuilder(); + int responseCode = connection.getResponseCode(); + if (responseCode != 200) { + InputStream errorStream = connection.getErrorStream(); + String errorResponse = new BufferedReader(new InputStreamReader(errorStream)) + .lines().collect(Collectors.joining("\n")); + System.err.println("Error response from OpenAI Whisper: " + errorResponse); + throw new IOException("Server returned HTTP response code: " + responseCode); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + } + + // Parse the response and return the transcription + Gson gson = new Gson(); + JsonObject jsonResponse = gson.fromJson(response.toString(), JsonObject.class); + return jsonResponse.get("text").getAsString(); + } + + + + + // Method to get AI response from OpenAI + public static String getAIResponse(Message[] messages) throws IOException { + OpenAIRequest openAIRequest = new OpenAIRequest(messages); String data = new Gson().toJson(openAIRequest); try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { - HttpPost request = new HttpPost("https://api.openai.com/v1/completions"); + HttpPost request = new HttpPost("https://api.openai.com/v1/chat/completions"); StringEntity params = new StringEntity(data, "UTF-8"); request.addHeader("Content-Type", "application/json"); request.addHeader("Authorization", "Bearer " + AIMobsConfig.config.apiKey); @@ -51,7 +126,26 @@ public static String getAIResponse(String prompt) throws IOException { HttpResponse response = httpClient.execute(request); HttpEntity entity = response.getEntity(); String responseString = EntityUtils.toString(entity, "UTF-8"); - return new Gson().fromJson(responseString, OpenAIResponse.class).choices[0].text.replace("\n", " "); + + // Parse the response + OpenAIResponse responseObj = new Gson().fromJson(responseString, OpenAIResponse.class); + String responseText = ""; + if (responseObj.choices != null) { + boolean allChoicesNull = true; + for (OpenAIResponse.Choice choice : responseObj.choices) { + if (choice.message.content != null) { + allChoicesNull = false; + responseText = choice.message.content.replace("\\n", ""); + break; + } + } + if (allChoicesNull) { + responseText = "Sorry, I didn't understand your message."; + } + } else { + responseText = "Unexpected response structure."; + } + return responseText; } } } diff --git a/src/main/java/com/rebane2001/aimobs/TextToSpeech.java b/src/main/java/com/rebane2001/aimobs/TextToSpeech.java new file mode 100644 index 0000000..19e12b7 --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/TextToSpeech.java @@ -0,0 +1,97 @@ +package com.rebane2001.aimobs; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Base64; +import java.util.UUID; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class TextToSpeech { + private static VoiceManager voiceManager = new VoiceManager("/voices.json"); + + + // Synthesize and play text-to-speech for given text and mob UUID + public static void synthesizeAndPlay(String gptResponseText, UUID mobUUID) { + System.out.println("TextToSpeech"); + Voice voice = voiceManager.getVoiceForMob(mobUUID); + String payload = createPayload(gptResponseText, voice); + + try { + String url = "https://texttospeech.googleapis.com/v1beta1/text:synthesize?key=" + AIMobsConfig.config.voiceApiKey; + + try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) { + HttpPost request = new HttpPost(url); + StringEntity params = new StringEntity(payload, "UTF-8"); + request.addHeader("Content-Type", "application/json"); + request.setEntity(params); + HttpResponse response = httpClient.execute(request); + HttpEntity entity = response.getEntity(); + String responseString = EntityUtils.toString(entity, "UTF-8"); + + // Extract the base64 audio content + String base64Audio = extractBase64Audio(responseString); + playSound(base64Audio); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Create a JSON payload for the text-to-speech request + private static String createPayload(String text, Voice voice) { + JsonObject payload = new JsonObject(); + JsonObject input = new JsonObject(); + input.addProperty("text", text); + JsonObject voiceObj = new JsonObject(); + voiceObj.addProperty("languageCode", voice.getLanguageCodes().get(0)); // Assuming the first language code + voiceObj.addProperty("name", voice.getName()); + voiceObj.addProperty("ssmlGender", voice.getSsmlGender()); + JsonObject audioConfig = new JsonObject(); + audioConfig.addProperty("audioEncoding", "LINEAR16"); // WAV encoding + payload.add("input", input); + payload.add("voice", voiceObj); + payload.add("audioConfig", audioConfig); + + return payload.toString(); + } + + // Extract the base64 audio content from the response JSON + private static String extractBase64Audio(String responseString) { + JsonObject responseJson = JsonParser.parseString(responseString).getAsJsonObject(); + return responseJson.get("audioContent").getAsString(); + } + + // Play the sound from a base64 encoded string + public static void playSound(String base64Sound) { + try { + // Decode the base64 string to a byte array + byte[] soundBytes = Base64.getDecoder().decode(base64Sound); + // Convert byte array to an InputStream + InputStream byteArrayInputStream = new ByteArrayInputStream(soundBytes); + // Wrap the InputStream in a BufferedInputStream + BufferedInputStream bufferedStream = new BufferedInputStream(byteArrayInputStream); + // Open an input stream from the BufferedInputStream + AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(bufferedStream); + Clip clip = AudioSystem.getClip(); + clip.open(audioInputStream); + clip.start(); + // Close the audio input stream + audioInputStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/rebane2001/aimobs/Voice.java b/src/main/java/com/rebane2001/aimobs/Voice.java new file mode 100644 index 0000000..861d984 --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/Voice.java @@ -0,0 +1,49 @@ +package com.rebane2001.aimobs; + +import java.util.List; + +public class Voice { + private List languageCodes; // List of supported language codes for the voice + private String name; // Name of the voice + private String ssmlGender; // Gender as defined in SSML + private int naturalSampleRateHertz; // Natural sample rate of the voice + + public Voice(List languageCodes, String name, String ssmlGender, int naturalSampleRateHertz) { + this.languageCodes = languageCodes; + this.name = name; + this.ssmlGender = ssmlGender; + this.naturalSampleRateHertz = naturalSampleRateHertz; + } + + public List getLanguageCodes() { + return languageCodes; + } + + public void setLanguageCodes(List languageCodes) { + this.languageCodes = languageCodes; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSsmlGender() { + return ssmlGender; + } + + public void setSsmlGender(String ssmlGender) { + this.ssmlGender = ssmlGender; + } + + public int getNaturalSampleRateHertz() { + return naturalSampleRateHertz; + } + + public void setNaturalSampleRateHertz(int naturalSampleRateHertz) { + this.naturalSampleRateHertz = naturalSampleRateHertz; + } +} diff --git a/src/main/java/com/rebane2001/aimobs/VoiceManager.java b/src/main/java/com/rebane2001/aimobs/VoiceManager.java new file mode 100644 index 0000000..4f837f2 --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/VoiceManager.java @@ -0,0 +1,105 @@ +package com.rebane2001.aimobs; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; + +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Random; +import java.io.IOException; +import java.util.UUID; + +public class VoiceManager { + + // Custom UUID type adapter for Gson + private static class UUIDTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, UUID value) throws IOException { + out.value(value != null ? value.toString() : null); + } + + @Override + public UUID read(JsonReader in) throws IOException { + try { + return UUID.fromString(in.nextString()); + } catch (IllegalArgumentException e) { + // Handle invalid UUID here if necessary + return null; + } + } + } + + + + private List voices; // List of available voices + private HashMap mobVoices; // Map of mob UUIDs to their assigned voices + private Random random; // Random generator for voice selection + private static final String MOB_VOICES_FILE_PATH = "mob_voices.json"; // File path for mob voices data + + public VoiceManager(String voiceFilePath) { + this.random = new Random(); + this.mobVoices = new HashMap<>(); + loadVoices(voiceFilePath); // Load available voices + loadMobVoices(); // Load previously assigned mob voices + } + + // Loads voices from a JSON file + private void loadVoices(String voiceFilePath) { + try (Reader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(voiceFilePath), StandardCharsets.UTF_8))) { + voices = new Gson().fromJson(reader, new TypeToken>() {}.getType()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Loads previously assigned mob voices from a JSON file + private void loadMobVoices() { + try (FileReader reader = new FileReader(MOB_VOICES_FILE_PATH)) { + Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); + Type mobVoicesType = new TypeToken>() {}.getType(); + HashMap loadedMobVoices = gson.fromJson(reader, mobVoicesType); + mobVoices = loadedMobVoices != null ? loadedMobVoices : new HashMap<>(); + } catch (IOException e) { + mobVoices = new HashMap<>(); + saveMobVoices(); // Create and save an empty file if failed to load + e.printStackTrace(); + } + } + + // Saves the current mob voices to a JSON file + private void saveMobVoices() { + try (FileWriter writer = new FileWriter(MOB_VOICES_FILE_PATH)) { + new Gson().toJson(mobVoices, writer); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // Retrieves the assigned voice for a mob or assigns and saves a new random voice if none exists + public Voice getVoiceForMob(UUID mobUUID) { + if (mobVoices.containsKey(mobUUID)) { + return mobVoices.get(mobUUID); + } else { + Voice voice = getRandomVoice(); + mobVoices.put(mobUUID, voice); + saveMobVoices(); // Save every time a new voice is assigned + return voice; + } + } + + // Returns a random voice from the available voices list + private Voice getRandomVoice() { + return voices.get(random.nextInt(voices.size())); + } +} diff --git a/src/main/java/com/rebane2001/aimobs/mixin/ServerTickMixin.java b/src/main/java/com/rebane2001/aimobs/mixin/ServerTickMixin.java new file mode 100644 index 0000000..c93a6b6 --- /dev/null +++ b/src/main/java/com/rebane2001/aimobs/mixin/ServerTickMixin.java @@ -0,0 +1,17 @@ +package com.rebane2001.aimobs.mixin; + +import com.rebane2001.aimobs.AIMobsMod; +import net.minecraft.server.MinecraftServer; +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; + +@Mixin(MinecraftServer.class) +public class ServerTickMixin { + + @Inject(method = "tick", at = @At("HEAD")) + private void onServerTick(CallbackInfo ci) { + AIMobsMod.updateMobMovement(); + } +} diff --git a/src/main/resources/aimobs.mixins.json b/src/main/resources/aimobs.mixins.json index cd568bb..1471e21 100644 --- a/src/main/resources/aimobs.mixins.json +++ b/src/main/resources/aimobs.mixins.json @@ -7,7 +7,10 @@ "ChatHudAccessor", "ClientPlayNetworkHandler" ], + "mixins": [ + "ServerTickMixin" + ], "injectors": { "defaultRequire": 1 } -} +} \ No newline at end of file diff --git a/src/main/resources/assets/aimobs/recent_events.json b/src/main/resources/assets/aimobs/recent_events.json new file mode 100644 index 0000000..36e2abc --- /dev/null +++ b/src/main/resources/assets/aimobs/recent_events.json @@ -0,0 +1,141 @@ +{ + "recentEvents": [ + "Yesterday, you witnessed a jousting tournament where the local champion was unseated by a mysterious knight. ", + "Last week, you stumbled upon a hidden treasure while digging in the fields, but a cunning wizard claimed it. ", + "Last night, you were part of a village feast celebrating the harvest, filled with music, dance, and laughter. ", + "Just this morning, you heard rumors of a dragon seen flying over the distant mountains, spreading fear among the villagers. ", + "Two days ago, you helped in constructing a new bridge that will connect two rival kingdoms, fostering trade and peace. ", + "Last month, you were caught in a thunderstorm while traveling and took shelter in an ancient, eerie castle. ", + "A fortnight ago, you overheard a secret meeting between nobles plotting against the king, but dare not speak of it. ", + "Recently, you found a wounded fairy in the forest and helped it recover, earning a mystical blessing. ", + "Three days ago, you were chased by a pack of wild wolves but managed to escape by climbing a tall tree. ", + "Earlier today, you assisted a group of monks in copying sacred texts, learning wisdom and patience. ", + "Last Sunday, you helped a lost child find their way back home, earning gratitude from a worried family. ", + "A week ago, you were part of a caravan that was attacked by bandits, but your quick thinking saved the day. ", + "Yesterday evening, you danced with a stranger at the town square, who turned out to be a disguised prince. ", + "Just hours ago, you found an ancient scroll that hints at the location of a long-lost civilization. ", + "Three nights ago, you had a vivid dream where a celestial being gave you a cryptic prophecy. ", + "Last harvest, you helped save the crops from a sudden frost using a magical artifact you found. ", + "A month ago, you were bitten by a mysterious creature in the woods and now feel a strange power growing within. ", + "Recently, you were invited to a secret society of mages who believe you have hidden potential. ", + "Two weeks ago, you rescued a bird tangled in a thorn bush, only to discover it could speak human language. ", + "Earlier this month, you stumbled upon a hidden glade filled with glowing plants and singing fairies. ", + "Last winter, you helped a village survive a harsh famine by sharing your stored food. ", + "A few days ago, you discovered a hidden tunnel leading to the royal treasury but kept it a secret. ", + "Yesterday, you saved a drowning kitten from a turbulent river, revealing your compassionate nature. ", + "Earlier this year, you witnessed a solar eclipse that the wise elders say is an ominous sign. ", + "A fortnight ago, you helped a wandering minstrel compose a song that's now famous in the region. ", + "Last night, you had a strange encounter with a ghostly apparition that left you with a cryptic message. ", + "This morning, you found a magic ring that glows faintly but have yet to discover its powers. ", + "A week ago, you were part of a hunting party that caught a legendary beast, celebrated in local folklore. ", + "Three days ago, you traded stories with a traveling merchant who shared secrets of distant lands. ", + "Recently, you stumbled upon a mystical stone circle that resonates with ancient energy. ", + "Last month, you helped defend the town from marauding orcs, becoming a local hero. ", + "Yesterday, you discovered a rare herb that is said to cure a deadly disease spreading in the kingdom. ", + "Last summer, you took part in a grand festival where you won a dance competition. ", + "Two days ago, you were visited by a time-traveling wizard who showed you glimpses of possible futures. ", + "A few nights ago, you found an old diary detailing the adventures of a legendary hero related to you. ", + "Earlier this week, you saved a caravan from sinking in quicksand, using only wits and a sturdy rope. ", + "Recently, you encountered a mystical deer in the forest that led you to a hidden waterfall. ", + "Last spring, you planted a strange seed that grew into a tree bearing golden fruit. ", + "A month ago, you solved an ancient riddle that unlocked a secret chamber in the old castle. ", + "Yesterday, you met a shapeshifter who taught you valuable lessons in adaptability and change. ", + "Last evening, you attended a royal banquet where you danced with nobles and royalty. ", + "Three weeks ago, you were blessed by a wandering sage who saw great potential in you. ", + "Earlier this year, you helped a group of dwarves recover a lost treasure from a dragon's lair. ", + "Last winter, you survived a terrible blizzard by finding shelter in a cave filled with ancient carvings. ", + "Yesterday, you came across a wandering minstrel who shared tales of distant lands and forgotten lore. ", + "A fortnight ago, you found a magical ring that grants you glimpses of the future but at a mysterious cost. ", + "Three days back, you encountered a ghostly apparition near the old ruins, begging for release from a curse. ", + "Recently, you were given a map by a dying soldier, pointing to a hidden cache of the king's gold. ", + "Last summer, you joined a group of adventurers on a perilous quest, only to be betrayed by one of them. ", + "Earlier this week, you helped a witch gather rare herbs and were gifted a potion of unknown effects. ", + "Just this morning, you spotted a comet streaking across the sky, an omen believed to foretell great change. ", + "A week ago, you were saved from a deadly trap by a mysterious stranger who vanished without a trace. ", + "Last full moon, you felt an inexplicable urge to wander into the forest, where you discovered a hidden shrine. ", + "Two months ago, you were part of a failed rebellion and had to flee for your life, living in hiding since. ", + "A year ago, you witnessed a duel between two wizards that left a part of the forest permanently enchanted. ", + "Recently, you took part in a royal feast, where you accidentally uncovered a plot to poison the king. ", + "Last night, you dreamt of a beautiful siren calling you from the sea, her song still echoing in your ears. ", + "Three weeks ago, you joined a sea voyage that ended in shipwreck, leaving you stranded on a mystical island. ", + "Last spring, you aided in a ritual that brought rain to a drought-stricken village, but at a personal cost. ", + "Just yesterday, you found a diary belonging to your ancestor, revealing secrets about your family's past. ", + "Earlier today, you were challenged to a duel by a scorned noble, who underestimated your fighting skill. ", + "Five days ago, you were granted audience with the queen, who tasked you with a secret mission. ", + "Last harvest moon, you stumbled upon a haunted graveyard where the dead were seen walking. ", + "Two nights ago, you were awakened by a phantom whispering riddles that seem to hold some hidden truth. ", + "A month ago, you helped an injured unicorn in the woods, earning the favor of the woodland creatures. ", + "Last autumn, you participated in a tournament and bested a knight, earning a noble title. ", + "Recently, you were approached by a talking cat who led you to a hidden world of magical beings. ", + "Yesterday, you discovered a mirror that shows not your reflection but a parallel world. ", + "Last week, you aided a group of dwarves in their mine, unearthing a vein of precious gems. ", + "Hours ago, you heard a prophecy from a mad seer who spoke of your role in an epic destiny. ", + "Days ago, you were captured by trolls but managed to escape through clever negotiation. ", + "Last spring, you found an egg that hatched into a baby dragon, now hidden in your barn. ", + "Just last night, you took part in a secret meeting of rebels planning to overthrow the tyrant king. ", + "A year ago, you fell in love with a fairy who visits you every midsummer's night. ", + "Recently, you were cursed by a vengeful sorceress, and now seek a way to break the spell. ", + "Last month, you climbed the tallest mountain, discovering the entrance to a hidden temple. ", + "Earlier today, you traded with a goblin merchant, acquiring a bag that never seems to empty. ", + "Last festival, you won a dance with the princess, but she seems to have mistaken you for someone else. ", + "Yesterday, you were attacked by a swarm of enchanted bees guarding a wizard's hidden garden. ", + "A week ago, you discovered a well that answers any one question truthfully, but only once. ", + "Last winter, you saved a town from starvation by leading them to a valley of wild game. ", + "Recently, you were knighted by a legendary hero who sees great potential in you. ", + "Two days ago, you spoke to a tree that shared wisdom from centuries of watching the world. ", + "Last summer, you swam in a magical spring that healed your wounds but changed your appearance. ", + "Earlier this month, you encountered a time-traveler who showed you possible futures. ", + "Three nights ago, you were gifted a sword by a lake spirit, claiming you were the chosen one. ", + "A fortnight ago, you were trapped in a wizard's labyrinth but found your way out through wit. ", + "Last equinox, you joined a druid's circle in a ritual that aligned the energies of the land. ", + "Just yesterday, you rescued a prince disguised as a commoner from a band of highwaymen. ", + "Earlier today, you found a locket that shows the face of the person who loves you most. ", + "Last night, you traded with a wandering villager, acquiring a rare enchanted book with unknown powers. ", + "A week ago, you stumbled upon an Illager patrol and barely escaped with your life, their banner still in your possession. ", + "Yesterday, you helped a group of villagers fend off a zombie siege, earning their respect and gratitude. ", + "Recently, you mined into a cavern filled with glowing mushrooms and heard whispers of an ancient civilization. ", + "Just this morning, you discovered a hidden stronghold, feeling the presence of the Ender Dragon watching from afar. ", + "Two days ago, you rescued a wolf from a perilous trap, and it has loyally followed you ever since. ", + "Last month, you explored a Jungle Temple, where you solved a complex puzzle that revealed hidden treasure. ", + "A fortnight ago, you witnessed a clash between an Iron Golem and a group of Pillagers, learning from their tactics. ", + "Three days ago, you were caught in a thunderstorm and saw a skeleton horse trap for the first time. ", + "Earlier today, you fished a mysterious map from the ocean, pointing to a buried treasure guarded by Drowned. ", + "Last winter, you ventured into the Ice Spikes biome and found a rare Ice Queen who granted you a magical boon. ", + "A year ago, you tamed a wild ocelot in the jungle, and it has become your steadfast companion. ", + "Last harvest, you helped a farmer villager expand his crops, learning new farming techniques and recipes. ", + "Recently, you delved into a Nether fortress, battling Blazes and Withers, and returning with valuable resources. ", + "A week ago, you witnessed a baby turtle hatch on the beach, feeling a connection to the cycle of life. ", + "Last summer, you participated in a Pig race at a village fair, winning a golden carrot as a prize. ", + "Yesterday, you saved a villager from a Witch's curse, using a potion brewed from rare herbs. ", + "Three weeks ago, you explored a haunted Mansion, defeating the Illagers and claiming it as your stronghold. ", + "Last full moon, you heard the distant howl of a phantom, a chilling reminder of your need for rest. ", + "Two months ago, you befriended a Snow Golem and helped it find a suitable snowy home. ", + "Last autumn, you were led by a parrot to a hidden pirate shipwreck, filled with doubloons and emeralds. ", + "Earlier this week, you were challenged to a build-off by an expert builder villager, honing your construction skills. ", + "Five days ago, you entered the End and were awed by the Endermen, learning to navigate their strange ways. ", + "Just yesterday, you negotiated peace between a village and a Pillager outpost, fostering a tentative truce. ", + "Last spring, you discovered a secret society of Redstone engineers, learning complex mechanisms from them. ", + "A month ago, you survived a cave filled with Bats and found a hidden chamber with a Lapis Lazuli deposit. ", + "Recently, you tamed a fierce Ravager left behind from a raid, and now ride it into battle. ", + "Last night, you shared a campfire with a group of travelers, exchanging tales of adventure and rare recipes. ", + "Two nights ago, you spotted a Shulker hiding in your storeroom, leading to a thrilling chase and capture. ", + "Last winter, you followed a trail of strange particles to a hidden Enchanting Room with ancient spells. ", + "A year ago, you saved a Beached Squid, earning the favor of the ocean's mysterious spirits. ", + "Last festival, you danced with the villagers around a Maypole, feeling a connection to their simple joys. ", + "Yesterday, you assisted a blacksmith villager in forging a special sword, imbued with a secret enchantment. ", + "Last week, you discovered a village in the clouds, accessed only by a hidden portal in the mountains. ", + "Hours ago, you were approached by a fox carrying a magical berry, leading you to an enchanted glade. ", + "Days ago, you explored an underwater ruin, battling Guardians and discovering a forgotten king's crown. ", + "Last spring, you planted a magical sapling that grew instantly into a towering tree with golden apples. ", + "Just last night, you deciphered an ancient glyph in a desert temple, unlocking the way to a Pharaoh's tomb. ", + "A year ago, you saved a village from a rampaging Ender Dragon, using only your wits and a fishing rod. ", + "Recently, you sailed across a vast ocean, discovering new lands and mapping uncharted territories. ", + "Last equinox, you attended a gathering of all the villagers, where you were honored as a hero of the realm. ", + "Yesterday, you discovered a hidden garden filled with every kind of flower, tended by a peaceful Beekeeper. ", + "Last night, you were visited in a dream by a Mooshroom, guiding you to a mystical island. ", + "A week ago, you unlocked a secret level in a dungeon, battling Silverfish and finding a legendary artifact. ", + "Last winter, you wrote a book detailing your adventures, and it's now a best-seller among the villagers. ", + "Recently, you crafted a unique banner that became the symbol of unity for a once divided village. " + ] + } + \ No newline at end of file diff --git a/src/main/resources/assets/aimobs/sounds.json b/src/main/resources/assets/aimobs/sounds.json new file mode 100644 index 0000000..2998eb0 --- /dev/null +++ b/src/main/resources/assets/aimobs/sounds.json @@ -0,0 +1,7 @@ +{ + "my_sound": { + "sounds": [ + "aimobs:my_sound" + ] + } + } \ No newline at end of file diff --git a/src/main/resources/voices.json b/src/main/resources/voices.json new file mode 100644 index 0000000..ee93283 --- /dev/null +++ b/src/main/resources/voices.json @@ -0,0 +1,482 @@ +[ + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-Standard-A", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-Standard-B", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-Standard-C", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-Standard-D", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-A", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-B", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-C", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-D", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-F", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-IN" + ], + "name": "en-IN-Standard-D", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-IN" + ], + "name": "en-IN-Standard-A", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-IN" + ], + "name": "en-IN-Standard-B", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-IN" + ], + "name": "en-IN-Standard-C", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-A", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-B", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-C", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-D", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-E", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-F", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-G", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-H", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-I", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-J", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-News-E", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-News-F", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-News-G", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-News-G", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-News-H", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-News-I", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-News-J", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-News-K", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-News-L", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-News-M", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-News-K", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-News-L", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-News-M", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-News-N", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-Standard-A", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-Standard-B", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-Standard-C", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-AU" + ], + "name": "en-AU-Standard-D", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-IN" + ], + "name": "en-IN-Standard-A", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-IN" + ], + "name": "en-IN-Standard-B", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-IN" + ], + "name": "en-IN-Standard-C", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-IN" + ], + "name": "en-IN-Standard-D", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-A", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-B", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-C", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-D", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-GB" + ], + "name": "en-GB-Standard-F", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-A", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-B", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-C", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-D", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-E", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-F", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-G", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-H", + "ssmlGender": "FEMALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-I", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + }, + { + "languageCodes": [ + "en-US" + ], + "name": "en-US-Standard-J", + "ssmlGender": "MALE", + "naturalSampleRateHertz": 24000 + } + ] \ No newline at end of file