Skip to content

Commit

Permalink
Structure commands
Browse files Browse the repository at this point in the history
  • Loading branch information
shartte committed Jan 28, 2025
1 parent f775e0d commit 0dfbca1
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 47 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ plugins {
apply plugin: ProjectDefaultsPlugin
apply plugin: FlatBuffersPlugin

evaluationDependsOn ":markdown"

group = "org.appliedenergistics"
base {
archivesName = "guideme"
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <guide>` will place all structures found in a given guidebook.
- `/guidemc importstructure <origin>` opens a system file open dialog and places the selected structure file at the given origin.
- `/guidemc exportstructure <origin> <size>` 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 <target> <guide>` to quickly give a guide item to an entity target (i.e. `@s`)
- Fix guidebook navbar closing when clicking links
Expand Down
1 change: 1 addition & 0 deletions src/generated/resources/assets/guideme/lang/en_us.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/guideme/internal/GuideMEClientProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/guideme/internal/GuidebookText.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
16 changes: 14 additions & 2 deletions src/main/java/guideme/internal/command/GuideClientCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -11,7 +12,11 @@ private GuideClientCommand() {
}

public static void register(CommandDispatcher<CommandSourceStack> 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 -> {
Expand All @@ -24,6 +29,11 @@ public static void register(CommandDispatcher<CommandSourceStack> 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;
Expand All @@ -38,6 +48,8 @@ public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
return 0;
}))

)));
));

dispatcher.register(rootCommand);
}
}
161 changes: 117 additions & 44 deletions src/main/java/guideme/internal/command/GuidebookStructureCommands.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -52,64 +55,99 @@
* {@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;

private static final String[] FILE_PATTERNS = { "*.snbt", "*.nbt" };

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<CommandSourceStack> dispatcher) {
LiteralArgumentBuilder<CommandSourceStack> rootCommand = literal(this.commandName);

public static void register(LiteralArgumentBuilder<CommandSourceStack> rootCommand) {
registerPlaceAllStructures(rootCommand);

registerImportCommand(rootCommand);

registerExportCommand(rootCommand);
}

dispatcher.register(rootCommand);
@Nullable
private static ServerLevel getIntegratedServerLevel(CommandContext<CommandSourceStack> 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<CommandSourceStack> rootCommand) {
private static void registerPlaceAllStructures(LiteralArgumentBuilder<CommandSourceStack> rootCommand) {
LiteralArgumentBuilder<CommandSourceStack> 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<CommandSourceStack> rootCommand) {
private static void registerImportCommand(LiteralArgumentBuilder<CommandSourceStack> rootCommand) {
LiteralArgumentBuilder<CommandSourceStack> 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);

Expand All @@ -119,35 +157,65 @@ private void placeAllStructures(ServerLevel level, BlockPos origin) {

}

private void placeAllStructures(ServerLevel level, MutableObject<BlockPos> origin, MutableGuide guide) {
var sourceFolder = guide.getDevelopmentSourceFolder();
if (sourceFolder == null) {
return;
}

private static void placeAllStructures(ServerLevel level, MutableObject<BlockPos> origin, MutableGuide guide) {
var minecraft = Minecraft.getInstance();
var server = minecraft.getSingleplayerServer();
var player = minecraft.player;
if (server == null || player == null) {
return;
}

List<Path> 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<Pair<String, Supplier<String>>> 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);
Expand All @@ -170,7 +238,7 @@ private void placeAllStructures(ServerLevel level, MutableObject<BlockPos> 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;
Expand Down Expand Up @@ -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();
Expand All @@ -232,19 +300,24 @@ private boolean placeStructure(ServerLevel level,
private static void registerExportCommand(LiteralArgumentBuilder<CommandSourceStack> rootCommand) {
LiteralArgumentBuilder<CommandSourceStack> 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);
Expand Down

0 comments on commit 0dfbca1

Please sign in to comment.