diff --git a/.architectury-transformer/debug.log b/.architectury-transformer/debug.log new file mode 100644 index 0000000..459be03 --- /dev/null +++ b/.architectury-transformer/debug.log @@ -0,0 +1 @@ +[Architectury Transformer DEBUG] Closed File Systems for /home/elefant/engine/common/build/libs/playerengine-common-1.0.0.jar diff --git a/common/build.gradle b/common/build.gradle index 816616f..88fd64d 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -10,4 +10,8 @@ dependencies { // Architectury API. This is optional, and you can comment it out if you don't need it. modImplementation "dev.architectury:architectury:$rootProject.architectury_api_version" + + // schematic parsing + implementation 'net.sandrohc:schematic4j:1.1.0' + include 'net.sandrohc:schematic4j:1.1.0' } diff --git a/common/src/main/java/com/player2/playerengine/MCCommands.java b/common/src/main/java/com/player2/playerengine/MCCommands.java index bca3099..7f936cc 100644 --- a/common/src/main/java/com/player2/playerengine/MCCommands.java +++ b/common/src/main/java/com/player2/playerengine/MCCommands.java @@ -1,5 +1,10 @@ package com.player2.playerengine; +import java.util.UUID; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.builder.LiteralArgumentBuilder; @@ -15,10 +20,15 @@ import org.apache.logging.log4j.Logger; import dev.architectury.event.events.common.LifecycleEvent; import net.minecraft.world.entity.player.Player; - - - - +import com.player2.playerengine.player2api.AgentSideEffects; +import net.minecraft.server.level.ServerPlayer; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.player2.playerengine.player2api.AgentConversationData; +import com.player2.playerengine.player2api.AgentSideEffects; +import com.player2.playerengine.player2api.Player2APIService; +import com.player2.playerengine.player2api.auth.AuthenticationManager; +import com.player2.playerengine.player2api.manager.ConversationManager; +import dev.architectury.event.events.common.LifecycleEvent; public class MCCommands { @@ -39,19 +49,77 @@ public static void register(MinecraftServer server) { private static void registerFromDispatch(CommandDispatcher dispatcher) { dispatcher.register( Commands.literal("playerengine") - .then(registerRelog())); + .then(registerRelog()) + .then(registerSummon()) + .then(registerHelp())); + } + private static LiteralArgumentBuilder registerHelp() { + return Commands.literal("help") + .executes(context -> { + LOGGER.info("help command"); + Player player = context.getSource().getPlayerOrException(); + + String message = """ + Help: here are the following + - 'playerengine relog': + - 'help': displays this help + - 'tpto ': teleports you to AI + - 'list': lists AI usernames + """; + + AgentSideEffects.broadcastChatToPlayer(player.level().getServer(), message, (ServerPlayer) player); + return 1; + }); } private static LiteralArgumentBuilder registerRelog() { + return Commands.literal("relog") .executes(context -> { for (Player2APIService service : PlayerEngineController.staticAPIServices.values()) { LOGGER.info("relog command"); String clientId = service.getClientId(); - Player player = context.getSource().getPlayer(); + Player player = context.getSource().getPlayerOrException(); AuthenticationManager.getInstance().invalidateToken(player, clientId); } return 1; }); } + + + private static final SuggestionProvider NPC_SUGGEST = (context, builder) -> { + CommandSourceStack src = context.getSource(); + Player player = src.getPlayerOrException(); + UUID ownerUUID = player.getUUID(); + String remaining = builder.getRemaining().toLowerCase(); + + for (AgentConversationData data : ConversationManager.getDataByOwner(ownerUUID)) { + String name = data.getName(); + + if (name.toLowerCase().startsWith(remaining)) { + builder.suggest(name); + } + } + return builder.buildFuture(); + }; + private static LiteralArgumentBuilder registerSummon() { + return Commands.literal("tpto") + .then(Commands.argument("username", StringArgumentType.string()) + .suggests(NPC_SUGGEST) + .executes(context -> { + String username = StringArgumentType.getString(context, "username"); + Player owner = context.getSource().getPlayerOrException(); + UUID ownerUUID = owner.getUUID(); + LOGGER.info("tpto command: {} {}", username, ownerUUID); + for (AgentConversationData data : ConversationManager.getDataByOwner(ownerUUID)) { + LOGGER.info("looking for name {}", data.getName()); + if(data.getName().equals(username)){ + LOGGER.info("Match on username: {}", username); + AgentSideEffects.teleportOwnerTo(data); + } + } + return 1; + })); + } + } \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/PlayerEngineClient.java b/common/src/main/java/com/player2/playerengine/PlayerEngineClient.java index f1b4d0e..f913724 100644 --- a/common/src/main/java/com/player2/playerengine/PlayerEngineClient.java +++ b/common/src/main/java/com/player2/playerengine/PlayerEngineClient.java @@ -19,6 +19,9 @@ public static void onInitializeClient() { EntityRendererRegistry.register(PlayerEngine.FISHING_BOBBER, CustomFishingBobberRenderer::new); STTUtils.onInitialize(); NetworkManager.registerReceiver(NetworkManager.Side.S2C, new ResourceLocation("playerengine", "stream_tts"), (buf, context) -> { + if(!enabledTTS){ + return; + } String clientId = buf.readUtf(); String token = buf.readUtf(); String text = buf.readUtf(); diff --git a/common/src/main/java/com/player2/playerengine/PlayerEngineCommands.java b/common/src/main/java/com/player2/playerengine/PlayerEngineCommands.java index 66c5e79..31f8356 100644 --- a/common/src/main/java/com/player2/playerengine/PlayerEngineCommands.java +++ b/common/src/main/java/com/player2/playerengine/PlayerEngineCommands.java @@ -1,29 +1,8 @@ package com.player2.playerengine; - -import com.player2.playerengine.commands.AttackPlayerOrMobCommand; -import com.player2.playerengine.commands.BodyLanguageCommand; -import com.player2.playerengine.commands.BuildStructureCommand; -import com.player2.playerengine.commands.DepositCommand; -import com.player2.playerengine.commands.EquipCommand; -import com.player2.playerengine.commands.FarmCommand; -import com.player2.playerengine.commands.FishCommand; -import com.player2.playerengine.commands.FollowCommand; -import com.player2.playerengine.commands.FoodCommand; -import com.player2.playerengine.commands.GamerCommand; -import com.player2.playerengine.commands.GetCommand; -import com.player2.playerengine.commands.GiveCommand; -import com.player2.playerengine.commands.GotoCommand; -import com.player2.playerengine.commands.HeroCommand; -import com.player2.playerengine.commands.IdleCommand; -import com.player2.playerengine.commands.LocateStructureCommand; -import com.player2.playerengine.commands.MeatCommand; -import com.player2.playerengine.commands.ReloadSettingsCommand; -import com.player2.playerengine.commands.ResetMemoryCommand; -import com.player2.playerengine.commands.SetAIBridgeEnabledCommand; -import com.player2.playerengine.commands.StopCommand; -import com.player2.playerengine.commands.random.ScanCommand; +import com.player2.playerengine.commands.*; +import com.player2.playerengine.commands.*; +import com.player2.playerengine.commands.random.*; import com.player2.playerengine.commands.base.CommandException; -import com.player2.playerengine.commands.SimpleExploreCommand; public class PlayerEngineCommands { public static void init(PlayerEngineController controller) throws CommandException { @@ -51,6 +30,9 @@ public static void init(PlayerEngineController controller) throws CommandExcepti new SetAIBridgeEnabledCommand(), new FarmCommand(), new SimpleExploreCommand(), + new EatFoodCommand(), + new SetHostileAttackCommand(), + new PickupDropsCommand(), new FishCommand()); } } diff --git a/common/src/main/java/com/player2/playerengine/PlayerEngineController.java b/common/src/main/java/com/player2/playerengine/PlayerEngineController.java index 00d6781..9baa7d5 100644 --- a/common/src/main/java/com/player2/playerengine/PlayerEngineController.java +++ b/common/src/main/java/com/player2/playerengine/PlayerEngineController.java @@ -84,6 +84,7 @@ public class PlayerEngineController { public boolean isStopping = false; private Player owner; public static HashMap staticAPIServices = new HashMap<>(); + private boolean shouldDefendFromHostiles = false; public PlayerEngineController(IBaritone baritone, Character character, String player2GameId) { this.baritone = baritone; @@ -407,4 +408,12 @@ public Optional getClosestPlayer() { return Float.compare(adist, bdist); }).findFirst(); } + + public boolean getShouldDefendFromHostiles(){ + return this.shouldDefendFromHostiles; + } + + public void setShouldDefendFromHostiles(boolean toSet){ + this.shouldDefendFromHostiles = toSet; + } } diff --git a/common/src/main/java/com/player2/playerengine/automaton/api/entity/LivingEntityHungerManager.java b/common/src/main/java/com/player2/playerengine/automaton/api/entity/LivingEntityHungerManager.java index 317d476..1c51718 100644 --- a/common/src/main/java/com/player2/playerengine/automaton/api/entity/LivingEntityHungerManager.java +++ b/common/src/main/java/com/player2/playerengine/automaton/api/entity/LivingEntityHungerManager.java @@ -37,7 +37,7 @@ public void add(int food, float saturationModifier) { this.foodSaturationLevel = Math.min(this.foodSaturationLevel + food * saturationModifier * 2.0F, (float)this.foodLevel); } - public void eat(Item item, ItemStack stack) { + public void eat(Item item) { if (item.isEdible()) { FoodProperties foodComponent = item.getFoodProperties(); this.add(foodComponent.getNutrition(), foodComponent.getSaturationModifier()); diff --git a/common/src/main/java/com/player2/playerengine/chains/MobDefenseChain.java b/common/src/main/java/com/player2/playerengine/chains/MobDefenseChain.java index a006508..74db1b6 100644 --- a/common/src/main/java/com/player2/playerengine/chains/MobDefenseChain.java +++ b/common/src/main/java/com/player2/playerengine/chains/MobDefenseChain.java @@ -150,6 +150,9 @@ private static int getDangerousnessScore(List toDealWithList) { @Override public float getPriority() { + if(this.controller.getShouldDefendFromHostiles()){ + return 0.0F; + } this.cachedLastPriority = this.getPriorityInner(); if (this.getCurrentTask() == null) { this.cachedLastPriority = 0.0F; diff --git a/common/src/main/java/com/player2/playerengine/commands/BuildStructureCommand.java b/common/src/main/java/com/player2/playerengine/commands/BuildStructureCommand.java index ec81837..1b0ce67 100644 --- a/common/src/main/java/com/player2/playerengine/commands/BuildStructureCommand.java +++ b/common/src/main/java/com/player2/playerengine/commands/BuildStructureCommand.java @@ -7,23 +7,38 @@ import com.player2.playerengine.commands.base.CommandException; import com.player2.playerengine.tasks.construction.build_structure.BuildStructureTask; +import net.minecraft.core.BlockPos; + public class BuildStructureCommand extends Command { public BuildStructureCommand() throws CommandException { super("build_structure", - "Agent can build any thing in Minecraft given the description and position. The description should be a string generated to capture a clear and concise summary of the structure the user asked to be built. Agent does not need to collect materials to build the structure when using this function.\\n" + "Agent can build any thing in Minecraft given a 3d coordinate, search query, and a description. The coordinates define the center bottom of the building. The search query comes first and should be a very brief query to search for a schematic that matches the build requirements. The description should be a longer (but still short) string generated to capture a clear and concise summary of the structure the user asked to be built. Agent does not need to collect materials to build the structure when using this function.\\n" + + // + "IMPORTANT: The first 3 arguments should be the coordinates. If the player you are talking to doesn't give any hints on where to build it, put in that player's position as the first 3 arguments, or some positional information. You MUST start with the coordinates to build at. If you don't know the player's position, then put your own position. \\n" + // - "IMPORTANT: 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" + "IMPORTANT: The latter 2 arguments should be surrounded by quotes. That is how we separate the search query and the description from each other.\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)`", - new Arg<>(String.class, "description")); + " Example call would be `build_structure -305 406 72 \"gray modern house\" \"a gray modern house with a garden of roses in front of it.\"`", + // new Arg<>(String.class, "arguments") + new Arg<>(Integer.class, "x"), + new Arg<>(Integer.class, "y"), + new Arg<>(Integer.class, "x"), + new Arg<>(String.class, "search_query"), + new Arg<>(String.class, "description") + ); } @Override protected void call(PlayerEngineController mod, ArgParser parser) throws CommandException { + // String args = parser.get(String.class); + int x = parser.get(Integer.class); + int y = parser.get(Integer.class); + int z = parser.get(Integer.class); + String query = parser.get(String.class); String description = parser.get(String.class); - mod.runUserTask(new BuildStructureTask(description, mod), () -> { + System.out.println("ASDF ARGS: " + x + ", " + y + ", " + z + ", " + query + ", " + description); + mod.runUserTask(new BuildStructureTask(new BlockPos(x, y, z), query, description, mod), () -> { this.finish(); }); } - } \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/commands/EatFoodCommand.java b/common/src/main/java/com/player2/playerengine/commands/EatFoodCommand.java new file mode 100644 index 0000000..4dd85ab --- /dev/null +++ b/common/src/main/java/com/player2/playerengine/commands/EatFoodCommand.java @@ -0,0 +1,35 @@ +package com.player2.playerengine.commands; + +import com.player2.playerengine.PlayerEngineController; +import com.player2.playerengine.commands.base.ArgParser; +import com.player2.playerengine.commands.base.Command; +import com.player2.playerengine.commands.base.CommandException; +import com.player2.playerengine.tasks.misc.EatFoodTask; +import java.util.Optional; +import net.minecraft.world.item.Item; +import com.player2.playerengine.util.helpers.ItemHelper; +public class EatFoodCommand extends Command { + public EatFoodCommand() throws CommandException { + super("eat_food", "Eats food item from your inventory. ONLY CALL IF hunger < 20"); + } + + @Override + protected void call(PlayerEngineController controller, ArgParser parser) throws CommandException { + if(controller.getBaritone().getEntityContext().hungerManager().getFoodLevel() >= 20){ + throw new CommandException("Tried to call eatFood, but hunger was too high. You may not eat if hunger is full."); + } + + Item[] items; + + if (parser.getArgUnits().length != 1) { + return; + } + String itemNameAsString = parser.getArgUnits()[0].toLowerCase(); + Optional ma = ItemHelper.getItemFromString(itemNameAsString, ItemHelper.FOODS); + if(!ma.isPresent()){ + return; + } + Item a = ma.get(); + controller.runUserTask(new EatFoodTask(a), () -> this.finish()); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/commands/PickupDropsCommand.java b/common/src/main/java/com/player2/playerengine/commands/PickupDropsCommand.java new file mode 100644 index 0000000..1107c26 --- /dev/null +++ b/common/src/main/java/com/player2/playerengine/commands/PickupDropsCommand.java @@ -0,0 +1,22 @@ +package com.player2.playerengine.commands; + +import com.player2.playerengine.PlayerEngineController; +import com.player2.playerengine.commands.base.Arg; +import com.player2.playerengine.commands.base.ArgParser; +import com.player2.playerengine.commands.base.Command; +import com.player2.playerengine.commands.base.CommandException; +import com.player2.playerengine.tasks.movement.PickupDroppedItemTask; +import com.player2.playerengine.util.ItemTarget; +public class PickupDropsCommand extends Command { + public PickupDropsCommand() throws CommandException { + super( + "pickup_drops", + "picks up all item drops nearby" + ); + } + + @Override + protected void call(PlayerEngineController mod, ArgParser parser) throws CommandException { + mod.runUserTask(new PickupDroppedItemTask(new ItemTarget[]{}, true), () -> this.finish()); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/commands/SetHostileAttackCommand.java b/common/src/main/java/com/player2/playerengine/commands/SetHostileAttackCommand.java new file mode 100644 index 0000000..8679baa --- /dev/null +++ b/common/src/main/java/com/player2/playerengine/commands/SetHostileAttackCommand.java @@ -0,0 +1,41 @@ +package com.player2.playerengine.commands; + +import com.player2.playerengine.PlayerEngineController; +import com.player2.playerengine.util.Debug; +import com.player2.playerengine.commands.base.Arg; +import com.player2.playerengine.commands.base.ArgParser; +import com.player2.playerengine.commands.base.Command; +import com.player2.playerengine.commands.base.CommandException; + +public class SetHostileAttackCommand extends Command { + public SetHostileAttackCommand() throws CommandException { + super( + "set_attack_hostiles", + "will disable automatic attacking of hostiles. Only use when the user tells you to stop attacking/etc.", + new Arg<>(SetHostileAttackCommand.ToggleState.class, "onOrOff") + ); + } + + @Override + protected void call(PlayerEngineController mod, ArgParser parser) throws CommandException { + SetHostileAttackCommand.ToggleState toggle = parser.get(SetHostileAttackCommand.ToggleState.class); + switch (toggle) { + case ON: + Debug.logMessage( + "Enabling attack hostiles! You will now automatically attack nearby hostiles" + ); + mod.setChatClefEnabled(true); + break; + case OFF: + Debug.logMessage("Disabling attack hostiles! You will NOT attack nearby hostiles automatically."); + mod.setChatClefEnabled(false); + } + + this.finish(); + } + + public static enum ToggleState { + ON, + OFF; + } +} diff --git a/common/src/main/java/com/player2/playerengine/player2api/AgentSideEffects.java b/common/src/main/java/com/player2/playerengine/player2api/AgentSideEffects.java index 66d55ef..116362a 100644 --- a/common/src/main/java/com/player2/playerengine/player2api/AgentSideEffects.java +++ b/common/src/main/java/com/player2/playerengine/player2api/AgentSideEffects.java @@ -15,6 +15,10 @@ import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.LivingEntity; public class AgentSideEffects { private static final Logger LOGGER = LogManager.getLogger(); @@ -81,9 +85,10 @@ public static void onCommandListGenerated(PlayerEngineController mod, String com } // add quotes to build_structure so it gets proccessed as one arg: - String processedCommandWithPrefix = commandWithPrefix.replaceFirst( - "^(@build_structure)\\s+(?![\"'])(.+)$", - "$1 \"$2\""); + String processedCommandWithPrefix = commandWithPrefix; + // String processedCommandWithPrefix = commandWithPrefix.replaceFirst( + // "^(@build_structure)\\s+(?![\"'])(.+)$", + // "$1 \"$2\""); cmdExecutor.execute(processedCommandWithPrefix, () -> { if (mod.isStopping) { @@ -110,7 +115,7 @@ public static void onCommandListGenerated(PlayerEngineController mod, String com }); } - private static void broadcastChatToPlayer(MinecraftServer server, String message, ServerPlayer player) { + public static void broadcastChatToPlayer(MinecraftServer server, String message, ServerPlayer player) { player.displayClientMessage(Component.literal(message), false); } private static void broadcastErrorMsgToPlayer(MinecraftServer server, String message, ServerPlayer player) { @@ -125,4 +130,15 @@ public static void broadcastChatToAllPlayers(MinecraftServer server, String mess } } + public static void teleportOwnerTo(AgentConversationData data){ + Player owner = data.getMod().getOwner(); + LivingEntity entity = data.getEntity(); + + double x = entity.getX() + 0.5; + double y = entity.getY() + 0.5; + double z = entity.getZ() + 0.5; + + owner.teleportTo(x, y, z); + } + } \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/player2api/Player2APIService.java b/common/src/main/java/com/player2/playerengine/player2api/Player2APIService.java index 09d1dc8..51ec4af 100644 --- a/common/src/main/java/com/player2/playerengine/player2api/Player2APIService.java +++ b/common/src/main/java/com/player2/playerengine/player2api/Player2APIService.java @@ -18,6 +18,9 @@ import net.minecraft.server.level.ServerPlayer; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public class Player2APIService { private static final Logger LOGGER = LogManager.getLogger(); @@ -178,4 +181,62 @@ public void sendHeartbeat() { System.err.printf("Heartbeat Fail: %s", var2.getMessage()); } } + + + + + + /** + * Search for schematics given a query + * + * @return List of schematics matching the query + */ + public List searchSchematics(String query) { + try { + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("query", query); + requestBody.addProperty("max_results", 10); + + Map responseMap = Player2HTTPUtils.sendRequest(controller.getOwner(), clientId, "/v1/minecraft/schematics/search", true, requestBody); + + JsonElement resultsJsonElement = responseMap.get("results"); + if (resultsJsonElement != null && resultsJsonElement.isJsonArray()) { + JsonArray resultsJsonArray = resultsJsonElement.getAsJsonArray(); + + List schematics = new ArrayList<>(); + for (JsonElement voiceElement : resultsJsonArray) { + JsonObject voiceObject = voiceElement.getAsJsonObject(); + schematics.add(voiceObject); + } + return schematics; + } else { + System.err.println( + "No results field array in response with keys: [" + String.join(",", responseMap.keySet()) + "]"); + } + } catch (Exception e) { + System.err.println("Search schematics request failed: " + e.getMessage()); + } + return Collections.emptyList(); + } + + /** + * Get schematic binary given a schematic ID + * + * @return Schematic binary data + */ + public String getSchematicBinary(String schematicId) { + try { + Map responseMap = Player2HTTPUtils.sendRequest(controller.getOwner(), clientId, "/v1/minecraft/schematics/" + schematicId, false, null); + JsonElement dataJsonElement = responseMap.get("data"); + if (dataJsonElement != null && dataJsonElement.isJsonPrimitive()) { + return dataJsonElement.getAsString(); + } else { + System.err.println( + "No data field primitive in response with keys: [" + String.join(",", responseMap.keySet()) + "]"); + } + } catch (Exception e) { + System.err.println("Get schematic binary request failed: " + e.getMessage()); + } + return ""; + } } \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/player2api/Prompts.java b/common/src/main/java/com/player2/playerengine/player2api/Prompts.java index a57e4b2..a71e503 100644 --- a/common/src/main/java/com/player2/playerengine/player2api/Prompts.java +++ b/common/src/main/java/com/player2/playerengine/player2api/Prompts.java @@ -356,4 +356,116 @@ public static String getBuildStructurePrompt() { return buildStructurePrompt; } + + + + + private static final String selectSchematicPrompt = """ + Given the following schematics, select the ID of the schematic that most clearly matches the query and has the highest quality. + Your input is JSON, and you can use the name, description, and download count fields to determine the best match. + Download count can be used to guess that something is of higher quality. Use this when there are a lot of similar results and avoid results with very low downloads. + Your output MUST be one of the "id" fields, without quotes. Do not output quotes in the reply, it should ONLY contain alphanumeric and dash characters. + FEEL FREE to pick a DIFFERENT ID from the one's we have below, the ones below are just examples. + EXAMPLES: + INPUT: + { + "query": "A large mansion made out of wood.", + "options": [ + { + "name": "empire state building", + "description": "The empire state building in New York", + "download_count": 1000, + "id": "01913299-8e85-7a8b-8764-e496315e217b" + }, + { + "name": "brick apartment", + "description": "A median sized apartment building made of bricks", + "download_count": 400, + "id": "01913299-c266-7e72-961c-b382af552cfb" + }, + { + "name": "ship", + "description": "A large wooden ship", + "download_count": 500, + "id": "01913299-d777-75ab-b69c-1acdba677003" + }, + { + "name": "cozy house", + "description": "A three story house with a spiral staircase, a big bed, and lots of goodies, fancier than the houses from buildSimpleHouse function but takes a lot longer to build", + "download_count": 500, + "id": "01913299-eb5f-7287-86c4-d0c1b3902e4b" + }, + { + "name": "fishing hut", + "description": "A fishing hut, a small wooden house with a dock, bot need to build it next to water block, find water block first then build", + "download_count": 500, + "id": "01913786-ef38-756b-b731-5fe0c60e0526" + } + ] + } + OUTPUT: + 01913299-eb5f-7287-86c4-d0c1b3902e4b + REASONING (NOT part of output, here so you understand why we picked this id): + You reply with the ID of the cozy house because it most closely matches a large house made out of wood. + INPUT: + { + "query": "A boat.", + "options": [ + { + "name": "small yacht", + "description": "A modern small yacht", + "id": "0191329a-a7e0-74df-9ab1-880217d10075" + }, + { + "name": "ship", + "description": "A large wooden ship", + "id": "01913299-d777-75ab-b69c-1acdba677003" + }, + ] + } + OUTPUT: + 0191329a-a7e0-74df-9ab1-880217d10075 + REASONING (NOT part of output, here so you understand why we picked this id): + The query was a boat. Both options are boats and equally fit the query, so just pick the one that is easier to make. A small yacht is probably easier to build than a large boat. + INPUT: + { + "query": "A house.", + "options": [ + { + "name": "house", + "description": "idk", + "download_count": 3, + "id": "48d4884d-0d04-4d34-9ab5-f3b13d6e8cfc" + }, + { + "name": "big house", + "description": "a big house with everything in it", + "download_count": 10, + "id": "52e292d4-8c63-4f05-b34d-f1f079ca2c88" + }, + { + "name": "house for me", + "description": "house", + "download_count": 2, + "id": "7f72b147-a464-4753-afcd-b71ef9e1f3db" + }, + { + "name": "ship", + "description": "A large wooden ship", + "download_count": 500, + "id": "01913299-d777-75ab-b69c-1acdba677003" + }, + ] + } + OUTPUT: + 52e292d4-8c63-4f05-b34d-f1f079ca2c88 + REASONING (NOT part of output, here so you understand why we picked this id): + The big house has the highest number of downloads AND the description is higher quality than the other houses. It also matches the query, unlike the ship which does not. + """; + + + public static String getSelectSchematicPrompt() { + return selectSchematicPrompt; + } + } \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/player2api/manager/ConversationManager.java b/common/src/main/java/com/player2/playerengine/player2api/manager/ConversationManager.java index 12915cf..a44d64f 100644 --- a/common/src/main/java/com/player2/playerengine/player2api/manager/ConversationManager.java +++ b/common/src/main/java/com/player2/playerengine/player2api/manager/ConversationManager.java @@ -26,7 +26,9 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; - +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; public class ConversationManager { @@ -120,6 +122,11 @@ public static void injectOnTick(MinecraftServer server) { if (!hasInit) { init(); } + queueData.forEach((k, v) -> { + if(v.getMod().getPlayer().level() == null || v.getMod().getPlayer().getServer() == null){ + despwnCompanion(k); + } + }); Consumer onCharacterEvent = (data) -> { AgentSideEffects.onEntityMessage(server, data); @@ -146,10 +153,32 @@ public static void resetMemory(PlayerEngineController mod) { } private static boolean isCloseToPlayer(AgentConversationData data, String userName) { + LOGGER.info("Passing msg btw {} <-> {}, owner {}", data.getName(), userName, data.getMod().getOwnerUsername()); + if(data.getMod().getOwnerUsername().equals(userName)){ + LOGGER.info("Passing b.c. is owner", data.getName(), userName); + return true; + } return StatusUtils.getDistanceToUsername(data.getMod(), userName) < messagePassingMaxDistance; } - public void despwnCompanion(UUID id) { + public static void despwnCompanion(UUID id) { queueData.remove(id); } + + public static void syncQueueData(Collection validUuids) { + queueData.keySet().retainAll(validUuids); + } + + public static Collection getDataByOwner(UUID ownerId) { + if (ownerId == null) return Collections.emptyList(); + return queueData.values().stream() + .filter(data -> { + if (data == null || data.getMod() == null) return false; + Player owner = data.getMod().getOwner(); + if(owner == null) return false; + LOGGER.info("getDataByOwner: ownerId={}, test={}", ownerId, owner.getUUID()); + return owner != null && ownerId.equals(owner.getUUID()); + }) + .collect(Collectors.toList()); + } } \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/tasks/construction/build_structure/BuildStructureTask.java b/common/src/main/java/com/player2/playerengine/tasks/construction/build_structure/BuildStructureTask.java index 1dbf0ce..9a9bb6c 100644 --- a/common/src/main/java/com/player2/playerengine/tasks/construction/build_structure/BuildStructureTask.java +++ b/common/src/main/java/com/player2/playerengine/tasks/construction/build_structure/BuildStructureTask.java @@ -1,5 +1,12 @@ package com.player2.playerengine.tasks.construction.build_structure; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -7,25 +14,45 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import com.mojang.datafixers.util.Either; +import net.sandrohc.schematic4j.SchematicLoader; +import net.sandrohc.schematic4j.exception.ParsingException; +import net.sandrohc.schematic4j.schematic.Schematic; +import net.sandrohc.schematic4j.schematic.types.SchematicBlock; +import com.google.gson.JsonObject; +import com.mojang.datafixers.util.Either; import com.player2.playerengine.PlayerEngineController; import com.player2.playerengine.player2api.ConversationHistory; import com.player2.playerengine.player2api.LLMCompleter; import com.player2.playerengine.player2api.Player2APIService; import com.player2.playerengine.player2api.Prompts; import com.player2.playerengine.tasks.base.Task; +import com.player2.playerengine.util.time.TimerGame; +import java.util.ArrayDeque; +import java.util.Deque; + import net.minecraft.core.BlockPos; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.StringRepresentable; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.IntegerProperty; +import net.minecraft.world.level.block.state.properties.Property; +import com.player2.playerengine.tasks.construction.build_structure.StructureFromCode.SetBlockCommand; public class BuildStructureTask extends Task { private static final int maxNumErrors = 2; private static Logger LOGGER = LogManager.getLogger(); private boolean isDone = false; + + private BlockPos buildPosition; + private String schematicQuery; private String description; + private PlayerEngineController mod; private Player2APIService service; private int numErrors; @@ -33,6 +60,211 @@ public class BuildStructureTask extends Task { private ConversationHistory history; private LLMCompleter completer; + private class BuildSchematicFromDescriptionTask extends Task { + + private Schematic schematic = null; + private boolean finished = false; + + private BlockPos origin; + + private TimerGame blockPlaceTimer = new TimerGame(0.1f); + private int blockPlaceProgress; + + public boolean hasSchematic() { + return schematic != null; + } + + @Override + protected void onStart() { + finished = false; + schematic = null; + blockPlaceProgress = 0; + origin = buildPosition;// mod.getPlayer().blockPosition(); + + String query = schematicQuery; + + LOGGER.info("Searching Schematic: {}", query); + + List schematics = service.searchSchematics(query); + + LOGGER.info("Got {} results", schematics.size()); + + if (schematics.size() == 0) { + // no schematics: Nothing. + finished = true; + return; + } + + + ConversationHistory selectPromptHistory = new ConversationHistory(Prompts.getSelectSchematicPrompt()); + StringBuilder result = new StringBuilder("{"); + result.append(String.join(",\n", schematics.stream().map(s -> s.toString()).toList())); + result.append("\n}"); + + selectPromptHistory.addUserMessage( + result.toString(), + service); + + LOGGER.info("Querying LLM to pick best schematic..."); + + completer.processToString(service, selectPromptHistory, schematicID -> { + + LOGGER.info("LLM Picked best schematic id: {}", schematicID); + + String b64String = service.getSchematicBinary(schematicID); + + LOGGER.debug("Got schematic b64: {}", b64String); + + if (b64String == null || b64String.length() == 0) { + finished = true; + return; + } + + ByteArrayInputStream input = new ByteArrayInputStream(Base64.getDecoder().decode(b64String)); + + // Load and store the schematic + try { + schematic = SchematicLoader.load(input); + LOGGER.info("Loaded schematic successfully! {}x{}x{}", schematic.width(), schematic.height(), schematic.length()); + } catch (ParsingException | IOException e) { + // Fail + e.printStackTrace(); + finished = true; + return; + } + + }, errStr -> { + LOGGER.info("LLM Transport Error={}", errStr); + finished = true; + }, false); + } + + private static & StringRepresentable & Comparable> BlockState setEnumProp(EnumProperty enumProp, BlockState blockState, String value) { + // Find the desired value + for (var possible : enumProp.getPossibleValues()) { + T possibleEnum = (T) possible; + if (possibleEnum.name().toLowerCase().equals(value)) { + return blockState.setValue(enumProp, possibleEnum); + } + } + // We did not find a valid enum! + LOGGER.warn("Unable to find enum prop that matched the value \"{}\" to any of following enums: [{}]", value, String.join(", ",enumProp.getPossibleValues().stream().map(x -> "\"" + x.toString() + "\"").toList())); + return blockState; + } + + @Override + protected Task onTick() { + if (schematic == null) { + // Wait for schematic to load + return null; + } + + if (!blockPlaceTimer.elapsed()) { + // Wait for block place delay + return null; + } + + boolean foundInvalidBlock = false; + while (!foundInvalidBlock) { + // one at a time, don't repeat since some blocks may not place correctly and we don't want to get stuck + int xs = schematic.width(); + int zs = schematic.length(); + int ys = schematic.height(); + int xx = blockPlaceProgress % xs; + int zz = (blockPlaceProgress / xs) % zs; + int yy = (blockPlaceProgress) / (zs * xs); + // We placed the last block + if (blockPlaceProgress > xs*zs*ys) { + finished = true; + return null; + } + + SchematicBlock desiredSchematicState = schematic.block(xx, yy, zz); + String blockName = desiredSchematicState.block(); + while (blockName.startsWith("minecraft:")) { + blockName = blockName.substring("minecraft:".length()); + } + ResourceLocation desiredSchematicId = new ResourceLocation("minecraft", blockName); + Block desiredSchematicBlock = BuiltInRegistries.BLOCK.get(desiredSchematicId); + + BlockPos worldPos = origin.offset(xx - xs / 2 , yy, zz - zs / 2); + BlockState currentState = mod.getWorld().getBlockState(worldPos); + + BlockState desiredState = desiredSchematicBlock.defaultBlockState(); + // Apply properties + Map> props = new HashMap<>(); + for (Property p : desiredState.getProperties()) { + props.put(p.getName().toLowerCase(), p); + } + for(Entry propEntry : desiredSchematicState.states().entrySet()) { + String propKey = propEntry.getKey().toLowerCase(); + String propValue = propEntry.getValue(); + Property prop = props.get(propKey); + if (prop == null) { + LOGGER.warn("Failed to find prop with name {} for block {}. Ignoring this prop.", propKey, desiredSchematicBlock.getName()); + continue; + } + if (prop instanceof BooleanProperty) { + desiredState = desiredState.setValue((BooleanProperty)prop, Boolean.parseBoolean(propValue)); + continue; + } + if (prop instanceof IntegerProperty) { + desiredState = desiredState.setValue((IntegerProperty) prop, Integer.parseInt(propValue)); + continue; + } + if (prop instanceof EnumProperty) { + EnumProperty enumProp = (EnumProperty) prop; + desiredState = setEnumProp(enumProp, desiredState, propValue); + continue; + } + LOGGER.warn("Did not account for this type of property: {}, {} when setting {}={}. Add support for this! Ignoring this state for now.", prop.getClass().getSimpleName(), prop.toString(), propKey, propValue); + } + + if (!currentState.getBlock().getName().equals(desiredSchematicBlock.getName())) { + LOGGER.info("ASDF REPLACING BLOCK({}): {} -> {} ({})", worldPos, currentState.getBlock().getName(), desiredState.toString()); + mod.getWorld().setBlock(worldPos, + desiredState, 3); + + // block place delay + // Hit a target of maximum 3 min build + // Speed up otherwise + float targetMaxBuildTime = 3f * 60f; + int blocksToPlace = xs * ys * zs; + float blockPlaceDelay = Math.min(blocksToPlace > 0 ? (targetMaxBuildTime / blocksToPlace) : 0.3f, 0.3f); + blockPlaceTimer.setInterval(blockPlaceDelay); + blockPlaceTimer.reset(); + foundInvalidBlock = true; + } else { + LOGGER.info("ASDF gucci {}", worldPos); + } + + blockPlaceProgress++; + } + + // next frame + return null; + } + + @Override + protected void onStop(Task var1) { + } + + @Override + protected boolean isEqual(Task var1) { + return var1 instanceof BuildSchematicFromDescriptionTask; + } + + @Override + protected String toDebugString() { + return String.format("Building structure at (%s) from schematic search: (%s)", buildPosition.toShortString(), description); + } + + @Override + public boolean isFinished() { + return finished; + } + } + private class RequestLLMCode extends Task { // outer option: isDone, either: (left=code (success), right=errStr) Optional> llmResult = Optional.empty(); @@ -66,7 +298,7 @@ protected Task onTick() { @Override protected String toDebugString() { - return String.format("Thinking about how to build structure with description (%s)", description); + return String.format("Thinking about how to build structure at (%s) with description (%s)", buildPosition.toShortString(), description); } @Override @@ -77,6 +309,8 @@ public boolean isFinished() { private class BuildFromCode extends Task { String code; + private TimerGame blockPlaceTimer = new TimerGame(0.1f); + Deque setBlockQueue = new ArrayDeque<>(); private ExecutorService buildThread; // outer Option: is done, inner option: is error @@ -85,17 +319,15 @@ private class BuildFromCode extends Task { public BuildFromCode(String code) { this.code = code; this.buildThread = Executors.newSingleThreadExecutor(); + synchronized (setBlockQueue) { + setBlockQueue.clear(); + } buildThread.submit(() -> { StructureFromCode.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); + // Queue up + synchronized (setBlockQueue) { + setBlockQueue.add(setBlockData); + } }, (errStr) -> { result = Optional.of(Optional.of(errStr)); }, () -> { @@ -120,10 +352,33 @@ protected void onStart() { protected void onStop(Task var1) { // TODO Auto-generated method stub buildThread.shutdownNow(); + synchronized (setBlockQueue) { + setBlockQueue.clear(); + } } @Override protected Task onTick() { + + synchronized (setBlockQueue) { + if (setBlockQueue.size() == 0) { + return null; + } + if (!blockPlaceTimer.elapsed()) { + return null; + } + SetBlockCommand setBlockData = setBlockQueue.poll(); + 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); + blockPlaceTimer.reset(); + } // TODO Auto-generated method stub return null; } @@ -135,25 +390,27 @@ public boolean isFinished() { @Override protected String toDebugString() { - return String.format("Currently building the structure from description (%s)", description); + return String.format("Currently building the structure at (%s) from description (%s)", buildPosition.toShortString(), description); } } - public BuildStructureTask(String description, PlayerEngineController mod) { + public BuildStructureTask(BlockPos position, String schematicQuery, String description, PlayerEngineController mod) { + this.buildPosition = position; + this.schematicQuery = schematicQuery; this.description = description; this.mod = mod; this.service = mod.getPlayer2APIService(); this.numErrors = 0; this.history = new ConversationHistory(Prompts.getBuildStructurePrompt()); history.addUserMessage( - String.format("Build with the following description: (%s)", description), + String.format("Build with the following description: (%s). Build at position (%s)", description, buildPosition.toShortString()), service); this.completer = new LLMCompleter(); } @Override protected void onStart() { - actuallyRunningTask = new RequestLLMCode(); + actuallyRunningTask = new BuildSchematicFromDescriptionTask(); } @Override @@ -169,8 +426,20 @@ protected Task onTick() { } // ---------- now task is finished, switch to next task: ------- + if (actuallyRunningTask instanceof BuildSchematicFromDescriptionTask) { + BuildSchematicFromDescriptionTask buildSchematicTask = (BuildSchematicFromDescriptionTask) actuallyRunningTask; + if (buildSchematicTask.hasSchematic()) { + // We finished building and are done + isDone = true; + actuallyRunningTask = null; + } else { + // We STOPPED building, move on to request LLM code + actuallyRunningTask = new RequestLLMCode(); + } + return actuallyRunningTask; + } if (actuallyRunningTask instanceof RequestLLMCode) { - LOGGER.info("Requesting llm code for description={}", description); + LOGGER.info("Requesting llm code for pos={} description={}", buildPosition.toShortString(), description); Either result = ((RequestLLMCode) actuallyRunningTask).llmResult.get(); // set actually running task to next task: result.mapBoth( @@ -232,6 +501,6 @@ protected void onStop(Task next) { @Override protected String toDebugString() { - return "BuildingStructure(" + description + ")"; + return "BuildingStructure(pos=" + buildPosition.toShortString() + ", description=" + description + ")"; } } \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/tasks/misc/EatFoodTask.java b/common/src/main/java/com/player2/playerengine/tasks/misc/EatFoodTask.java new file mode 100644 index 0000000..96267a3 --- /dev/null +++ b/common/src/main/java/com/player2/playerengine/tasks/misc/EatFoodTask.java @@ -0,0 +1,54 @@ +package com.player2.playerengine.tasks.misc; + +import com.player2.playerengine.tasks.base.Task; +import java.util.Arrays; +import net.minecraft.world.item.Item; + +public class EatFoodTask extends Task { + Item foodItem; + boolean hasAte; + public EatFoodTask(Item foodItem) { + this.foodItem = foodItem; + this.hasAte = false; + } + + @Override + protected void onStart() { + + } + + @Override + protected Task onTick() { + if(this.controller.getBaritone().getEntityContext().hungerManager().getFoodLevel() >= 20){ + this.fail("Hunger already at 20 (full), cannot eat."); + hasAte = true; + return null; + } + if (this.controller.getSlotHandler().forceEquipItem(foodItem)) { + this.controller.getBaritone().getEntityContext().hungerManager().eat(foodItem); + this.controller.getInventory().removeItem(this.controller.getInventory().selectedSlot, 1); + hasAte = true; + } + return null; + } + + @Override + protected void onStop(Task interruptTask) { + + } + + @Override + public boolean isFinished() { + return hasAte; + } + + @Override + protected boolean isEqual(Task other) { + return other instanceof EatFoodTask task ? true : false; + } + + @Override + protected String toDebugString() { + return "Eating food: " + foodItem.toString(); + } +} \ No newline at end of file diff --git a/common/src/main/java/com/player2/playerengine/trackers/EntityTracker.java b/common/src/main/java/com/player2/playerengine/trackers/EntityTracker.java index 141bf83..90613a2 100644 --- a/common/src/main/java/com/player2/playerengine/trackers/EntityTracker.java +++ b/common/src/main/java/com/player2/playerengine/trackers/EntityTracker.java @@ -13,6 +13,7 @@ import com.player2.playerengine.util.helpers.WorldHelper; import java.util.ArrayList; import java.util.Collections; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -95,12 +96,38 @@ public Optional getClosestItemDrop(Vec3 position, Predicate getClosestItemDropWithoutItemTarget(Vec3 position, Predicate acceptPredicate) { + Collection items = new ArrayList<>(this.itemDropLocations.keySet()); + + ItemEntity closestEntity = null; + float minCost = Float.POSITIVE_INFINITY; + + for (Item item : items) { + if (this.itemDropped(item)) { + Collection entities = this.itemDropLocations.get(item); + if (entities == null) continue; + for (ItemEntity entity : entities) { + if (!this.entityBlacklist.unreachable(entity) + && entity.getItem().getItem().equals(item) + && acceptPredicate.test(entity)) { + float cost = (float) BaritoneHelper.calculateGenericHeuristic(position, entity.position()); + if (cost < minCost) { + minCost = cost; + closestEntity = entity; + } + } + } + } + } + + return Optional.ofNullable(closestEntity); + } public Optional getClosestItemDrop(Vec3 position, Predicate acceptPredicate, ItemTarget... targets) { this.ensureUpdated(); if (targets.length == 0) { - Debug.logError("You asked for the drop position of zero items... Most likely a typo."); - return Optional.empty(); + //Debug.logError("You asked for the drop position of zero items... Most likely a typo."); + return this.getClosestItemDropWithoutItemTarget(position, acceptPredicate); } else if (!this.itemDropped(targets)) { return Optional.empty(); } else { diff --git a/common/src/main/java/com/player2/playerengine/util/helpers/ItemHelper.java b/common/src/main/java/com/player2/playerengine/util/helpers/ItemHelper.java index 534561f..dd53ed8 100644 --- a/common/src/main/java/com/player2/playerengine/util/helpers/ItemHelper.java +++ b/common/src/main/java/com/player2/playerengine/util/helpers/ItemHelper.java @@ -777,8 +777,11 @@ void p(WoodType type, String prefix, Item planks, Item log, Item strippedLog, It this.put(Items.POTATO, Items.BAKED_POTATO); } }; + public static final Item[] RAW_FOODS = cookableFoodMap.keySet().toArray(Item[]::new); public static final Item[] COOKED_FOODS = cookableFoodMap.values().toArray(Item[]::new); + public static final Item[] OTHER_FOODS = new Item[] { Items.BEETROOT, Items.BEETROOT_SOUP, Items.CARROT, Items.ENCHANTED_GOLDEN_APPLE, Items.BREAD, Items.APPLE, Items.COOKIE, Items.DRIED_KELP, Items.GOLDEN_APPLE, Items.GOLDEN_CARROT, Items.MELON_SLICE, Items.MUSHROOM_STEW, Items.PUMPKIN_PIE, Items.RABBIT_STEW }; + public static final Item[] FOODS = Stream.of(RAW_FOODS, COOKED_FOODS, OTHER_FOODS).flatMap(Arrays::stream).toArray(Item[]::new); private static Map fuelTimeMap = null; public static String stripItemName(Item item) { @@ -1055,4 +1058,15 @@ public boolean isNetherWood() { return this.planks == Items.CRIMSON_PLANKS || this.planks == Items.WARPED_PLANKS; } } + + public static Optional getItemFromString(String query, Item[] itemsToPickFrom) { + for (Item item : itemsToPickFrom) { + if (stripItemName(item).equalsIgnoreCase(query)) { + return Optional.of(item); + } + } + return Optional.empty(); + } + + } diff --git a/fabric/build.gradle b/fabric/build.gradle index 31d58a0..7533ae3 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -35,6 +35,9 @@ dependencies { common(project(path: ':common', configuration: 'namedElements')) { transitive = false } shadowBundle project(path: ':common', configuration: 'transformProductionFabric') + + implementation 'net.sandrohc:schematic4j:1.1.0' + include 'net.sandrohc:schematic4j:1.1.0' } processResources { diff --git a/forge/build.gradle b/forge/build.gradle index 89fef1a..fab86fa 100644 --- a/forge/build.gradle +++ b/forge/build.gradle @@ -36,6 +36,9 @@ dependencies { // Architectury API. This is optional, and you can comment it out if you don't need it. modImplementation "dev.architectury:architectury-forge:$rootProject.architectury_api_version" + implementation 'net.sandrohc:schematic4j:1.1.0' + include 'net.sandrohc:schematic4j:1.1.0' + common(project(path: ':common', configuration: 'namedElements')) { transitive = false } shadowBundle project(path: ':common', configuration: 'transformProductionForge') }