mirror of
https://github.com/ritonioz/minecraft-ai-companion-mod-EMVs12-Project.git
synced 2026-06-20 20:25:01 +02:00
Compare commits
6 Commits
beta-v1.4
...
29ae555047
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29ae555047 | ||
|
|
9ec6786f6f | ||
|
|
63d6a32648 | ||
|
|
8cc6fa53d9 | ||
|
|
271d43e6e6 | ||
|
|
f1171adf40 |
@@ -19,6 +19,7 @@ There are three commands included in this mod:
|
||||
- `/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
|
||||
- `/ai delete-key`: This lets you delete your API-Key, incase you deleted your account on the WebUI or experience other problems, you can use this command to enter a new key.
|
||||
- Note: Depending on the question the AI will take 1 - 3 minutes to respond. Please be patient while your companion "thinks"!
|
||||
|
||||
## AI Companion Entity
|
||||
@@ -26,7 +27,7 @@ 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.
|
||||
- `API-Key-Verification`: The first time you play this mod, you may need to use the companion to enter your API key.
|
||||
|
||||
|
||||
## Installation
|
||||
@@ -43,8 +44,9 @@ This mod requires an API key. To acquire an API key, follow these steps:
|
||||
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!
|
||||
5. Use /ai question and enter a question. A new window with "Enter your Open-WebUI API-Key" should appear. In the text box, enter your API Key and confirm it by pressing enter.
|
||||
6. Alternatively, if that didn't work, use /ai spawn to spawn your AI Companion. THen right-click it and you should see the "Enter your Open-WebUI API-Key" window. Enter it into the text box and confirm it by pressing enter.
|
||||
7. If everything worked, you can now send messages to your AI Companion. Have fun!
|
||||
|
||||
## License & Credits
|
||||
* **Authors:** [Cametendo](https://www.github.com/Cametendo), [ritonioz](https://www.github.com/ritonioz), [Adam237A](https://www.github.com/Adam237A)
|
||||
|
||||
@@ -31,6 +31,13 @@ public class AiChatSession {
|
||||
public void sendMessage(String userMessage, Consumer<String> onResponse) {
|
||||
history.add(new String[]{"user", userMessage});
|
||||
|
||||
if (AiCompanionClient.tryTriggerArchEasterEgg(userMessage, response -> {
|
||||
history.add(new String[]{"assistant", response});
|
||||
onResponse.accept(response);
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String response = callOpenWebUI();
|
||||
|
||||
@@ -4,19 +4,29 @@ 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.fabricmc.fabric.api.networking.v1.PacketByteBufs;
|
||||
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;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class AiCompanionClient implements ClientModInitializer {
|
||||
private static final Identifier DEFAULT_TEXTURE = new Identifier("aicompanion2_0", "textures/entity/skin.png");
|
||||
private static final Identifier TUX_TEXTURE = new Identifier("aicompanion2_0", "textures/entity/tux-mc.png");
|
||||
|
||||
// One session per world load, shared across all right-clicks
|
||||
private static AiChatSession currentSession = null;
|
||||
private static final Deque<Consumer<String>> pendingArchResponses = new ArrayDeque<>();
|
||||
|
||||
@Override
|
||||
public void onInitializeClient() {
|
||||
@@ -29,7 +39,7 @@ public class AiCompanionClient implements ClientModInitializer {
|
||||
) {
|
||||
@Override
|
||||
public Identifier getTexture(AIEntity entity) {
|
||||
return new Identifier("aicompanion2_0", "textures/entity/skin.png");
|
||||
return entity.isTuxMode() ? TUX_TEXTURE : DEFAULT_TEXTURE;
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -39,28 +49,131 @@ 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));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
ClientPlayNetworking.registerGlobalReceiver(Aicompanion2_0.ARCH_EASTER_EGG_RESPONSE_PACKET_ID, (client, handler, buf, responseSender) -> {
|
||||
String response = buf.readString(32767);
|
||||
client.execute(() -> finishArchEasterEgg(response));
|
||||
});
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
public static boolean tryTriggerArchEasterEgg(String message, Consumer<String> onResponse) {
|
||||
if (!isArchEasterEggTrigger(message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ClientPlayNetworking.canSend(Aicompanion2_0.ARCH_EASTER_EGG_PACKET_ID)) {
|
||||
onResponse.accept("§cError: The server does not support the Arch easter egg.");
|
||||
return true;
|
||||
}
|
||||
|
||||
pendingArchResponses.addLast(onResponse);
|
||||
|
||||
try {
|
||||
ClientPlayNetworking.send(Aicompanion2_0.ARCH_EASTER_EGG_PACKET_ID, PacketByteBufs.create());
|
||||
} catch (RuntimeException e) {
|
||||
pendingArchResponses.pollLast();
|
||||
String error = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
|
||||
onResponse.accept("§cError: " + error);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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 boolean isArchEasterEggTrigger(String message) {
|
||||
String normalized = message.trim().replaceAll("\\s+", " ").toLowerCase();
|
||||
return "i use arch btw".equals(normalized)
|
||||
|| "sudo".equals(normalized)
|
||||
|| normalized.startsWith("sudo ");
|
||||
}
|
||||
|
||||
private static void finishArchEasterEgg(String response) {
|
||||
Consumer<String> callback = pendingArchResponses.pollFirst();
|
||||
if (callback != null) {
|
||||
callback.accept(response);
|
||||
}
|
||||
}
|
||||
|
||||
private static void openChatScreen(MinecraftClient client) {
|
||||
if (currentSession == null) {
|
||||
currentSession = new AiChatSession(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ 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.data.DataTracker;
|
||||
import net.minecraft.entity.data.TrackedData;
|
||||
import net.minecraft.entity.data.TrackedDataHandlerRegistry;
|
||||
import net.minecraft.entity.damage.DamageSource;
|
||||
import net.minecraft.entity.passive.PassiveEntity;
|
||||
import net.minecraft.entity.passive.TameableEntity;
|
||||
@@ -17,6 +20,9 @@ import net.minecraft.world.World;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class AIEntity extends TameableEntity {
|
||||
private static final TrackedData<Boolean> TUX_MODE =
|
||||
DataTracker.registerData(AIEntity.class, TrackedDataHandlerRegistry.BOOLEAN);
|
||||
private int tuxTicksRemaining = 0;
|
||||
|
||||
public AIEntity(EntityType<? extends TameableEntity> entityType, World world) {
|
||||
super(entityType, world);
|
||||
@@ -34,6 +40,12 @@ public class AIEntity extends TameableEntity {
|
||||
.add(EntityAttributes.GENERIC_FOLLOW_RANGE, 32.0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initDataTracker() {
|
||||
super.initDataTracker();
|
||||
this.dataTracker.startTracking(TUX_MODE, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initGoals() {
|
||||
this.goalSelector.add(1, new FollowOwnerGoal(this, 1.0, 5.0f, 2.0f, false));
|
||||
@@ -42,6 +54,19 @@ public class AIEntity extends TameableEntity {
|
||||
this.goalSelector.add(4, new LookAroundGoal(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
|
||||
if (!this.getWorld().isClient() && tuxTicksRemaining > 0) {
|
||||
tuxTicksRemaining--;
|
||||
|
||||
if (tuxTicksRemaining == 0) {
|
||||
this.dataTracker.set(TUX_MODE, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean damage(DamageSource source, float amount) {
|
||||
// Allow kill command through, block everything else
|
||||
@@ -75,8 +100,17 @@ public class AIEntity extends TameableEntity {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void activateTuxMode(int durationTicks) {
|
||||
tuxTicksRemaining = Math.max(tuxTicksRemaining, durationTicks);
|
||||
this.dataTracker.set(TUX_MODE, true);
|
||||
}
|
||||
|
||||
public boolean isTuxMode() {
|
||||
return this.dataTracker.get(TUX_MODE);
|
||||
}
|
||||
|
||||
// Bridge method fix
|
||||
public EntityView method_48926() {
|
||||
return (EntityView) this.getWorld();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,30 +9,52 @@ import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Files;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
|
||||
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;
|
||||
import net.minecraft.entity.EntityType;
|
||||
import net.minecraft.entity.SpawnGroup;
|
||||
import net.minecraft.entity.effect.StatusEffectInstance;
|
||||
import net.minecraft.entity.effect.StatusEffects;
|
||||
import net.minecraft.registry.Registries;
|
||||
import net.minecraft.registry.Registry;
|
||||
import net.minecraft.server.command.CommandManager;
|
||||
import net.minecraft.server.network.ServerPlayerEntity;
|
||||
import net.minecraft.server.world.ServerWorld;
|
||||
import net.minecraft.text.Text;
|
||||
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");
|
||||
private static final long ARCH_EASTER_EGG_COOLDOWN_MS = 15L * 60L * 1000L;
|
||||
private static final int ARCH_EASTER_EGG_BUFF_DURATION_TICKS = 20 * 60;
|
||||
private static final int ARCH_EASTER_EGG_TUX_DURATION_TICKS = 20 * 10;
|
||||
private static final Map<UUID, Long> ARCH_EASTER_EGG_LAST_USED = new HashMap<>();
|
||||
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 Identifier ARCH_EASTER_EGG_PACKET_ID = new Identifier(MOD_ID, "arch_easter_egg");
|
||||
public static final Identifier ARCH_EASTER_EGG_RESPONSE_PACKET_ID = new Identifier(MOD_ID, "arch_easter_egg_response");
|
||||
|
||||
public static final EntityType<AIEntity> AI_COMPANION = Registry.register(
|
||||
Registries.ENTITY_TYPE,
|
||||
@@ -52,6 +74,9 @@ public class Aicompanion2_0 implements ModInitializer {
|
||||
System.out.println("[" + MOD_ID + "] MOD STARTET!");
|
||||
|
||||
FabricDefaultAttributeRegistry.register(AI_COMPANION, AIEntity.createMobAttributes());
|
||||
ServerPlayNetworking.registerGlobalReceiver(ARCH_EASTER_EGG_PACKET_ID, (server, player, handler, buf, responseSender) ->
|
||||
server.execute(() -> sendArchEasterEggResponse(player))
|
||||
);
|
||||
|
||||
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> {
|
||||
System.out.println("[" + MOD_ID + "] Registriere /ai command");
|
||||
@@ -94,6 +119,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 <question>
|
||||
.then(CommandManager.literal("question")
|
||||
.then(CommandManager.argument("question", StringArgumentType.greedyString())
|
||||
@@ -103,8 +161,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 +206,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 +303,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 +347,68 @@ public class Aicompanion2_0 implements ModInitializer {
|
||||
}
|
||||
return props.getProperty("api.key", null);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void sendArchEasterEggResponse(ServerPlayerEntity player) {
|
||||
if (!ServerPlayNetworking.canSend(player, ARCH_EASTER_EGG_RESPONSE_PACKET_ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String response = triggerArchEasterEgg(player);
|
||||
var buf = PacketByteBufs.create();
|
||||
buf.writeString(response);
|
||||
ServerPlayNetworking.send(player, ARCH_EASTER_EGG_RESPONSE_PACKET_ID, buf);
|
||||
}
|
||||
|
||||
private String triggerArchEasterEgg(ServerPlayerEntity player) {
|
||||
long now = System.currentTimeMillis();
|
||||
Long lastUsed = ARCH_EASTER_EGG_LAST_USED.get(player.getUuid());
|
||||
|
||||
if (lastUsed != null) {
|
||||
long remainingMs = ARCH_EASTER_EGG_COOLDOWN_MS - (now - lastUsed);
|
||||
if (remainingMs > 0) {
|
||||
return "Optimization already complete. Cooldown remaining: " + formatCooldown(remainingMs) + ".";
|
||||
}
|
||||
}
|
||||
|
||||
ARCH_EASTER_EGG_LAST_USED.put(player.getUuid(), now);
|
||||
player.addStatusEffect(new StatusEffectInstance(StatusEffects.SPEED, ARCH_EASTER_EGG_BUFF_DURATION_TICKS, 1));
|
||||
player.addStatusEffect(new StatusEffectInstance(StatusEffects.HASTE, ARCH_EASTER_EGG_BUFF_DURATION_TICKS, 1));
|
||||
activateTuxModeForOwnedCompanions(player);
|
||||
return "Optimization complete. Bloatware removed.";
|
||||
}
|
||||
|
||||
private void activateTuxModeForOwnedCompanions(ServerPlayerEntity player) {
|
||||
for (ServerWorld world : player.getServer().getWorlds()) {
|
||||
for (AIEntity companion : world.getEntitiesByType(AI_COMPANION, entity ->
|
||||
player.getUuid().equals(entity.getOwnerUuid()))) {
|
||||
companion.activateTuxMode(ARCH_EASTER_EGG_TUX_DURATION_TICKS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String formatCooldown(long remainingMs) {
|
||||
long totalSeconds = Math.max(1, (remainingMs + 999) / 1000);
|
||||
long minutes = totalSeconds / 60;
|
||||
long seconds = totalSeconds % 60;
|
||||
return minutes + "m " + seconds + "s";
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
Reference in New Issue
Block a user