From f1171adf409245fb06be7a3f5b156c42ba6a9470 Mon Sep 17 00:00:00 2001 From: Cametendo Date: Wed, 18 Mar 2026 09:04:41 +0100 Subject: [PATCH] Fixed chat not working in multiplayer and added method to delete api key --- .../client/AiCompanionClient.java | 85 ++++++++-- .../aicompanion2_0/client/ApiKeyScreen.java | 2 +- .../aicompanion2_0/client/ClientConfig.java | 31 +++- .../aicompanion2_0/Aicompanion2_0.java | 150 ++++++++++++++---- 4 files changed, 219 insertions(+), 49 deletions(-) diff --git a/src/client/java/AiCompanion/aicompanion2_0/client/AiCompanionClient.java b/src/client/java/AiCompanion/aicompanion2_0/client/AiCompanionClient.java index da72c3a..398303c 100644 --- a/src/client/java/AiCompanion/aicompanion2_0/client/AiCompanionClient.java +++ b/src/client/java/AiCompanion/aicompanion2_0/client/AiCompanionClient.java @@ -4,12 +4,14 @@ import AiCompanion.aicompanion2_0.AIEntity; import AiCompanion.aicompanion2_0.Aicompanion2_0; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; import net.fabricmc.fabric.api.event.player.UseEntityCallback; import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.entity.BipedEntityRenderer; import net.minecraft.client.render.entity.model.EntityModelLayers; import net.minecraft.client.render.entity.model.PlayerEntityModel; +import net.minecraft.text.Text; import net.minecraft.util.ActionResult; import net.minecraft.util.Identifier; @@ -39,28 +41,89 @@ public class AiCompanionClient implements ClientModInitializer { currentSession = null; }); + ClientPlayNetworking.registerGlobalReceiver(Aicompanion2_0.QUESTION_PACKET_ID, (client, handler, buf, responseSender) -> { + String question = buf.readString(32767); + client.execute(() -> ensureApiKeyAndRun(client, () -> askQuestion(client, question))); + }); + + ClientPlayNetworking.registerGlobalReceiver(Aicompanion2_0.DELETE_KEY_PACKET_ID, (client, handler, buf, responseSender) -> { + client.execute(() -> { + currentSession = null; + + try { + boolean deleted = ClientConfig.deleteApiKey(); + if (deleted) { + sendChatMessage(client, Text.literal("§6[AI] §fAPI key deleted.")); + } else { + sendChatMessage(client, Text.literal("§6[AI] §fno api key deleted: none found")); + } + } catch (Exception e) { + String error = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(); + sendChatMessage(client, Text.literal("§c[AI] Could not delete API key: " + error)); + } + }); + }); + // Open chat GUI on right-click UseEntityCallback.EVENT.register((player, world, hand, entity, hitResult) -> { if (world.isClient() && entity instanceof AIEntity) { MinecraftClient client = MinecraftClient.getInstance(); - client.execute(() -> { - if (!ClientConfig.hasApiKey()) { - // First time: ask for API key, then open chat - client.setScreen(new ApiKeyScreen(() -> { - currentSession = null; // reset so session uses new key - openChatScreen(client); - })); - } else { - openChatScreen(client); - } - }); + client.execute(() -> ensureApiKeyAndRun(client, () -> openChatScreen(client))); return ActionResult.SUCCESS; } return ActionResult.PASS; }); } + private static void ensureApiKeyAndRun(MinecraftClient client, Runnable action) { + if (!ClientConfig.hasApiKey()) { + currentSession = null; + client.setScreen(new ApiKeyScreen(() -> { + currentSession = null; + action.run(); + })); + return; + } + + action.run(); + } + + private static void askQuestion(MinecraftClient client, String question) { + sendChatMessage(client, Text.literal("§6[AI] §fThink about: " + question)); + + AiChatSession session = new AiChatSession( + Aicompanion2_0.getApiBaseUrl(), + ClientConfig.getApiKey(), + Aicompanion2_0.getModel() + ); + + session.sendMessage(question, response -> client.execute(() -> { + if (response == null || response.isEmpty()) { + return; + } + + if (response.startsWith("§cError: ")) { + String error = response.substring("§cError: ".length()); + sendChatMessage(client, Text.literal("§c[AI] Error with /ai question: " + error)); + return; + } + + String answer = response; + if (answer.length() > 2000) { + answer = answer.substring(0, 2000) + "..."; + } + + sendChatMessage(client, Text.literal("§6[AI] §fAnswer: " + answer)); + })); + } + + private static void sendChatMessage(MinecraftClient client, Text message) { + if (client.player != null) { + client.player.sendMessage(message, false); + } + } + private static void openChatScreen(MinecraftClient client) { if (currentSession == null) { currentSession = new AiChatSession( diff --git a/src/client/java/AiCompanion/aicompanion2_0/client/ApiKeyScreen.java b/src/client/java/AiCompanion/aicompanion2_0/client/ApiKeyScreen.java index 82ce781..6a93358 100644 --- a/src/client/java/AiCompanion/aicompanion2_0/client/ApiKeyScreen.java +++ b/src/client/java/AiCompanion/aicompanion2_0/client/ApiKeyScreen.java @@ -13,7 +13,7 @@ public class ApiKeyScreen extends Screen { private final Runnable onSuccess; public ApiKeyScreen(Runnable onSuccess) { - super(Text.literal("Enter API Key")); + super(Text.literal("Enter API Key (available on ai.cametendo.org)")); this.onSuccess = onSuccess; } diff --git a/src/client/java/AiCompanion/aicompanion2_0/client/ClientConfig.java b/src/client/java/AiCompanion/aicompanion2_0/client/ClientConfig.java index 3539748..73ba57b 100644 --- a/src/client/java/AiCompanion/aicompanion2_0/client/ClientConfig.java +++ b/src/client/java/AiCompanion/aicompanion2_0/client/ClientConfig.java @@ -11,7 +11,7 @@ public class ClientConfig { private static String apiKey = null; public static String getApiKey() { - if (apiKey == null) load(); + load(); return apiKey; } @@ -20,8 +20,15 @@ public class ClientConfig { save(); } + public static boolean deleteApiKey() throws IOException { + boolean deleted = deleteApiKeyFrom(SHARED_CONFIG_PATH, "AI Companion Shared Config"); + deleted = deleteApiKeyFrom(CONFIG_PATH, "AI Companion Client Config") || deleted; + apiKey = null; + return deleted; + } + public static boolean hasApiKey() { - if (apiKey == null) load(); + load(); return apiKey != null && !apiKey.isBlank(); } @@ -33,6 +40,7 @@ public class ClientConfig { apiKey = loadApiKeyFrom(CONFIG_PATH); } } catch (IOException e) { + apiKey = null; System.out.println("[aicompanion2_0] Could not load Client Config."); } } @@ -68,4 +76,23 @@ public class ClientConfig { props.store(out, comment); } } + + private static boolean deleteApiKeyFrom(Path path, String comment) throws IOException { + if (!Files.exists(path)) return false; + + Properties props = new Properties(); + try (FileInputStream in = new FileInputStream(path.toFile())) { + props.load(in); + } + + String existingKey = props.getProperty("api.key", null); + if (existingKey == null || existingKey.isBlank()) return false; + + props.remove("api.key"); + Files.createDirectories(path.getParent()); + try (FileOutputStream out = new FileOutputStream(path.toFile())) { + props.store(out, comment); + } + return true; + } } diff --git a/src/main/java/AiCompanion/aicompanion2_0/Aicompanion2_0.java b/src/main/java/AiCompanion/aicompanion2_0/Aicompanion2_0.java index 5bbb10e..45c5aaf 100644 --- a/src/main/java/AiCompanion/aicompanion2_0/Aicompanion2_0.java +++ b/src/main/java/AiCompanion/aicompanion2_0/Aicompanion2_0.java @@ -15,6 +15,8 @@ import com.mojang.brigadier.arguments.StringArgumentType; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.fabricmc.fabric.api.object.builder.v1.entity.FabricDefaultAttributeRegistry; import net.fabricmc.fabric.api.object.builder.v1.entity.FabricEntityTypeBuilder; import net.minecraft.entity.EntityDimensions; @@ -29,10 +31,17 @@ import net.minecraft.util.Identifier; public class Aicompanion2_0 implements ModInitializer { public static final String MOD_ID = "aicompanion2_0"; - private static String API_BASE_URL = "https://ai.cametendo.org"; - private static String MODEL = "minecraft-helper"; + private static final String DEFAULT_API_BASE_URL = "https://ai.cametendo.org"; + private static final String DEFAULT_MODEL = "minecraft-helper"; + private static final String DEFAULT_API_PATH = "/api/chat/completions"; + private static String API_BASE_URL = DEFAULT_API_BASE_URL; + private static String MODEL = DEFAULT_MODEL; private static String API_KEY = ""; - private static String API_PATH = "/api/chat/completions"; + private static String API_PATH = DEFAULT_API_PATH; + private static final Path SHARED_CONFIG_PATH = Path.of("config", "aicompanion2_0.properties"); + private static final Path CLIENT_CONFIG_PATH = Path.of("config", "aicompanion2_0_client.properties"); + public static final Identifier QUESTION_PACKET_ID = new Identifier(MOD_ID, "question"); + public static final Identifier DELETE_KEY_PACKET_ID = new Identifier(MOD_ID, "delete_key"); public static final EntityType AI_COMPANION = Registry.register( Registries.ENTITY_TYPE, @@ -94,6 +103,39 @@ public class Aicompanion2_0 implements ModInitializer { return 1; }) ) + // /ai delete-key + .then(CommandManager.literal("delete-key") + .executes(ctx -> { + var player = ctx.getSource().getPlayer(); + if (player != null) { + if (!ServerPlayNetworking.canSend(player, DELETE_KEY_PACKET_ID)) { + ctx.getSource().sendFeedback( + () -> Text.literal("§c[AI] delete-key requires the AI Companion client mod."), false); + return 0; + } + + ServerPlayNetworking.send(player, DELETE_KEY_PACKET_ID, PacketByteBufs.create()); + return 1; + } + + try { + boolean deleted = deleteStoredApiKey(); + if (deleted) { + ctx.getSource().sendFeedback( + () -> Text.literal("§6[AI] §fAPI key deleted."), false); + } else { + ctx.getSource().sendFeedback( + () -> Text.literal("§6[AI] §fno api key deleted: none found"), false); + } + return 1; + } catch (IOException e) { + String error = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName(); + ctx.getSource().sendFeedback( + () -> Text.literal("§c[AI] Could not delete API key: " + error), false); + return 0; + } + }) + ) // /ai question .then(CommandManager.literal("question") .then(CommandManager.argument("question", StringArgumentType.greedyString()) @@ -103,8 +145,16 @@ public class Aicompanion2_0 implements ModInitializer { String frage = StringArgumentType.getString(ctx, "question"); if (player != null) { - player.sendMessage( - Text.literal("§6[AI] §fThink about: " + frage), false); + if (!ServerPlayNetworking.canSend(player, QUESTION_PACKET_ID)) { + ctx.getSource().sendFeedback( + () -> Text.literal("§c[AI] question requires the AI Companion client mod."), false); + return 0; + } + + var buf = PacketByteBufs.create(); + buf.writeString(frage); + ServerPlayNetworking.send(player, QUESTION_PACKET_ID, buf); + return 1; } new Thread(() -> { @@ -140,33 +190,40 @@ public class Aicompanion2_0 implements ModInitializer { // Innerhalb deiner Klasse Aicompanion2_0 private String callOllama(String prompt) throws Exception { - String json = "{\"model\":\"" + jsonEscape(MODEL) + "\",\"messages\":[{\"role\":\"user\",\"content\":\"" + loadConfig(); + + String apiBaseUrl = API_BASE_URL; + String apiPath = API_PATH; + String model = MODEL; + String apiKey = API_KEY; + + String json = "{\"model\":\"" + jsonEscape(model) + "\",\"messages\":[{\"role\":\"user\",\"content\":\"" + jsonEscape(prompt) + "\"}],\"stream\":false}"; - HttpResult primary = postChatCompletion(API_PATH, json); + HttpResult primary = postChatCompletion(apiBaseUrl, apiPath, apiKey, json); if (primary.status == 200) { return extractAssistantContent(primary.body); } // Some deployments expose OpenAI-compatible chat under /v1/chat/completions. - if ((primary.status >= 500 || primary.status == 404) && !"/v1/chat/completions".equals(API_PATH)) { - HttpResult fallback = postChatCompletion("/v1/chat/completions", json); + if ((primary.status >= 500 || primary.status == 404) && !"/v1/chat/completions".equals(apiPath)) { + HttpResult fallback = postChatCompletion(apiBaseUrl, "/v1/chat/completions", apiKey, json); if (fallback.status == 200) { return extractAssistantContent(fallback.body); } return formatHttpError(fallback.status, fallback.body, "/v1/chat/completions"); } - return formatHttpError(primary.status, primary.body, API_PATH); + return formatHttpError(primary.status, primary.body, apiPath); } - private HttpResult postChatCompletion(String path, String json) throws Exception { - URL url = new URL(API_BASE_URL + path); + private HttpResult postChatCompletion(String apiBaseUrl, String path, String apiKey, String json) throws Exception { + URL url = new URL(apiBaseUrl + path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); - if (API_KEY != null && !API_KEY.isBlank()) { - conn.setRequestProperty("Authorization", "Bearer " + API_KEY); + if (apiKey != null && !apiKey.isBlank()) { + conn.setRequestProperty("Authorization", "Bearer " + apiKey); } conn.setDoOutput(true); @@ -230,38 +287,42 @@ public class Aicompanion2_0 implements ModInitializer { } } - private void loadConfig() { + private synchronized void loadConfig() { + API_BASE_URL = DEFAULT_API_BASE_URL; + API_PATH = DEFAULT_API_PATH; + MODEL = DEFAULT_MODEL; + API_KEY = ""; + try { - Path configPath = Path.of("config", "aicompanion2_0.properties"); - Properties props = new Properties(); - try (FileInputStream in = new FileInputStream(configPath.toFile())) { - props.load(in); + if (Files.exists(SHARED_CONFIG_PATH)) { + Properties props = new Properties(); + try (FileInputStream in = new FileInputStream(SHARED_CONFIG_PATH.toFile())) { + props.load(in); + } + API_BASE_URL = props.getProperty("api.baseUrl", DEFAULT_API_BASE_URL); + API_PATH = props.getProperty("api.path", DEFAULT_API_PATH); + MODEL = props.getProperty("api.model", DEFAULT_MODEL); + API_KEY = props.getProperty("api.key", "").trim(); } - API_BASE_URL = props.getProperty("api.baseUrl", API_BASE_URL); - API_PATH = props.getProperty("api.path", API_PATH); - MODEL = props.getProperty("api.model", MODEL); - API_KEY = props.getProperty("api.key", API_KEY); if (API_KEY == null || API_KEY.isBlank()) { - String clientFallbackKey = readApiKeyFrom(Path.of("config", "aicompanion2_0_client.properties")); + String clientFallbackKey = readApiKeyFrom(CLIENT_CONFIG_PATH); if (clientFallbackKey != null && !clientFallbackKey.isBlank()) { API_KEY = clientFallbackKey; } } } catch (IOException e) { - System.out.println("[" + MOD_ID + "] Keine Config gefunden, benutze Default-API."); - - try { - String clientFallbackKey = readApiKeyFrom(Path.of("config", "aicompanion2_0_client.properties")); - if (clientFallbackKey != null && !clientFallbackKey.isBlank()) { - API_KEY = clientFallbackKey; - } - } catch (IOException ignored) { - // Keep defaults when no config files are available. - } + System.out.println("[" + MOD_ID + "] Could not reload config, using defaults."); } } + private boolean deleteStoredApiKey() throws IOException { + boolean deleted = deleteApiKeyFrom(SHARED_CONFIG_PATH, "AI Companion Shared Config"); + deleted = deleteApiKeyFrom(CLIENT_CONFIG_PATH, "AI Companion Client Config") || deleted; + API_KEY = ""; + return deleted; + } + private String readApiKeyFrom(Path configPath) throws IOException { if (!Files.exists(configPath)) return null; Properties props = new Properties(); @@ -270,4 +331,23 @@ public class Aicompanion2_0 implements ModInitializer { } return props.getProperty("api.key", null); } -} \ No newline at end of file + + private boolean deleteApiKeyFrom(Path configPath, String comment) throws IOException { + if (!Files.exists(configPath)) return false; + + Properties props = new Properties(); + try (FileInputStream in = new FileInputStream(configPath.toFile())) { + props.load(in); + } + + String existingKey = props.getProperty("api.key", null); + if (existingKey == null || existingKey.isBlank()) return false; + + props.remove("api.key"); + Files.createDirectories(configPath.getParent()); + try (OutputStream out = Files.newOutputStream(configPath)) { + props.store(out, comment); + } + return true; + } +}