mirror of
https://github.com/ritonioz/minecraft-ai-companion-mod-EMVs12-Project.git
synced 2026-03-18 07:10:20 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
012d20eaa7 | ||
|
|
7774c68e8f | ||
|
|
05dd4ff24c | ||
|
|
6561762c9c | ||
|
|
9543cc38c3 | ||
|
|
97734483fd | ||
|
|
ef2567e2ee |
2
.env
Normal file
2
.env
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
API_BASE_URL="ollama.cametendo.org"
|
||||||
|
MODEL="gpt-oss:20b"
|
||||||
0
.env.example
Normal file
0
.env.example
Normal file
29
README.md
29
README.md
@@ -11,20 +11,41 @@ This Minecraft mod allows players, especially new ones, to get real-time assista
|
|||||||
* **Game Version:** 1.21.1
|
* **Game Version:** 1.21.1
|
||||||
* **Loader:** [Fabric Loader](https://fabricmc.net/)
|
* **Loader:** [Fabric Loader](https://fabricmc.net/)
|
||||||
* **Dependencies:** [Fabric API](https://www.curseforge.com/minecraft/mc-mods/fabric-api)
|
* **Dependencies:** [Fabric API](https://www.curseforge.com/minecraft/mc-mods/fabric-api)
|
||||||
* **Information**: This mod uses a Cloud-AI. Active Internet connection is required. (No API Keys required)
|
* **Information**: This mod uses a self-hosted-AI. Active Internet connection and API key required (no subscription).
|
||||||
|
* **API Key**: This Mod requires an API key from [ai.cametendo.org](https://ai.cametendo.org). Steps on how to acquire an API will be listed further down in the README.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
- Command: `/ai <propmpt>`. With this command, you can ask the AI your question.
|
There are three commands included in this mod:
|
||||||
- Use this command to talk to your AI Companion at any time during gameplay.
|
- `/ai question`: This will let you communicate with the AI directly from the In-Game Chat
|
||||||
|
- `/ai spawn`: This will spawn an actual entity, your 'Companion' (multiple companions can be spawned). It will follow you around and can be interacted with by right-clicking.
|
||||||
|
- `/ai kill`: This will kill / remove all companions you spawned
|
||||||
- Note: Depending on the question the AI will take 1 - 3 minutes to respond. Please be patient while your companion "thinks"!
|
- Note: Depending on the question the AI will take 1 - 3 minutes to respond. Please be patient while your companion "thinks"!
|
||||||
|
|
||||||
|
## AI Companion Entity
|
||||||
|
The AI Companion doesn't just stand around, he too has his own features:
|
||||||
|
|
||||||
|
- `Chat-Window`: Right-clicking the Companion will open a chat window. In this, you will have a chat-interface that keeps your chat until you leave the world.
|
||||||
|
- `Follows you`: Instead of just standing around, the companion will ffollow you around wherever you go and walk around the world.
|
||||||
|
- `API-Key-Verification`: The first time you play this mod, you will need to use the companion to enter your API key.
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
1. If not already done, install [Fabric 1.21.1](https://maven.fabricmc.net/net/fabricmc/fabric-installer/1.1.1/fabric-installer-1.1.1.jar). (Click to download the Fabric Installer instantly)
|
1. If not already done, install [Fabric 1.21.1](https://maven.fabricmc.net/net/fabricmc/fabric-installer/1.1.1/fabric-installer-1.1.1.jar). (Click to download the Fabric Installer instantly)
|
||||||
2. Download the mod from the [Releases](https://github.com/Cametendo/minecraft-ai-companion-EMVs12-Project/releases) Tab.
|
2. Download the mod from the [Releases](https://github.com/Cametendo/minecraft-ai-companion-EMVs12-Project/releases) Tab.
|
||||||
3. Place the `.jar` file in your `%appdata%/.minecraft/mods` folder.
|
3. Place the `.jar` file in your `%appdata%/.minecraft/mods` folder.
|
||||||
4. Launch the game in your preferred launcher with the Fabric 1.21.1 Profile ([List of Minecraft Launchers](https://github.com/TayouVR/MinecraftLauncherComparison)
|
4. Launch the game in your preferred launcher with the Fabric 1.21.1 Profile ([List of Minecraft Launchers](https://github.com/TayouVR/MinecraftLauncherComparison)) <br>
|
||||||
|
|
||||||
**IMPORTANT**: This Installation Process is strictly for the offical version of Minecraft (including the official Launcher). We are not not responsible for any issues, data loss, or crashes that may occur when using third-party launchers or unofficial versions of the game. Support is only guaranteed for the official Fabric environment.
|
**IMPORTANT**: This Installation Process is strictly for the offical version of Minecraft (including the official Launcher). We are not not responsible for any issues, data loss, or crashes that may occur when using third-party launchers or unofficial versions of the game. Support is only guaranteed for the official Fabric environment.
|
||||||
|
|
||||||
|
## API-Key
|
||||||
|
This mod requires an API key. To acquire an API key, follow these steps:
|
||||||
|
1. Create an account on [ai.cametendo.org](https://ai.cametendo.org)
|
||||||
|
2. Press your user profile (bottom-left corner) and switch to the tab "Account".
|
||||||
|
3. Find the option "API Keys" and press show. A very long line of dots should appear. This is your API key (hidden by default).
|
||||||
|
4. Copy it and start your game (or go back to it if it's already open)
|
||||||
|
5. Use /ai spawn to spawn your companion. Once spawned, right-click him and a new window should appear (this is very important). In the newly appeared text-field, enter your API key and press enter.
|
||||||
|
6. If everything worked, you can now send messages to the AI. Have fun!
|
||||||
|
|
||||||
## License & Credits
|
## License & Credits
|
||||||
* **Authors:** [Cametendo](https://www.github.com/Cametendo), [ritonioz](https://www.github.com/ritonioz), [Adam237A](https://www.github.com/Adam237A)
|
* **Authors:** [Cametendo](https://www.github.com/Cametendo), [ritonioz](https://www.github.com/ritonioz), [Adam237A](https://www.github.com/Adam237A)
|
||||||
* **License:** CC0 1.0 (Public Domain). Feel free to include this in any modpack! (Credits are appreciated but not required).
|
* **License:** CC0 1.0 (Public Domain). Feel free to include this in any modpack! (Credits are appreciated but not required).
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package AiCompanion.aicompanion2_0.client;
|
||||||
|
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
|
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||||
|
import net.minecraft.client.gui.widget.TextFieldWidget;
|
||||||
|
import net.minecraft.text.OrderedText;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AiChatScreen extends Screen {
|
||||||
|
|
||||||
|
private final AiChatSession session;
|
||||||
|
private TextFieldWidget inputField;
|
||||||
|
private int scrollOffset = 0;
|
||||||
|
|
||||||
|
public AiChatScreen(AiChatSession session) {
|
||||||
|
super(Text.literal("AI Companion"));
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> displayLines() {
|
||||||
|
return session.getDisplayLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
inputField = new TextFieldWidget(
|
||||||
|
textRenderer, width / 2 - 150, height - 35, 270, 20,
|
||||||
|
Text.literal("Message...")
|
||||||
|
);
|
||||||
|
inputField.setMaxLength(500);
|
||||||
|
inputField.setFocused(true);
|
||||||
|
addDrawableChild(inputField);
|
||||||
|
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("Send"), btn -> sendMessage())
|
||||||
|
.dimensions(width / 2 + 125, height - 35, 60, 20)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("Close"), btn -> close())
|
||||||
|
.dimensions(width / 2 - 30, height - 10, 60, 15)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||||
|
if (keyCode == 257 || keyCode == 335) { // Enter
|
||||||
|
sendMessage();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendMessage() {
|
||||||
|
String msg = inputField.getText().trim();
|
||||||
|
if (msg.isEmpty()) return;
|
||||||
|
inputField.setText("");
|
||||||
|
|
||||||
|
displayLines().add("§eIch: §f" + msg);
|
||||||
|
displayLines().add("§7[AI thinking about...]");
|
||||||
|
session.sendMessage(msg, response -> {
|
||||||
|
displayLines().remove("§7[AI thinking about...]");
|
||||||
|
displayLines().add("§6AI: §f" + response);
|
||||||
|
// auto-scroll to bottom
|
||||||
|
scrollOffset = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||||
|
renderBackground(context);
|
||||||
|
|
||||||
|
// Chat box background
|
||||||
|
context.fill(width / 2 - 155, 10, width / 2 + 155, height - 45, 0xAA000000);
|
||||||
|
|
||||||
|
// Title
|
||||||
|
context.drawCenteredTextWithShadow(textRenderer, "§6AI Companion", width / 2, 15, 0xFFFFFF);
|
||||||
|
|
||||||
|
// Pre-wrap all lines first so we know total height
|
||||||
|
int lineHeight = 11;
|
||||||
|
List<OrderedText> allWrapped = new ArrayList<>();
|
||||||
|
for (String dl : displayLines()) {
|
||||||
|
allWrapped.addAll(textRenderer.wrapLines(Text.literal(dl), 290));
|
||||||
|
}
|
||||||
|
|
||||||
|
int chatAreaHeight = height - 65;
|
||||||
|
int maxLines = chatAreaHeight / lineHeight;
|
||||||
|
int totalLines = allWrapped.size();
|
||||||
|
|
||||||
|
int startIdx = Math.max(0, totalLines - maxLines - scrollOffset);
|
||||||
|
int endIdx = Math.min(totalLines, startIdx + maxLines);
|
||||||
|
|
||||||
|
int y = 25;
|
||||||
|
for (int i = startIdx; i < endIdx; i++) {
|
||||||
|
context.drawTextWithShadow(textRenderer, allWrapped.get(i), width / 2 - 148, y, 0xFFFFFF);
|
||||||
|
y += lineHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.render(context, mouseX, mouseY, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
|
||||||
|
scrollOffset = Math.max(0, scrollOffset - (int) amount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldPause() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package AiCompanion.aicompanion2_0.client;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class AiChatSession {
|
||||||
|
|
||||||
|
private final String apiBaseUrl;
|
||||||
|
private final String apiKey;
|
||||||
|
private final String model;
|
||||||
|
|
||||||
|
// OpenAI-format message history: alternating user/assistant
|
||||||
|
private final List<String[]> history = new ArrayList<>(); // [role, content]
|
||||||
|
private final List<String> displayLines = new ArrayList<>();
|
||||||
|
|
||||||
|
public AiChatSession(String apiBaseUrl, String apiKey, String model) {
|
||||||
|
this.apiBaseUrl = apiBaseUrl;
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.model = model;
|
||||||
|
displayLines.add("§7Start a conversation with your AI companion!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getDisplayLines() {
|
||||||
|
return displayLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMessage(String userMessage, Consumer<String> onResponse) {
|
||||||
|
history.add(new String[]{"user", userMessage});
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
String response = callOpenWebUI();
|
||||||
|
history.add(new String[]{"assistant", response});
|
||||||
|
onResponse.accept(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
onResponse.accept("§cError: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String callOpenWebUI() throws Exception {
|
||||||
|
// Build messages array
|
||||||
|
StringBuilder messages = new StringBuilder("[");
|
||||||
|
for (int i = 0; i < history.size(); i++) {
|
||||||
|
String[] msg = history.get(i);
|
||||||
|
messages.append("{\"role\":\"").append(msg[0])
|
||||||
|
.append("\",\"content\":\"").append(jsonEscape(msg[1]))
|
||||||
|
.append("\"}");
|
||||||
|
if (i < history.size() - 1) messages.append(",");
|
||||||
|
}
|
||||||
|
messages.append("]");
|
||||||
|
|
||||||
|
String json = "{\"model\":\"" + jsonEscape(model) + "\",\"messages\":" + messages + ",\"stream\":false}";
|
||||||
|
HttpResult primary = postChatCompletion("/api/chat/completions", json);
|
||||||
|
if (primary.status == 200) {
|
||||||
|
return extractAssistantContent(primary.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (primary.status >= 500 || primary.status == 404) {
|
||||||
|
HttpResult fallback = postChatCompletion("/v1/chat/completions", 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/chat/completions");
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResult postChatCompletion(String path, 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 (apiKey != null && !apiKey.isBlank()) {
|
||||||
|
conn.setRequestProperty("Authorization", "Bearer " + apiKey);
|
||||||
|
}
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(json.getBytes("utf-8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
int status = conn.getResponseCode();
|
||||||
|
String body = readResponseBody(conn, status >= 400);
|
||||||
|
conn.disconnect();
|
||||||
|
return new HttpResult(status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readResponseBody(HttpURLConnection conn, boolean error) throws IOException {
|
||||||
|
var stream = error ? conn.getErrorStream() : conn.getInputStream();
|
||||||
|
if (stream == null) return "";
|
||||||
|
|
||||||
|
StringBuilder resp = new StringBuilder();
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(stream, "utf-8"))) {
|
||||||
|
String line;
|
||||||
|
while ((line = br.readLine()) != null) resp.append(line);
|
||||||
|
}
|
||||||
|
return resp.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractAssistantContent(String body) {
|
||||||
|
int idx = body.indexOf("\"content\":\"");
|
||||||
|
if (idx >= 0) {
|
||||||
|
int start = idx + 11;
|
||||||
|
int end = body.indexOf("\"", start);
|
||||||
|
while (end > 0 && body.charAt(end - 1) == '\\') {
|
||||||
|
end = body.indexOf("\"", end + 1);
|
||||||
|
}
|
||||||
|
return body.substring(start, end).replace("\\n", "\n").replace("\\\"", "\"");
|
||||||
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatHttpError(int status, String body, String path) {
|
||||||
|
if (body == null || body.isBlank()) {
|
||||||
|
return "Error: HTTP " + status + " (" + path + ")";
|
||||||
|
}
|
||||||
|
return "Error: HTTP " + status + " (" + path + ") - " + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String jsonEscape(String value) {
|
||||||
|
if (value == null) return "";
|
||||||
|
return value.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\t", "\\t");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HttpResult {
|
||||||
|
private final int status;
|
||||||
|
private final String body;
|
||||||
|
|
||||||
|
private HttpResult(int status, String body) {
|
||||||
|
this.status = status;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package AiCompanion.aicompanion2_0.client;
|
||||||
|
|
||||||
|
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.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.util.ActionResult;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
|
public class AiCompanionClient implements ClientModInitializer {
|
||||||
|
|
||||||
|
// One session per world load, shared across all right-clicks
|
||||||
|
private static AiChatSession currentSession = null;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInitializeClient() {
|
||||||
|
// Register renderer
|
||||||
|
EntityRendererRegistry.register(Aicompanion2_0.AI_COMPANION, (context) ->
|
||||||
|
new BipedEntityRenderer<AIEntity, PlayerEntityModel<AIEntity>>(
|
||||||
|
context,
|
||||||
|
new PlayerEntityModel<>(context.getPart(EntityModelLayers.PLAYER), false),
|
||||||
|
0.5f
|
||||||
|
) {
|
||||||
|
@Override
|
||||||
|
public Identifier getTexture(AIEntity entity) {
|
||||||
|
return new Identifier("aicompanion2_0", "textures/entity/skin.png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clear session when leaving a world
|
||||||
|
ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {
|
||||||
|
currentSession = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ActionResult.SUCCESS;
|
||||||
|
}
|
||||||
|
return ActionResult.PASS;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openChatScreen(MinecraftClient client) {
|
||||||
|
if (currentSession == null) {
|
||||||
|
currentSession = new AiChatSession(
|
||||||
|
Aicompanion2_0.getApiBaseUrl(),
|
||||||
|
ClientConfig.getApiKey(),
|
||||||
|
Aicompanion2_0.getModel()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
client.setScreen(new AiChatScreen(currentSession));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package AiCompanion.aicompanion2_0.client;
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
|
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||||
|
import net.minecraft.client.gui.widget.TextFieldWidget;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
public class ApiKeyScreen extends Screen {
|
||||||
|
|
||||||
|
private TextFieldWidget keyField;
|
||||||
|
private final Runnable onSuccess;
|
||||||
|
|
||||||
|
public ApiKeyScreen(Runnable onSuccess) {
|
||||||
|
super(Text.literal("Enter API Key"));
|
||||||
|
this.onSuccess = onSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
keyField = new TextFieldWidget(
|
||||||
|
textRenderer, width / 2 - 150, height / 2 - 10, 300, 20,
|
||||||
|
Text.literal("API Key...")
|
||||||
|
);
|
||||||
|
keyField.setMaxLength(200);
|
||||||
|
keyField.setFocused(true);
|
||||||
|
addDrawableChild(keyField);
|
||||||
|
|
||||||
|
addDrawableChild(ButtonWidget.builder(Text.literal("Confirm"), btn -> confirm())
|
||||||
|
.dimensions(width / 2 - 75, height / 2 + 20, 150, 20)
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||||
|
if (keyCode == 257 || keyCode == 335) { // Enter
|
||||||
|
confirm();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirm() {
|
||||||
|
String key = keyField.getText().trim();
|
||||||
|
if (key.isEmpty()) return;
|
||||||
|
|
||||||
|
ClientConfig.setApiKey(key);
|
||||||
|
MinecraftClient.getInstance().execute(() -> {
|
||||||
|
close();
|
||||||
|
onSuccess.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||||
|
renderBackground(context);
|
||||||
|
context.drawCenteredTextWithShadow(textRenderer,
|
||||||
|
"§6Please enter your Open WebUI API key:", width / 2, height / 2 - 30, 0xFFFFFF);
|
||||||
|
context.drawCenteredTextWithShadow(textRenderer,
|
||||||
|
"§7(Stored locally, only required once.)", width / 2, height / 2 - 20, 0xAAAAAA);
|
||||||
|
super.render(context, mouseX, mouseY, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldPause() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package AiCompanion.aicompanion2_0.client;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class ClientConfig {
|
||||||
|
|
||||||
|
private static final Path CONFIG_PATH = Path.of("config", "aicompanion2_0_client.properties");
|
||||||
|
private static final Path SHARED_CONFIG_PATH = Path.of("config", "aicompanion2_0.properties");
|
||||||
|
private static String apiKey = null;
|
||||||
|
|
||||||
|
public static String getApiKey() {
|
||||||
|
if (apiKey == null) load();
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setApiKey(String key) {
|
||||||
|
apiKey = key;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasApiKey() {
|
||||||
|
if (apiKey == null) load();
|
||||||
|
return apiKey != null && !apiKey.isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void load() {
|
||||||
|
try {
|
||||||
|
// Prefer shared config so GUI and /ai frage use the same key source.
|
||||||
|
apiKey = loadApiKeyFrom(SHARED_CONFIG_PATH);
|
||||||
|
if (apiKey == null || apiKey.isBlank()) {
|
||||||
|
apiKey = loadApiKeyFrom(CONFIG_PATH);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println("[aicompanion2_0] Could not load Client Config.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void save() {
|
||||||
|
try {
|
||||||
|
saveApiKeyTo(CONFIG_PATH, "AI Companion Client Config");
|
||||||
|
saveApiKeyTo(SHARED_CONFIG_PATH, "AI Companion Shared Config");
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println("[aicompanion2_0] Could not save Client Config.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String loadApiKeyFrom(Path path) throws IOException {
|
||||||
|
if (!Files.exists(path)) return null;
|
||||||
|
Properties props = new Properties();
|
||||||
|
try (FileInputStream in = new FileInputStream(path.toFile())) {
|
||||||
|
props.load(in);
|
||||||
|
}
|
||||||
|
return props.getProperty("api.key", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveApiKeyTo(Path path, String comment) throws IOException {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Properties props = new Properties();
|
||||||
|
if (Files.exists(path)) {
|
||||||
|
try (FileInputStream in = new FileInputStream(path.toFile())) {
|
||||||
|
props.load(in);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
props.setProperty("api.key", apiKey != null ? apiKey : "");
|
||||||
|
try (FileOutputStream out = new FileOutputStream(path.toFile())) {
|
||||||
|
props.store(out, comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/main/java/AiCompanion/aicompanion2_0/AIEntity.java
Normal file
82
src/main/java/AiCompanion/aicompanion2_0/AIEntity.java
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package AiCompanion.aicompanion2_0;
|
||||||
|
|
||||||
|
import net.minecraft.entity.EntityType;
|
||||||
|
import net.minecraft.entity.ai.goal.*;
|
||||||
|
import net.minecraft.entity.ai.pathing.MobNavigation;
|
||||||
|
import net.minecraft.entity.attribute.DefaultAttributeContainer;
|
||||||
|
import net.minecraft.entity.attribute.EntityAttributes;
|
||||||
|
import net.minecraft.entity.damage.DamageSource;
|
||||||
|
import net.minecraft.entity.passive.PassiveEntity;
|
||||||
|
import net.minecraft.entity.passive.TameableEntity;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.minecraft.server.world.ServerWorld;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.world.EntityView;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
public class AIEntity extends TameableEntity {
|
||||||
|
|
||||||
|
public AIEntity(EntityType<? extends TameableEntity> entityType, World world) {
|
||||||
|
super(entityType, world);
|
||||||
|
this.setInvulnerable(true);
|
||||||
|
this.setCustomName(Text.translatable("entity.aicompanion2_0.ai_companion"));
|
||||||
|
this.setCustomNameVisible(true);
|
||||||
|
// Ensure navigation is set up for following
|
||||||
|
this.navigation = new MobNavigation(this, world);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DefaultAttributeContainer.Builder createMobAttributes() {
|
||||||
|
return TameableEntity.createMobAttributes()
|
||||||
|
.add(EntityAttributes.GENERIC_MAX_HEALTH, 20.0)
|
||||||
|
.add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.3)
|
||||||
|
.add(EntityAttributes.GENERIC_FOLLOW_RANGE, 32.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initGoals() {
|
||||||
|
this.goalSelector.add(1, new FollowOwnerGoal(this, 1.0, 5.0f, 2.0f, false));
|
||||||
|
this.goalSelector.add(2, new WanderAroundFarGoal(this, 0.8));
|
||||||
|
this.goalSelector.add(3, new LookAtEntityGoal(this, PlayerEntity.class, 8.0f));
|
||||||
|
this.goalSelector.add(4, new LookAroundGoal(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean damage(DamageSource source, float amount) {
|
||||||
|
// Allow kill command through, block everything else
|
||||||
|
if (source == this.getDamageSources().outOfWorld()) return super.damage(source, amount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void kill() {
|
||||||
|
super.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Text getName() {
|
||||||
|
return Text.translatable("entity.aicompanion2_0.ai_companion");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Text getDisplayName() {
|
||||||
|
return Text.translatable("entity.aicompanion2_0.ai_companion");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBreedingItem(ItemStack stack) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public PassiveEntity createChild(ServerWorld world, PassiveEntity entity) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bridge method fix
|
||||||
|
public EntityView method_48926() {
|
||||||
|
return (EntityView) this.getWorld();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,134 +1,273 @@
|
|||||||
package AiCompanion.aicompanion2_0;
|
package AiCompanion.aicompanion2_0;
|
||||||
|
|
||||||
import net.fabricmc.api.ModInitializer;
|
|
||||||
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
|
|
||||||
import net.minecraft.server.command.CommandManager;
|
|
||||||
import net.minecraft.text.Text;
|
|
||||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||||
|
|
||||||
|
import net.fabricmc.api.ModInitializer;
|
||||||
|
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
|
||||||
|
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;
|
||||||
|
import net.minecraft.entity.EntityType;
|
||||||
|
import net.minecraft.entity.SpawnGroup;
|
||||||
|
import net.minecraft.registry.Registries;
|
||||||
|
import net.minecraft.registry.Registry;
|
||||||
|
import net.minecraft.server.command.CommandManager;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.Identifier;
|
||||||
|
|
||||||
public class Aicompanion2_0 implements ModInitializer {
|
public class Aicompanion2_0 implements ModInitializer {
|
||||||
|
|
||||||
public static final String MOD_ID = "aicompanion2_0";
|
public static final String MOD_ID = "aicompanion2_0";
|
||||||
private static String API_BASE_URL = "http://localhost:11434";
|
private static String API_BASE_URL = "https://ai.cametendo.org";
|
||||||
private static String API_PATH = "/api/generate";
|
private static String MODEL = "minecraft-helper";
|
||||||
|
private static String API_KEY = "";
|
||||||
|
private static String API_PATH = "/api/chat/completions";
|
||||||
|
|
||||||
|
public static final EntityType<AIEntity> AI_COMPANION = Registry.register(
|
||||||
|
Registries.ENTITY_TYPE,
|
||||||
|
new Identifier(MOD_ID, "ai_companion"),
|
||||||
|
FabricEntityTypeBuilder.create(SpawnGroup.CREATURE, AIEntity::new)
|
||||||
|
.dimensions(EntityDimensions.fixed(0.6f, 1.8f))
|
||||||
|
.build()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Static getters used by the client side
|
||||||
|
public static String getApiBaseUrl() { return API_BASE_URL; }
|
||||||
|
public static String getModel() { return MODEL; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitialize() {
|
public void onInitialize() {
|
||||||
|
loadConfig();
|
||||||
System.out.println("[" + MOD_ID + "] MOD STARTET!");
|
System.out.println("[" + MOD_ID + "] MOD STARTET!");
|
||||||
|
|
||||||
CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> {
|
FabricDefaultAttributeRegistry.register(AI_COMPANION, AIEntity.createMobAttributes());
|
||||||
|
|
||||||
|
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
|
||||||
System.out.println("[" + MOD_ID + "] Registriere /ai command");
|
System.out.println("[" + MOD_ID + "] Registriere /ai command");
|
||||||
|
|
||||||
dispatcher.register(
|
dispatcher.register(
|
||||||
CommandManager.literal("ai")
|
CommandManager.literal("ai")
|
||||||
.then(CommandManager.argument("frage", StringArgumentType.greedyString())
|
// /ai spawn
|
||||||
.executes(ctx -> {
|
.then(CommandManager.literal("spawn")
|
||||||
var player = ctx.getSource().getPlayer();
|
.executes(ctx -> {
|
||||||
String frage = StringArgumentType.getString(ctx, "frage");
|
var player = ctx.getSource().getPlayer();
|
||||||
|
if (player != null) {
|
||||||
|
AIEntity companion = new AIEntity(AI_COMPANION, player.getWorld());
|
||||||
|
companion.refreshPositionAndAngles(player.getX(), player.getY(), player.getZ(), 0, 0);
|
||||||
|
companion.setOwner(player);
|
||||||
|
companion.setTamed(true);
|
||||||
|
player.getWorld().spawnEntity(companion);
|
||||||
|
player.sendMessage(Text.literal("§6[AI] §fThe companion has been called!"), false);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// /ai kill
|
||||||
|
.then(CommandManager.literal("kill")
|
||||||
|
.executes(ctx -> {
|
||||||
|
var server = ctx.getSource().getServer();
|
||||||
|
int removed = 0;
|
||||||
|
|
||||||
|
for (var world : server.getWorlds()) {
|
||||||
|
var companions = world.getEntitiesByType(AI_COMPANION, entity -> true);
|
||||||
|
for (var companion : companions) {
|
||||||
|
// Force-remove companion even when invulnerable.
|
||||||
|
companion.discard();
|
||||||
|
removed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final int removedCount = removed;
|
||||||
|
ctx.getSource().sendFeedback(
|
||||||
|
() -> Text.literal("§6[AI] §fCompanion removed: " + removedCount), false);
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// /ai question <question>
|
||||||
|
.then(CommandManager.literal("question")
|
||||||
|
.then(CommandManager.argument("question", StringArgumentType.greedyString())
|
||||||
|
.executes(ctx -> {
|
||||||
|
var player = ctx.getSource().getPlayer();
|
||||||
|
var server = ctx.getSource().getServer();
|
||||||
|
String frage = StringArgumentType.getString(ctx, "question");
|
||||||
|
|
||||||
|
if (player != null) {
|
||||||
|
player.sendMessage(
|
||||||
|
Text.literal("§6[AI] §fThink about: " + frage), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
String antwort = callOllama(frage);
|
||||||
|
if (antwort == null || antwort.isEmpty()) return;
|
||||||
|
if (antwort.length() > 2000) antwort = antwort.substring(0, 2000) + "...";
|
||||||
if (player != null) {
|
if (player != null) {
|
||||||
player.sendMessage(
|
String finalAntwort = antwort;
|
||||||
Text.literal("§6[AI] §fDenke nach über: " + frage),
|
server.execute(() -> player.sendMessage(
|
||||||
false
|
Text.literal("§6[AI] §fAnswer: " + finalAntwort), false));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
if (player != null) {
|
||||||
|
String error = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
|
||||||
|
server.execute(() -> player.sendMessage(
|
||||||
|
Text.literal("§c[AI] Error with /ai question: " + error), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
|
||||||
// KI‑Request in neuem Thread, damit der Server nicht hängt
|
return 1;
|
||||||
new Thread(() -> {
|
})
|
||||||
try {
|
)
|
||||||
String antwort = callOllama(frage);
|
)
|
||||||
if (antwort == null || antwort.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Antwort etwas kürzen, damit der Chat nicht explodiert
|
|
||||||
if (antwort.length() > 2000) {
|
|
||||||
antwort = antwort.substring(0, 2000) + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (player != null) {
|
|
||||||
player.sendMessage(
|
|
||||||
Text.literal("§6[AI] §fAntwort: " + antwort),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
System.out.println("[" + MOD_ID + "] MOD GELADEN!");
|
System.out.println("[" + MOD_ID + "] MOD GELADEN!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Innerhalb deiner Klasse Aicompanion2_0
|
||||||
|
|
||||||
private String callOllama(String prompt) throws Exception {
|
private String callOllama(String prompt) throws Exception {
|
||||||
// URL deines Ollama‑Servers, ggf. anpassen
|
String json = "{\"model\":\"" + jsonEscape(MODEL) + "\",\"messages\":[{\"role\":\"user\",\"content\":\""
|
||||||
URL url = new URL(API_BASE_URL + API_PATH);
|
+ jsonEscape(prompt) + "\"}],\"stream\":false}";
|
||||||
|
|
||||||
|
HttpResult primary = postChatCompletion(API_PATH, 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 (fallback.status == 200) {
|
||||||
|
return extractAssistantContent(fallback.body);
|
||||||
|
}
|
||||||
|
return formatHttpError(fallback.status, fallback.body, "/v1/chat/completions");
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatHttpError(primary.status, primary.body, API_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResult postChatCompletion(String path, String json) throws Exception {
|
||||||
|
URL url = new URL(API_BASE_URL + path);
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
conn.setRequestMethod("POST");
|
conn.setRequestMethod("POST");
|
||||||
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
conn.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
||||||
|
if (API_KEY != null && !API_KEY.isBlank()) {
|
||||||
|
conn.setRequestProperty("Authorization", "Bearer " + API_KEY);
|
||||||
|
}
|
||||||
conn.setDoOutput(true);
|
conn.setDoOutput(true);
|
||||||
|
|
||||||
String json = """
|
|
||||||
{
|
|
||||||
"model": "llama3",
|
|
||||||
"prompt": "%s"
|
|
||||||
}
|
|
||||||
""".formatted(prompt.replace("\"", "\\\""));
|
|
||||||
|
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
byte[] input = json.getBytes("utf-8");
|
os.write(json.getBytes("utf-8"));
|
||||||
os.write(input, 0, input.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int status = conn.getResponseCode();
|
int status = conn.getResponseCode();
|
||||||
if (status != 200) {
|
String body = readResponseBody(conn, status >= 400);
|
||||||
System.out.println("[" + MOD_ID + "] Ollama HTTP Status: " + status);
|
conn.disconnect();
|
||||||
return null;
|
return new HttpResult(status, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractAssistantContent(String body) {
|
||||||
|
int idx = body.indexOf("\"content\":\"");
|
||||||
|
if (idx >= 0) {
|
||||||
|
int start = idx + 11;
|
||||||
|
int end = body.indexOf("\"", start);
|
||||||
|
while (end > 0 && body.charAt(end - 1) == '\\') end = body.indexOf("\"", end + 1);
|
||||||
|
return body.substring(start, end).replace("\\n", "\n").replace("\\\"", "\"");
|
||||||
}
|
}
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatHttpError(int status, String body, String path) {
|
||||||
|
if (body == null || body.isBlank()) {
|
||||||
|
return "Fehler: HTTP " + status + " (" + path + ")";
|
||||||
|
}
|
||||||
|
return "Fehler: HTTP " + status + " (" + path + ") - " + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readResponseBody(HttpURLConnection conn, boolean error) throws IOException {
|
||||||
|
var stream = error ? conn.getErrorStream() : conn.getInputStream();
|
||||||
|
if (stream == null) return "";
|
||||||
|
|
||||||
StringBuilder resp = new StringBuilder();
|
StringBuilder resp = new StringBuilder();
|
||||||
try (BufferedReader br = new BufferedReader(
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(stream, "utf-8"))) {
|
||||||
new InputStreamReader(conn.getInputStream(), "utf-8"))) {
|
|
||||||
String line;
|
String line;
|
||||||
while ((line = br.readLine()) != null) {
|
while ((line = br.readLine()) != null) resp.append(line);
|
||||||
resp.append(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.disconnect();
|
|
||||||
|
|
||||||
// Sehr einfach: komplette JSON‑Antwort zurückgeben
|
|
||||||
// Später können wir das noch sauber parsen
|
|
||||||
return resp.toString();
|
return resp.toString();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
private void loadConfig() {
|
private String jsonEscape(String value) {
|
||||||
try {
|
if (value == null) return "";
|
||||||
Path configPath = Path.of("config", "aicompanion2_0.properties");
|
String escaped = value.replace("\\", "\\\\")
|
||||||
|
.replace("\"", "\\\"")
|
||||||
|
.replace("\n", "\\n")
|
||||||
|
.replace("\r", "\\r")
|
||||||
|
.replace("\t", "\\t");
|
||||||
|
return escaped;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HttpResult {
|
||||||
|
private final int status;
|
||||||
|
private final String body;
|
||||||
|
|
||||||
|
private HttpResult(int status, String body) {
|
||||||
|
this.status = status;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConfig() {
|
||||||
|
try {
|
||||||
|
Path configPath = Path.of("config", "aicompanion2_0.properties");
|
||||||
|
Properties props = new Properties();
|
||||||
|
try (FileInputStream in = new FileInputStream(configPath.toFile())) {
|
||||||
|
props.load(in);
|
||||||
|
}
|
||||||
|
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"));
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readApiKeyFrom(Path configPath) throws IOException {
|
||||||
|
if (!Files.exists(configPath)) return null;
|
||||||
Properties props = new Properties();
|
Properties props = new Properties();
|
||||||
try (FileInputStream in = new FileInputStream(configPath.toFile())) {
|
try (FileInputStream in = new FileInputStream(configPath.toFile())) {
|
||||||
props.load(in);
|
props.load(in);
|
||||||
}
|
}
|
||||||
API_BASE_URL = props.getProperty("api.baseUrl", API_BASE_URL);
|
return props.getProperty("api.key", null);
|
||||||
API_PATH = props.getProperty("api.path", API_PATH);
|
|
||||||
} catch (IOException e) {
|
|
||||||
System.out.println("[" + MOD_ID + "] Keine Config gefunden, benutze Default-API.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
src/main/resources/aicompanion2_0.mixins.json
Normal file
10
src/main/resources/aicompanion2_0.mixins.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"required": true,
|
||||||
|
"minVersion": "0.8",
|
||||||
|
"package": "AiCompanion.aicompanion2_0.mixin",
|
||||||
|
"compatibilityLevel": "JAVA_17",
|
||||||
|
"mixins": [],
|
||||||
|
"injectors": {
|
||||||
|
"defaultRequire": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/main/resources/assets/aicompanion2_0/lang/de_de.json
Normal file
1
src/main/resources/assets/aicompanion2_0/lang/de_de.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"entity.aicompanion2_0.ai_companion": "KI-Begleiter"}
|
||||||
1
src/main/resources/assets/aicompanion2_0/lang/en_us.json
Normal file
1
src/main/resources/assets/aicompanion2_0/lang/en_us.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"entity.aicompanion2_0.ai_companion": "AI-Companion"}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -12,9 +12,14 @@
|
|||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"main": [
|
"main": [
|
||||||
"AiCompanion.aicompanion2_0.Aicompanion2_0"
|
"AiCompanion.aicompanion2_0.Aicompanion2_0"
|
||||||
|
],
|
||||||
|
"client": [
|
||||||
|
"AiCompanion.aicompanion2_0.client.AiCompanionClient"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"mixins": [],
|
"mixins": [
|
||||||
|
"aicompanion2_0.mixins.json"
|
||||||
|
],
|
||||||
"depends": {
|
"depends": {
|
||||||
"fabricloader": ">=0.14.0",
|
"fabricloader": ">=0.14.0",
|
||||||
"fabric-api": "*",
|
"fabric-api": "*",
|
||||||
|
|||||||
Reference in New Issue
Block a user