From 0dfbca183f95a17b52564199eef73824df817152 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Tue, 28 Jan 2025 01:18:12 +0100 Subject: [PATCH] Structure commands --- build.gradle | 3 + docs/docs/changelog.md | 6 + .../resources/assets/guideme/lang/en_us.json | 1 + .../guideme/internal/GuideMEClientProxy.java | 3 + .../java/guideme/internal/GuidebookText.java | 3 +- .../internal/command/GuideClientCommand.java | 16 +- .../command/GuidebookStructureCommands.java | 161 +++++++++++++----- 7 files changed, 146 insertions(+), 47 deletions(-) diff --git a/build.gradle b/build.gradle index 7eb1aa1..7fa732a 100644 --- a/build.gradle +++ b/build.gradle @@ -14,6 +14,8 @@ plugins { apply plugin: ProjectDefaultsPlugin apply plugin: FlatBuffersPlugin +evaluationDependsOn ":markdown" + group = "org.appliedenergistics" base { archivesName = "guideme" @@ -291,6 +293,7 @@ tasks.register('apiJar', Jar) { // api jar ist just a development aid and serves as both a binary and source jar simultaneously from sourceSets.main.output from sourceSets.main.allJava + from project(":markdown").sourceSets.main.output include "**/*.class" } apiJar publicApiIncludePatterns diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 97c43ae..3897981 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -3,6 +3,12 @@ ## 2.4.0 +- Add missing Markdown node classes to API jar +- Add structure editing commands that only work in singleplayer: + - `/guidemec placeallstructures x y z` will place all structures found in all guidebooks. + - `/guidemec placeallstructures x y z ` will place all structures found in a given guidebook. + - `/guidemc importstructure ` opens a system file open dialog and places the selected structure file at the given origin. + - `/guidemc exportstructure ` opens a system file save dialog and exports the given bounds as a structure file at the chosen location. - Fixes a resource reload crash when a page references a non-existing item as its navigation icon - Added op command `/guideme give ` to quickly give a guide item to an entity target (i.e. `@s`) - Fix guidebook navbar closing when clicking links diff --git a/src/generated/resources/assets/guideme/lang/en_us.json b/src/generated/resources/assets/guideme/lang/en_us.json index 1c26bbe..79181b6 100644 --- a/src/generated/resources/assets/guideme/lang/en_us.json +++ b/src/generated/resources/assets/guideme/lang/en_us.json @@ -1,5 +1,6 @@ { "guideme.guidebook.Close": "Close", + "guideme.guidebook.CommandOnlyWorksInSinglePlayer": "This command only works in single-player.", "guideme.guidebook.ContentFrom": "Content from", "guideme.guidebook.HideAnnotations": "Hide Annotations", "guideme.guidebook.HistoryGoBack": "Go back one page", diff --git a/src/main/java/guideme/internal/GuideMEClientProxy.java b/src/main/java/guideme/internal/GuideMEClientProxy.java index cd59142..380fadd 100644 --- a/src/main/java/guideme/internal/GuideMEClientProxy.java +++ b/src/main/java/guideme/internal/GuideMEClientProxy.java @@ -61,6 +61,9 @@ public boolean openGuide(Player player, ResourceLocation id, PageAnchor anchor) player.sendSystemMessage(GuidebookText.ItemInvalidGuideId.text(id.toString())); return false; } else { + if (anchor == null) { + return GuideMEClient.openGuideAtPreviousPage(guide, guide.getStartPage()); + } return GuideMEClient.openGuideAtAnchor(guide, anchor); } } diff --git a/src/main/java/guideme/internal/GuidebookText.java b/src/main/java/guideme/internal/GuidebookText.java index edfa4a4..3d3a661 100644 --- a/src/main/java/guideme/internal/GuidebookText.java +++ b/src/main/java/guideme/internal/GuidebookText.java @@ -17,7 +17,8 @@ public enum GuidebookText implements LocalizationEnum { SearchNoResults("No Results"), ContentFrom("Content from"), ItemNoGuideId("No guide id set"), - ItemInvalidGuideId("Invalid guide id set: %s"); + ItemInvalidGuideId("Invalid guide id set: %s"), + CommandOnlyWorksInSinglePlayer("This command only works in single-player."); private final String englishText; diff --git a/src/main/java/guideme/internal/command/GuideClientCommand.java b/src/main/java/guideme/internal/command/GuideClientCommand.java index 7fa77dd..916f3e3 100644 --- a/src/main/java/guideme/internal/command/GuideClientCommand.java +++ b/src/main/java/guideme/internal/command/GuideClientCommand.java @@ -3,6 +3,7 @@ import com.mojang.brigadier.CommandDispatcher; import guideme.Guides; import guideme.internal.GuideMEClient; +import guideme.internal.GuidebookText; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; @@ -11,7 +12,11 @@ private GuideClientCommand() { } public static void register(CommandDispatcher dispatcher) { - dispatcher.register(Commands.literal("guidemec").then( + var rootCommand = Commands.literal("guidemec"); + + GuidebookStructureCommands.register(rootCommand); + + rootCommand.then( Commands.argument("guide", GuideIdArgument.argument()) .then(Commands.literal("export") .executes(context -> { @@ -24,6 +29,11 @@ public static void register(CommandDispatcher dispatcher) { .executes(context -> { var guideId = GuideIdArgument.getGuide(context, "guide"); var guide = Guides.getById(guideId); + if (guide == null) { + context.getSource() + .sendFailure(GuidebookText.ItemInvalidGuideId.text(guideId.toString())); + return 1; + } GuideMEClient.openGuideAtPreviousPage(guide, guide.getStartPage()); return 0; @@ -38,6 +48,8 @@ public static void register(CommandDispatcher dispatcher) { return 0; })) - ))); + )); + + dispatcher.register(rootCommand); } } diff --git a/src/main/java/guideme/internal/command/GuidebookStructureCommands.java b/src/main/java/guideme/internal/command/GuidebookStructureCommands.java index 798a5e5..17ecef8 100644 --- a/src/main/java/guideme/internal/command/GuidebookStructureCommands.java +++ b/src/main/java/guideme/internal/command/GuidebookStructureCommands.java @@ -2,22 +2,24 @@ import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; -import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import guideme.internal.GuideRegistry; +import guideme.internal.GuidebookText; import guideme.internal.MutableGuide; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.PauseScreen; import net.minecraft.commands.CommandSourceStack; @@ -39,6 +41,7 @@ import net.neoforged.api.distmarker.Dist; import net.neoforged.api.distmarker.OnlyIn; import org.apache.commons.lang3.mutable.MutableObject; +import org.apache.commons.lang3.tuple.Pair; import org.jetbrains.annotations.Nullable; import org.lwjgl.PointerBuffer; import org.lwjgl.system.MemoryStack; @@ -52,10 +55,13 @@ * {@link appeng.server.testplots.GuidebookPlot}. */ @OnlyIn(Dist.CLIENT) -public class GuidebookStructureCommands { +final class GuidebookStructureCommands { private static final Logger LOG = LoggerFactory.getLogger(GuidebookStructureCommands.class); + private GuidebookStructureCommands() { + } + @Nullable private static String lastOpenedOrSavedPath; @@ -63,53 +69,85 @@ public class GuidebookStructureCommands { private static final String FILE_PATTERN_DESC = "Structure NBT Files (*.snbt, *.nbt)"; - private final String commandName; - - public GuidebookStructureCommands(String commandName) { - this.commandName = commandName; - } - - public void register(CommandDispatcher dispatcher) { - LiteralArgumentBuilder rootCommand = literal(this.commandName); - + public static void register(LiteralArgumentBuilder rootCommand) { registerPlaceAllStructures(rootCommand); registerImportCommand(rootCommand); registerExportCommand(rootCommand); + } - dispatcher.register(rootCommand); + @Nullable + private static ServerLevel getIntegratedServerLevel(CommandContext context) { + var minecraft = Minecraft.getInstance(); + if (!minecraft.hasSingleplayerServer()) { + context.getSource().sendFailure(GuidebookText.CommandOnlyWorksInSinglePlayer.text()); + return null; + } + return minecraft.getSingleplayerServer().getLevel( + Minecraft.getInstance().player.level().dimension()); } - private void registerPlaceAllStructures(LiteralArgumentBuilder rootCommand) { + private static void registerPlaceAllStructures(LiteralArgumentBuilder rootCommand) { LiteralArgumentBuilder subcommand = literal("placeallstructures"); // Only usable on singleplayer worlds and only by the local player (in case it is opened to LAN) - subcommand.requires(source -> Minecraft.getInstance().hasSingleplayerServer()); + subcommand = subcommand.requires(c -> c.hasPermission(2)); + + subcommand.then(Commands.argument("origin", BlockPosArgument.blockPos()) + .executes(context -> { + var level = getIntegratedServerLevel(context); + if (level == null) { + return 1; + } + + var origin = BlockPosArgument.getBlockPos(context, "origin"); + placeAllStructures(level, origin); + return 0; + })); + subcommand .then(Commands.argument("origin", BlockPosArgument.blockPos()) - .executes(context -> { - var origin = BlockPosArgument.getBlockPos(context, "origin"); - placeAllStructures(context.getSource().getLevel(), origin); - return 0; - })); + .then(Commands.argument("guide", GuideIdArgument.argument()) + .executes(context -> { + var level = getIntegratedServerLevel(context); + if (level == null) { + return 1; + } + + var guideId = GuideIdArgument.getGuide(context, "guide"); + var guide = GuideRegistry.getById(guideId); + if (guide == null) { + return 1; + } + + var origin = BlockPosArgument.getBlockPos(context, "origin"); + placeAllStructures(level, new MutableObject<>(origin), guide); + return 0; + }))); + rootCommand.then(subcommand); } - private void registerImportCommand(LiteralArgumentBuilder rootCommand) { + private static void registerImportCommand(LiteralArgumentBuilder rootCommand) { LiteralArgumentBuilder importSubcommand = literal("importstructure"); // Only usable on singleplayer worlds and only by the local player (in case it is opened to LAN) - importSubcommand.requires(source -> Minecraft.getInstance().hasSingleplayerServer()); importSubcommand + .requires(c -> c.hasPermission(2)) .then(Commands.argument("origin", BlockPosArgument.blockPos()) .executes(context -> { + var level = getIntegratedServerLevel(context); + if (level == null) { + return 1; + } + var origin = BlockPosArgument.getBlockPos(context, "origin"); - importStructure(context.getSource().getLevel(), origin); + importStructure(getIntegratedServerLevel(context), origin); return 0; })); rootCommand.then(importSubcommand); } - private void placeAllStructures(ServerLevel level, BlockPos origin) { + private static void placeAllStructures(ServerLevel level, BlockPos origin) { var currentPos = new MutableObject<>(origin); @@ -119,12 +157,7 @@ private void placeAllStructures(ServerLevel level, BlockPos origin) { } - private void placeAllStructures(ServerLevel level, MutableObject origin, MutableGuide guide) { - var sourceFolder = guide.getDevelopmentSourceFolder(); - if (sourceFolder == null) { - return; - } - + private static void placeAllStructures(ServerLevel level, MutableObject origin, MutableGuide guide) { var minecraft = Minecraft.getInstance(); var server = minecraft.getSingleplayerServer(); var player = minecraft.player; @@ -132,22 +165,57 @@ private void placeAllStructures(ServerLevel level, MutableObject origi return; } - List snbtFiles; - try (var s = Files.walk(sourceFolder) - .filter(p -> Files.isRegularFile(p) && p.getFileName().toString().endsWith(".snbt"))) { - snbtFiles = s.toList(); - } catch (IOException e) { - LOG.error("Failed to find all structures.", e); - player.sendSystemMessage(Component.literal(e.toString())); - return; + var sourceFolder = guide.getDevelopmentSourceFolder(); + + List>> structures = new ArrayList<>(); + if (sourceFolder == null) { + var resourceManager = Minecraft.getInstance().getResourceManager(); + var resources = resourceManager.listResources( + guide.getContentRootFolder(), + location -> location.getPath().endsWith(".snbt")); + for (var entry : resources.entrySet()) { + structures.add(Pair.of(entry.getKey().toString(), () -> { + try (var in = entry.getValue().open()) { + return new String(in.readAllBytes()); + } catch (IOException e) { + LOG.error("Failed to read structure {}", entry.getKey(), e); + return null; + } + })); + } + } else { + try (var s = Files.walk(sourceFolder) + .filter(p -> Files.isRegularFile(p) && p.getFileName().toString().endsWith(".snbt"))) { + s.forEach(path -> { + structures.add(Pair.of( + path.toString(), + () -> { + try { + return Files.readString(path); + } catch (IOException e) { + LOG.error("Failed to read structure {}", path, e); + return null; + } + })); + }); + } catch (IOException e) { + LOG.error("Failed to find all structures.", e); + player.sendSystemMessage(Component.literal(e.toString())); + return; + } } - for (var snbtFile : snbtFiles) { + for (var pair : structures) { + var snbtFile = pair.getLeft(); + var contentSupplier = pair.getRight(); LOG.info("Placing {}", snbtFile); try { var manager = level.getServer().getStructureManager(); CompoundTag compound; - var textInFile = Files.readString(snbtFile, StandardCharsets.UTF_8); + var textInFile = contentSupplier.get(); + if (textInFile == null) { + continue; + } compound = NbtUtils.snbtToStructure(textInFile); var structure = manager.readStructure(compound); @@ -170,7 +238,7 @@ private void placeAllStructures(ServerLevel level, MutableObject origi } } - private void importStructure(ServerLevel level, BlockPos origin) { + private static void importStructure(ServerLevel level, BlockPos origin) { var minecraft = Minecraft.getInstance(); var server = minecraft.getSingleplayerServer(); var player = minecraft.player; @@ -206,7 +274,7 @@ private void importStructure(ServerLevel level, BlockPos origin) { }, minecraft); } - private boolean placeStructure(ServerLevel level, + private static boolean placeStructure(ServerLevel level, BlockPos origin, String structurePath) throws CommandSyntaxException, IOException { var manager = level.getServer().getStructureManager(); @@ -232,19 +300,24 @@ private boolean placeStructure(ServerLevel level, private static void registerExportCommand(LiteralArgumentBuilder rootCommand) { LiteralArgumentBuilder exportSubcommand = literal("exportstructure"); // Only usable on singleplayer worlds and only by the local player (in case it is opened to LAN) - exportSubcommand.requires(source -> Minecraft.getInstance().hasSingleplayerServer()); exportSubcommand + .requires(c -> c.hasPermission(2)) .then(Commands.argument("origin", BlockPosArgument.blockPos()) .then(Commands.argument("sizeX", IntegerArgumentType.integer(1)) .then(Commands.argument("sizeY", IntegerArgumentType.integer(1)) .then(Commands.argument("sizeZ", IntegerArgumentType.integer(1)) .executes(context -> { + var level = getIntegratedServerLevel(context); + if (level == null) { + return 1; + } + var origin = BlockPosArgument.getBlockPos(context, "origin"); var sizeX = IntegerArgumentType.getInteger(context, "sizeX"); var sizeY = IntegerArgumentType.getInteger(context, "sizeY"); var sizeZ = IntegerArgumentType.getInteger(context, "sizeZ"); var size = new Vec3i(sizeX, sizeY, sizeZ); - exportStructure(context.getSource().getLevel(), origin, size); + exportStructure(level, origin, size); return 0; }))))); rootCommand.then(exportSubcommand);