diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandStructure.java b/core/src/main/java/art/arcane/iris/core/commands/CommandStructure.java
index 42acea60f..17e0f4c8e 100644
--- a/core/src/main/java/art/arcane/iris/core/commands/CommandStructure.java
+++ b/core/src/main/java/art/arcane/iris/core/commands/CommandStructure.java
@@ -19,6 +19,7 @@
package art.arcane.iris.core.commands;
import art.arcane.iris.core.loader.IrisData;
+import art.arcane.iris.core.structure.BulkStructureImporter;
import art.arcane.iris.core.structure.StructureImporter;
import art.arcane.iris.core.structure.StructureIndexService;
import art.arcane.iris.core.structure.VillageImporter;
@@ -134,6 +135,44 @@ public class CommandStructure implements DirectorExecutor {
}
}
+ @Director(description = "Import EVERY vanilla structure into a pack: jigsaw structures rebuilt as pool graphs, single-template structures imported as objects. Idempotent in add-only mode.", aliases = {"import-all", "ia"}, origin = DirectorOrigin.BOTH)
+ public void importAllVanilla(
+ @Param(description = "The dimension whose pack to import into", aliases = "dim")
+ IrisDimension dimension,
+ @Param(description = "overwrite | add-only | merge", defaultValue = "add-only")
+ String mode,
+ @Param(description = "Also import non-jigsaw single-template structures", name = "include-non-jigsaw", aliases = {"single", "nbt"}, defaultValue = "true")
+ boolean includeNonJigsaw
+ ) {
+ IrisData data = dimension.getLoader();
+ if (data == null) {
+ sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
+ return;
+ }
+ BulkStructureImporter.Report report = BulkStructureImporter.importAllVanilla(data, StructureImporter.parseMode(mode), includeNonJigsaw, sender());
+ if (report.imported() > 0) {
+ sender().sendMessage(C.GRAY + "Reference imported structures from a biome/region/dimension structures list, or run /iris structure list " + dimension.getLoadKey() + " to see the refreshed index.");
+ }
+ }
+
+ @Director(description = "Import EVERY vanilla structure TEMPLATE (the piece NBTs under minecraft:.../...) as editable Iris objects, including the non-jigsaw templates that import-all cannot reach. Idempotent in add-only mode.", aliases = {"import-templates", "it"}, origin = DirectorOrigin.BOTH)
+ public void importTemplates(
+ @Param(description = "The dimension whose pack to import into", aliases = "dim")
+ IrisDimension dimension,
+ @Param(description = "overwrite | add-only | merge", defaultValue = "add-only")
+ String mode
+ ) {
+ IrisData data = dimension.getLoader();
+ if (data == null) {
+ sender().sendMessage(C.RED + "Could not resolve the pack for dimension " + dimension.getLoadKey());
+ return;
+ }
+ BulkStructureImporter.Report report = BulkStructureImporter.importAllTemplates(data, StructureImporter.parseMode(mode), sender());
+ if (report.imported() > 0) {
+ sender().sendMessage(C.GRAY + "Reference imported templates from a biome/region/dimension structures list, or run /iris structure list " + dimension.getLoadKey() + " to see the refreshed index.");
+ }
+ }
+
@Director(description = "Resolve an iris structure's jigsaw graph and report piece count & bounds", origin = DirectorOrigin.BOTH)
public void info(
@Param(description = "The dimension whose pack holds the structure", aliases = "dim")
diff --git a/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java
new file mode 100644
index 000000000..d2b3583e4
--- /dev/null
+++ b/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java
@@ -0,0 +1,319 @@
+/*
+ * Iris is a World Generator for Minecraft Bukkit Servers
+ * Copyright (c) 2022 Arcane Arts (Volmit Software)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package art.arcane.iris.core.structure;
+
+import art.arcane.iris.core.loader.IrisData;
+import art.arcane.iris.core.nms.INMS;
+import art.arcane.iris.util.common.format.C;
+import art.arcane.iris.util.common.plugin.VolmitSender;
+import art.arcane.volmlib.util.collection.KList;
+import org.bukkit.Bukkit;
+import org.bukkit.NamespacedKey;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+import java.util.function.Predicate;
+
+public final class BulkStructureImporter {
+ public record Report(int total, int imported, int skipped, int failed) {
+ }
+
+ private BulkStructureImporter() {
+ }
+
+ public static Report importAllVanilla(IrisData data, StructureImporter.Mode mode, boolean includeNonJigsaw, VolmitSender sender) {
+ KList keys = INMS.get().getStructureKeys();
+ List vanilla = new ArrayList<>();
+ for (String k : keys) {
+ if (k != null && k.startsWith("minecraft:")) {
+ vanilla.add(k);
+ }
+ }
+ Collections.sort(vanilla);
+
+ int total = vanilla.size();
+ int imported = 0;
+ int skipped = 0;
+ int failed = 0;
+
+ sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " vanilla structures (mode=" + mode + ", includeNonJigsaw=" + includeNonJigsaw + ")...");
+
+ for (String keyString : vanilla) {
+ NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase());
+ if (nk == null) {
+ failed++;
+ sender.sendMessage(C.RED + "[fail] " + keyString + ": invalid key");
+ continue;
+ }
+ String name = StructureImporter.deriveName(nk);
+
+ try {
+ VillageImporter.Result jigsaw = VillageImporter.importVillage(data, nk, name, mode);
+ if (jigsaw.success()) {
+ imported++;
+ sender.sendMessage(C.GRAY + "[jigsaw] " + keyString + " -> " + name);
+ continue;
+ }
+
+ String message = jigsaw.message() == null ? "" : jigsaw.message();
+ if (message.contains("is not a jigsaw structure")) {
+ if (!includeNonJigsaw) {
+ skipped++;
+ continue;
+ }
+ StructureImporter.Result single = StructureImporter.importStructure(data, nk, name, mode);
+ if (single.success()) {
+ imported++;
+ sender.sendMessage(C.GRAY + "[single] " + keyString + " -> " + name);
+ } else if (single.message() != null && single.message().startsWith("Skipped")) {
+ skipped++;
+ } else {
+ failed++;
+ sender.sendMessage(C.RED + "[fail] " + keyString + ": " + single.message());
+ }
+ continue;
+ }
+
+ if (message.startsWith("Skipped")) {
+ skipped++;
+ continue;
+ }
+
+ failed++;
+ sender.sendMessage(C.RED + "[fail] " + keyString + ": " + message);
+ } catch (Throwable e) {
+ failed++;
+ sender.sendMessage(C.RED + "[fail] " + keyString + ": " + e.getMessage());
+ }
+ }
+
+ StructureIndexService.write(data);
+
+ sender.sendMessage(C.GREEN + "Bulk import complete: " + C.WHITE + imported + C.GREEN + " imported, " + C.WHITE + skipped + C.GREEN + " skipped, " + C.WHITE + failed + C.GREEN + " failed (" + C.WHITE + total + C.GREEN + " total).");
+ return new Report(total, imported, skipped, failed);
+ }
+
+ public static Report importAllTemplates(IrisData data, StructureImporter.Mode mode, VolmitSender sender) {
+ List templateKeys;
+ try {
+ templateKeys = enumerateTemplateKeys();
+ } catch (Throwable e) {
+ sender.sendMessage(C.RED + "Failed to enumerate vanilla structure templates via the server ResourceManager: " + e);
+ return new Report(0, 0, 0, 0);
+ }
+
+ List vanilla = new ArrayList<>();
+ for (String key : templateKeys) {
+ if (key != null && key.startsWith("minecraft:")) {
+ vanilla.add(key);
+ }
+ }
+ Collections.sort(vanilla);
+
+ int total = vanilla.size();
+ int imported = 0;
+ int skipped = 0;
+ int failed = 0;
+
+ if (total == 0) {
+ sender.sendMessage(C.YELLOW + "No vanilla structure templates were found under the 'structure' resource path.");
+ return new Report(0, 0, 0, 0);
+ }
+
+ sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " vanilla structure templates (mode=" + mode + ")...");
+
+ for (String keyString : vanilla) {
+ NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase());
+ if (nk == null) {
+ failed++;
+ sender.sendMessage(C.RED + "[fail] " + keyString + ": invalid key");
+ continue;
+ }
+ String name = templateNameFor(keyString);
+
+ try {
+ StructureImporter.Result result = StructureImporter.importStructure(data, nk, name, mode);
+ if (result.success()) {
+ imported++;
+ } else if (result.message() != null && result.message().startsWith("Skipped")) {
+ skipped++;
+ } else {
+ failed++;
+ sender.sendMessage(C.RED + "[fail] " + keyString + ": " + result.message());
+ }
+ } catch (Throwable e) {
+ failed++;
+ sender.sendMessage(C.RED + "[fail] " + keyString + ": " + e.getMessage());
+ }
+
+ int processed = imported + skipped + failed;
+ if (processed % 50 == 0) {
+ sender.sendMessage(C.GRAY + "..." + processed + "/" + total + " (" + imported + " imported, " + skipped + " skipped, " + failed + " failed)");
+ }
+ }
+
+ StructureIndexService.write(data);
+
+ sender.sendMessage(C.GREEN + "Template import complete: " + C.WHITE + imported + C.GREEN + " imported, " + C.WHITE + skipped + C.GREEN + " skipped, " + C.WHITE + failed + C.GREEN + " failed (" + C.WHITE + total + C.GREEN + " total).");
+ return new Report(total, imported, skipped, failed);
+ }
+
+ private static String templateNameFor(String key) {
+ int colon = key.indexOf(':');
+ String namespace = colon >= 0 ? key.substring(0, colon) : "minecraft";
+ String path = colon >= 0 ? key.substring(colon + 1) : key;
+ String safePath = path.toLowerCase().replaceAll("[^a-z0-9_/-]", "_");
+ while (safePath.contains("//")) {
+ safePath = safePath.replace("//", "/");
+ }
+ if (safePath.startsWith("/")) {
+ safePath = safePath.substring(1);
+ }
+ return "minecraft".equals(namespace) ? safePath : namespace + "/" + safePath;
+ }
+
+ private static List enumerateTemplateKeys() throws Exception {
+ Object craftServer = Bukkit.getServer();
+ Object dedicated = invoke(craftServer, "getHandle");
+ Object server = invoke(dedicated, "getServer");
+ Object resourceManager = resolveResourceManager(server);
+ if (resourceManager == null) {
+ throw new IllegalStateException("Could not resolve the server ResourceManager via reflection");
+ }
+
+ Method listResources = null;
+ for (Method m : resourceManager.getClass().getMethods()) {
+ if (m.getName().equals("listResources") && m.getParameterCount() == 2
+ && m.getParameterTypes()[0] == String.class
+ && m.getParameterTypes()[1] == Predicate.class
+ && Map.class.isAssignableFrom(m.getReturnType())) {
+ listResources = m;
+ break;
+ }
+ }
+ if (listResources == null) {
+ throw new NoSuchMethodException("listResources(String, Predicate) on " + resourceManager.getClass().getName());
+ }
+ listResources.setAccessible(true);
+
+ Predicate