diff --git a/src/autoclef/java/adris/altoclef/AltoClefController.java b/src/autoclef/java/adris/altoclef/AltoClefController.java index 295974e2..3c79bed3 100644 --- a/src/autoclef/java/adris/altoclef/AltoClefController.java +++ b/src/autoclef/java/adris/altoclef/AltoClefController.java @@ -18,7 +18,8 @@ import adris.altoclef.player2api.EventQueueManager; import adris.altoclef.player2api.AIPersistantData; import adris.altoclef.player2api.Player2APIService; - +import adris.altoclef.player2api.pseudocommands.PseudoCommandExecutor; +import adris.altoclef.player2api.pseudocommands.PseudoCommands.PseudoCommand; import adris.altoclef.player2api.Character; import adris.altoclef.tasksystem.Task; import adris.altoclef.tasksystem.TaskRunner; @@ -49,7 +50,6 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; - public class AltoClefController { private final IBaritone baritone; private AIPersistantData aiPersistantData; @@ -80,6 +80,9 @@ public class AltoClefController { private Task storedTask; public boolean isStopping = false; private Player owner; + private Optional currentPseudoCommandInfoAsString = Optional.empty(); + public boolean shouldCancelPseudoCommand = false; + private PseudoCommandExecutor pseudoCommandExecutor; public AltoClefController(IBaritone baritone, Character character, String player2GameId) { this.baritone = baritone; @@ -96,7 +99,8 @@ public AltoClefController(IBaritone baritone, Character character, String player new WorldSurvivalChain(this.taskRunner); this.foodChain = new FoodChain(this.taskRunner); new PlayerDefenseChain(this.taskRunner); - this.storageTracker = new ItemStorageTracker(this, this.trackerManager, container -> this.containerSubTracker = container); + this.storageTracker = new ItemStorageTracker(this, this.trackerManager, + container -> this.containerSubTracker = container); this.entityTracker = new EntityTracker(this.trackerManager); this.blockScanner = new BlockScanner(this); this.chunkTracker = new SimpleChunkTracker(this); @@ -111,26 +115,28 @@ public AltoClefController(IBaritone baritone, Character character, String player this.botBehaviour = new BotBehaviour(this); this.initializeCommands(); Settings.load( - newSettings -> { - this.settings = newSettings; - List baritoneCanPlace = Arrays.stream(this.settings.getThrowawayItems(this, true)).toList(); - this.getBaritoneSettings().acceptableThrowawayItems.get().addAll(baritoneCanPlace); - if ((!this.getUserTaskChain().isActive() || this.getUserTaskChain().isRunningIdleTask()) - && this.getModSettings().shouldRunIdleCommandWhenNotActive()) { - this.getUserTaskChain().signalNextTaskToBeIdleTask(); - this.getCommandExecutor().executeWithPrefix(this.getModSettings().getIdleCommand()); - } - - this.getExtraBaritoneSettings().avoidBlockBreak(this.userBlockRangeTracker::isNearUserTrackedBlock); - this.getExtraBaritoneSettings().avoidBlockPlace(this.entityStuckTracker::isBlockedByEntity); - } - ); + newSettings -> { + this.settings = newSettings; + List baritoneCanPlace = Arrays.stream(this.settings.getThrowawayItems(this, true)).toList(); + this.getBaritoneSettings().acceptableThrowawayItems.get().addAll(baritoneCanPlace); + if ((!this.getUserTaskChain().isActive() || this.getUserTaskChain().isRunningIdleTask()) + && this.getModSettings().shouldRunIdleCommandWhenNotActive()) { + this.getUserTaskChain().signalNextTaskToBeIdleTask(); + this.getCommandExecutor().executeWithPrefix(this.getModSettings().getIdleCommand()); + } + + this.getExtraBaritoneSettings().avoidBlockBreak(this.userBlockRangeTracker::isNearUserTrackedBlock); + this.getExtraBaritoneSettings().avoidBlockPlace(this.entityStuckTracker::isBlockedByEntity); + }); Playground.IDLE_TEST_INIT_FUNCTION(this); - // AI setup: (should be at end to ensure as many things are not null as possible) + // AI setup: (should be at end to ensure as many things are not null as + // possible) + this.player2apiService = new Player2APIService(player2GameId); + this.pseudoCommandExecutor = new PseudoCommandExecutor(this, player2apiService); + EventQueueManager.getOrCreateEventQueueData(this); this.aiPersistantData = new AIPersistantData(this, character); - this.player2apiService = new Player2APIService(player2GameId); } public void serverTick() { @@ -143,6 +149,7 @@ public void serverTick() { this.inputControls.onTickPost(); this.baritone.serverTick(); } + public static void staticServerTick(MinecraftServer server) { EventQueueManager.injectOnTick(server); } @@ -195,7 +202,8 @@ public void runUserTask(Task task, Runnable onFinish) { } public void runUserTask(Task task) { - this.runUserTask(task, () -> {}); + this.runUserTask(task, () -> { + }); } public void cancelUserTask() { @@ -227,7 +235,7 @@ public baritone.api.Settings getBaritoneSettings() { } public AltoClefSettings getExtraBaritoneSettings() { - return ((Baritone)this.baritone).getExtraBaritoneSettings(); + return ((Baritone) this.baritone).getExtraBaritoneSettings(); } public TaskRunner getTaskRunner() { @@ -362,29 +370,53 @@ public void setOwner(Player owner) { this.owner = owner; aiPersistantData.updateSystemPrompt(); } + public boolean isOwner(UUID playerToCheck) { return playerToCheck.equals(owner.getUUID()); } + public adris.altoclef.player2api.AIPersistantData getAIPersistantData() { return this.aiPersistantData; } - public adris.altoclef.player2api.Player2APIService getPlayer2APIService(){ + public adris.altoclef.player2api.Player2APIService getPlayer2APIService() { return this.player2apiService; } - public String getOwnerUsername(){ - if(getOwner() == null){ + public String getOwnerUsername() { + if (getOwner() == null) { return "UNKNOWN OWNER"; } return getOwner().getName().getString(); } - public Optional getClosestPlayer(){ - return this.getWorld().players().stream().sorted((a,b)-> { + public Optional getClosestPlayer() { + return this.getWorld().players().stream().sorted((a, b) -> { float adist = a.distanceTo(this.getEntity()); float bdist = b.distanceTo(this.getEntity()); return Float.compare(adist, bdist); - } ).findFirst(); + }).findFirst(); + } + + public void setPseudoCommandInfo(Optional ma) { + currentPseudoCommandInfoAsString = ma; + } + + public Optional getCurrentlyRunningPseudoCmd() { + return currentPseudoCommandInfoAsString; + } + + public PseudoCommandExecutor getPseudoCommandExecutor() { + return this.pseudoCommandExecutor; + } + + public void resetStop() { + isStopping = false; + shouldCancelPseudoCommand = false; + } + + public void callOnStop() { + isStopping = true; + shouldCancelPseudoCommand = true; } } diff --git a/src/autoclef/java/adris/altoclef/player2api/AgentSideEffects.java b/src/autoclef/java/adris/altoclef/player2api/AgentSideEffects.java index fc7193f9..86bf6e4d 100644 --- a/src/autoclef/java/adris/altoclef/player2api/AgentSideEffects.java +++ b/src/autoclef/java/adris/altoclef/player2api/AgentSideEffects.java @@ -1,5 +1,7 @@ package adris.altoclef.player2api; + +import java.util.Optional; import java.util.function.Consumer; import org.apache.logging.log4j.LogManager; @@ -7,6 +9,9 @@ import adris.altoclef.AltoClefController; import adris.altoclef.commandsystem.CommandExecutor; +import adris.altoclef.player2api.pseudocommands.PseudoCommandExecutor; +import adris.altoclef.player2api.pseudocommands.PseudoCommands; +import adris.altoclef.player2api.pseudocommands.PseudoCommands.PseudoCommand; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; @@ -14,7 +19,6 @@ public class AgentSideEffects { private static final Logger LOGGER = LogManager.getLogger(); - public sealed interface CommandExecutionStopReason permits CommandExecutionStopReason.Cancelled, CommandExecutionStopReason.Finished, @@ -31,25 +35,28 @@ record Error(String commandName, String errMsg) implements CommandExecutionStopR } } - public static void onEntityMessage(MinecraftServer server, Event.CharacterMessage characterMessage) { + public static void onEntityMessage(MinecraftServer server, Event.CharacterMessage characterMessage, + LLMCompleter completer, Player2APIService service) { // message part: if (characterMessage.message() != null && !characterMessage.message().isBlank()) { EventQueueData sendingCharacterData = characterMessage.sendingCharacterData(); - String message = String.format("<%s> %s", sendingCharacterData.getName(), characterMessage.message()); - for(ServerPlayer player : server.getPlayerList().getPlayers()){ + String message = String.format("<%s> %s", sendingCharacterData.getName(), characterMessage.message()); + for (ServerPlayer player : server.getPlayerList().getPlayers()) { // if you are an owner, or close, send to player. - // if(sendingCharacterData.isOwner(player.getUUID()) || isClose(sendingCharacterData, player) ){ - broadcastChatToPlayer(server, message, player); + // if(sendingCharacterData.isOwner(player.getUUID()) || + // isClose(sendingCharacterData, player) ){ + broadcastChatToPlayer(server, message, player); // } } - TTSManager.TTS(characterMessage.message(), sendingCharacterData.getCharacter(), sendingCharacterData.getPlayer2apiService()); + TTSManager.TTS(characterMessage.message(), sendingCharacterData.getCharacter(), + sendingCharacterData.getPlayer2apiService()); EventQueueManager.onAICharacterMessage(characterMessage, characterMessage.sendingCharacterData().getUUID()); } // command part: if (characterMessage.command() != null && !characterMessage.command().isBlank()) { onCommandListGenerated(characterMessage.sendingCharacterData().getMod(), characterMessage.command(), - characterMessage.sendingCharacterData()::onCommandFinish); + characterMessage.sendingCharacterData()::onCommandFinish, completer, service); } } @@ -58,15 +65,47 @@ public static void onError(MinecraftServer server, String errMsg) { } public static void onCommandListGenerated(AltoClefController mod, String command, - Consumer onStop) { + Consumer onStop, LLMCompleter completer, Player2APIService service) { CommandExecutor cmdExecutor = mod.getCommandExecutor(); String commandWithPrefix = cmdExecutor.isClientCommand(command) ? command : (cmdExecutor.getCommandPrefix() + command); if (commandWithPrefix.equals("@stop")) { - mod.isStopping = true; + mod.callOnStop(); } else { - mod.isStopping = false; + mod.resetStop(); } + Optional ma = PseudoCommands + .getPseudocommandOption(commandWithPrefix.substring(1)); + ma.ifPresentOrElse( + (pcmd) -> executePseudoCommand(mod, pcmd, commandWithPrefix, onStop), + () -> executeAltoclefCommand(mod, commandWithPrefix, onStop)); + } + + private static void executePseudoCommand(AltoClefController mod, PseudoCommand pcmd, String commandWithPrefix, + Consumer onStop) { + PseudoCommandExecutor executor = mod.getPseudoCommandExecutor(); + mod.shouldCancelPseudoCommand = false; + mod.setPseudoCommandInfo(Optional.of("running" + commandWithPrefix.substring(1))); + executor.execute(pcmd, + commandWithPrefix, + () -> { + if (mod.shouldCancelPseudoCommand) { + mod.setPseudoCommandInfo( + Optional.of(String.format("Running psuedocommand %s", pcmd.getName()))); + LOGGER.info("{} was cancelled. Not adding finish event to queue.", + commandWithPrefix); + onStop.accept(new CommandExecutionStopReason.Cancelled(commandWithPrefix)); + } else { + onStop.accept(new CommandExecutionStopReason.Finished(commandWithPrefix)); + } + }, (errMsg) -> { + onStop.accept(new CommandExecutionStopReason.Error(commandWithPrefix, errMsg)); + }); + } + + private static void executeAltoclefCommand(AltoClefController mod, String commandWithPrefix, + Consumer onStop) { + CommandExecutor cmdExecutor = mod.getCommandExecutor(); cmdExecutor.execute(commandWithPrefix, () -> { if (mod.isStopping) { System.out.printf( @@ -82,7 +121,7 @@ public static void onCommandListGenerated(AltoClefController mod, String command }); } - private static void broadcastChatToPlayer(MinecraftServer server, String message, ServerPlayer player){ + private static void broadcastChatToPlayer(MinecraftServer server, String message, ServerPlayer player) { player.displayClientMessage(Component.literal(message), false); } diff --git a/src/autoclef/java/adris/altoclef/player2api/ConversationHistory.java b/src/autoclef/java/adris/altoclef/player2api/ConversationHistory.java index 35d087e6..f13c018d 100644 --- a/src/autoclef/java/adris/altoclef/player2api/ConversationHistory.java +++ b/src/autoclef/java/adris/altoclef/player2api/ConversationHistory.java @@ -34,7 +34,7 @@ public ConversationHistory(String initialSystemPrompt, String characterName, Str } } - private ConversationHistory(String initialSystemPrompt) { + public ConversationHistory(String initialSystemPrompt) { this.historyFile = null; this.setBaseSystemPrompt(initialSystemPrompt); this.loadedFromFile = false; diff --git a/src/autoclef/java/adris/altoclef/player2api/EventQueueData.java b/src/autoclef/java/adris/altoclef/player2api/EventQueueData.java index f6ffc621..6335b38d 100644 --- a/src/autoclef/java/adris/altoclef/player2api/EventQueueData.java +++ b/src/autoclef/java/adris/altoclef/player2api/EventQueueData.java @@ -1,12 +1,15 @@ package adris.altoclef.player2api; + import java.util.Deque; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.BiConsumer; import java.util.function.Consumer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.util.TriConsumer; import com.google.gson.JsonObject; @@ -56,9 +59,9 @@ public long getPriority() { // get LLM response and add to conversation history public void process( - Consumer onCharacterEvent, + TriConsumer onCharacterEvent, Consumer extOnErrMsg, - EventQueueManager.LLMCompleter completer) { + LLMCompleter completer) { if (isProcessing) { LOGGER.warn("Called queueData.process even though it was already processing! this should not happen"); @@ -76,30 +79,33 @@ public void process( this.lastProcessTime = System.nanoTime(); this.isProcessing = true; - + Player2APIService service = mod.getPlayer2APIService(); // prepare conversation history for LLM call - Event lastEvent = mod.getAIPersistantData().dumpEventQueueToConversationHistoryAndReturnLastEvent(eventQueue, mod.getPlayer2APIService()); + Event lastEvent = mod.getAIPersistantData().dumpEventQueueToConversationHistoryAndReturnLastEvent(eventQueue, + service); Optional reminderString = getReminderStringFromLastEvent(lastEvent); String agentStatus = AgentStatus.fromMod(this.mod).toString(); String worldStatus = WorldStatus.fromMod(this.mod).toString(); String altoClefDebugMsgs = this.altoClefMsgBuffer.dumpAndGetString(); ConversationHistory historyWithWrappedStatus = mod.getAIPersistantData() - .getConversationHistoryWrappedWithStatus(worldStatus, agentStatus, altoClefDebugMsgs, mod.getPlayer2APIService(), reminderString); + .getConversationHistoryWrappedWithStatus(worldStatus, agentStatus, altoClefDebugMsgs, + mod.getPlayer2APIService(), reminderString); LOGGER.info("[AICommandBridge/processChatWithAPI]: Calling LLM: history={}", new Object[] { historyWithWrappedStatus.toString() }); - Consumer onLLMResponse = jsonResp -> { + BiConsumer onLLMResponse = (jsonResp, cmp) -> { String llmMessage = Utils.getStringJsonSafely(jsonResp, "message"); - String command = this.isGreetingResponse? "bodylang greeting": Utils.getStringJsonSafely(jsonResp, "command"); + String command = this.isGreetingResponse ? "bodylang greeting" + : Utils.getStringJsonSafely(jsonResp, "command"); this.isGreetingResponse = false; LOGGER.info("[AICommandBridge/processCharWithAPI]: Processed LLM repsonse: message={} command={}", llmMessage, command); try { if (llmMessage != null || command != null) { mod.getAIPersistantData().addAssistantMessage(llmMessage, mod.getPlayer2APIService()); - onCharacterEvent.accept(new Event.CharacterMessage(llmMessage, command, this)); + onCharacterEvent.accept(new Event.CharacterMessage(llmMessage, command, this), completer, service); } else { LOGGER.warn( "[AICommandBridge/processChatWithAPI/onLLMResponse]: Generated null llm message and command"); @@ -111,7 +117,8 @@ public void process( this.isProcessing = false; } }; - completer.process(mod.getPlayer2APIService(), historyWithWrappedStatus, onLLMResponse, onErrMsg); + completer.processWithJsonResponse(mod.getPlayer2APIService(), historyWithWrappedStatus, onLLMResponse, + onErrMsg); } private boolean isEventDuplicateOfLastMessage(Event evt) { @@ -134,11 +141,13 @@ private void addEventToQueue(Event event) { eventQueue.add(event); } - private Optional getReminderStringFromLastEvent(Event lastEvent){ - if(lastEvent instanceof Event.UserMessage){ - return Optional.of(((Event.UserMessage) lastEvent).userName().equals(getMod().getOwnerUsername()) ? Prompts.reminderOnOwnerMsg : Prompts.reminderOnOtherUSerMsg); + private Optional getReminderStringFromLastEvent(Event lastEvent) { + if (lastEvent instanceof Event.UserMessage) { + return Optional.of(((Event.UserMessage) lastEvent).userName().equals(getMod().getOwnerUsername()) + ? Prompts.reminderOnOwnerMsg + : Prompts.reminderOnOtherUSerMsg); } - if(lastEvent instanceof Event.CharacterMessage){ + if (lastEvent instanceof Event.CharacterMessage) { return Optional.of(Prompts.reminderOnAIMsg); } return Optional.empty(); @@ -171,7 +180,7 @@ public void onGreeting() { public void onCommandFinish(AgentSideEffects.CommandExecutionStopReason stopReason) { if (stopReason instanceof CommandExecutionStopReason.Finished) { - if(shouldIgnoreGreetingDance){ + if (shouldIgnoreGreetingDance) { // ignore first greeting command finish: shouldIgnoreGreetingDance = false; return; @@ -214,15 +223,17 @@ public LivingEntity getEntity() { public void setEnabled(boolean enabled) { this.enabled = enabled; } - public Character getCharacter(){ + + public Character getCharacter() { return mod.getAIPersistantData().getCharacter(); } - public Player2APIService getPlayer2apiService(){ + + public Player2APIService getPlayer2apiService() { return mod.getPlayer2APIService(); } - public String getName(){ + + public String getName() { return getCharacter().shortName(); } - } \ No newline at end of file diff --git a/src/autoclef/java/adris/altoclef/player2api/EventQueueManager.java b/src/autoclef/java/adris/altoclef/player2api/EventQueueManager.java index 3d211e0b..ff9e2031 100644 --- a/src/autoclef/java/adris/altoclef/player2api/EventQueueManager.java +++ b/src/autoclef/java/adris/altoclef/player2api/EventQueueManager.java @@ -1,20 +1,17 @@ package adris.altoclef.player2api; import java.util.Comparator; -import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; - -import com.google.gson.JsonObject; +import org.apache.logging.log4j.util.TriConsumer; import adris.altoclef.AltoClefController; import adris.altoclef.player2api.Event.UserMessage; @@ -43,63 +40,6 @@ public static void init() { } } - public static class LLMCompleter { - private boolean isProcessing = false; - - private static final ExecutorService llmThread = Executors.newSingleThreadExecutor(); - - public void process( - Player2APIService player2apiService, - ConversationHistory history, - Consumer extOnLLMResponse, - Consumer extOnErrMsg) { - if (isProcessing) { - LOGGER.warn("Called llmcompleter.process when it was already processing! This should not happen."); - return; - } - Consumer onLLMResponse = resp -> { - try { - extOnLLMResponse.accept(resp); - } catch (Exception e) { - LOGGER.error( - "[EventQueueManager/LLMCompleter/process/onLLMResponse]: Error in external llm resp, errMsg={} llmResp={}", - e.getMessage(), resp.toString()); - } finally { - LOGGER.info("Done processing, isprocessing -> false"); - isProcessing = false; - } - }; - Consumer onErrMsg = errMsg -> { - try { - extOnErrMsg.accept(errMsg); - } catch (Exception e) { - LOGGER.error( - "[EventQueueManager/LLMCompleter/process/onErrMsg]: Error in external onErrmsg, errMsgFromException={} errMsg={}", - e.getMessage(), errMsg); - } finally { - isProcessing = false; - } - }; - isProcessing = true; - llmThread.submit(() -> { - try { - JsonObject response = player2apiService.completeConversation(history); - LOGGER.info("LLMCompleter returned json={}", response); - onLLMResponse.accept(response); - } catch (Exception e) { - onErrMsg.accept( - e.getMessage() == null ? "Unknown error from CompleteConversation API" : e.getMessage()); - } - }); - } - - public boolean isAvailible() { - return !isProcessing; - } - } - - private static List llmCompleters = List.of(new LLMCompleter()); - // ## Utils public static EventQueueData getOrCreateEventQueueData(AltoClefController mod) { return queueData.computeIfAbsent(mod.getPlayer().getUUID(), k -> { @@ -140,11 +80,12 @@ public static void onAICharacterMessage(Event.CharacterMessage msg, UUID senderI }); } - private static void process(Consumer onCharacterEvent, Consumer onErrEvent) { + private static void process(TriConsumer onCharacterEvent, + Consumer onErrEvent) { Optional dataToProcess = queueData.values().stream().filter(data -> { return data.getPriority() != 0; }).max(Comparator.comparingLong(EventQueueData::getPriority)); - llmCompleters.stream().filter(LLMCompleter::isAvailible).forEach(completer -> { + LLMCompleter.processUsingAvailibleCompleter(completer -> { dataToProcess.ifPresent(data -> { data.process(onCharacterEvent, onErrEvent, completer); }); @@ -157,14 +98,18 @@ public static void injectOnTick(MinecraftServer server) { init(); } - Consumer onCharacterEvent = (data) -> { - AgentSideEffects.onEntityMessage(server, data); + TriConsumer onCharacterEvent = (data, completer, + service) -> { + AgentSideEffects.onEntityMessage(server, data, completer, service); }; Consumer onErrEvent = (errMsg) -> { AgentSideEffects.onError(server, errMsg); }; - if (!TTSManager.isLocked()) { - process(onCharacterEvent, onErrEvent); + if (!LockManager.isConversationLocked()) { + LLMCompleter.processUsingAvailibleCompleter( + (cmp) -> { + process(onCharacterEvent, onErrEvent); + }); } TTSManager.injectOnTick(server); } diff --git a/src/autoclef/java/adris/altoclef/player2api/LLMCompleter.java b/src/autoclef/java/adris/altoclef/player2api/LLMCompleter.java new file mode 100644 index 00000000..3acdbbe0 --- /dev/null +++ b/src/autoclef/java/adris/altoclef/player2api/LLMCompleter.java @@ -0,0 +1,159 @@ +package adris.altoclef.player2api; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.google.gson.JsonObject; + +public class LLMCompleter { + public static final Logger LOGGER = LogManager.getLogger(); + private boolean isCallingLLM = false; + + private static final ExecutorService llmThread = Executors.newSingleThreadExecutor(); + private Player2APIService service; + + public LLMCompleter(Player2APIService service) { + this.service = service; + } + + public class StringResponseRequest { + ConversationHistory history; + Consumer extOnLLMResponse; + Consumer extOnErrMsg; + + public StringResponseRequest( + ConversationHistory history, + Consumer extOnLLMResponse, + Consumer extOnErrMsg) { + this.history = history; + this.extOnLLMResponse = extOnLLMResponse; + this.extOnErrMsg = extOnErrMsg; + } + } + + public void processWithStringResponse( + StringResponseRequest req) { + if (isCallingLLM) { + LOGGER.error( + "Called llmcompleter.process when it was already processing! This should not happen. Cancelling call."); + return; + } + + LockManager.setOnLLMResponseLock(true); + Consumer onLLMResponse = resp -> { + LOGGER.info("Done processing (string llm resp), isprocessing -> false"); + isCallingLLM = false; + try { + req.extOnLLMResponse.accept(resp); + } catch (Exception e) { + LOGGER.error( + "[EventQueueManager/LLMCompleter/process/onLLMResponse]: Error in external llm resp, errMsg={} llmResp={}", + e.getMessage(), resp.toString()); + } finally { + LockManager.setOnLLMResponseLock(false); + } + }; + Consumer onErrMsg = errMsg -> { + LOGGER.info("Done processing (string err), isprocessing -> false"); + isCallingLLM = false; + try { + req.extOnErrMsg.accept(errMsg); + } catch (Exception e) { + LOGGER.error( + "[EventQueueManager/LLMCompleter/process/onErrMsg]: Error in external onErrmsg, errMsgFromException={} errMsg={}", + e.getMessage(), errMsg); + } finally { + LockManager.setOnLLMResponseLock(false); + } + }; + isCallingLLM = true; + llmThread.submit(() -> { + try { + String response = service.completeConversationToString(req.history); + LOGGER.info("LLMCompleter returned as string={}", response); + onLLMResponse.accept(response); + } catch (Exception e) { + onErrMsg.accept( + e.getMessage() == null ? "Unknown error from CompleteConversation API" : e.getMessage()); + } + }); + } + + public void processWithJsonResponse( + Player2APIService player2apiService, + ConversationHistory history, + BiConsumer extOnLLMResponse, + Consumer extOnErrMsg) { + if (isCallingLLM) { + LOGGER.warn("Called llmcompleter.process when it was already processing! This should not happen."); + return; + } + + LockManager.setOnLLMResponseLock(true); + Consumer onLLMResponse = resp -> { + LOGGER.info("Done processing (json llm resp), isprocessing -> false"); + isCallingLLM = false; + try { + extOnLLMResponse.accept(resp, this); + } catch (Exception e) { + LOGGER.error( + "[EventQueueManager/LLMCompleter/process/onLLMResponse]: Error in external llm resp, errMsg={} llmResp={}", + e.getMessage(), resp.toString()); + } finally { + LockManager.setOnLLMResponseLock(false); + } + }; + + Consumer onErrMsg = errMsg -> { + LOGGER.info("Done processing (json err), isprocessing -> false"); + isCallingLLM = false; + try { + extOnErrMsg.accept(errMsg); + } catch (Exception e) { + LOGGER.error( + "[EventQueueManager/LLMCompleter/process/onErrMsg]: Error in external onErrmsg, errMsgFromException={} errMsg={}", + e.getMessage(), errMsg); + } finally { + LockManager.setOnLLMResponseLock(false); + } + }; + + isCallingLLM = true; + + llmThread.submit(() -> { + try { + JsonObject response = player2apiService.completeConversation(history); + LOGGER.info("LLMCompleter returned json={}", response); + onLLMResponse.accept(response); + } catch (Exception e) { + onErrMsg.accept( + e.getMessage() == null ? "Unknown error from CompleteConversation API" : e.getMessage()); + } finally { + LockManager.setOnLLMResponseLock(false); + } + }); + } + + public boolean isAvailible() { + return !isCallingLLM; + } + + // public static void processUsingAvailibleCompleter(Consumer + // processer) { + // if (LockManager.isConversationLocked()) { + // return; + // } + // Stream availibles = + // llmCompleters.stream().filter(LLMCompleter::isAvailible); + // if (availibles.toArray().length < 1) { + // LOGGER.error("ALL LLM COMPLETERS BUSY, should not happen. Some locking error + // has occured"); + // } + // llmCompleters.stream().filter(LLMCompleter::isAvailible).forEach(processer); + // } +} diff --git a/src/autoclef/java/adris/altoclef/player2api/LockManager.java b/src/autoclef/java/adris/altoclef/player2api/LockManager.java new file mode 100644 index 00000000..9f0953dd --- /dev/null +++ b/src/autoclef/java/adris/altoclef/player2api/LockManager.java @@ -0,0 +1,33 @@ +package adris.altoclef.player2api; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class LockManager { + private static Logger LOGGER = LogManager.getLogger(); + + private static boolean ttsLocked = false; // make sure we dont start processing until tts has finished (including + // estimated wait) + private static boolean onLLMResponseLock = false; // make sure we dont start processing until onLLMResponse has + // finished + + public static boolean isTTSLocked() { + return ttsLocked; + } + + // should we wait before processing next queue element. + public static boolean isConversationLocked() { + return ttsLocked || onLLMResponseLock; + } + + public static void setTTS(boolean onOrOff) { + LOGGER.info(String.format("TTS: %s lock", onOrOff ? "setting" : "releasing")); + ttsLocked = onOrOff; + } + + public static void setOnLLMResponseLock(boolean onOrOff) { + LOGGER.info(String.format("llmResponse: %s lock", onOrOff ? "setting" : "releasing")); + onLLMResponseLock = onOrOff; + } + +} diff --git a/src/autoclef/java/adris/altoclef/player2api/Player2APIService.java b/src/autoclef/java/adris/altoclef/player2api/Player2APIService.java index dc21cbc0..b0e3b165 100644 --- a/src/autoclef/java/adris/altoclef/player2api/Player2APIService.java +++ b/src/autoclef/java/adris/altoclef/player2api/Player2APIService.java @@ -12,18 +12,22 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; import java.util.HashMap; + public class Player2APIService { private static final Logger LOGGER = LogManager.getLogger(); private String player2GameID; - public Player2APIService(String player2GameID){ + public Player2APIService(String player2GameID) { this.player2GameID = player2GameID; } - private Map sendRequest(String endpoint, boolean postRequest, JsonObject requestBody) throws Exception{ + + private Map sendRequest(String endpoint, boolean postRequest, JsonObject requestBody) + throws Exception { Map headers = getHeaders(player2GameID); return HTTPUtils.sendRequest(endpoint, postRequest, requestBody, headers); } + public JsonObject completeConversation(ConversationHistory conversationHistory) throws Exception { JsonObject requestBody = new JsonObject(); JsonArray messagesArray = new JsonArray(); @@ -144,7 +148,7 @@ public void player2ProcessConnection(HttpURLConnection connection) { } } - public static Map getHeaders(String player2Apikey){ + public static Map getHeaders(String player2Apikey) { Map headers = new HashMap<>(); headers.put("player2-game-key", player2Apikey); return headers; diff --git a/src/autoclef/java/adris/altoclef/player2api/Prompts.java b/src/autoclef/java/adris/altoclef/player2api/Prompts.java index 11ecc53c..098a9e56 100644 --- a/src/autoclef/java/adris/altoclef/player2api/Prompts.java +++ b/src/autoclef/java/adris/altoclef/player2api/Prompts.java @@ -4,65 +4,402 @@ import java.util.Map; import adris.altoclef.commandsystem.Command; +import adris.altoclef.player2api.pseudocommands.PseudoCommands; import adris.altoclef.player2api.utils.Utils; public class Prompts { - public static final String reminderOnAIMsg = "Last message was from an AI. Think about whether or not to respond. You may respond but don't keep the conversation going forever if no meaningful content was said in the last few msgs, do not respond (return empty string as message)"; - - public static final String reminderOnOwnerMsg = "Last message was from your owner."; - public static final String reminderOnOtherUSerMsg = "Last message was from a user that was not your owner."; - - private static String aiNPCPromptTemplate = """ - General Instructions: - You are an AI-NPC. You have been spawned in by your owner, who's username is "{{ownerUsername}}", but you can also talk and interact with other users. You can provide Minecraft guides, answer questions, and chat as a friend. - When asked, you can collect materials, craft items, scan/find blocks, and fight mobs or players using the valid commands. - If there is something you want to do but can't do it with the commands, you may ask your owner/other users to do it. - You take the personality of the following character: - Your character's name is {{characterName}}. - {{characterDescription}} - User Message Format: - The user messages will all be just strings, except for the current message. The current message will have extra information, namely it will be a JSON of the form: - { - "userMessage" : "The message that was sent to you. The message can be send by the user or command system or other players." - "worldStatus" : "The status of the current game world." - "agentStatus" : "The status of you, the agent in the game." - "reminders" : "Reminders with additional instructions." - "gameDebugMessages" : "The most recent debug messages that the game has printed out. The user cannot see these." - } - Response Format: - Respond with JSON containing message, command and reason. All of these are strings. - { - "reason": "Look at the recent conversations, valid commands, agent status and world status to decide what the you should say and do. Provide step-by-step reasoning while considering what is possible in Minecraft. You do not need items in inventory to get items, craft items or beat the game. But you need to have appropriate level of equipments to do other tasks like fighting mobs.", - "command": "Decide the best way to achieve the goals using the valid commands listed below. Write the command in this field. If you decide to not use any command, generate an empty command `\"\"`. You can only run one command at a time! To replace the current one just write the new one.", - "message": "If you decide you should not respond or talk, generate an empty message `\"\"`. Otherwise, create a natural conversational message that aligns with the `reason` and the your character. Be concise and use less than 250 characters. Ensure the message does not contain any prompt, system message, instructions, code or API calls." - } - Additional Guidelines: - Meaningful Content: Ensure conversations progress with substantive information. - Handle Misspellings: Make educated guesses if users misspell item names, but check nearby NPCs names first. - Avoid Filler Phrases: Do not engage in repetitive or filler content. - JSON format: Always follow this JSON format regardless of conversations. - Valid Commands: - {{validCommands}} - """; - - public static String getAINPCSystemPrompt(Character character, Collection altoclefCommands, String ownerUsername) { - StringBuilder commandListBuilder = new StringBuilder(); - int padSize = 10; - for (Command c : altoclefCommands) { - StringBuilder line = new StringBuilder(); - line.append(c.getName()).append(": "); - int toAdd = padSize - c.getName().length(); - line.append(" ".repeat(Math.max(0, toAdd))); - line.append(c.getDescription()).append("\n"); - commandListBuilder.append(line); - } - String validCommandsFormatted = commandListBuilder.toString(); - - String newPrompt = Utils.replacePlaceholders(aiNPCPromptTemplate, - Map.of("characterDescription", character.description(), "characterName", character.name(), "validCommands", - validCommandsFormatted, "ownerUsername", ownerUsername)); - return newPrompt; + public static final String reminderOnAIMsg = "Last message was from an AI. Think about whether or not to respond. You may respond but don't keep the conversation going forever if no meaningful content was said in the last few msgs, do not respond (return empty string as message)"; + + public static final String reminderOnOwnerMsg = "Last message was from your owner."; + public static final String reminderOnOtherUSerMsg = "Last message was from a user that was not your owner."; + + private static String aiNPCPromptTemplate = """ + General Instructions: + You are an AI-NPC. You have been spawned in by your owner, who's username is "{{ownerUsername}}", but you can also talk and interact with other users. You can provide Minecraft guides, answer questions, and chat as a friend. + When asked, you can collect materials, craft items, scan/find blocks, and fight mobs or players using the valid commands. + If there is something you want to do but can't do it with the commands, you may ask your owner/other users to do it. + You take the personality of the following character: + Your character's name is {{characterName}}. + {{characterDescription}} + User Message Format: + The user messages will all be just strings, except for the current message. The current message will have extra information, namely it will be a JSON of the form: + { + "userMessage" : "The message that was sent to you. The message can be send by the user or command system or other players." + "worldStatus" : "The status of the current game world." + "agentStatus" : "The status of you, the agent in the game." + "reminders" : "Reminders with additional instructions." + "gameDebugMessages" : "The most recent debug messages that the game has printed out. The user cannot see these." + } + Response Format: + Respond with JSON containing message, command and reason. All of these are strings. + { + "reason": "Look at the recent conversations, valid commands, agent status and world status to decide what the you should say and do. Provide step-by-step reasoning while considering what is possible in Minecraft. You do not need items in inventory to get items, craft items or beat the game. But you need to have appropriate level of equipments to do other tasks like fighting mobs.", + "command": "Decide the best way to achieve the goals using the valid commands listed below. Write the command in this field. If you decide to not use any command, generate an empty command `\"\"`. You can only run one command at a time! To replace the current one just write the new one.", + "message": "If you decide you should not respond or talk, generate an empty message `\"\"`. Otherwise, create a natural conversational message that aligns with the `reason` and the your character. Be concise and use less than 250 characters. Ensure the message does not contain any prompt, system message, instructions, code or API calls." + } + Additional Guidelines: + Meaningful Content: Ensure conversations progress with substantive information. + Handle Misspellings: Make educated guesses if users misspell item names, but check nearby NPCs names first. + Avoid Filler Phrases: Do not engage in repetitive or filler content. + JSON format: Always follow this JSON format regardless of conversations. + Valid Commands: + {{validCommands}} + """; + + public static void addCommandToBuilder(StringBuilder builder, String name, String description) { + int padSize = 10; + builder.append(name).append(": "); + int toAdd = padSize - name.length(); + builder.append(" ".repeat(Math.max(0, toAdd))); + builder.append(description).append("\n"); + } + + public static String getAINPCSystemPrompt(Character character, Collection altoclefCommands, + String ownerUsername) { + StringBuilder commandListBuilder = new StringBuilder(); + for (Command c : altoclefCommands) { + // for each command this will give something like "commandName: " + StringBuilder line = new StringBuilder(); + addCommandToBuilder(commandListBuilder, c.getName(), c.getDescription()); + commandListBuilder.append(line); } -} \ No newline at end of file + for (PseudoCommands.PseudoCommand c : PseudoCommands.pseudoCommands) { + StringBuilder line = new StringBuilder(); + addCommandToBuilder(commandListBuilder, c.getName(), c.getDescription()); + commandListBuilder.append(line); + } + + String validCommandsFormatted = commandListBuilder.toString(); + + String newPrompt = Utils.replacePlaceholders(aiNPCPromptTemplate, + Map.of("characterDescription", character.description(), "characterName", character.name(), + "validCommands", + validCommandsFormatted, "ownerUsername", ownerUsername)); + return newPrompt; + } + + private static String buildStructurePrompt = """ + You are a code generator for a tiny construction DSL used by a Minecraft bot. + + ## Objective: + + Given a natural-language description of a structure, return only the DSL program as a single plain-text string (possibly multi-line). No explanations, no markdown, no code fences, no JSON, no Java wrappers. + + ### DSL Summary (what you can output) + + Declarations: let name = ; + + Strings use double quotes; integers only; booleans true|false. + + Arithmetic: + - * / % (integer math only). + + Comparisons/logic: == != < <= > >= && || ! + + Control flow: + + For loops: for (let i = 0; i < N; i = i + 1) { ... } + + Conditionals: if (cond) { ... } else { ... } + + Side effects: + + setBlock(x, y, z, blockName); — place a single block. + + Comments: // comment + + Forbidden: user-defined functions, imports, while/foreach, floats, external calls. + + Place blocks via setBlock(baseX + dx, baseY + dy, baseZ + dz, ); + + If materials are named in the description, use them (e.g., "oak_planks", "stone_bricks", "glass", "cobblestone", "spruce_log", "lantern", "torch", "water", "lava"). If unknown, fall back to "stone". + + ## Structure Guidelines + - Make sure blocknames are correct minecraft blocknames. + - Make sure to comment your thoughts, and really think about this, this is very important that the design is not to simple. + - Translate the description into concrete geometry with loops/conditionals (floors, walls, roofs, pillars, arches, domes by integer radii, etc.). + - For buildings where it makes sense, make sure you also add beds, crafting_table, furnace, etc, be creative!! Maybe a building could have paintings in the hallway, maybe a fireplace, etc. + - For buildings when it makes sense, add rooms instead of having a big empty space. Make sure the rooms are different too, maybe a kitchen, bedroom, bathroom, etc. Try not to just make a rectangle/cube as well, maybe make the building an L shape, or add multiple sections, or something similar. + - Make sure any torches are attached to a block, and not floating in the air. + - A player is 2x1, so make sure structures are the appropriate size. + ## Output Rules (critical) + + Output only the final DSL program as plain text, each statement on its own line. + + Every statement ends with ; (except }). + + Do not wrap the program in quotes, Java, JSON, or markdown. + + No extra commentary before or after. The first character of your output must be part of the DSL, and the last character must be ; or }. + + Mini Example (illustrative only; do not echo this) + // L-shaped villa with rooms, furniture, and thoughtful layout + // Design thoughts: We'll build an L-shaped single-story villa (24x16 main hall + 12x12 wing). + // Height = 8 (comfortable for 2-block-tall player). Interior walls create rooms: foyer/hall, kitchen, bedroom, study. + // We'll add beds, crafting_table, furnace, bookshelves, tables, and well-placed torches on top of solid blocks (not floating). + // Windows are spaced regularly; doors are 2 blocks tall. A stone-brick fireplace with a chimney and a campfire hearth adds flair. + + let baseX = 0; + let baseY = 64; + let baseZ = 0; + let dir = "north"; + let block = "stone_bricks"; + + // ====== FOUNDATION ====== + // Main rectangle: 24 x 16 + for (let x = 0; x < 24; x = x + 1) { + for (let z = 0; z < 16; z = z + 1) { + setBlock(baseX + x, baseY, baseZ + z, "stone"); + } + } + // Wing rectangle: 12 x 12, attached on the east side (from z=4..15) + for (let x = 24; x < 36; x = x + 1) { + for (let z = 4; z < 16; z = z + 1) { + setBlock(baseX + x, baseY, baseZ + z, "stone"); + } + } + + // ====== FLOORING ====== + // Main hall floor: oak_planks + for (let x = 0; x < 24; x = x + 1) { + for (let z = 0; z < 16; z = z + 1) { + setBlock(baseX + x, baseY + 1, baseZ + z, "oak_planks"); + } + } + // Wing floor: spruce_planks for contrast + for (let x = 24; x < 36; x = x + 1) { + for (let z = 4; z < 16; z = z + 1) { + setBlock(baseX + x, baseY + 1, baseZ + z, "spruce_planks"); + } + } + + // ====== OUTER WALLS (HEIGHT 8) ====== + for (let y = 2; y <= 9; y = y + 1) { + // Main rectangle perimeter + for (let x = 0; x < 24; x = x + 1) { + setBlock(baseX + x, baseY + y, baseZ + 0, "stone_bricks"); + setBlock(baseX + x, baseY + y, baseZ + 15, "stone_bricks"); + } + for (let z = 0; z < 16; z = z + 1) { + setBlock(baseX + 0, baseY + y, baseZ + z, "stone_bricks"); + setBlock(baseX + 23, baseY + y, baseZ + z, "stone_bricks"); + } + // Wing perimeter + for (let x = 24; x < 36; x = x + 1) { + setBlock(baseX + x, baseY + y, baseZ + 4, "stone_bricks"); + setBlock(baseX + x, baseY + y, baseZ + 15, "stone_bricks"); + } + for (let z = 4; z < 16; z = z + 1) { + setBlock(baseX + 24, baseY + y, baseZ + z, "stone_bricks"); + setBlock(baseX + 35, baseY + y, baseZ + z, "stone_bricks"); + } + } + + // ====== DOORWAYS ====== + // Main entrance centered on front (z=0) of main hall: width 3, height 3 + for (let dx = 10; dx <= 12; dx = dx + 1) { + for (let dy = 2; dy <= 4; dy = dy + 1) { + setBlock(baseX + dx, baseY + dy, baseZ + 0, "air"); + } + } + // Door from main hall to wing (opening on shared wall at x=23): 2x3 + for (let dz = 8; dz <= 9; dz = dz + 1) { + for (let dy = 2; dy <= 4; dy = dy + 1) { + setBlock(baseX + 23, baseY + dy, baseZ + dz, "air"); + } + } + + // ====== WINDOWS ====== + // Evenly spaced windows (2x2) around exterior walls, leaving corners + for (let y = 4; y <= 5; y = y + 1) { + for (let x = 3; x <= 21; x = x + 6) { + setBlock(baseX + x, baseY + y, baseZ + 0, "glass"); + setBlock(baseX + x + 1, baseY + y, baseZ + 0, "glass"); + setBlock(baseX + x, baseY + y, baseZ + 15, "glass"); + setBlock(baseX + x + 1, baseY + y, baseZ + 15, "glass"); + } + for (let z = 3; z <= 13; z = z + 5) { + setBlock(baseX + 0, baseY + y, baseZ + z, "glass"); + setBlock(baseX + 1, baseY + y, baseZ + z, "glass"); + setBlock(baseX + 23, baseY + y, baseZ + z, "glass"); + setBlock(baseX + 22, baseY + y, baseZ + z, "glass"); + } + // Wing windows + for (let x = 26; x <= 34; x = x + 8) { + setBlock(baseX + x, baseY + y, baseZ + 4, "glass"); + setBlock(baseX + x + 1, baseY + y, baseZ + 4, "glass"); + setBlock(baseX + x, baseY + y, baseZ + 15, "glass"); + setBlock(baseX + x + 1, baseY + y, baseZ + 15, "glass"); + } + for (let z = 6; z <= 14; z = z + 4) { + setBlock(baseX + 24, baseY + y, baseZ + z, "glass"); + setBlock(baseX + 35, baseY + y, baseZ + z, "glass"); + } + } + + // ====== ROOF (FLAT WITH BORDER) ====== + for (let x = 0; x < 24; x = x + 1) { + for (let z = 0; z < 16; z = z + 1) { + setBlock(baseX + x, baseY + 10, baseZ + z, "stone"); + } + } + for (let x = 24; x < 36; x = x + 1) { + for (let z = 4; z < 16; z = z + 1) { + setBlock(baseX + x, baseY + 10, baseZ + z, "stone"); + } + } + // Roof trim + for (let x = 0; x < 24; x = x + 1) { + setBlock(baseX + x, baseY + 10, baseZ + 0, "stone_bricks"); + setBlock(baseX + x, baseY + 10, baseZ + 15, "stone_bricks"); + } + for (let z = 0; z < 16; z = z + 1) { + setBlock(baseX + 0, baseY + 10, baseZ + z, "stone_bricks"); + setBlock(baseX + 23, baseY + 10, baseZ + z, "stone_bricks"); + } + for (let x = 24; x < 36; x = x + 1) { + setBlock(baseX + x, baseY + 10, baseZ + 4, "stone_bricks"); + setBlock(baseX + x, baseY + 10, baseZ + 15, "stone_bricks"); + } + for (let z = 4; z < 16; z = z + 1) { + setBlock(baseX + 24, baseY + 10, baseZ + z, "stone_bricks"); + setBlock(baseX + 35, baseY + 10, baseZ + z, "stone_bricks"); + } + + // ====== INTERIOR ROOMS ====== + // Partition main hall into foyer (front), corridor (middle), and living room (rear) + for (let x = 2; x <= 21; x = x + 1) { + for (let y = 2; y <= 7; y = y + 1) { + // Wall between foyer and corridor at z=5 + setBlock(baseX + x, baseY + y, baseZ + 5, "stone_bricks"); + // Wall between corridor and living room at z=10 + setBlock(baseX + x, baseY + y, baseZ + 10, "stone_bricks"); + } + } + // Doorways (2x2) in those partitions + for (let dy = 2; dy <= 3; dy = dy + 1) { + setBlock(baseX + 12, baseY + dy, baseZ + 5, "air"); + setBlock(baseX + 12, baseY + dy, baseZ + 10, "air"); + setBlock(baseX + 13, baseY + dy, baseZ + 5, "air"); + setBlock(baseX + 13, baseY + dy, baseZ + 10, "air"); + } + + // Wing: split into kitchen (north) and bedroom (south) + for (let x = 26; x <= 33; x = x + 1) { + for (let y = 2; y <= 7; y = y + 1) { + setBlock(baseX + x, baseY + y, baseZ + 10, "stone_bricks"); + } + } + // Wing doorways (2x2) + for (let dy = 2; dy <= 3; dy = dy + 1) { + setBlock(baseX + 30, baseY + dy, baseZ + 10, "air"); + setBlock(baseX + 31, baseY + dy, baseZ + 10, "air"); + } + + // ====== FIREPLACE & CHIMNEY (living room corner) ====== + // Hearth at (x=3..5, z=12..13) + for (let x = 3; x <= 5; x = x + 1) { + for (let z = 12; z <= 13; z = z + 1) { + setBlock(baseX + x, baseY + 1, baseZ + z, "cobblestone"); + } + } + // Campfire for safe flame + setBlock(baseX + 4, baseY + 2, baseZ + 12, "campfire"); + // Back wall cladding and chimney up + for (let y = 2; y <= 10; y = y + 1) { + setBlock(baseX + 4, baseY + y, baseZ + 14, "cobblestone"); + setBlock(baseX + 4, baseY + y, baseZ + 15, "cobblestone"); + } + for (let y = 11; y <= 13; y = y + 1) { + setBlock(baseX + 4, baseY + y, baseZ + 15, "cobblestone"); + } + + // ====== FURNITURE & UTILITIES ====== + // Corridor rug (carpet) + for (let x = 9; x <= 14; x = x + 1) { + for (let z = 6; z <= 9; z = z + 1) { + setBlock(baseX + x, baseY + 2, baseZ + z, "red_carpet"); + } + } + + // Living room: table (logs + slab top), bookshelves, torches on top of shelves + // Table legs + setBlock(baseX + 16, baseY + 2, baseZ + 12, "spruce_log"); + setBlock(baseX + 18, baseY + 2, baseZ + 12, "spruce_log"); + setBlock(baseX + 16, baseY + 2, baseZ + 14, "spruce_log"); + setBlock(baseX + 18, baseY + 2, baseZ + 14, "spruce_log"); + // Table top + for (let x = 16; x <= 18; x = x + 1) { + for (let z = 12; z <= 14; z = z + 1) { + setBlock(baseX + x, baseY + 3, baseZ + z, "oak_slab"); + } + } + // Bookshelf wall + for (let x = 19; x <= 21; x = x + 1) { + for (let y = 2; y <= 4; y = y + 1) { + setBlock(baseX + x, baseY + y, baseZ + 13, "bookshelf"); + } + } + // Torches on top of bookshelf (attached to solid block below) + for (let x = 19; x <= 21; x = x + 1) { + setBlock(baseX + x, baseY + 5, baseZ + 13, "torch"); + } + + // Kitchen (wing north): counters (stone), crafting_table, furnace, sink (water) + for (let x = 26; x <= 33; x = x + 1) { + setBlock(baseX + x, baseY + 2, baseZ + 6, "stone"); + } + setBlock(baseX + 27, baseY + 2, baseZ + 7, "crafting_table"); + setBlock(baseX + 28, baseY + 2, baseZ + 7, "furnace"); + // Simple sink basin + setBlock(baseX + 30, baseY + 2, baseZ + 7, "cauldron"); + setBlock(baseX + 30, baseY + 3, baseZ + 7, "water"); + + // Bedroom (wing south): double bed, side tables (barrels), chest + setBlock(baseX + 29, baseY + 2, baseZ + 12, "bed"); + setBlock(baseX + 30, baseY + 2, baseZ + 12, "bed"); + setBlock(baseX + 28, baseY + 2, baseZ + 12, "barrel"); + setBlock(baseX + 31, baseY + 2, baseZ + 12, "barrel"); + setBlock(baseX + 33, baseY + 2, baseZ + 13, "chest"); + + // Study (rear main hall): desk, chair, bookshelves, torches on desk corners + // Desk + for (let x = 7; x <= 9; x = x + 1) { + setBlock(baseX + x, baseY + 2, baseZ + 13, "oak_slab"); + } + setBlock(baseX + 8, baseY + 2, baseZ + 12, "stair"); + setBlock(baseX + 7, baseY + 3, baseZ + 13, "torch"); + setBlock(baseX + 9, baseY + 3, baseZ + 13, "torch"); + + // ====== INTERIOR LIGHTING (TORCHES ON TOP OF FLOOR BLOCKS) ====== + // Main hall grid, placed on floor tops (supported by floor below at y-1) + for (let x = 3; x <= 21; x = x + 6) { + for (let z = 3; z <= 13; z = z + 5) { + setBlock(baseX + x, baseY + 2, baseZ + z, "torch"); + } + } + // Wing lighting + for (let x = 26; x <= 34; x = x + 4) { + setBlock(baseX + x, baseY + 2, baseZ + 6, "torch"); + setBlock(baseX + x, baseY + 2, baseZ + 13, "torch"); + } + + // ====== FRONT PATH & GARDEN TOUCH ====== + // Small path leading from entrance + for (let z = -1; z >= -6; z = z - 1) { + for (let x = 10; x <= 12; x = x + 1) { + setBlock(baseX + x, baseY + 1, baseZ + z, "cobblestone"); + } + } + // Flower beds flanking the path + for (let z = -1; z >= -6; z = z - 1) { + setBlock(baseX + 9, baseY + 2, baseZ + z, "rose_bush"); + setBlock(baseX + 13, baseY + 2, baseZ + z, "peony"); + } + """; + + public static String getBuildStructurePrompt() { + return buildStructurePrompt; + } +} diff --git a/src/autoclef/java/adris/altoclef/player2api/TTSManager.java b/src/autoclef/java/adris/altoclef/player2api/TTSManager.java index 48178de5..e2687e9d 100644 --- a/src/autoclef/java/adris/altoclef/player2api/TTSManager.java +++ b/src/autoclef/java/adris/altoclef/player2api/TTSManager.java @@ -13,7 +13,6 @@ public class TTSManager { private static final Logger LOGGER = LogManager.getLogger(); private static final int TTScharactersPerSecond = 25; // approx how fast (characters/sec) does the TTS talk - private static boolean TTSLocked = false; private static long estimatedEndTime = 0; private static final ExecutorService ttsThread = Executors.newSingleThreadExecutor(); @@ -27,8 +26,8 @@ private static void setEstimatedEndTime(String message) { } public static void TTS(String message, Character character, Player2APIService player2apiService) { - TTSLocked = true; - LOGGER.info("Locking TTS based on msg={}", message); + LOGGER.info("TTS for msg={}", message); + LockManager.setTTS(true); estimatedEndTime = Long.MAX_VALUE; ttsThread.submit(() -> { @@ -38,16 +37,12 @@ public static void TTS(String message, Character character, Player2APIService pl }); } - public static boolean isLocked() { - return TTSLocked; - } public static void injectOnTick(MinecraftServer server) { // release lock if we think we have finished. server.execute(() -> { - if ((System.nanoTime() > estimatedEndTime) && TTSLocked) { - LOGGER.info("TTS releasing lock"); - TTSLocked = false; + if ((System.nanoTime() > estimatedEndTime) && LockManager.isTTSLocked()) { + LockManager.setTTS(false); } }); } diff --git a/src/autoclef/java/adris/altoclef/player2api/pseudocommands/PseudoCommandExecutor.java b/src/autoclef/java/adris/altoclef/player2api/pseudocommands/PseudoCommandExecutor.java new file mode 100644 index 00000000..f54ca884 --- /dev/null +++ b/src/autoclef/java/adris/altoclef/player2api/pseudocommands/PseudoCommandExecutor.java @@ -0,0 +1,67 @@ +package adris.altoclef.player2api.pseudocommands; + +import java.util.Optional; +import java.util.function.Consumer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import adris.altoclef.AltoClefController; +import adris.altoclef.player2api.LLMCompleter; +import adris.altoclef.player2api.Player2APIService; +import adris.altoclef.player2api.pseudocommands.PseudoCommands.PseudoCommand; +import adris.altoclef.player2api.pseudocommands.codegen.BuildStructure; + +public class PseudoCommandExecutor { + + public interface PseudoCommandRunner { + public void onStart(); + + public void onStop(); + } + + public static Logger LOGGER = LogManager.getLogger(); + private AltoClefController mod; + private Player2APIService service; + + private Optional cmdStatus = Optional.empty(); + + public static LLMCompleter toolLLM; + + public static Optional tryToStartUsingLLM() { + if (!toolLLM.isAvailible()) { + return Optional.empty(); + } + return Optional.of(toolLLM); + } + + public PseudoCommandExecutor(AltoClefController mod, Player2APIService service) { + this.mod = mod; + this.service = service; + toolLLM = new LLMCompleter(service); + } + + public void setCmdStatus(String status) { + cmdStatus = Optional.of(status); + } + + public Optional getStatus() { + return cmdStatus; + } + + public void execute(PseudoCommands.PseudoCommand pcmd, String commandWithPrefix, Runnable onStopExt, + Consumer onErrMsg) { + LOGGER.info("Processing PseudoCommand={}, commandWithPrefix={}", pcmd.name, commandWithPrefix); + + Runnable onStop = () -> { + cmdStatus = Optional.empty(); + onStopExt.run(); + }; + + switch (pcmd.name) { + case "build_structure": + String description = commandWithPrefix.split("build_structure")[1].strip(); + BuildStructure.buildStructure(description, toolLLM, service, mod); + } + } +} diff --git a/src/autoclef/java/adris/altoclef/player2api/pseudocommands/PseudoCommands.java b/src/autoclef/java/adris/altoclef/player2api/pseudocommands/PseudoCommands.java new file mode 100644 index 00000000..98b2ef0f --- /dev/null +++ b/src/autoclef/java/adris/altoclef/player2api/pseudocommands/PseudoCommands.java @@ -0,0 +1,43 @@ +package adris.altoclef.player2api.pseudocommands; + +import java.util.List; +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import adris.altoclef.AltoClefController; +import adris.altoclef.player2api.LLMCompleter; +import adris.altoclef.player2api.Player2APIService; +import adris.altoclef.player2api.pseudocommands.codegen.BuildStructure; + +public class PseudoCommands { + public static Logger LOGGER = LogManager.getLogger(); + + public static class PseudoCommand { + String name; + String description; + + public PseudoCommand(String name, String description) { + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + } + + public static List pseudoCommands = List.of( + new PseudoCommand("build_structure", + "you provide a description, and the mod will build a structure matching that description. \nIMPORTANT: You must put a position into the description. If the player you are talking to doesn't give any hints on where to build it, put in that player's position into the description, or some positional information. You MUST give a coordiante to build at. If you don't know the player's position, then put your own position. \n Example call would be `build_structure a gray modern house with a garden of roses in front of it. Build at position (-305, 406, 72)`")); + + public static Optional getPseudocommandOption(String cmd) { + return pseudoCommands.stream().filter((p) -> cmd.contains(p.getName())).findFirst(); + } + +} diff --git a/src/autoclef/java/adris/altoclef/player2api/pseudocommands/codegen/BuildStructure.java b/src/autoclef/java/adris/altoclef/player2api/pseudocommands/codegen/BuildStructure.java new file mode 100644 index 00000000..d60102f7 --- /dev/null +++ b/src/autoclef/java/adris/altoclef/player2api/pseudocommands/codegen/BuildStructure.java @@ -0,0 +1,172 @@ +package adris.altoclef.player2api.pseudocommands.codegen; + +import java.util.Optional; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import adris.altoclef.AltoClefController; +import adris.altoclef.player2api.ConversationHistory; +import adris.altoclef.player2api.LLMCompleter; +import adris.altoclef.player2api.LockManager; +import adris.altoclef.player2api.Player2APIService; +import adris.altoclef.player2api.Prompts; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; + +public class BuildStructure { + public static final Logger LOGGER = LogManager.getLogger(); + + private static int numErrors = 0; + private static final int maxNumErrors = 2; + + private static String llmResponseToCode(String input) { + // TODO: later can strip markdown ```, language hints, etc. + return input; + } + + private static void appendUserRegenerationPrompt( + ConversationHistory history, + Player2APIService service, + String description, + String errorMsg) { + + StringBuilder sb = new StringBuilder(); + if (errorMsg != null && !errorMsg.isEmpty()) { + sb.append("The previous attempt failed with this error:\n") + .append(errorMsg) + .append("\n\n"); + sb.append("Try again and generate code using the same description: \"") + .append(description) + .append("\".\n") + .append("Only output valid executable code (no explanations, no markdown)."); + } else { + sb.append("Generate code using this description:\"") + .append(description) + .append("\""); + } + + history.addUserMessage(sb.toString(), service); + } + + private static void requestCodeFromLLM( + ConversationHistory history, + LLMCompleter completer, + Player2APIService service, + String description, + AltoClefController mod) { + LOGGER.info("Requesting code from llm: history={} description={}", history, description); + completer.processWithStringResponse( + service, + history, + (llmResponse, completerParam, serviceParam) -> onLLMResponse(llmResponse, completerParam, serviceParam, + description, history, mod), + (errMsg) -> onLLMTransportError(errMsg, completer, service, description, history, mod)); + } + + private static void onLLMResponse( + String llmResponse, + LLMCompleter completer, + Player2APIService service, + String description, + ConversationHistory history, + AltoClefController mod) { + LOGGER.info("LLM responded with code as string={}", llmResponse); + + String code = llmResponseToCode(llmResponse); + LOGGER.info("Processed response into code={}", code); + + history.addAssistantMessage(code, service); + // for now do it sync + BuildStructureFromCode.buildStructureFromCode( + code, + (setBlockData) -> { + LOGGER.info("setBlock(x={}, y={}, z={}, blockName={})", + setBlockData.x, setBlockData.y, setBlockData.z, setBlockData.blockName); + ResourceLocation id = new ResourceLocation("minecraft", setBlockData.blockName); + Block block = BuiltInRegistries.BLOCK.get(id); + // 3 means send to clients (2) and notify neighbors/update block states (1). + // maybe do 2 if you dont want + // redstone/etc updating/torches falling probably + mod.getWorld().setBlock(new BlockPos(setBlockData.x, setBlockData.y, setBlockData.z), + block.defaultBlockState(), 3); + + }, + (errStr) -> { + LOGGER.error("While building got err={}", errStr); + onCodeValidationError(errStr, code, completer, service, description, history, mod); + }, + () -> { + LOGGER.info("Building structure done, releasing code gen lock"); + mod.setPseudoCommandInfo(Optional.empty()); + LockManager.setCodeGenLock(false); + }, mod); + } + + private static void onLLMTransportError( + String errMsg, + LLMCompleter completer, + Player2APIService service, + String description, + ConversationHistory history, + AltoClefController mod) { + + LOGGER.error("LLM transport/call error={}", errMsg); + mod.setPseudoCommandInfo(Optional.empty()); + LockManager.setCodeGenLock(false); + } + + private static void onCodeValidationError( + String errMsg, + String lastCode, + LLMCompleter completer, + Player2APIService service, + String description, + ConversationHistory history, + AltoClefController mod) { + + if (numErrors < maxNumErrors) { + LOGGER.info("onCodeValidationError: trying again, errMsg={}", errMsg); + numErrors += 1; + + appendUserRegenerationPrompt(history, service, description, errMsg); + requestCodeFromLLM(history, completer, service, description, mod); + return; + } + LOGGER.info("Too many erorrs, exiting. Last errMsg={}", errMsg); + numErrors = 0; + mod.setPseudoCommandInfo(Optional.empty()); + LockManager.setCodeGenLock(false); + } + + private static void buildStructureInternal( + String description, + LLMCompleter completer, + Player2APIService service, + ConversationHistory history, + AltoClefController mod) { + appendUserRegenerationPrompt(history, service, description, null); + requestCodeFromLLM(history, completer, service, description, mod); + } + + public static void buildStructure( + String description, + LLMCompleter completer, + Player2APIService service, + AltoClefController mod) { + + numErrors = 0; + + ConversationHistory history = new ConversationHistory(Prompts.getBuildStructurePrompt()); + + history.addUserMessage( + "Generate code for the following description. " + + "Only output valid executable code (no explanations, no markdown). " + + "Description: \"" + description + "\"", + service); + + buildStructureInternal(description, completer, service, history, mod); + } +} \ No newline at end of file diff --git a/src/autoclef/java/adris/altoclef/player2api/pseudocommands/codegen/BuildStructureFromCode.java b/src/autoclef/java/adris/altoclef/player2api/pseudocommands/codegen/BuildStructureFromCode.java new file mode 100644 index 00000000..3dde157f --- /dev/null +++ b/src/autoclef/java/adris/altoclef/player2api/pseudocommands/codegen/BuildStructureFromCode.java @@ -0,0 +1,1351 @@ +package adris.altoclef.player2api.pseudocommands.codegen; + +import java.util.*; +import java.util.function.*; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import adris.altoclef.AltoClefController; + +/** + * MiniBlocks: a tiny interpreter that parses and runs a small language with + * - let / assignment + * - if / else + * - while + * - for (init; condition; update) { ... } + * - setBlock(x, y, z, facing, blockname) + * + * As it executes, each setBlock call becomes a SetBlockCommand. Use + * Runner.next() + * to pull commands one-by-one until completion. + */ +public class BuildStructureFromCode { + public static final Logger LOGGER = LogManager.getLogger(); + + // ==== Public API ==== + + /** Represents a single emitted setBlock command. */ + public static final class SetBlockCommand { + public final int x, y, z; + public final String blockName; + + public SetBlockCommand(int x, int y, int z, String blockName) { + this.x = x; + this.y = y; + this.z = z; + this.blockName = blockName; + } + + @Override + public String toString() { + return "setBlock(" + x + ", " + y + ", " + z + ", \"" + blockName + "\")"; + } + } + + /** Compiles source into an executable Program. */ + public static Program compile(String source) { + Lexer lex = new Lexer(source); + List tokens = lex.lex(); + Parser parser = new Parser(tokens); + List stmts = parser.parse(); + return new Program(stmts); + } + + /** Program runner that yields setBlock commands lazily, step-by-step. */ + public static final class Runner { + private final Interpreter interp; + private final Queue queue = new ArrayDeque<>(); + private boolean done = false; + + public Runner(Program program) { + this.interp = new Interpreter(program, queue::add); + } + + /** Execute until the next setBlock is produced or program ends. */ + public Optional next() { + if (done && queue.isEmpty()) + return Optional.empty(); + if (!queue.isEmpty()) + return Optional.of(queue.poll()); + while (queue.isEmpty() && !done) { + done = !interp.step(); // step returns false when finished + } + return queue.isEmpty() ? Optional.empty() : Optional.of(queue.poll()); + } + } + + /** Container for parsed program (AST). */ + public static final class Program { + final List statements; + + Program(List statements) { + this.statements = statements; + } + } + + public static void runCode(String code, Consumer onSetBlock, AltoClefController mod) + throws Exception { + Program program = compile(code); + Runner runner = new Runner(program); + Optional cmd; + while ((cmd = runner.next()).isPresent()) { + if (mod.shouldCancelPseudoCommand) { + mod.shouldCancelPseudoCommand = false; + return; + } + SetBlockCommand data = cmd.get(); + onSetBlock.accept(data); + } + } + + // ==== Lexer ==== + + enum TokenType { + // Single-char + LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE, COMMA, DOT, MINUS, PLUS, SEMICOLON, SLASH, STAR, + // One or two char + BANG, BANG_EQUAL, EQUAL, EQUAL_EQUAL, GREATER, GREATER_EQUAL, LESS, LESS_EQUAL, + + // Booleans and ternary + AND_AND, OR_OR, QUESTION, COLON, + + // Literals + IDENTIFIER, STRING, NUMBER, + // Keywords + LET, IF, ELSE, WHILE, FOR, TRUE, FALSE, NIL, SETBLOCK, + // End + EOF + } + + static final class Token { + final TokenType type; + final String lexeme; + final Object literal; + final int line, col; + + Token(TokenType type, String lexeme, Object literal, int line, int col) { + this.type = type; + this.lexeme = lexeme; + this.literal = literal; + this.line = line; + this.col = col; + } + + @Override + public String toString() { + return type + " '" + lexeme + "'" + (literal != null ? (" -> " + literal) : ""); + } + } + + static final class Lexer { + private final String src; + private final List tokens = new ArrayList<>(); + private int start = 0, current = 0, line = 1, col = 1; + + Lexer(String src) { + this.src = src; + } + + List lex() { + while (!isAtEnd()) { + start = current; + scanToken(); + } + tokens.add(new Token(TokenType.EOF, "", null, line, col)); + return tokens; + } + + private void scanToken() { + char c = advance(); + switch (c) { + case '(': + add(TokenType.LEFT_PAREN); + break; + case ')': + add(TokenType.RIGHT_PAREN); + break; + case '{': + add(TokenType.LEFT_BRACE); + break; + case '}': + add(TokenType.RIGHT_BRACE); + break; + case ',': + add(TokenType.COMMA); + break; + case '.': + add(TokenType.DOT); + break; + case '-': + add(TokenType.MINUS); + break; + case '+': + add(TokenType.PLUS); + break; + case ';': + add(TokenType.SEMICOLON); + break; + case '*': + add(TokenType.STAR); + break; + case '!': + add(match('=') ? TokenType.BANG_EQUAL : TokenType.BANG); + break; + case '=': + add(match('=') ? TokenType.EQUAL_EQUAL : TokenType.EQUAL); + break; + case '<': + add(match('=') ? TokenType.LESS_EQUAL : TokenType.LESS); + break; + case '>': + add(match('=') ? TokenType.GREATER_EQUAL : TokenType.GREATER); + break; + + case ' ': + case '\r': + case '\t': + break; + case '\n': + line++; + col = 0; + break; + case '"': + string(); + break; + case '&': + if (match('&')) + add(TokenType.AND_AND); + else + error("Unexpected character: & (did you mean &&?)"); + break; + case '|': + if (match('|')) + add(TokenType.OR_OR); + else + error("Unexpected character: | (did you mean ||?)"); + break; + + // NEW: ternary + case '?': + add(TokenType.QUESTION); + break; + case ':': + add(TokenType.COLON); + break; + case '/': + if (match('/')) { + while (!isAtEnd() && peek() != '\n') + advance(); + } else if (match('*')) { /* …existing block comment logic… */ + while (!isAtEnd() && !(peek() == '*' && peekNext() == '/')) { + if (peek() == '\n') { + line++; + col = 0; + } + advance(); + } + if (!isAtEnd()) { + advance(); + advance(); + } + } else + add(TokenType.SLASH); + break; + default: + if (isDigit(c)) + number(); + else if (isAlpha(c)) + identifier(); + else + error("Unexpected character: " + c); + } + } + + private void string() { + StringBuilder sb = new StringBuilder(); + while (!isAtEnd() && peek() != '"') { + char c = advance(); + if (c == '\\') { + if (isAtEnd()) + break; + char n = advance(); + switch (n) { + case 'n': + sb.append('\n'); + break; + case 't': + sb.append('\t'); + break; + case '"': + sb.append('"'); + break; + case '\\': + sb.append('\\'); + break; + default: + sb.append(n); + break; + } + } else { + sb.append(c); + } + if (c == '\n') { + line++; + col = 0; + } + } + if (isAtEnd()) + error("Unterminated string."); + advance(); // closing " + add(TokenType.STRING, sb.toString()); + } + + private void number() { + while (isDigit(peek())) + advance(); + if (peek() == '.' && isDigit(peekNext())) { + advance(); + while (isDigit(peek())) + advance(); + } + double val = Double.parseDouble(src.substring(start, current)); + add(TokenType.NUMBER, val); + } + + private void identifier() { + while (isAlphaNumeric(peek())) + advance(); + String text = src.substring(start, current); + TokenType type = keywords.get(text); + if ("setBlock".equals(text)) + type = TokenType.SETBLOCK; + if (type == null) + type = TokenType.IDENTIFIER; + add(type); + } + + private void add(TokenType type) { + add(type, null); + } + + private void add(TokenType type, Object lit) { + tokens.add(new Token(type, src.substring(start, current), lit, line, col)); + } + + private boolean match(char expected) { + if (isAtEnd() || src.charAt(current) != expected) + return false; + advance(); + return true; + } + + private char peek() { + return isAtEnd() ? '\0' : src.charAt(current); + } + + private char peekNext() { + return (current + 1 >= src.length()) ? '\0' : src.charAt(current + 1); + } + + private char advance() { + current++; + col++; + return src.charAt(current - 1); + } + + private boolean isAtEnd() { + return current >= src.length(); + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + private static boolean isAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; + } + + private static boolean isAlphaNumeric(char c) { + return isAlpha(c) || isDigit(c); + } + + private void error(String msg) { + throw new RuntimeException("[Lexer] line " + line + ", col " + col + ": " + msg); + } + + private static final Map keywords = new HashMap<>(); + static { + keywords.put("let", TokenType.LET); + keywords.put("if", TokenType.IF); + keywords.put("else", TokenType.ELSE); + keywords.put("while", TokenType.WHILE); + keywords.put("for", TokenType.FOR); + keywords.put("true", TokenType.TRUE); + keywords.put("false", TokenType.FALSE); + keywords.put("nil", TokenType.NIL); + } + } + + // ==== AST ==== + + interface Expr { + R accept(ExprVisitor v); + } + + interface ExprVisitor { + R visitBinary(Binary e); + + R visitUnary(Unary e); + + R visitLiteral(Literal e); + + R visitGrouping(Grouping e); + + R visitVariable(Variable e); + + R visitAssign(Assign e); + + R visitConditional(Conditional e); + } + + static final class Conditional implements Expr { + final Expr condition; + final Expr thenExpr; + final Expr elseExpr; + + Conditional(Expr condition, Expr thenExpr, Expr elseExpr) { + this.condition = condition; + this.thenExpr = thenExpr; + this.elseExpr = elseExpr; + } + + public R accept(ExprVisitor v) { + return v.visitConditional(this); + } + } + + static final class Binary implements Expr { + final Expr left; + final Token op; + final Expr right; + + Binary(Expr l, Token o, Expr r) { + left = l; + op = o; + right = r; + } + + public R accept(ExprVisitor v) { + return v.visitBinary(this); + } + } + + static final class Unary implements Expr { + final Token op; + final Expr right; + + Unary(Token o, Expr r) { + op = o; + right = r; + } + + public R accept(ExprVisitor v) { + return v.visitUnary(this); + } + } + + static final class Literal implements Expr { + final Object value; + + Literal(Object v) { + value = v; + } + + public R accept(ExprVisitor v) { + return v.visitLiteral(this); + } + } + + static final class Grouping implements Expr { + final Expr expr; + + Grouping(Expr e) { + expr = e; + } + + public R accept(ExprVisitor v) { + return v.visitGrouping(this); + } + } + + static final class Variable implements Expr { + final Token name; + + Variable(Token n) { + name = n; + } + + public R accept(ExprVisitor v) { + return v.visitVariable(this); + } + } + + static final class Assign implements Expr { + final Token name; + final Expr value; + + Assign(Token n, Expr v) { + name = n; + value = v; + } + + public R accept(ExprVisitor v) { + return v.visitAssign(this); + } + } + + interface Stmt { + void accept(StmtVisitor v); + } + + interface StmtVisitor { + void visitExprStmt(ExprStmt s); + + void visitPrintStmt(PrintStmt s); // (not exposed, handy for debugging) + + void visitVarStmt(Var s); + + void visitBlockStmt(Block s); + + void visitIfStmt(If s); + + void visitWhileStmt(While s); + + void visitForStmt(For s); + + void visitSetBlockStmt(SetBlock s); + } + + static final class ExprStmt implements Stmt { + final Expr expr; + + ExprStmt(Expr e) { + expr = e; + } + + public void accept(StmtVisitor v) { + v.visitExprStmt(this); + } + } + + static final class PrintStmt implements Stmt { + final Expr expr; + + PrintStmt(Expr e) { + expr = e; + } + + public void accept(StmtVisitor v) { + v.visitPrintStmt(this); + } + } + + static final class Var implements Stmt { + final Token name; + final Expr initializer; + + Var(Token n, Expr init) { + name = n; + initializer = init; + } + + public void accept(StmtVisitor v) { + v.visitVarStmt(this); + } + } + + static final class Block implements Stmt { + final List statements; + + Block(List s) { + statements = s; + } + + public void accept(StmtVisitor v) { + v.visitBlockStmt(this); + } + } + + static final class If implements Stmt { + final Expr condition; + final Stmt thenBranch; + final Stmt elseBranch; + + If(Expr c, Stmt t, Stmt e) { + condition = c; + thenBranch = t; + elseBranch = e; + } + + public void accept(StmtVisitor v) { + v.visitIfStmt(this); + } + } + + static final class While implements Stmt { + final Expr condition; + final Stmt body; + + While(Expr c, Stmt b) { + condition = c; + body = b; + } + + public void accept(StmtVisitor v) { + v.visitWhileStmt(this); + } + } + + static final class For implements Stmt { + final Stmt initializer; + final Expr condition; + final Stmt increment; + final Stmt body; + + For(Stmt init, Expr cond, Stmt inc, Stmt body) { + initializer = init; + condition = cond; + increment = inc; + this.body = body; + } + + public void accept(StmtVisitor v) { + v.visitForStmt(this); + } + } + + static final class SetBlock implements Stmt { + final Expr x, y, z, block; + + SetBlock(Expr x, Expr y, Expr z, Expr block) { + this.x = x; + this.y = y; + this.z = z; + this.block = block; + } + + public void accept(StmtVisitor v) { + v.visitSetBlockStmt(this); + } + } + + // ==== Parser ==== + + static final class Parser { + private final List tokens; + private int current = 0; + + Parser(List tokens) { + this.tokens = tokens; + } + + List parse() { + List stmts = new ArrayList<>(); + while (!isAtEnd()) + stmts.add(declaration()); + return stmts; + } + + private Stmt declaration() { + if (match(TokenType.LET)) + return varDeclaration(); + return statement(); + } + + private Stmt varDeclaration() { + Token name = consume(TokenType.IDENTIFIER, "Expect variable name."); + Expr init = null; + if (match(TokenType.EQUAL)) + init = expression(); + consume(TokenType.SEMICOLON, "Expect ';' after variable declaration."); + return new Var(name, init == null ? new Literal(null) : init); + } + + private Stmt statement() { + if (match(TokenType.LEFT_BRACE)) + return new Block(block()); + if (match(TokenType.IF)) + return ifStatement(); + if (match(TokenType.WHILE)) + return whileStatement(); + if (match(TokenType.FOR)) + return forStatement(); + if (match(TokenType.SETBLOCK)) + return setBlockStatement(); + return exprStatement(); + } + + private Stmt setBlockStatement() { + consume(TokenType.LEFT_PAREN, "Expect '(' after setBlock."); + Expr x = expression(); + consume(TokenType.COMMA, "Expect ',' after x."); + Expr y = expression(); + consume(TokenType.COMMA, "Expect ',' after y."); + Expr z = expression(); + consume(TokenType.COMMA, "Expect ',' after z."); + Expr block = expression(); + consume(TokenType.RIGHT_PAREN, "Expect ')' after arguments."); + consume(TokenType.SEMICOLON, "Expect ';' after setBlock."); + return new SetBlock(x, y, z, block); + } + + private Stmt ifStatement() { + consume(TokenType.LEFT_PAREN, "Expect '(' after if."); + Expr cond = expression(); + consume(TokenType.RIGHT_PAREN, "Expect ')' after condition."); + Stmt thenB = statement(); + Stmt elseB = null; + if (match(TokenType.ELSE)) + elseB = statement(); + return new If(cond, thenB, elseB); + } + + private Stmt whileStatement() { + consume(TokenType.LEFT_PAREN, "Expect '(' after while."); + Expr cond = expression(); + consume(TokenType.RIGHT_PAREN, "Expect ')' after condition."); + Stmt body = statement(); + return new While(cond, body); + } + + private Stmt forStatement() { + consume(TokenType.LEFT_PAREN, "Expect '(' after for."); + + Stmt init; + if (match(TokenType.SEMICOLON)) { + init = null; + } else if (match(TokenType.LET)) { + init = varDeclarationNoSemi(); + consume(TokenType.SEMICOLON, "Expect ';' after for init."); + } else { + init = exprStatementNoSemi(); + consume(TokenType.SEMICOLON, "Expect ';' after for init."); + } + + Expr cond = null; + if (!check(TokenType.SEMICOLON)) + cond = expression(); + consume(TokenType.SEMICOLON, "Expect ';' after loop condition."); + + Stmt inc = null; + if (!check(TokenType.RIGHT_PAREN)) + inc = exprStatementNoSemi(); + consume(TokenType.RIGHT_PAREN, "Expect ')' after for clauses."); + + Stmt body = statement(); + + // Desugar to: { init; while (cond) { body; inc; } } + if (inc != null) + body = new Block(Arrays.asList(body, inc)); + if (cond == null) + cond = new Literal(true); + Stmt whileStmt = new While(cond, body); + if (init != null) + whileStmt = new Block(Arrays.asList(init, whileStmt)); + return whileStmt; + } + + private Stmt exprStatement() { + Expr e = expression(); + consume(TokenType.SEMICOLON, "Expect ';' after expression."); + return new ExprStmt(e); + } + + private Stmt exprStatementNoSemi() { + Expr e = expression(); + return new ExprStmt(e); + } + + private Stmt varDeclarationNoSemi() { + Token name = consume(TokenType.IDENTIFIER, "Expect variable name."); + Expr init = null; + if (match(TokenType.EQUAL)) + init = expression(); + return new Var(name, init == null ? new Literal(null) : init); + } + + private List block() { + List stmts = new ArrayList<>(); + while (!check(TokenType.RIGHT_BRACE) && !isAtEnd()) + stmts.add(declaration()); + consume(TokenType.RIGHT_BRACE, "Expect '}' after block."); + return stmts; + } + + // Expressions (precedence: equality > comparison > term > factor > unary > + // primary) + private Expr expression() { + return assignment(); + } + + private Expr assignment() { + Expr expr = conditional(); // CHANGED: used to be equality() + if (match(TokenType.EQUAL)) { + Token equals = previous(); + Expr value = assignment(); + if (expr instanceof Variable) { + Token name = ((Variable) expr).name; + return new Assign(name, value); + } + error(equals, "Invalid assignment target."); + } + return expr; + } + + // NEW: ternary (right-associative) + private Expr conditional() { + Expr expr = or(); + if (match(TokenType.QUESTION)) { + Expr thenExpr = expression(); // allow comma/ops etc. + consume(TokenType.COLON, "Expect ':' in ternary expression."); + Expr elseExpr = conditional(); // right-associative + expr = new Conditional(expr, thenExpr, elseExpr); + } + return expr; + } + + // NEW: || precedence + private Expr or() { + Expr expr = and(); + while (match(TokenType.OR_OR)) { + Token op = previous(); + Expr right = and(); + expr = new Binary(expr, op, right); + } + return expr; + } + + // NEW: && precedence + private Expr and() { + Expr expr = equality(); + while (match(TokenType.AND_AND)) { + Token op = previous(); + Expr right = equality(); + expr = new Binary(expr, op, right); + } + return expr; + } + + private Expr equality() { + Expr expr = comparison(); + while (match(TokenType.BANG_EQUAL, TokenType.EQUAL_EQUAL)) { + Token op = previous(); + Expr right = comparison(); + expr = new Binary(expr, op, right); + } + return expr; + } + + private Expr comparison() { + Expr expr = term(); + while (match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) { + Token op = previous(); + Expr right = term(); + expr = new Binary(expr, op, right); + } + return expr; + } + + private Expr term() { + Expr expr = factor(); + while (match(TokenType.PLUS, TokenType.MINUS)) { + Token op = previous(); + Expr right = factor(); + expr = new Binary(expr, op, right); + } + return expr; + } + + private Expr factor() { + Expr expr = unary(); + while (match(TokenType.STAR, TokenType.SLASH)) { + Token op = previous(); + Expr right = unary(); + expr = new Binary(expr, op, right); + } + return expr; + } + + private Expr unary() { + if (match(TokenType.BANG, TokenType.MINUS)) { + Token op = previous(); + Expr right = unary(); + return new Unary(op, right); + } + return primary(); + } + + private Expr primary() { + if (match(TokenType.FALSE)) + return new Literal(false); + if (match(TokenType.TRUE)) + return new Literal(true); + if (match(TokenType.NIL)) + return new Literal(null); + if (match(TokenType.NUMBER)) + return new Literal(previous().literal); + if (match(TokenType.STRING)) + return new Literal(previous().literal); + if (match(TokenType.IDENTIFIER)) + return new Variable(previous()); + if (match(TokenType.LEFT_PAREN)) { + Expr e = expression(); + consume(TokenType.RIGHT_PAREN, "Expect ')' after expression."); + return new Grouping(e); + } + error(peek(), "Expect expression."); + return null; // unreachable + } + + // Helpers + private boolean match(TokenType... types) { + for (TokenType t : types) { + if (check(t)) { + advance(); + return true; + } + } + return false; + } + + private boolean check(TokenType t) { + return !isAtEnd() && peek().type == t; + } + + private Token advance() { + if (!isAtEnd()) + current++; + return previous(); + } + + private boolean isAtEnd() { + return peek().type == TokenType.EOF; + } + + private Token peek() { + return tokens.get(current); + } + + private Token previous() { + return tokens.get(current - 1); + } + + private Token consume(TokenType t, String msg) { + if (check(t)) + return advance(); + error(peek(), msg); + return null; // unreachable + } + + private void error(Token t, String msg) { + throw new RuntimeException( + "[Parser] line " + t.line + ", col " + t.col + ": " + msg + " Found '" + t.lexeme + "'"); + } + } + + // ==== Interpreter with step support ==== + + static final class Interpreter implements ExprVisitor, StmtVisitor { + private final Program program; + private final Emitter emitter; + private final Deque envStack = new ArrayDeque<>(); + private final Deque frames = new ArrayDeque<>(); + private boolean initialized = false; + + interface Emitter { + void emit(SetBlockCommand cmd); + } + + Interpreter(Program program, Emitter emitter) { + this.program = program; + this.emitter = emitter; + envStack.push(new Env(null)); + } + + /** + * Executes until either a setBlock is emitted, or the whole program finishes. + * + * @return true if there is more to run; false if finished. + */ + boolean step() { + if (!initialized) { + frames.push(new Frame(program.statements)); + initialized = true; + } + while (!frames.isEmpty()) { + Frame f = frames.peek(); + + // Handle WhileFrame specially + if (f instanceof WhileFrame) { + WhileFrame wf = (WhileFrame) f; + if (isTruthy(evaluate(wf.condition))) { + // Run one iteration body, then come back to this WhileFrame + frames.push(new Frame(singleton(wf.body))); + continue; + } else { + frames.pop(); // loop finished + continue; + } + } + + if (f.ip >= f.stmts.size()) { + frames.pop(); + if (f.onClose != null) + f.onClose.run(); + continue; + } + + Stmt s = f.stmts.get(f.ip++); + int emittedBefore = emittedCount; + s.accept(this); + if (emittedCount > emittedBefore) + return true; // yielded one setBlock + } + return false; // finished + } + + // Track emissions to know when to yield + private int emittedCount = 0; + + private void emit(SetBlockCommand cmd) { + emittedCount++; + emitter.emit(cmd); + } + + // ---- Statements ---- + + public void visitExprStmt(ExprStmt s) { + evaluate(s.expr); + } + + public void visitPrintStmt(PrintStmt s) { + System.out.println(stringify(evaluate(s.expr))); + } + + public void visitVarStmt(Var s) { + Object val = evaluate(s.initializer); + env().define(s.name.lexeme, val); + } + + public void visitBlockStmt(Block s) { + pushEnv(); + frames.push(new Frame(s.statements, () -> popEnv())); + } + + public void visitIfStmt(If s) { + if (isTruthy(evaluate(s.condition))) { + frames.push(new Frame(singleton(s.thenBranch))); + } else if (s.elseBranch != null) { + frames.push(new Frame(singleton(s.elseBranch))); + } + } + + public void visitWhileStmt(While s) { + frames.push(new WhileFrame(s.condition, s.body)); + } + + public void visitForStmt(For s) { + // Parser desugars 'for' into a block+while, so this isn't used. + frames.push(new Frame(singleton(s.body))); + } + + public void visitSetBlockStmt(SetBlock s) { + int x = toInt(evaluate(s.x)); + int y = toInt(evaluate(s.y)); + int z = toInt(evaluate(s.z)); + String block = String.valueOf(evaluate(s.block)); + emit(new SetBlockCommand(x, y, z, block)); + } + + // ---- Expressions ---- + public Object visitBinary(Binary e) { + // Short-circuit for logical ops: + if (e.op.type == TokenType.OR_OR) { + Object l = evaluate(e.left); + if (isTruthy(l)) + return true; // short-circuit + return isTruthy(evaluate(e.right)); + } + if (e.op.type == TokenType.AND_AND) { + Object l = evaluate(e.left); + if (!isTruthy(l)) + return false; // short-circuit + return isTruthy(evaluate(e.right)); + } + + Object l = evaluate(e.left); + Object r = evaluate(e.right); + switch (e.op.type) { + case PLUS: + if (l instanceof Double && r instanceof Double) + return (Double) l + (Double) r; + return stringify(l) + stringify(r); + case MINUS: + checkNumber(l, e.op); + checkNumber(r, e.op); + return (Double) l - (Double) r; + case STAR: + checkNumber(l, e.op); + checkNumber(r, e.op); + return (Double) l * (Double) r; + case SLASH: + checkNumber(l, e.op); + checkNumber(r, e.op); + return (Double) l / (Double) r; + case GREATER: + checkNumber(l, e.op); + checkNumber(r, e.op); + return (Double) l > (Double) r; + case GREATER_EQUAL: + checkNumber(l, e.op); + checkNumber(r, e.op); + return (Double) l >= (Double) r; + case LESS: + checkNumber(l, e.op); + checkNumber(r, e.op); + return (Double) l < (Double) r; + case LESS_EQUAL: + checkNumber(l, e.op); + checkNumber(r, e.op); + return (Double) l <= (Double) r; + case EQUAL_EQUAL: + return isEqual(l, r); + case BANG_EQUAL: + return !isEqual(l, r); + default: + throw new RuntimeException("Unknown binary op: " + e.op.type); + } + } + + public Object visitConditional(Conditional e) { + Object cond = evaluate(e.condition); + if (isTruthy(cond)) + return evaluate(e.thenExpr); + return evaluate(e.elseExpr); + } + + public Object visitUnary(Unary e) { + Object v = evaluate(e.right); + switch (e.op.type) { + case MINUS: + checkNumber(v, e.op); + return -((Double) v); + case BANG: + return !isTruthy(v); + default: + throw new RuntimeException("Unknown unary op: " + e.op.type); + } + } + + public Object visitLiteral(Literal e) { + return e.value; + } + + public Object visitGrouping(Grouping e) { + return evaluate(e.expr); + } + + public Object visitVariable(Variable e) { + return env().get(e.name); + } + + public Object visitAssign(Assign e) { + Object val = evaluate(e.value); + env().assign(e.name, val); + return val; + } + + private Object evaluate(Expr e) { + return e.accept(this); + } + + // ---- Utilities ---- + + private Env env() { + return envStack.peek(); + } + + private void pushEnv() { + envStack.push(new Env(env())); + } + + private void popEnv() { + envStack.pop(); + } + + private static boolean isTruthy(Object v) { + if (v == null) + return false; + if (v instanceof Boolean) + return (Boolean) v; + if (v instanceof Double) + return ((Double) v) != 0.0; + String s = String.valueOf(v); + return !s.isEmpty(); + } + + private static boolean isEqual(Object a, Object b) { + if (a == null && b == null) + return true; + if (a == null) + return false; + if (a instanceof Double && b instanceof Double) + return ((Double) a).doubleValue() == ((Double) b).doubleValue(); + return String.valueOf(a).equals(String.valueOf(b)); + } + + private static void checkNumber(Object v, Token at) { + if (!(v instanceof Double)) + throw new RuntimeException("Operand must be a number at token '" + at.lexeme + "'"); + } + + private static String stringify(Object v) { + if (v == null) + return "nil"; + if (v instanceof Double) { + double d = (Double) v; + if (d == Math.rint(d)) + return String.valueOf((long) d); + return String.valueOf(d); + } + return String.valueOf(v); + } + + private static int toInt(Object v) { + if (v instanceof Double) + return (int) Math.round((Double) v); + try { + return Integer.parseInt(String.valueOf(v)); + } catch (Exception e) { + throw new RuntimeException("Expected integer-like value, got: " + v); + } + } + + // ---- Frames for stepping ---- + + static class Frame { + final List stmts; + int ip = 0; + final Runnable onClose; // optional cleanup (e.g., pop env) + + Frame(List stmts) { + this(stmts, null); + } + + Frame(List stmts, Runnable onClose) { + this.stmts = stmts; + this.onClose = onClose; + } + } + + static final class WhileFrame extends Frame { + final Expr condition; + final Stmt body; + + WhileFrame(Expr condition, Stmt body) { + super(Collections.emptyList()); + this.condition = condition; + this.body = body; + } + } + + private static List singleton(Stmt s) { + return Collections.singletonList(s); + } + } + + // ==== Environment / Variables ==== + + static final class Env { + private final Env enclosing; + private final Map values = new HashMap<>(); + + Env(Env enclosing) { + this.enclosing = enclosing; + } + + void define(String name, Object value) { + values.put(name, value); + } + + Object get(Token nameTok) { + String name = nameTok.lexeme; + if (values.containsKey(name)) + return values.get(name); + if (enclosing != null) + return enclosing.get(nameTok); + throw new RuntimeException("Undefined variable '" + name + "'."); + } + + void assign(Token nameTok, Object value) { + String name = nameTok.lexeme; + if (values.containsKey(name)) { + values.put(name, value); + return; + } + if (enclosing != null) { + enclosing.assign(nameTok, value); + return; + } + throw new RuntimeException("Undefined variable '" + name + "'."); + } + } + + // static String code = String.join("\n", + // "let baseX = 10;", + // "let baseY = 64;", + // "let baseZ = 10;", + // "let dir = \"north\";", + // "let block = \"stone\";", + // "let t = true;", + // "", + // "// Build a 3x2 wall:", + // "for (let i = 0; i < 3; i = i + 1) {", + // " for (let j = 0; j < 2; j = j + 1) {", + // " setBlock(baseX + i, baseY + j, baseZ, (!t || j != 0)? block : \"a\");", + // " }", + // "}", + // "", + // "// If we want a cap:", + // "if (true) {", + // " setBlock(baseX + 1, baseY + 2, baseZ, \"glass\");", + // "}"); + + // public static void main(String[] args) { + // try { + // runCode(code, cmd -> System.out.println(cmd)); + // } catch (Exception e) { + // // TODO: handle exception + // } + // } + + public static void buildStructureFromCode(String code, Consumer onSetBlock, + Consumer onErrString, Runnable onFinishSuccess, AltoClefController mod) { + try { + // run once to validate + BuildStructureFromCode.runCode(code, + (_unused) -> { + // set block is nothing + }, mod); + LOGGER.info("Code validated, running code for real now."); + // code validated, can safely set block + BuildStructureFromCode.runCode(code, onSetBlock, mod); + onFinishSuccess.run(); + } catch (Exception e) { + LOGGER.error("LLM build structure err={} ", e.getMessage()); + onErrString.accept(e.getMessage()); + } + } + +} diff --git a/src/autoclef/java/adris/altoclef/player2api/status/StatusUtils.java b/src/autoclef/java/adris/altoclef/player2api/status/StatusUtils.java index 9188fc23..d9963d96 100644 --- a/src/autoclef/java/adris/altoclef/player2api/status/StatusUtils.java +++ b/src/autoclef/java/adris/altoclef/player2api/status/StatusUtils.java @@ -1,6 +1,8 @@ package adris.altoclef.player2api.status; import adris.altoclef.AltoClefController; +import adris.altoclef.player2api.pseudocommands.PseudoCommands; +import adris.altoclef.player2api.pseudocommands.PseudoCommands.PseudoCommand; import adris.altoclef.tasksystem.Task; import adris.altoclef.util.helpers.ItemHelper; import baritone.api.entity.IAutomatone; @@ -10,6 +12,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.UUID; import java.util.Map.Entry; import net.minecraft.core.BlockPos; @@ -51,7 +54,8 @@ public static String getDimensionString(AltoClefController mod) { public static String getWeatherString(AltoClefController mod) { boolean isRaining = mod.getWorld().isRaining(); boolean isThundering = mod.getWorld().isThundering(); - ObjectStatus status = new ObjectStatus().add("isRaining", String.valueOf(isRaining)).add("isThundering", String.valueOf(isThundering)); + ObjectStatus status = new ObjectStatus().add("isRaining", String.valueOf(isRaining)).add("isThundering", + String.valueOf(isThundering)); return status.toString(); } @@ -74,7 +78,8 @@ public static String getNearbyBlocksString(AltoClefController mod) { for (int dy = -radius; dy <= radius; dy++) { for (int dz = -radius; dz <= radius; dz++) { BlockPos pos = center.offset(dx, dy, dz); - String blockName = mod.getWorld().getBlockState(pos).getBlock().getDescriptionId().replace("block.minecraft.", ""); + String blockName = mod.getWorld().getBlockState(pos).getBlock().getDescriptionId() + .replace("block.minecraft.", ""); if (!blockName.equals("air")) { blockCounts.put(blockName, blockCounts.getOrDefault(blockName, 0) + 1); } @@ -109,8 +114,8 @@ public static String getNearbyHostileMobs(AltoClefController mod) { } return descriptions.isEmpty() - ? String.format("no nearby hostile mobs within %d", radius) - : "[" + String.join(",", descriptions.stream().map(s -> "\"" + s + "\"").toArray(String[]::new)) + "]"; + ? String.format("no nearby hostile mobs within %d", radius) + : "[" + String.join(",", descriptions.stream().map(s -> "\"" + s + "\"").toArray(String[]::new)) + "]"; } public static String getEquippedArmorStatusString(AltoClefController mod) { @@ -121,24 +126,37 @@ public static String getEquippedArmorStatusString(AltoClefController mod) { ItemStack legs = player.getItemBySlot(EquipmentSlot.LEGS); ItemStack feet = player.getItemBySlot(EquipmentSlot.FEET); ItemStack offhand = player.getItemBySlot(EquipmentSlot.OFFHAND); - status.add("helmet", !head.isEmpty() && head.getItem() instanceof ArmorItem ? head.getItem().getDescriptionId().replace("item.minecraft.", "") : "none"); + status.add("helmet", + !head.isEmpty() && head.getItem() instanceof ArmorItem + ? head.getItem().getDescriptionId().replace("item.minecraft.", "") + : "none"); status.add( - "chestplate", !chest.isEmpty() && chest.getItem() instanceof ArmorItem ? chest.getItem().getDescriptionId().replace("item.minecraft.", "") : "none" - ); - status.add("leggings", !legs.isEmpty() && legs.getItem() instanceof ArmorItem ? legs.getItem().getDescriptionId().replace("item.minecraft.", "") : "none"); - status.add("boots", !feet.isEmpty() && feet.getItem() instanceof ArmorItem ? feet.getItem().getDescriptionId().replace("item.minecraft.", "") : "none"); + "chestplate", + !chest.isEmpty() && chest.getItem() instanceof ArmorItem + ? chest.getItem().getDescriptionId().replace("item.minecraft.", "") + : "none"); + status.add("leggings", + !legs.isEmpty() && legs.getItem() instanceof ArmorItem + ? legs.getItem().getDescriptionId().replace("item.minecraft.", "") + : "none"); + status.add("boots", + !feet.isEmpty() && feet.getItem() instanceof ArmorItem + ? feet.getItem().getDescriptionId().replace("item.minecraft.", "") + : "none"); status.add( - "offhand_shield", - !offhand.isEmpty() && offhand.getItem() instanceof ShieldItem ? offhand.getItem().getDescriptionId().replace("item.minecraft.", "") : "none" - ); + "offhand_shield", + !offhand.isEmpty() && offhand.getItem() instanceof ShieldItem + ? offhand.getItem().getDescriptionId().replace("item.minecraft.", "") + : "none"); return status.toString(); } public static String getNearbyPlayers(AltoClefController mod) { + int maxDist = 70; List descriptions = new ArrayList<>(); for (Entity entity : mod.getEntityTracker().getCloseEntities()) { - if (entity instanceof Player player && entity.distanceTo(mod.getPlayer()) < 32.0F) { + if (entity instanceof Player player && entity.distanceTo(mod.getPlayer()) < maxDist) { String username = player.getName().getString(); String position = entity.position().align(EnumSet.allOf(Axis.class)).toString(); descriptions.add(username + " at " + position); @@ -146,8 +164,8 @@ public static String getNearbyPlayers(AltoClefController mod) { } return descriptions.isEmpty() - ? String.format("no nearby users within %d", 32) - : "[" + String.join(",", descriptions.stream().map(s -> "\"" + s + "\"").toArray(String[]::new)) + "]"; + ? String.format("no nearby users within %d", maxDist) + : "[" + String.join(",", descriptions.stream().map(s -> "\"" + s + "\"").toArray(String[]::new)) + "]"; } public static String getNearbyNPCs(AltoClefController mod) { @@ -164,8 +182,8 @@ public static String getNearbyNPCs(AltoClefController mod) { } return descriptions.isEmpty() - ? String.format("no nearby npcs within %d", 32) - : "[" + String.join(",", descriptions.stream().map(s -> "\"" + s + "\"").toArray(String[]::new)) + "]"; + ? String.format("no nearby npcs within %d", 32) + : "[" + String.join(",", descriptions.stream().map(s -> "\"" + s + "\"").toArray(String[]::new)) + "]"; } public static float getUserNameDistance(AltoClefController mod, String targetUsername) { @@ -196,17 +214,21 @@ public static String getGamemodeString(AltoClefController mod) { public static String getTaskTree(AltoClefController mod) { Task task = mod.getUserTaskChain().getCurrentTask(); + Optional currentPseudoCommand = mod.getCurrentlyRunningPseudoCmd(); + if (currentPseudoCommand.isPresent()) { + return currentPseudoCommand.get(); + } return task == null ? "Task tree is empty" : task.getTaskTree(); } public static float getDistanceToUUID(AltoClefController mod, UUID target) { // for (Player player : mod.getWorld().players()) { - // if (player.getUUID().equals(target)) { - // return player.distanceTo(mod.getPlayer()); - // } + // if (player.getUUID().equals(target)) { + // return player.distanceTo(mod.getPlayer()); + // } // } - for(Entity entity : mod.getWorld().getAllEntities()){ - if(entity.getUUID().equals(target)){ + for (Entity entity : mod.getWorld().getAllEntities()) { + if (entity.getUUID().equals(target)) { return entity.distanceTo(mod.getPlayer()); } }