From 1a6fff35e8574f1f48afee710530fcc10717410a Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 31 May 2026 19:40:17 -0400 Subject: [PATCH] fff --- core/src/main/java/art/arcane/iris/Iris.java | 2 + .../art/arcane/iris/core/IrisSettings.java | 1 + .../arcane/iris/core/ServerConfigurator.java | 4 +- .../iris/core/commands/CommandDatapack.java | 78 +++ .../iris/core/commands/CommandIris.java | 12 + .../iris/core/commands/CommandStructure.java | 13 +- .../core/datapack/DatapackIngestService.java | 572 ++++++++++++++++++ .../iris/core/datapack/ModrinthResolver.java | 365 +++++++++++ .../core/lifecycle/BukkitPublicBackend.java | 5 - .../lifecycle/PaperLikeRuntimeBackend.java | 7 +- .../core/lifecycle/WorldLifecycleBackend.java | 2 - .../core/lifecycle/WorldLifecycleService.java | 10 +- .../core/lifecycle/WorldsProviderBackend.java | 5 - .../art/arcane/iris/core/loader/IrisData.java | 2 +- .../core/pregenerator/IrisPregenerator.java | 2 +- .../methods/AsyncPregenMethod.java | 16 - .../core/runtime/StudioOpenCoordinator.java | 6 +- .../core/structure/BulkStructureImporter.java | 6 +- .../core/structure/StructureImporter.java | 15 +- .../core/structure/StructureIndexService.java | 2 +- .../core/structure/StructureModeHandler.java | 53 ++ .../arcane/iris/core/tools/IrisCreator.java | 6 +- .../art/arcane/iris/engine/IrisEngine.java | 24 +- .../framework/IrisStructureLocator.java | 23 +- .../components/IrisStructureComponent.java | 7 - .../iris/engine/object/IrisDimension.java | 9 +- ...java => IrisImportedStructureControl.java} | 4 +- .../arcane/iris/engine/object/IrisRegion.java | 10 +- .../engine/object/IrisStructurePlacement.java | 35 +- .../object/StructurePlacementRoute.java | 30 - .../engine/platform/BukkitChunkGenerator.java | 18 +- .../core/nms/v1_21_R7/IrisChunkGenerator.java | 93 +-- .../iris/core/nms/v1_21_R7/NMSBinding.java | 2 +- .../core/nms/v26_1_R1/IrisChunkGenerator.java | 93 +-- .../iris/core/nms/v26_1_R1/NMSBinding.java | 2 +- 35 files changed, 1196 insertions(+), 338 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java create mode 100644 core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java create mode 100644 core/src/main/java/art/arcane/iris/core/datapack/ModrinthResolver.java create mode 100644 core/src/main/java/art/arcane/iris/core/structure/StructureModeHandler.java rename core/src/main/java/art/arcane/iris/engine/object/{IrisVanillaStructureControl.java => IrisImportedStructureControl.java} (81%) delete mode 100644 core/src/main/java/art/arcane/iris/engine/object/StructurePlacementRoute.java diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index 74fb92c7e..1f691ea2d 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -24,6 +24,7 @@ import com.google.gson.JsonParser; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.datapack.DatapackIngestService; import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.runtime.TransientWorldCleanupSupport; import art.arcane.iris.core.runtime.WorldRuntimeControlService; @@ -583,6 +584,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware { J.sr(this::tickQueue, 0); J.s(this::setupPapi); J.a(ServerConfigurator::configureIfDeferred, 20); + J.a(DatapackIngestService::autoIngestOnStartup, 60); autoStartStudio(); if (!J.isFolia()) { diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index a009a6c97..8813a11ea 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -198,6 +198,7 @@ public class IrisSettings { public boolean useConsoleCustomColors = true; public boolean useCustomColorsIngame = true; public boolean adjustVanillaHeight = false; + public boolean autoIngestDatapacks = true; public String forceMainWorld = ""; public int spinh = -20; public int spins = 7; diff --git a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java index 6ebf8229b..2b9a1ce58 100644 --- a/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/core/ServerConfigurator.java @@ -19,6 +19,7 @@ package art.arcane.iris.core; import art.arcane.iris.Iris; +import art.arcane.iris.core.datapack.DatapackIngestService; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.nms.datapack.DataVersion; @@ -119,7 +120,7 @@ public class ServerConfigurator { } } - private static KList getDatapacksFolder() { + public static KList getDatapacksFolder() { if (!IrisSettings.get().getGeneral().forceMainWorld.isEmpty()) { return new KList().qadd(new File(Bukkit.getWorldContainer(), IrisSettings.get().getGeneral().forceMainWorld + "/datapacks")); } @@ -158,6 +159,7 @@ public class ServerConfigurator { } DimensionHeight height = new DimensionHeight(fixer); KList folders = getDatapacksFolder(); + DatapackIngestService.reapplyFromStaging(folders); java.util.concurrent.ConcurrentMap> biomes = new java.util.concurrent.ConcurrentHashMap<>(); try (Stream stream = allPacks()) { diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java b/core/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java new file mode 100644 index 000000000..3579f5b16 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandDatapack.java @@ -0,0 +1,78 @@ +/* + * 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.commands; + +import art.arcane.iris.core.datapack.DatapackIngestService; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.common.director.DirectorExecutor; +import art.arcane.iris.util.common.director.DirectorHelp; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.volmlib.util.director.annotations.Director; +import art.arcane.volmlib.util.director.annotations.Param; + +import java.util.List; + +@Director(name = "datapack", aliases = {"datapacks", "dp"}, description = "Download & manage external datapack imports (Modrinth)") +public class CommandDatapack implements DirectorExecutor { + @Director(description = "Show help tree for this command group", aliases = {"?"}) + public void help() { + DirectorHelp.print(sender(), getClass()); + } + + @Director(description = "Download/update every datapack listed in a pack dimension's 'datapackImports' and install it into the world so its structures register like vanilla", aliases = {"pull", "update", "sync"}) + public void ingest( + @Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false") + boolean restart + ) { + VolmitSender sender = sender(); + sender.sendMessage(C.GRAY + "Starting datapack ingest..."); + J.a(() -> DatapackIngestService.ingestAll(sender, restart)); + } + + @Director(description = "List configured datapack imports and their installed versions", aliases = {"ls", "status"}) + public void list() { + VolmitSender sender = sender(); + KList configured = DatapackIngestService.collectConfiguredImports(); + List installed = DatapackIngestService.installed(); + + sender.sendMessage(C.GREEN + "Configured datapack imports: " + C.WHITE + configured.size()); + for (String url : configured) { + sender.sendMessage(C.GRAY + " - " + C.WHITE + url); + } + + sender.sendMessage(C.GREEN + "Installed datapacks: " + C.WHITE + installed.size()); + for (DatapackIngestService.Entry entry : installed) { + sender.sendMessage(C.GRAY + " - " + C.WHITE + entry.id + C.GRAY + " " + (entry.versionNumber == null ? "?" : entry.versionNumber)); + } + + if (configured.isEmpty()) { + sender.sendMessage(C.YELLOW + "Add Modrinth URLs to a dimension's 'datapackImports' list, then run /iris datapack ingest."); + } + } + + @Director(description = "Remove an installed datapack by id (also delete its URL from datapackImports to keep it gone)", aliases = {"rm", "delete"}) + public void remove( + @Param(description = "The datapack id (folder name) shown by /iris datapack list") + String id + ) { + DatapackIngestService.remove(sender(), id); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java b/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java index 3c5e691e2..b647f5450 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandIris.java @@ -21,6 +21,7 @@ package art.arcane.iris.core.commands; import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.IrisWorlds; +import art.arcane.iris.core.datapack.DatapackIngestService; import art.arcane.iris.core.lifecycle.WorldLifecycleService; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.service.StudioSVC; @@ -92,6 +93,16 @@ public class CommandIris implements DirectorExecutor { DirectorHelp.print(sender(), getClass()); } + @Director(description = "Download/update external datapack imports declared in pack dimensions (alias of /iris datapack ingest)", aliases = {"ingestpacks"}) + public void ingest( + @Param(description = "Restart the server when new datapacks are installed (required for new structures to register and generate)", defaultValue = "false") + boolean restart + ) { + VolmitSender ingestSender = sender(); + ingestSender.sendMessage(C.GRAY + "Starting datapack ingest..."); + J.a(() -> DatapackIngestService.ingestAll(ingestSender, restart)); + } + private CommandStudio studio; private CommandPregen pregen; private CommandSettings settings; @@ -102,6 +113,7 @@ public class CommandIris implements DirectorExecutor { private CommandDeveloper developer; private CommandPack pack; private CommandFind find; + private CommandDatapack datapack; public static boolean worldCreation = false; private static final AtomicReference mainWorld = new AtomicReference<>(); String WorldEngine; 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 709a8e907..d48206d5e 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 @@ -22,6 +22,7 @@ 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.StructureModeHandler; import art.arcane.iris.core.structure.VillageImporter; import art.arcane.iris.engine.framework.PlacedStructurePiece; import art.arcane.iris.engine.framework.StructureAssembler; @@ -82,7 +83,7 @@ public class CommandStructure implements DirectorExecutor { String key, @Param(description = "Name for the imported structure (defaults from the key)", defaultValue = "") String name, - @Param(description = "overwrite | add-only | merge", defaultValue = "overwrite") + @Param(description = "overwrite | add-only | merge", defaultValue = "overwrite", customHandler = StructureModeHandler.class) String mode ) { IrisData data = dimension.getLoader(); @@ -111,7 +112,7 @@ public class CommandStructure implements DirectorExecutor { String key, @Param(description = "Name for the imported structure (defaults from the key)", defaultValue = "") String name, - @Param(description = "overwrite | add-only | merge", defaultValue = "overwrite") + @Param(description = "overwrite | add-only | merge", defaultValue = "overwrite", customHandler = StructureModeHandler.class) String mode ) { IrisData data = dimension.getLoader(); @@ -133,11 +134,11 @@ 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) + @Director(description = "Import EVERY registered structure - vanilla AND ingested datapacks - 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") + @Param(description = "overwrite | add-only | merge", defaultValue = "add-only", customHandler = StructureModeHandler.class) String mode, @Param(description = "Also import non-jigsaw single-template structures", name = "include-non-jigsaw", aliases = {"single", "nbt"}, defaultValue = "true") boolean includeNonJigsaw @@ -157,7 +158,7 @@ public class CommandStructure implements DirectorExecutor { 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") + @Param(description = "overwrite | add-only | merge", defaultValue = "add-only", customHandler = StructureModeHandler.class) String mode ) { IrisData data = dimension.getLoader(); @@ -171,7 +172,7 @@ public class CommandStructure implements DirectorExecutor { } } - @Director(description = "Re-ingest EVERY vanilla structure and jigsaw template from scratch (overwrite), regenerating all .iob objects so they pick up the latest jigsaw/structure-block conversion. Use this after updating Iris if imported structures show raw markers.", aliases = {"reimport", "ri"}, origin = DirectorOrigin.BOTH) + @Director(description = "Re-ingest EVERY registered structure - vanilla AND ingested datapacks - plus every jigsaw template from scratch (overwrite), regenerating all .iob objects so they pick up the latest jigsaw/structure-block conversion. Use this after updating Iris or after ingesting datapacks if imported structures show raw markers.", aliases = {"reimport", "ri"}, origin = DirectorOrigin.BOTH) public void reingest( @Param(description = "The dimension whose pack to re-ingest", aliases = "dim") IrisDimension dimension diff --git a/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java new file mode 100644 index 000000000..ddefa0d4a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/datapack/DatapackIngestService.java @@ -0,0 +1,572 @@ +/* + * 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.datapack; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.ServerConfigurator; +import art.arcane.iris.core.datapack.ModrinthResolver.ResolvedDatapack; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.project.IrisProject; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.util.common.format.C; +import art.arcane.iris.util.common.plugin.VolmitSender; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.io.ZipUtils; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.bukkit.Bukkit; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.stream.Stream; + +public final class DatapackIngestService { + private static final String USER_AGENT = "VolmitSoftware/Iris (datapack-ingest)"; + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private DatapackIngestService() { + } + + public static Report ingestAll(VolmitSender sender, boolean restart) { + return ingest(sender, collectConfiguredImports(), restart); + } + + public static void autoIngestOnStartup() { + boolean restarting = false; + if (IrisSettings.get().getGeneral().autoIngestDatapacks) { + KList urls = collectConfiguredImports(); + if (!urls.isEmpty()) { + Iris.info("Auto-ingesting " + urls.size() + " external datapack import(s) from pack datapackImports..."); + Report report = ingest(null, urls, true); + restarting = report.changed(); + } + } + if (!restarting) { + refreshWorkspaces(); + } + } + + public static void refreshWorkspaces() { + try (Stream stream = ServerConfigurator.allPacks()) { + stream.forEach(DatapackIngestService::refreshWorkspace); + } + } + + public static void refreshWorkspace(IrisData data) { + if (data == null || !hasImports(data)) { + return; + } + try { + new IrisProject(data.getDataFolder()).updateWorkspace(); + } catch (Throwable e) { + Iris.reportError(e); + } + } + + public static Report ingest(VolmitSender sender, KList urls, boolean restart) { + Report report = new Report(); + if (urls == null || urls.isEmpty()) { + message(sender, C.YELLOW + "No datapackImports configured in any loaded pack. Add Modrinth URLs to a dimension's 'datapackImports' list, then run /iris datapack ingest."); + return report; + } + + File root = Iris.instance.getDataFolder("datapacks"); + File cacheDir = new File(root, "cache"); + File stagingDir = new File(root, "staging"); + cacheDir.mkdirs(); + stagingDir.mkdirs(); + + KList worldFolders = ServerConfigurator.getDatapacksFolder(); + String mcVersion = serverMcVersion(); + Manifest manifest = readManifest(root); + + message(sender, C.GRAY + "Ingesting " + C.WHITE + urls.size() + C.GRAY + " datapack import(s)" + (mcVersion == null ? "" : " for MC " + mcVersion) + "..."); + + for (String url : urls) { + try { + ingestSingle(sender, url, mcVersion, cacheDir, stagingDir, worldFolders, manifest, report); + } catch (Exception e) { + report.failed.add(url + " - " + e.getMessage()); + message(sender, C.RED + " Failed: " + C.WHITE + url + C.RED + " - " + e.getMessage()); + Iris.reportError(e); + } + } + + writeManifest(root, manifest); + message(sender, C.GREEN + "Datapack ingest complete: " + C.WHITE + report.updated.size() + C.GREEN + " updated, " + C.WHITE + report.upToDate.size() + C.GREEN + " up to date, " + C.WHITE + report.failed.size() + C.GREEN + " failed."); + + if (report.changed()) { + message(sender, C.YELLOW + "New datapack structures were installed. A server restart is required for them to register and generate."); + message(sender, C.GRAY + "Disable a vanilla structure via the dimension 'importedStructures.disabled' list to let an imported replacement take over, or place any imported key through a 'structures' placement."); + message(sender, C.GRAY + "To edit or manually place them as Iris resources, after the restart run /iris structure importAllVanilla - it imports vanilla and datapack structures together as editable jigsaw pools, pieces & objects."); + if (restart) { + ServerConfigurator.restart(); + } else { + message(sender, C.GRAY + "Run with restart=true to restart now, or restart manually. After restart, run /iris structure list to see the new keys."); + } + } + + return report; + } + + public static void reapplyFromStaging(KList worldFolders) { + File stagingDir = Iris.instance.getDataFolderNoCreate("datapacks", "staging"); + if (stagingDir == null || !stagingDir.isDirectory()) { + return; + } + File[] staged = stagingDir.listFiles(File::isDirectory); + if (staged == null || staged.length == 0) { + return; + } + for (File stagedDir : staged) { + if (!new File(stagedDir, "pack.mcmeta").isFile()) { + continue; + } + try { + install(stagedDir, worldFolders, stagedDir.getName(), false); + } catch (IOException e) { + Iris.reportError(e); + } + } + } + + public static boolean remove(VolmitSender sender, String id) { + String cleaned = sanitizeId(id); + File root = Iris.instance.getDataFolder("datapacks"); + Manifest manifest = readManifest(root); + boolean removed = false; + + File stagedDir = new File(new File(root, "staging"), cleaned); + if (stagedDir.isDirectory()) { + IO.delete(stagedDir); + removed = true; + } + for (File worldFolder : ServerConfigurator.getDatapacksFolder()) { + File target = new File(worldFolder, cleaned); + if (target.isDirectory()) { + IO.delete(target); + removed = true; + } + } + if (manifest.removeById(cleaned)) { + removed = true; + } + writeManifest(root, manifest); + + if (removed) { + message(sender, C.GREEN + "Removed datapack '" + C.WHITE + cleaned + C.GREEN + "'. Restart for it to stop generating, and delete its URL from the pack's datapackImports to keep it gone."); + } else { + message(sender, C.YELLOW + "No installed datapack named '" + cleaned + "'. Run /iris datapack list to see installed ids."); + } + return removed; + } + + public static KList collectConfiguredImports() { + LinkedHashSet urls = new LinkedHashSet<>(); + try (Stream stream = ServerConfigurator.allPacks()) { + stream.forEach(data -> collectImports(data, urls)); + } + KList result = new KList<>(); + result.addAll(urls); + return result; + } + + public static List installed() { + return readManifest(Iris.instance.getDataFolder("datapacks")).entries; + } + + private static void ingestSingle(VolmitSender sender, String url, String mcVersion, File cacheDir, File stagingDir, KList worldFolders, Manifest manifest, Report report) throws IOException { + ResolvedDatapack resolved = ModrinthResolver.resolve(url, mcVersion); + String id = deriveId(resolved); + File stagedDir = new File(stagingDir, id); + Entry existing = manifest.find(url); + boolean sameVersion = existing != null + && Objects.equals(existing.versionId, resolved.getVersionId()) + && (resolved.getSha1() == null || Objects.equals(existing.sha1, resolved.getSha1())) + && stagedDir.isDirectory() + && new File(stagedDir, "pack.mcmeta").isFile(); + + if (sameVersion) { + install(stagedDir, worldFolders, id, false); + report.upToDate.add(id + " (" + safe(resolved.getVersionNumber()) + ")"); + message(sender, C.GRAY + " Up to date: " + C.WHITE + id + C.GRAY + " " + safe(resolved.getVersionNumber())); + return; + } + + message(sender, C.GRAY + " Downloading " + C.WHITE + id + C.GRAY + " " + safe(resolved.getVersionNumber()) + "..."); + File zip = new File(cacheDir, id + "-" + safeFile(resolved.getVersionId()) + ".zip"); + download(resolved.getDownloadUrl(), zip); + + String checksum = sha1(zip); + if (resolved.getSha1() != null && !resolved.getSha1().isBlank() && !resolved.getSha1().equalsIgnoreCase(checksum)) { + IO.delete(zip); + throw new IOException("Checksum mismatch for " + id + " (expected " + resolved.getSha1() + ", got " + checksum + ")"); + } + + IO.delete(stagedDir); + stagedDir.mkdirs(); + ZipUtils.unzipFile(zip, stagedDir); + flattenIfWrapped(stagedDir); + if (!new File(stagedDir, "pack.mcmeta").isFile()) { + IO.delete(stagedDir); + throw new IOException(id + " is not a valid datapack (missing pack.mcmeta)"); + } + + install(stagedDir, worldFolders, id, true); + + Entry entry = existing != null ? existing : new Entry(); + entry.url = url; + entry.id = id; + entry.versionId = resolved.getVersionId(); + entry.versionNumber = resolved.getVersionNumber(); + entry.sha1 = checksum; + entry.filename = resolved.getFileName(); + entry.installedEpoch = System.currentTimeMillis(); + manifest.put(entry); + + report.updated.add(id + " (" + safe(resolved.getVersionNumber()) + ")"); + message(sender, C.GREEN + " Installed " + C.WHITE + id + C.GREEN + " " + safe(resolved.getVersionNumber())); + } + + private static void collectImports(IrisData data, LinkedHashSet urls) { + if (data == null || data.getDimensionLoader() == null) { + return; + } + for (IrisDimension dimension : data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys())) { + if (dimension == null) { + continue; + } + KList imports = dimension.getDatapackImports(); + if (imports == null) { + continue; + } + for (String url : imports) { + if (url != null && !url.isBlank()) { + urls.add(url.trim()); + } + } + } + } + + private static boolean hasImports(IrisData data) { + if (data.getDimensionLoader() == null) { + return false; + } + for (IrisDimension dimension : data.getDimensionLoader().loadAll(data.getDimensionLoader().getPossibleKeys())) { + if (dimension == null) { + continue; + } + KList imports = dimension.getDatapackImports(); + if (imports != null && !imports.isEmpty()) { + return true; + } + } + return false; + } + + private static void install(File stagedDir, KList worldFolders, String id, boolean force) throws IOException { + for (File worldFolder : worldFolders) { + File target = new File(worldFolder, id); + if (!force && target.isDirectory() && new File(target, "pack.mcmeta").isFile()) { + continue; + } + if (!worldFolder.exists()) { + worldFolder.mkdirs(); + } + IO.delete(target); + IO.copyDirectory(stagedDir.toPath(), target.toPath()); + } + } + + private static void flattenIfWrapped(File dir) throws IOException { + if (new File(dir, "pack.mcmeta").isFile()) { + return; + } + File[] children = dir.listFiles(); + if (children == null) { + return; + } + File singleDir = null; + int dirCount = 0; + int fileCount = 0; + for (File child : children) { + if (child.isDirectory()) { + dirCount++; + singleDir = child; + } else { + fileCount++; + } + } + if (dirCount != 1 || fileCount != 0 || singleDir == null || !new File(singleDir, "pack.mcmeta").isFile()) { + return; + } + File[] inner = singleDir.listFiles(); + if (inner != null) { + for (File item : inner) { + File moved = new File(dir, item.getName()); + if (item.renameTo(moved)) { + continue; + } + if (item.isDirectory()) { + IO.copyDirectory(item.toPath(), moved.toPath()); + } else { + Files.copy(item.toPath(), moved.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + } + IO.delete(singleDir); + } + + private static void download(String url, File dest) throws IOException { + String current = url; + for (int attempt = 0; attempt < 5; attempt++) { + URL target = URI.create(current).toURL(); + HttpURLConnection connection = (HttpURLConnection) target.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setConnectTimeout(20000); + connection.setReadTimeout(60000); + connection.setInstanceFollowRedirects(false); + + int code = connection.getResponseCode(); + if (code / 100 == 3) { + String location = connection.getHeaderField("Location"); + connection.disconnect(); + if (location == null || location.isBlank()) { + throw new IOException("Redirect without a location header from " + current); + } + current = location; + continue; + } + if (code != 200) { + connection.disconnect(); + throw new IOException("HTTP " + code + " downloading " + current); + } + + File parent = dest.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + File temp = new File(parent, dest.getName() + ".part"); + try (InputStream in = connection.getInputStream(); + OutputStream out = new FileOutputStream(temp)) { + byte[] buffer = new byte[8192]; + int length; + while ((length = in.read(buffer)) > 0) { + out.write(buffer, 0, length); + } + } finally { + connection.disconnect(); + } + Files.move(temp.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING); + return; + } + throw new IOException("Too many redirects downloading " + url); + } + + private static String sha1(File file) throws IOException { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + try (InputStream in = new FileInputStream(file)) { + byte[] buffer = new byte[8192]; + int length; + while ((length = in.read(buffer)) > 0) { + digest.update(buffer, 0, length); + } + } + byte[] hash = digest.digest(); + StringBuilder builder = new StringBuilder(hash.length * 2); + for (byte value : hash) { + builder.append(String.format("%02x", value)); + } + return builder.toString(); + } catch (NoSuchAlgorithmException e) { + throw new IOException("SHA-1 algorithm unavailable", e); + } + } + + private static String deriveId(ResolvedDatapack resolved) { + String base = resolved.getProjectSlug(); + if (base == null || base.isBlank()) { + base = resolved.getFileName(); + int dot = base.lastIndexOf('.'); + if (dot > 0) { + base = base.substring(0, dot); + } + } + return sanitizeId(base); + } + + private static String sanitizeId(String value) { + if (value == null) { + return "datapack"; + } + String lower = value.toLowerCase(Locale.ROOT).trim(); + StringBuilder builder = new StringBuilder(lower.length()); + for (int i = 0; i < lower.length(); i++) { + char c = lower.charAt(i); + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.') { + builder.append(c); + } else if (c == ' ' || c == '/' || c == '\\') { + builder.append('-'); + } + } + String cleaned = builder.toString().replaceAll("-+", "-"); + cleaned = cleaned.replaceAll("^[-_.]+", "").replaceAll("[-_.]+$", ""); + return cleaned.isBlank() ? "datapack" : cleaned; + } + + private static String serverMcVersion() { + String bukkit = Bukkit.getBukkitVersion(); + if (bukkit == null || bukkit.isBlank()) { + return null; + } + int dash = bukkit.indexOf('-'); + return dash > 0 ? bukkit.substring(0, dash) : bukkit; + } + + private static String safe(String value) { + return value == null || value.isBlank() ? "?" : value; + } + + private static String safeFile(String value) { + return value == null || value.isBlank() ? "unknown" : value.replaceAll("[^a-zA-Z0-9._-]", "_"); + } + + private static void message(VolmitSender sender, String text) { + if (sender != null) { + sender.sendMessage(text); + return; + } + Iris.info(text); + } + + private static Manifest readManifest(File root) { + File file = new File(root, "manifest.json"); + if (!file.isFile()) { + return new Manifest(); + } + try { + String json = Files.readString(file.toPath(), StandardCharsets.UTF_8); + Manifest manifest = GSON.fromJson(json, Manifest.class); + if (manifest == null) { + return new Manifest(); + } + if (manifest.entries == null) { + manifest.entries = new ArrayList<>(); + } + return manifest; + } catch (Exception e) { + Iris.reportError(e); + return new Manifest(); + } + } + + private static void writeManifest(File root, Manifest manifest) { + File file = new File(root, "manifest.json"); + try { + File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + Files.writeString(file.toPath(), GSON.toJson(manifest), StandardCharsets.UTF_8); + } catch (IOException e) { + Iris.reportError(e); + } + } + + public static final class Report { + private final KList updated = new KList<>(); + private final KList upToDate = new KList<>(); + private final KList failed = new KList<>(); + + public boolean changed() { + return !updated.isEmpty(); + } + + public KList getUpdated() { + return updated; + } + + public KList getUpToDate() { + return upToDate; + } + + public KList getFailed() { + return failed; + } + } + + public static final class Entry { + public String url; + public String id; + public String versionId; + public String versionNumber; + public String sha1; + public String filename; + public long installedEpoch; + } + + private static final class Manifest { + private List entries = new ArrayList<>(); + + private Entry find(String url) { + for (Entry entry : entries) { + if (entry.url != null && entry.url.equals(url)) { + return entry; + } + } + return null; + } + + private void put(Entry entry) { + for (int i = 0; i < entries.size(); i++) { + Entry current = entries.get(i); + if (current.url != null && current.url.equals(entry.url)) { + entries.set(i, entry); + return; + } + } + entries.add(entry); + } + + private boolean removeById(String id) { + return entries.removeIf(entry -> id.equals(entry.id)); + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/datapack/ModrinthResolver.java b/core/src/main/java/art/arcane/iris/core/datapack/ModrinthResolver.java new file mode 100644 index 000000000..7e5588a18 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/datapack/ModrinthResolver.java @@ -0,0 +1,365 @@ +/* + * 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.datapack; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public final class ModrinthResolver { + private static final String API = "https://api.modrinth.com/v2"; + private static final String USER_AGENT = "VolmitSoftware/Iris (datapack-ingest)"; + private static final String DATAPACK_LOADER = "datapack"; + + private ModrinthResolver() { + } + + public static ResolvedDatapack resolve(String rawUrl, String serverMcVersion) throws IOException { + String url = rawUrl == null ? "" : rawUrl.trim(); + if (url.isEmpty()) { + throw new IOException("Empty datapack url"); + } + + ModrinthRef ref = parse(url); + if (ref == null) { + return directResolve(url); + } + + JsonArray versions = getJsonArray(API + "/project/" + ref.slug + "/version"); + if (versions == null || versions.isEmpty()) { + throw new IOException("Modrinth project '" + ref.slug + "' returned no versions"); + } + + JsonObject version = ref.versionToken == null + ? selectLatestDatapackVersion(versions, serverMcVersion) + : selectVersionByToken(versions, ref.versionToken); + if (version == null) { + String detail = ref.versionToken == null ? "no datapack-loader version" : "no version matching '" + ref.versionToken + "'"; + throw new IOException("Modrinth project '" + ref.slug + "' has " + detail); + } + + JsonObject file = selectFile(version); + if (file == null) { + throw new IOException("Modrinth version for '" + ref.slug + "' has no downloadable file"); + } + + return toResolved(version, file, ref.slug); + } + + private static ModrinthRef parse(String url) { + if (!url.toLowerCase(Locale.ROOT).contains("modrinth.com/")) { + return null; + } + + String path = url.replaceFirst("^[a-zA-Z][a-zA-Z0-9+.-]*://", ""); + int query = path.indexOf('?'); + if (query >= 0) { + path = path.substring(0, query); + } + int fragment = path.indexOf('#'); + if (fragment >= 0) { + path = path.substring(0, fragment); + } + + List parts = new ArrayList<>(); + for (String segment : path.split("/")) { + if (!segment.isBlank()) { + parts.add(segment); + } + } + + int base = -1; + for (int i = 0; i < parts.size(); i++) { + if (parts.get(i).equalsIgnoreCase("modrinth.com")) { + base = i; + break; + } + } + if (base < 0 || parts.size() < base + 3) { + return null; + } + + String slug = parts.get(base + 2); + String token = null; + for (int i = base + 3; i + 1 < parts.size(); i++) { + if (parts.get(i).equalsIgnoreCase("version")) { + token = parts.get(i + 1); + break; + } + } + return new ModrinthRef(slug, token); + } + + private static JsonObject selectVersionByToken(JsonArray versions, String token) { + JsonObject datapackMatch = null; + JsonObject anyMatch = null; + String normalizedToken = normalizeVersion(token); + + for (JsonElement element : versions) { + JsonObject version = element.getAsJsonObject(); + String id = optString(version, "id"); + String number = optString(version, "version_number"); + boolean matches = token.equals(id) + || token.equalsIgnoreCase(number) + || normalizedToken.equals(normalizeVersion(number)); + if (!matches) { + continue; + } + if (anyMatch == null) { + anyMatch = version; + } + if (isDatapack(version)) { + datapackMatch = version; + break; + } + } + + return datapackMatch != null ? datapackMatch : anyMatch; + } + + private static JsonObject selectLatestDatapackVersion(JsonArray versions, String serverMcVersion) { + JsonObject firstDatapack = null; + for (JsonElement element : versions) { + JsonObject version = element.getAsJsonObject(); + if (!isDatapack(version)) { + continue; + } + if (firstDatapack == null) { + firstDatapack = version; + } + if (serverMcVersion != null && !serverMcVersion.isBlank() && gameVersionsContains(version, serverMcVersion)) { + return version; + } + } + return firstDatapack; + } + + private static JsonObject selectFile(JsonObject version) { + JsonArray files = version.getAsJsonArray("files"); + if (files == null || files.isEmpty()) { + return null; + } + + JsonObject primaryZip = null; + JsonObject anyZip = null; + JsonObject primary = null; + JsonObject first = null; + for (JsonElement element : files) { + JsonObject file = element.getAsJsonObject(); + if (first == null) { + first = file; + } + boolean isPrimary = file.has("primary") && file.get("primary").getAsBoolean(); + boolean isZip = optString(file, "filename").toLowerCase(Locale.ROOT).endsWith(".zip"); + if (isPrimary && primary == null) { + primary = file; + } + if (isZip && anyZip == null) { + anyZip = file; + } + if (isPrimary && isZip && primaryZip == null) { + primaryZip = file; + } + } + + if (primaryZip != null) { + return primaryZip; + } + if (anyZip != null) { + return anyZip; + } + if (primary != null) { + return primary; + } + return first; + } + + private static ResolvedDatapack toResolved(JsonObject version, JsonObject file, String slug) { + String downloadUrl = optString(file, "url"); + String filename = optString(file, "filename"); + String sha1 = null; + if (file.has("hashes") && file.get("hashes").isJsonObject()) { + sha1 = optString(file.getAsJsonObject("hashes"), "sha1"); + } + return new ResolvedDatapack(downloadUrl, filename, sha1, optString(version, "id"), optString(version, "version_number"), slug); + } + + private static ResolvedDatapack directResolve(String url) { + String filename = url; + int slash = filename.lastIndexOf('/'); + if (slash >= 0) { + filename = filename.substring(slash + 1); + } + int query = filename.indexOf('?'); + if (query >= 0) { + filename = filename.substring(0, query); + } + if (filename.isBlank()) { + filename = "datapack.zip"; + } + return new ResolvedDatapack(url, filename, null, "direct", "direct", null); + } + + private static boolean isDatapack(JsonObject version) { + JsonArray loaders = version.getAsJsonArray("loaders"); + if (loaders == null) { + return false; + } + for (JsonElement loader : loaders) { + if (DATAPACK_LOADER.equalsIgnoreCase(loader.getAsString())) { + return true; + } + } + return false; + } + + private static boolean gameVersionsContains(JsonObject version, String mc) { + JsonArray gameVersions = version.getAsJsonArray("game_versions"); + if (gameVersions == null) { + return false; + } + for (JsonElement gv : gameVersions) { + if (mc.equalsIgnoreCase(gv.getAsString())) { + return true; + } + } + return false; + } + + private static String normalizeVersion(String value) { + if (value == null) { + return ""; + } + String normalized = value.trim().toLowerCase(Locale.ROOT); + if (normalized.startsWith("v")) { + normalized = normalized.substring(1); + } + return normalized; + } + + private static String optString(JsonObject object, String key) { + if (object == null || !object.has(key) || object.get(key).isJsonNull()) { + return ""; + } + return object.get(key).getAsString(); + } + + private static JsonArray getJsonArray(String url) throws IOException { + String body = httpGet(url); + JsonElement parsed = JsonParser.parseString(body); + if (!parsed.isJsonArray()) { + throw new IOException("Unexpected response from " + url); + } + return parsed.getAsJsonArray(); + } + + private static String httpGet(String url) throws IOException { + URL target = URI.create(url).toURL(); + HttpURLConnection connection = (HttpURLConnection) target.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setRequestProperty("Accept", "application/json"); + connection.setConnectTimeout(15000); + connection.setReadTimeout(20000); + connection.setInstanceFollowRedirects(true); + + int code = connection.getResponseCode(); + if (code != 200) { + connection.disconnect(); + throw new IOException("HTTP " + code + " from " + url); + } + + StringBuilder builder = new StringBuilder(); + try (InputStream input = connection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + } finally { + connection.disconnect(); + } + return builder.toString(); + } + + private static final class ModrinthRef { + private final String slug; + private final String versionToken; + + private ModrinthRef(String slug, String versionToken) { + this.slug = slug; + this.versionToken = versionToken; + } + } + + public static final class ResolvedDatapack { + private final String downloadUrl; + private final String fileName; + private final String sha1; + private final String versionId; + private final String versionNumber; + private final String projectSlug; + + public ResolvedDatapack(String downloadUrl, String fileName, String sha1, String versionId, String versionNumber, String projectSlug) { + this.downloadUrl = downloadUrl; + this.fileName = fileName; + this.sha1 = sha1; + this.versionId = versionId; + this.versionNumber = versionNumber; + this.projectSlug = projectSlug; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public String getFileName() { + return fileName; + } + + public String getSha1() { + return sha1; + } + + public String getVersionId() { + return versionId; + } + + public String getVersionNumber() { + return versionNumber; + } + + public String getProjectSlug() { + return projectSlug; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java index 5f3c03aaf..1be1572f3 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/BukkitPublicBackend.java @@ -50,9 +50,4 @@ final class BukkitPublicBackend implements WorldLifecycleBackend { public String backendName() { return "bukkit_public"; } - - @Override - public String describeSelectionReason() { - return "public Bukkit world lifecycle path"; - } } diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java index 24cda371a..b7681f6e8 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/PaperLikeRuntimeBackend.java @@ -38,7 +38,7 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend { WorldLifecycleStaging.stageGenerator(request.worldName(), request.generator(), request.biomeProvider()); WorldLifecycleSupport.stageRuntimeConfiguration(request.worldName()); - Iris.info("WorldLifecycle runtime LevelStem: world=" + request.worldName() + Iris.debug("WorldLifecycle runtime LevelStem: world=" + request.worldName() + ", backend=paper_like_runtime, flavor=" + capabilities.paperLikeFlavor().name().toLowerCase(Locale.ROOT) + ", registrySource=" + WorldLifecycleSupport.runtimeLevelStemRegistrySource(request)); Object levelStem = WorldLifecycleSupport.resolveRuntimeLevelStem(capabilities, request); @@ -81,9 +81,4 @@ final class PaperLikeRuntimeBackend implements WorldLifecycleBackend { public String backendName() { return "paper_like_runtime"; } - - @Override - public String describeSelectionReason() { - return "server family " + capabilities.serverFamily().id() + " exposes paper-like runtime world lifecycle capabilities"; - } } diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleBackend.java index 109656193..67e103e3f 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleBackend.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleBackend.java @@ -12,6 +12,4 @@ public interface WorldLifecycleBackend { boolean unload(World world, boolean save); String backendName(); - - String describeSelectionReason(); } diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java index b5fc3cea9..b70edc1ae 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldLifecycleService.java @@ -60,11 +60,10 @@ public final class WorldLifecycleService { + "\", caller=" + request.callerKind().name().toLowerCase() + ".", e); return CompletableFuture.failedFuture(e); } - Iris.info("WorldLifecycle create: world=%s, caller=%s, backend=%s, reason=%s", + Iris.info("WorldLifecycle create: world=%s, caller=%s, backend=%s", request.worldName(), request.callerKind().name().toLowerCase(), - backend.backendName(), - backend.describeSelectionReason()); + backend.backendName()); return backend.create(request).whenComplete((world, throwable) -> { if (throwable != null) { Throwable cause = WorldLifecycleSupport.unwrap(throwable); @@ -106,10 +105,9 @@ public final class WorldLifecycleService { private boolean unloadDirect(World world, boolean save) { WorldLifecycleBackend backend = selectUnloadBackend(world.getName()); - Iris.info("WorldLifecycle unload: world=%s, backend=%s, reason=%s", + Iris.info("WorldLifecycle unload: world=%s, backend=%s", world.getName(), - backend.backendName(), - backend.describeSelectionReason()); + backend.backendName()); boolean unloaded; try { unloaded = backend.unload(world, save); diff --git a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java index aec57b696..602d1276a 100644 --- a/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java +++ b/core/src/main/java/art/arcane/iris/core/lifecycle/WorldsProviderBackend.java @@ -57,11 +57,6 @@ final class WorldsProviderBackend implements WorldLifecycleBackend { return "worlds_provider"; } - @Override - public String describeSelectionReason() { - return "external Worlds provider is registered and healthy"; - } - @SuppressWarnings({"rawtypes", "unchecked"}) private Object resolveLevelStem(World.Environment environment) { String key; diff --git a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java index d02f0998b..1856f176d 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/IrisData.java +++ b/core/src/main/java/art/arcane/iris/core/loader/IrisData.java @@ -518,6 +518,6 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory { } b.complete(); - Iris.info("Loaded Prefetch Cache to reduce generation disk use."); + Iris.debug("Loaded Prefetch Cache to reduce generation disk use."); } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java index 3ad852baf..341328fe2 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java @@ -73,7 +73,7 @@ public class IrisPregenerator { public IrisPregenerator(PregenTask task, PregeneratorMethod generator, PregenListener listener) { benchmarking = IrisPackBenchmarking.getInstance(); this.listener = listenify(listener); - cl = new ChronoLatch(5000); + cl = new ChronoLatch(10000); saveLatch = new ChronoLatch(IrisSettings.get().getPregen().getSaveIntervalMs()); generatedRegions = new KSet<>(); this.shutdown = new AtomicBoolean(false); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index 81ff4da55..eea9ba83d 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -84,7 +84,6 @@ public class AsyncPregenMethod implements PregeneratorMethod { private final AtomicLong completed = new AtomicLong(); private final AtomicLong failed = new AtomicLong(); private final AtomicLong lastProgressAt = new AtomicLong(M.ms()); - private final AtomicLong lastPermitWaitLog = new AtomicLong(0L); private final AtomicLong lastChunkCleanup = new AtomicLong(M.ms()); private final Object permitMonitor = new Object(); private volatile Engine metricsEngine; @@ -468,18 +467,6 @@ public class AsyncPregenMethod implements PregeneratorMethod { } } - private void logPermitWaitIfNeeded(int x, int z, long waitedMs) { - long now = M.ms(); - long last = lastPermitWaitLog.get(); - if (now - last < 5000L) { - return; - } - - if (lastPermitWaitLog.compareAndSet(last, now)) { - Iris.warn("Async pregen waiting for permit at chunk " + x + "," + z + " waitedMs=" + waitedMs + " " + metricsSnapshot()); - } - } - @Override public void init() { Iris.info("Async pregen init: world=" + world.getName() @@ -539,8 +526,6 @@ public class AsyncPregenMethod implements PregeneratorMethod { long waitStart = M.ms(); synchronized (permitMonitor) { while (inFlight.get() >= adaptiveInFlightLimit.get()) { - long waited = Math.max(0L, M.ms() - waitStart); - logPermitWaitIfNeeded(x, z, waited); permitMonitor.wait(500L); } } @@ -551,7 +536,6 @@ public class AsyncPregenMethod implements PregeneratorMethod { long permitWaitStart = M.ms(); while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) { - logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart)); } long permitWait = Math.max(0L, M.ms() - permitWaitStart); if (permitWait > 0L) { diff --git a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java index 2a63b91b6..7c276bcda 100644 --- a/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java +++ b/core/src/main/java/art/arcane/iris/core/runtime/StudioOpenCoordinator.java @@ -87,7 +87,7 @@ public final class StudioOpenCoordinator { try { long openStart = System.currentTimeMillis(); long t = openStart; - Iris.info("[Studio timing] ===== studio open START: " + request.worldName() + " ====="); + Iris.debug("[Studio timing] ===== studio open START: " + request.worldName() + " ====="); updateStage(request, "resolve_dimension", 0.04D); if (IrisToolbelt.getDimension(request.dimensionKey()) == null) { throw new IrisException("Dimension cannot be found for id " + request.dimensionKey() + "."); @@ -165,7 +165,7 @@ public final class StudioOpenCoordinator { } t = logStudioPhase("finalize + openVSCode", t, openStart); - Iris.info("[Studio timing] ===== server-side open PREP done = " + (System.currentTimeMillis() - openStart) + "ms (world " + world.getName() + ") — player-view terrain gen follows; the REAL wait is the [Studio timing] player-view lines below ====="); + Iris.info("Studio open: " + world.getName() + " ready in " + (System.currentTimeMillis() - openStart) + "ms"); future.complete(new StudioOpenResult(world, safeEntry)); } catch (Throwable e) { Iris.reportError("Studio open failed for world \"" + request.worldName() + "\".", e); @@ -183,7 +183,7 @@ public final class StudioOpenCoordinator { private long logStudioPhase(String phase, long t, long openStart) { long now = System.currentTimeMillis(); - Iris.info("[Studio timing] " + phase + " = " + (now - t) + "ms (cumulative " + (now - openStart) + "ms)"); + Iris.debug("[Studio timing] " + phase + " = " + (now - t) + "ms (cumulative " + (now - openStart) + "ms)"); return now; } 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 index 1e03bb364..0f6a9cf51 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java +++ b/core/src/main/java/art/arcane/iris/core/structure/BulkStructureImporter.java @@ -45,7 +45,7 @@ public final class BulkStructureImporter { KList keys = INMS.get().getStructureKeys(); List vanilla = new ArrayList<>(); for (String k : keys) { - if (k != null && k.startsWith("minecraft:")) { + if (k != null && !k.isBlank()) { vanilla.add(k); } } @@ -56,7 +56,7 @@ public final class BulkStructureImporter { int skipped = 0; int failed = 0; - sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " vanilla structures (mode=" + mode + ", includeNonJigsaw=" + includeNonJigsaw + ")..."); + sender.sendMessage(C.GREEN + "Importing " + C.WHITE + total + C.GREEN + " vanilla & datapack structures (mode=" + mode + ", includeNonJigsaw=" + includeNonJigsaw + ")..."); for (String keyString : vanilla) { NamespacedKey nk = NamespacedKey.fromString(keyString.toLowerCase()); @@ -191,7 +191,7 @@ public final class BulkStructureImporter { String name = templateNameFor(keyString); try { - StructureImporter.Result result = StructureImporter.importStructure(data, nk, name, mode); + StructureImporter.Result result = StructureImporter.importStructure(data, nk, name, mode, true); if (result.success()) { imported++; } else if (result.message() != null && result.message().startsWith("Skipped")) { diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java b/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java index d9a5cbcd4..3a8c58c10 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureImporter.java @@ -78,6 +78,10 @@ public final class StructureImporter { } public static Result importStructure(IrisData data, NamespacedKey key, String name, Mode mode) { + return importStructure(data, key, name, mode, false); + } + + public static Result importStructure(IrisData data, NamespacedKey key, String name, Mode mode, boolean objectOnly) { Structure structure; try { structure = Bukkit.getStructureManager().loadStructure(key); @@ -98,7 +102,7 @@ public final class StructureImporter { File poolFile = new File(data.getDataFolder(), "jigsaw-pools/" + name + ".json"); File structureFile = new File(data.getDataFolder(), "structures/" + name + ".json"); - boolean exists = objectFile.exists() || structureFile.exists(); + boolean exists = objectOnly ? objectFile.exists() : (objectFile.exists() || structureFile.exists()); if (mode == Mode.ADD_ONLY && exists) { return new Result(false, "Skipped (add-only): '" + name + "' already exists", 0); } @@ -145,8 +149,13 @@ public final class StructureImporter { objectFile.getParentFile().mkdirs(); object.write(objectFile); writeJson(pieceFile, pieceJson(name)); - writeJson(poolFile, poolJson(name)); - writeJson(structureFile, structureJson(name, key.toString(), Math.max(w, d))); + if (objectOnly) { + poolFile.delete(); + structureFile.delete(); + } else { + writeJson(poolFile, poolJson(name)); + writeJson(structureFile, structureJson(name, key.toString(), Math.max(w, d))); + } } catch (Throwable e) { return new Result(false, "Failed writing import for '" + name + "': " + e.getMessage(), count); } diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java b/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java index d0b8699bc..805ecf37d 100644 --- a/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureIndexService.java @@ -111,7 +111,7 @@ public final class StructureIndexService { counts.put("irisStructures", iris.size()); Map root = new LinkedHashMap<>(); - root.put("note", "Generated index of every structure available at runtime. Toggle vanilla/datapack generation via the dimension vanillaStructures block (by structure SET key). Place structures via biome/region/dimension structures lists."); + root.put("note", "Generated index of every structure available at runtime. Toggle vanilla/datapack generation via the dimension importedStructures block (by structure key). Place structures via biome/region/dimension structures lists."); root.put("counts", counts); root.put("structures", structuresNode); root.put("structureSets", setsNode); diff --git a/core/src/main/java/art/arcane/iris/core/structure/StructureModeHandler.java b/core/src/main/java/art/arcane/iris/core/structure/StructureModeHandler.java new file mode 100644 index 000000000..754c42ca7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/structure/StructureModeHandler.java @@ -0,0 +1,53 @@ +/* + * 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.volmlib.util.collection.KList; +import art.arcane.volmlib.util.director.DirectorParameterHandler; + +public class StructureModeHandler implements DirectorParameterHandler { + @Override + public KList getPossibilities() { + KList options = new KList<>(); + options.add("overwrite"); + options.add("add-only"); + options.add("merge"); + return options; + } + + @Override + public String toString(String value) { + return value; + } + + @Override + public String parse(String in, boolean force) { + return in == null || in.isBlank() ? "overwrite" : in.trim(); + } + + @Override + public boolean supports(Class type) { + return type.equals(String.class); + } + + @Override + public String getRandomDefault() { + return "overwrite"; + } +} diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index 9d3ed65ab..869b48600 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -182,7 +182,7 @@ public class IrisCreator { } if (studio()) { IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen()); - Iris.info("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT) + Iris.debug("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT) + ", regionizedRuntime=" + FoliaScheduler.isRegionizedRuntime(Bukkit.getServer())); } @@ -199,7 +199,7 @@ public class IrisCreator { IrisWorlds.get().put(name(), dimension()); } ServerConfigurator.installDataPacksIfChanged(!studio()); - Iris.info("[Studio timing] create.packPrep + datapacks = " + (System.currentTimeMillis() - createStart) + "ms (cumulative in create)"); + Iris.debug("[Studio timing] create.packPrep + datapacks = " + (System.currentTimeMillis() - createStart) + "ms (cumulative in create)"); reportStudioProgress(0.40D, "install_datapacks"); PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator(); @@ -216,7 +216,7 @@ public class IrisCreator { world = J.sfut(() -> INMS.get().createWorldAsync(wc, request)) .thenCompose(Function.identity()) .get(); - Iris.info("[Studio timing] create.createWorldAsync (NMS bukkit world load + spawn prep) = " + (System.currentTimeMillis() - nmsStart) + "ms"); + Iris.debug("[Studio timing] create.createWorldAsync (NMS bukkit world load + spawn prep) = " + (System.currentTimeMillis() - nmsStart) + "ms"); } catch (Throwable e) { done.set(true); cancelRepeatingTask(createProgressTask); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 0192355f9..1f50b2735 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -24,6 +24,7 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.events.IrisEngineHotloadEvent; +import art.arcane.iris.core.datapack.DatapackIngestService; import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.loader.ResourceLoader; @@ -132,7 +133,7 @@ public class IrisEngine implements Engine { generated = new AtomicInteger(0); long _t0 = M.ms(); mantle = new IrisEngineMantle(this); - Iris.info("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - _t0) + "ms"); + Iris.debug("[IrisEngine timing] new IrisEngineMantle=" + (M.ms() - _t0) + "ms"); context = new IrisContext(this); cleaning = new AtomicBoolean(false); modeFallbackLogged = new AtomicBoolean(false); @@ -141,25 +142,25 @@ public class IrisEngine implements Engine { getData().dump(); getData().clearLists(); getTarget().setDimension(getData().getDimensionLoader().load(getDimension().getLoadKey())); - Iris.info("[IrisEngine timing] dump+clearLists+reload=" + (M.ms() - _t0) + "ms"); + Iris.debug("[IrisEngine timing] dump+clearLists+reload=" + (M.ms() - _t0) + "ms"); } context.touch(); getData().setEngine(this); _t0 = M.ms(); getData().loadPrefetch(this); - Iris.info("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms"); + Iris.debug("[IrisEngine timing] loadPrefetch=" + (M.ms() - _t0) + "ms"); try { StructureIndexService.writeOnce(getData()); } catch (Throwable e) { Iris.reportError(e); } - Iris.info("Initializing Engine: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " (" + target.getDimension().getDimensionHeight() + " height) Seed: " + getSeedManager().getSeed()); + Iris.info("Engine init: " + target.getWorld().name() + "/" + target.getDimension().getLoadKey() + " seed=" + getSeedManager().getSeed()); failing = false; closed = false; art = J.ar(this::tickRandomPlayer, 0); _t0 = M.ms(); setupEngine(); - Iris.info("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms"); + Iris.debug("[IrisEngine timing] setupEngine total=" + (M.ms() - _t0) + "ms"); Iris.debug("Engine Initialized " + getCacheID()); } @@ -224,25 +225,25 @@ public class IrisEngine implements Engine { cacheId = RNG.r.nextInt(); long t0 = M.ms(); complex = ensureComplex(); - Iris.info("[IrisEngine timing] ensureComplex=" + (M.ms() - t0) + "ms"); + Iris.debug("[IrisEngine timing] ensureComplex=" + (M.ms() - t0) + "ms"); t0 = M.ms(); upperContext = buildUpperContext(); - Iris.info("[IrisEngine timing] buildUpperContext=" + (M.ms() - t0) + "ms"); + Iris.debug("[IrisEngine timing] buildUpperContext=" + (M.ms() - t0) + "ms"); t0 = M.ms(); effects = new IrisEngineEffects(this); - Iris.info("[IrisEngine timing] IrisEngineEffects=" + (M.ms() - t0) + "ms"); + Iris.debug("[IrisEngine timing] IrisEngineEffects=" + (M.ms() - t0) + "ms"); hash32 = new CompletableFuture<>(); t0 = M.ms(); mantle.hotload(); - Iris.info("[IrisEngine timing] mantle.hotload=" + (M.ms() - t0) + "ms"); + Iris.debug("[IrisEngine timing] mantle.hotload=" + (M.ms() - t0) + "ms"); t0 = M.ms(); setupMode(); - Iris.info("[IrisEngine timing] setupMode=" + (M.ms() - t0) + "ms"); + Iris.debug("[IrisEngine timing] setupMode=" + (M.ms() - t0) + "ms"); t0 = M.ms(); IrisWorldManager manager = new IrisWorldManager(this); worldManager = manager; manager.startManager(); - Iris.info("[IrisEngine timing] IrisWorldManager=" + (M.ms() - t0) + "ms"); + Iris.debug("[IrisEngine timing] IrisWorldManager=" + (M.ms() - t0) + "ms"); J.a(this::computeBiomeMaxes); J.a(() -> { File[] roots = getData().getLoaders() @@ -255,6 +256,7 @@ public class IrisEngine implements Engine { .toArray(File[]::new); hash32.complete(IO.hashRecursiveMeta(roots)); }); + J.a(() -> DatapackIngestService.refreshWorkspace(getData())); } catch (Throwable e) { Iris.error("FAILED TO SETUP ENGINE!"); e.printStackTrace(); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java b/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java index 5b42f3fbc..8c18d28f4 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/IrisStructureLocator.java @@ -25,7 +25,6 @@ import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisStructure; import art.arcane.iris.engine.object.IrisStructurePlacement; import art.arcane.iris.engine.object.StructureDistribution; -import art.arcane.iris.engine.object.StructurePlacementRoute; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; @@ -73,6 +72,18 @@ public final class IrisStructureLocator { return idx.loadKeys.contains(key) || idx.vanillaSources.contains(key); } + public static boolean suppressesVanilla(Engine engine, String vanillaKey) { + if (engine == null || vanillaKey == null || vanillaKey.isEmpty()) { + return false; + } + for (String source : index(engine).vanillaSources) { + if (source.equalsIgnoreCase(vanillaKey)) { + return true; + } + } + return false; + } + public static boolean startsInChunk(Engine engine, String key, int cx, int cz) { if (!isPlaced(engine, key)) { return false; @@ -80,9 +91,6 @@ public final class IrisStructureLocator { long seed = engine.getSeedManager().getMantle(); RNG rng = new RNG(Cache.key(cx, cz) + seed); for (IrisStructurePlacement placement : placementsAt(engine, cx, cz)) { - if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { - continue; - } if (!matches(placement, key, engine.getData())) { continue; } @@ -197,7 +205,7 @@ public final class IrisStructureLocator { } for (IrisStructurePlacement placement : all) { - if (placement == null || placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { + if (placement == null) { continue; } if (!matches(placement, key, data)) { @@ -258,9 +266,6 @@ public final class IrisStructureLocator { int bx = 8 + (cx << 4); int bz = 8 + (cz << 4); for (IrisStructurePlacement placement : placementsAt(engine, cx, cz)) { - if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { - continue; - } if (!matches(placement, key, data)) { continue; } @@ -302,7 +307,7 @@ public final class IrisStructureLocator { return; } for (IrisStructurePlacement placement : placements) { - if (placement == null || placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { + if (placement == null) { continue; } for (String structureKey : placement.getStructures()) { diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java index f9707e8cf..77aa748da 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisStructureComponent.java @@ -37,7 +37,6 @@ import art.arcane.iris.engine.object.ObjectPlaceMode; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.iris.engine.object.IrisStructure; import art.arcane.iris.engine.object.IrisStructurePlacement; -import art.arcane.iris.engine.object.StructurePlacementRoute; import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.project.context.ChunkContext; @@ -82,9 +81,6 @@ public class IrisStructureComponent extends IrisMantleComponent { @ChunkCoordinates private void placeFromPlacement(MantleWriter writer, IrisStructurePlacement placement, int cx, int cz, RNG rng) { - if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { - return; - } if (placement.getStructures().isEmpty()) { return; } @@ -368,9 +364,6 @@ public class IrisStructureComponent extends IrisMantleComponent { private int maxBlocksFrom(KList placements) { int max = 0; for (IrisStructurePlacement placement : placements) { - if (placement.getRoute() == StructurePlacementRoute.NATIVE_AT_POINT) { - continue; - } int carvePadding = placement.isOverbore() ? Math.max(0, placement.getOverboreRadius()) : placement.isBore() ? Math.max(0, placement.getBorePadding()) : 0; for (String key : placement.getStructures()) { diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java index 229b858f2..e205316c1 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimension.java @@ -255,11 +255,14 @@ public class IrisDimension extends IrisRegistrant { @Desc("Collection of ores to be generated") @ArrayType(type = IrisOreGenerator.class, min = 1) private KList ores = new KList<>(); - @Desc("Dimension-wide structure placements (jigsaw or vanilla/datapack), independent of biome/region placements") + @Desc("Dimension-wide Iris structure placements, independent of biome/region placements") @ArrayType(type = IrisStructurePlacement.class, min = 1) private KList structures = new KList<>(); - @Desc("Controls native vanilla & datapack structure generation for this dimension. Defaults to ALL_ON.") - private IrisVanillaStructureControl vanillaStructures = new IrisVanillaStructureControl(); + @Desc("Controls native vanilla & ingested datapack structure generation for this dimension. Defaults to ALL_ON. The 'disabled'/'enabled' lists autocomplete every live vanilla and ingested datapack structure key.") + private IrisImportedStructureControl importedStructures = new IrisImportedStructureControl(); + @ArrayType(type = String.class, min = 1) + @Desc("External datapack imports for this dimension. List Modrinth datapack page URLs ('https://modrinth.com/datapack/' resolves the latest datapack version, or a specific '.../version/' link pins a version) or a direct .zip URL. Run '/iris datapack ingest' (or '/iris ingest') to download/update them and install them into the world's datapacks folder. Once ingested, their structures behave exactly like vanilla jigsaw structures. To REPLACE a vanilla structure with an imported one, add a 'structures' placement (route IRIS_PLACED) referencing the imported structure key (which carries the matching 'vanillaSource', e.g. 'minecraft:stronghold'): its vanilla counterpart is then automatically suppressed, so it never double-generates - no 'importedStructures.disabled' entry needed. Use 'importedStructures.disabled' only to turn a vanilla structure OFF entirely when you are NOT placing a replacement, or place any imported structure key arbitrarily through a 'structures' placement.") + private KList datapackImports = new KList<>(); @MinNumber(0) @MaxNumber(318) @Desc("The Subterrain Fluid Layer Height") diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureControl.java b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java similarity index 81% rename from core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureControl.java rename to core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java index 12c1436b7..8c646506e 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisVanillaStructureControl.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisImportedStructureControl.java @@ -32,9 +32,9 @@ import lombok.experimental.Accessors; @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor -@Desc("Controls native vanilla & datapack structure generation for this dimension (set as the dimension's 'vanillaStructures' field). Default mode is ALL_ON (everything generates). Blacklist a few: mode=ALL_ON + list them in 'disabled'. Whitelist a few: mode=ALL_OFF + list them in 'enabled'. Key matching is exact OR prefix (an entry matches when the structure key equals it or starts with it), so 'minecraft:village' covers every village variant. Run '/iris structure list ' to dump every valid key. Only affects NEWLY generated chunks, and is separate from the IRIS_PLACED import system (imported structures are controlled by biome/region/dimension 'structures' placements, not here).") +@Desc("Controls native vanilla & ingested datapack structure generation for this dimension (set as the dimension's 'importedStructures' field). Default mode is ALL_ON (everything generates). Blacklist a few: mode=ALL_ON + list them in 'disabled'. Whitelist a few: mode=ALL_OFF + list them in 'enabled'. Both lists autocomplete every live vanilla AND ingested datapack structure key. Key matching is exact OR prefix (an entry matches when the structure key equals it or starts with it), so 'minecraft:village' covers every village variant. Run '/iris structure list ' to dump every valid key. Only affects NEWLY generated chunks, and is separate from the Iris 'structures' placement system (imported structures are placed by biome/region/dimension 'structures' lists, not here).") @Data -public class IrisVanillaStructureControl { +public class IrisImportedStructureControl { @Desc("Master toggle. ALL_ON generates every vanilla & datapack structure except those in 'disabled'. ALL_OFF (or CUSTOM) generates nothing except those in 'enabled'.") private VanillaStructureMode mode = VanillaStructureMode.ALL_ON; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java index 1170853c3..747db19f8 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisRegion.java @@ -128,14 +128,12 @@ public class IrisRegion extends IrisRegistrant implements IRare { @Desc("A list of root-level biomes in this region. Don't specify child biomes of other biomes here. Just the root parents.") private KList landBiomes = new KList<>(); @RegistryListResource(IrisBiome.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("A list of root-level biomes in this region. Don't specify child biomes of other biomes here. Just the root parents.") + @ArrayType(type = String.class) + @Desc("A list of root-level sea biomes in this region. Optional: leave empty for land-only dimensions (flat, island, or fully-land worlds where no ocean is generated). Don't specify child biomes of other biomes here. Just the root parents.") private KList seaBiomes = new KList<>(); @RegistryListResource(IrisBiome.class) - @Required - @ArrayType(min = 1, type = String.class) - @Desc("A list of root-level biomes in this region. Don't specify child biomes of other biomes here. Just the root parents.") + @ArrayType(type = String.class) + @Desc("A list of root-level shore biomes in this region. Optional: leave empty for land-only dimensions (flat, island, or fully-land worlds where no shoreline is generated). Don't specify child biomes of other biomes here. Just the root parents.") private KList shoreBiomes = new KList<>(); @RegistryListResource(IrisBiome.class) @ArrayType(min = 1, type = String.class) diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java b/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java index 3a85c87f8..e7c01c04d 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisStructurePlacement.java @@ -23,7 +23,6 @@ import art.arcane.iris.engine.object.annotations.Desc; import art.arcane.iris.engine.object.annotations.MaxNumber; import art.arcane.iris.engine.object.annotations.MinNumber; import art.arcane.iris.engine.object.annotations.RegistryListResource; -import art.arcane.iris.engine.object.annotations.RegistryListVanillaStructure; import art.arcane.volmlib.util.collection.KList; import lombok.AllArgsConstructor; import lombok.Data; @@ -38,17 +37,9 @@ import lombok.experimental.Accessors; public class IrisStructurePlacement { @ArrayType(type = String.class, min = 1) @RegistryListResource(IrisStructure.class) - @Desc("Iris structures to place here (route IRIS_PLACED). Use this for imported or hand-authored structures with full block control.") + @Desc("Iris structures to place here. Use this for imported or hand-authored structures with full block control.") private KList structures = new KList<>(); - @ArrayType(type = String.class, min = 1) - @RegistryListVanillaStructure - @Desc("Vanilla or datapack structure keys to place here faithfully (route NATIVE_AT_POINT), e.g. minecraft:igloo. Real loot/mobs/variety, no block editing.") - private KList vanilla = new KList<>(); - - @Desc("How the placement is realized. Defaults sensibly by which list is populated, but you may set it explicitly.") - private StructurePlacementRoute route = StructurePlacementRoute.IRIS_PLACED; - @Desc("How start positions are scattered.") private StructureDistribution distribution = StructureDistribution.RANDOM_SPREAD; @@ -81,46 +72,46 @@ public class IrisStructurePlacement { @Desc("CONCENTRIC_RINGS only: how many placements share each ring before moving outward.") private int ringSpread = 3; - @Desc("IRIS_PLACED only: rotation applied to the placed structure.") + @Desc("rotation applied to the placed structure.") private IrisObjectRotation rotation = new IrisObjectRotation(); - @Desc("IRIS_PLACED only: translation applied to the placed structure.") + @Desc("translation applied to the placed structure.") private IrisObjectTranslate translate = new IrisObjectTranslate(); - @Desc("IRIS_PLACED only: scale applied to the placed structure.") + @Desc("scale applied to the placed structure.") private IrisObjectScale scale = new IrisObjectScale(); - @Desc("IRIS_PLACED: when underground=false this is the minimum surface Y the placement is allowed at (a gate); when underground=true this is the lower bound of the Y band the structure is placed within.") + @Desc("When underground=false this is the minimum surface Y the placement is allowed at (a gate); when underground=true this is the lower bound of the Y band the structure is placed within.") private int minHeight = -2032; - @Desc("IRIS_PLACED: when underground=false this is the maximum surface Y the placement is allowed at (a gate); when underground=true this is the upper bound of the Y band the structure is placed within.") + @Desc("When underground=false this is the maximum surface Y the placement is allowed at (a gate); when underground=true this is the upper bound of the Y band the structure is placed within.") private int maxHeight = 2032; - @Desc("IRIS_PLACED only: if true the structure is placed underground at a random world Y inside [minHeight, maxHeight] (raw stamp, no terrain matching) instead of being dropped onto the terrain surface. Use this for deep structures like ancient cities in a deep cave band.") + @Desc("if true the structure is placed underground at a random world Y inside [minHeight, maxHeight] (raw stamp, no terrain matching) instead of being dropped onto the terrain surface. Use this for deep structures like ancient cities in a deep cave band.") private boolean underground = false; - @Desc("IRIS_PLACED only: if true, the structure's full bounding box (floor up to roof) is bored out to air before the pieces are stamped, so the structure sits inside an open cavern instead of being encased in solid terrain. Essential for underground structures such as ancient cities to be visible and enterable.") + @Desc("if true, the structure's full bounding box (floor up to roof) is bored out to air before the pieces are stamped, so the structure sits inside an open cavern instead of being encased in solid terrain. Essential for underground structures such as ancient cities to be visible and enterable.") private boolean bore = false; - @Desc("IRIS_PLACED only: extra blocks of air clearance added around the bored bounding box (horizontally and above) when bore=true. The floor is never bored below the structure so support is preserved.") + @Desc("extra blocks of air clearance added around the bored bounding box (horizontally and above) when bore=true. The floor is never bored below the structure so support is preserved.") private int borePadding = 0; - @Desc("IRIS_PLACED only: if true, massively carves the surrounding terrain into an open cavern around the structure instead of only clearing its tight bounding box. The structure's full interior is cleared and the surrounding terrain is excavated out to overboreRadius blocks, doming down to meet the ground at the edges so the structure is no longer buried in solid terrain. The mantle write window and the carve volume cap are expanded automatically to fit. Use for deep structures such as ancient cities so both their interior and surroundings are open and enterable. Takes precedence over bore when both are set.") + @Desc("if true, massively carves the surrounding terrain into an open cavern around the structure instead of only clearing its tight bounding box. The structure's full interior is cleared and the surrounding terrain is excavated out to overboreRadius blocks, doming down to meet the ground at the edges so the structure is no longer buried in solid terrain. The mantle write window and the carve volume cap are expanded automatically to fit. Use for deep structures such as ancient cities so both their interior and surroundings are open and enterable. Takes precedence over bore when both are set.") private boolean overbore = false; @MinNumber(0) @MaxNumber(128) - @Desc("IRIS_PLACED only: when overbore=true, how many blocks of terrain to carve horizontally outward from the structure's bounding box. Larger values produce a wider open cavern around the structure. Larger values also widen the mantle write window for every chunk near the structure, so increase it deliberately.") + @Desc("when overbore=true, how many blocks of terrain to carve horizontally outward from the structure's bounding box. Larger values produce a wider open cavern around the structure. Larger values also widen the mantle write window for every chunk near the structure, so increase it deliberately.") private int overboreRadius = 24; @MinNumber(0) @MaxNumber(128) - @Desc("IRIS_PLACED only: when overbore=true, how many extra blocks of air to carve above the structure's roof at the cavern apex.") + @Desc("when overbore=true, how many extra blocks of air to carve above the structure's roof at the cavern apex.") private int overboreHeight = 8; @MinNumber(0) @MaxNumber(64) - @Desc("IRIS_PLACED only: when overbore=true, how many blocks of terrain to carve below the structure's floor. 0 keeps the floor solid for support; small values recess the cavern floor around the structure.") + @Desc("when overbore=true, how many blocks of terrain to carve below the structure's floor. 0 keeps the floor solid for support; small values recess the cavern floor around the structure.") private int overboreFloor = 0; @Desc("If false, this placement is skipped underwater.") diff --git a/core/src/main/java/art/arcane/iris/engine/object/StructurePlacementRoute.java b/core/src/main/java/art/arcane/iris/engine/object/StructurePlacementRoute.java deleted file mode 100644 index a3a80bd44..000000000 --- a/core/src/main/java/art/arcane/iris/engine/object/StructurePlacementRoute.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.engine.object; - -import art.arcane.iris.engine.object.annotations.Desc; - -@Desc("How a custom structure placement is realized in the world.") -public enum StructurePlacementRoute { - @Desc("Place an imported or hand-authored Iris structure through the object pipeline. Full block & content control. References structures (IrisStructure keys).") - IRIS_PLACED, - - @Desc("Invoke the real vanilla/datapack structure generation at the chosen coordinates (faithful loot, mobs, jigsaw variety). No Iris block editing. References vanilla (structure keys).") - NATIVE_AT_POINT -} diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java index e6e0b774a..0e9e5cfa0 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -96,9 +96,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun private final AtomicBoolean setup; private final boolean studio; private final AtomicInteger a = new AtomicInteger(0); - private final AtomicBoolean firstChunkLogged = new AtomicBoolean(false); - private volatile long firstChunkTime = 0L; - private final AtomicInteger chunkGenCount = new AtomicInteger(0); private volatile long lastChunkGenTime = 0L; private final CompletableFuture spawnChunks = new CompletableFuture<>(); private final AtomicCache targetCache = new AtomicCache<>(); @@ -153,7 +150,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun if (engine == null) return false; try { INMS.get().inject(world.getSeed(), engine, world); - Iris.info("Injected Iris Biome Source into " + world.getName()); + Iris.debug("Injected Iris Biome Source into " + world.getName()); if (!studio) { J.s(() -> updateSpawnLocation(world), 1); } @@ -704,17 +701,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun try { Engine engine = getEngine(world); lastChunkGenTime = System.currentTimeMillis(); - if (firstChunkLogged.compareAndSet(false, true)) { - firstChunkTime = System.currentTimeMillis(); - Iris.info("[Studio timing] FIRST chunk generating in " + world.getName() + " at chunk " + x + "," + z + " (player-view terrain starts here — server-side open prep already done)"); - } - if (studio) { - int gen = chunkGenCount.incrementAndGet(); - if (gen % 64 == 0) { - long el = Math.max(1L, System.currentTimeMillis() - firstChunkTime); - Iris.info("[Studio timing] player-view terrain: " + gen + " chunks in " + el + "ms (" + (gen * 1000L / el) + " chunks/sec) — THIS is the wait you actually see"); - } - } computeStudioGenerator(); TerrainChunk tc = TerrainChunk.create(d); this.world.bind(world); @@ -843,7 +829,7 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun if (e == null) { return true; } - return e.getDimension().getVanillaStructures().getMode() != VanillaStructureMode.ALL_OFF; + return e.getDimension().getImportedStructures().getMode() != VanillaStructureMode.ALL_OFF; } catch (Throwable t) { return true; } diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java index 9eb4e793a..ad3b981be 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java @@ -4,7 +4,7 @@ import com.mojang.datafixers.util.Pair; import com.mojang.serialization.MapCodec; import art.arcane.iris.Iris; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.IrisVanillaStructureControl; +import art.arcane.iris.engine.object.IrisImportedStructureControl; import art.arcane.iris.util.common.reflect.WrappedField; import art.arcane.iris.util.common.reflect.WrappedReturningMethod; import net.minecraft.core.*; @@ -32,14 +32,6 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp import net.minecraft.core.registries.Registries; import java.util.stream.Collectors; import art.arcane.iris.engine.framework.IrisStructureLocator; -import art.arcane.iris.engine.framework.StructurePlacementGrid; -import art.arcane.iris.engine.object.IrisStructurePlacement; -import art.arcane.iris.engine.object.StructurePlacementRoute; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.data.cache.Cache; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.math.RNG; import org.bukkit.World; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.generator.CustomChunkGenerator; @@ -96,7 +88,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } catch (Throwable e) { Iris.reportError(e); } - if (!vanillaControl().active()) { + if (!importedControl().active()) { return null; } try { @@ -130,14 +122,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey levelKey) { - if (!vanillaControl().active()) { + if (!importedControl().active()) { return; } delegate.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey); } - private IrisVanillaStructureControl vanillaControl() { - return engine.getDimension().getVanillaStructures(); + private IrisImportedStructureControl importedControl() { + return engine.getDimension().getImportedStructures(); } @Override @@ -188,10 +180,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) { addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager); - if (vanillaControl().active()) { + if (importedControl().active()) { placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager); } - placeIrisNativeStructures(generatoraccessseed, ichunkaccess); delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false); } @@ -208,13 +199,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator { long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); BoundingBox area = writableArea(chunk); int steps = GenerationStep.Decoration.values().length; - IrisVanillaStructureControl control = vanillaControl(); + IrisImportedStructureControl control = importedControl(); int undergroundShift = control.getUndergroundYShift(); for (int step = 0; step < steps; step++) { int index = 0; for (Structure structure : byStep.getOrDefault(step, List.of())) { Object id = registry.getKey(structure); - if (control.shouldGenerate(id == null ? null : id.toString())) { + String structureId = id == null ? null : id.toString(); + if (control.shouldGenerate(structureId) && !IrisStructureLocator.suppressesVanilla(engine, structureId)) { random.setFeatureSeed(decoSeed, index, step); int yShift = (undergroundShift != 0 && isUndergroundStep(structure.step())) ? undergroundShift : 0; WorldGenLevel target = yShift == 0 ? world : yShiftedLevel(world, yShift); @@ -264,73 +256,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator { return new BoundingBox(i, minY, j, i + 15, maxY, j + 15); } - private void placeIrisNativeStructures(WorldGenLevel world, ChunkAccess chunk) { - if (!(world instanceof WorldGenRegion region)) { - return; - } - ChunkPos chunkPos = chunk.getPos(); - int cx = chunkPos.getMinBlockX() >> 4; - int cz = chunkPos.getMinBlockZ() >> 4; - int bx = 8 + (cx << 4); - int bz = 8 + (cz << 4); - IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz); - IrisRegion ireg = engine.getComplex().getRegionStream().get(bx, bz); - KList placements = new KList<>(); - if (biome != null) { - placements.addAll(biome.getStructures()); - } - if (ireg != null) { - placements.addAll(ireg.getStructures()); - } - placements.addAll(engine.getDimension().getStructures()); - if (placements.isEmpty()) { - return; - } - - long seed = world.getSeed(); - RNG rng = new RNG(Cache.key(cx, cz) + seed); - ServerLevel level = region.getLevel(); - Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); - RandomState randomState = level.getChunkSource().randomState(); - StructureTemplateManager templateManager = level.getStructureManager(); - StructureManager structureManager = level.structureManager(); - BiomeSource biomeSource = getBiomeSource(); - ResourceKey levelKey = level.dimension(); - int minY = getMinY() + 1; - int maxY = getMinY() + engine.getHeight() - 1; - - for (IrisStructurePlacement placement : placements) { - if (placement.getRoute() != StructurePlacementRoute.NATIVE_AT_POINT || placement.getVanilla().isEmpty()) { - continue; - } - if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) { - continue; - } - String key = placement.getVanilla().get(rng.i(0, placement.getVanilla().size() - 1)).toLowerCase(); - try { - String[] parts = key.split(":", 2); - Identifier id = parts.length > 1 ? Identifier.fromNamespaceAndPath(parts[0], parts[1]) : Identifier.fromNamespaceAndPath("minecraft", parts[0]); - Holder holder = registry.getOrThrow(ResourceKey.create(Registries.STRUCTURE, id)); - Structure structure = holder.value(); - StructureStart start = structure.generate(holder, levelKey, world.registryAccess(), this, biomeSource, randomState, templateManager, seed, chunkPos, 0, level, b -> true); - if (start == null || !start.isValid()) { - continue; - } - BoundingBox box = start.getBoundingBox(); - WorldgenRandom placeRandom = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); - for (int scx = box.minX() >> 4; scx <= box.maxX() >> 4; scx++) { - for (int scz = box.minZ() >> 4; scz <= box.maxZ() >> 4; scz++) { - ChunkPos target = new ChunkPos(scx, scz); - BoundingBox chunkBox = new BoundingBox(target.getMinBlockX(), minY, target.getMinBlockZ(), target.getMaxBlockX(), maxY, target.getMaxBlockZ()); - start.placeInChunk(world, structureManager, this, placeRandom, chunkBox, target); - } - } - } catch (Throwable e) { - Iris.reportError(e); - } - } - } - @Override public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) { SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY()); diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java index b5b835a6e..0f639a8b0 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/NMSBinding.java @@ -585,7 +585,7 @@ public class NMSBinding implements INMSBinding { if (!dimensionType.identifier().getNamespace().equals("iris")) { Iris.error("Loaded world %s with invalid dimension type! expected=%s actual=%s", world.getName(), expectedDimensionType, actualDimensionType); } else { - Iris.info("Loaded world %s with Iris dimension type %s", world.getName(), actualDimensionType); + Iris.debug("Loaded world " + world.getName() + " with Iris dimension type " + actualDimensionType); } } else { Iris.error("Loaded world %s with unknown dimension type! expected=%s", world.getName(), expectedDimensionType); diff --git a/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/IrisChunkGenerator.java b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/IrisChunkGenerator.java index 2e63a70fd..8f53c2f34 100644 --- a/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/IrisChunkGenerator.java +++ b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/IrisChunkGenerator.java @@ -4,7 +4,7 @@ import com.mojang.datafixers.util.Pair; import com.mojang.serialization.MapCodec; import art.arcane.iris.Iris; import art.arcane.iris.engine.framework.Engine; -import art.arcane.iris.engine.object.IrisVanillaStructureControl; +import art.arcane.iris.engine.object.IrisImportedStructureControl; import art.arcane.iris.util.common.reflect.WrappedField; import art.arcane.iris.util.common.reflect.WrappedReturningMethod; import net.minecraft.core.*; @@ -32,14 +32,6 @@ import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemp import net.minecraft.core.registries.Registries; import java.util.stream.Collectors; import art.arcane.iris.engine.framework.IrisStructureLocator; -import art.arcane.iris.engine.framework.StructurePlacementGrid; -import art.arcane.iris.engine.object.IrisStructurePlacement; -import art.arcane.iris.engine.object.StructurePlacementRoute; -import art.arcane.iris.engine.object.IrisBiome; -import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.engine.data.cache.Cache; -import art.arcane.volmlib.util.collection.KList; -import art.arcane.volmlib.util.math.RNG; import org.bukkit.World; import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.generator.CustomChunkGenerator; @@ -96,7 +88,7 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } catch (Throwable e) { Iris.reportError(e); } - if (!vanillaControl().active()) { + if (!importedControl().active()) { return null; } try { @@ -130,14 +122,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public void createStructures(RegistryAccess registryAccess, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess access, StructureTemplateManager templateManager, ResourceKey levelKey) { - if (!vanillaControl().active()) { + if (!importedControl().active()) { return; } delegate.createStructures(registryAccess, structureState, structureManager, access, templateManager, levelKey); } - private IrisVanillaStructureControl vanillaControl() { - return engine.getDimension().getVanillaStructures(); + private IrisImportedStructureControl importedControl() { + return engine.getDimension().getImportedStructures(); } @Override @@ -188,10 +180,9 @@ public class IrisChunkGenerator extends CustomChunkGenerator { @Override public void applyBiomeDecoration(WorldGenLevel generatoraccessseed, ChunkAccess ichunkaccess, StructureManager structuremanager, boolean vanilla) { addVanillaDecorations(generatoraccessseed, ichunkaccess, structuremanager); - if (vanillaControl().active()) { + if (importedControl().active()) { placeVanillaStructures(generatoraccessseed, ichunkaccess, structuremanager); } - placeIrisNativeStructures(generatoraccessseed, ichunkaccess); delegate.applyBiomeDecoration(generatoraccessseed, ichunkaccess, structuremanager, false); } @@ -208,13 +199,14 @@ public class IrisChunkGenerator extends CustomChunkGenerator { long decoSeed = random.setDecorationSeed(world.getSeed(), origin.getX(), origin.getZ()); BoundingBox area = writableArea(chunk); int steps = GenerationStep.Decoration.values().length; - IrisVanillaStructureControl control = vanillaControl(); + IrisImportedStructureControl control = importedControl(); int undergroundShift = control.getUndergroundYShift(); for (int step = 0; step < steps; step++) { int index = 0; for (Structure structure : byStep.getOrDefault(step, List.of())) { Object id = registry.getKey(structure); - if (control.shouldGenerate(id == null ? null : id.toString())) { + String structureId = id == null ? null : id.toString(); + if (control.shouldGenerate(structureId) && !IrisStructureLocator.suppressesVanilla(engine, structureId)) { random.setFeatureSeed(decoSeed, index, step); int yShift = (undergroundShift != 0 && isUndergroundStep(structure.step())) ? undergroundShift : 0; WorldGenLevel target = yShift == 0 ? world : yShiftedLevel(world, yShift); @@ -264,73 +256,6 @@ public class IrisChunkGenerator extends CustomChunkGenerator { return new BoundingBox(i, minY, j, i + 15, maxY, j + 15); } - private void placeIrisNativeStructures(WorldGenLevel world, ChunkAccess chunk) { - if (!(world instanceof WorldGenRegion region)) { - return; - } - ChunkPos chunkPos = chunk.getPos(); - int cx = chunkPos.getMinBlockX() >> 4; - int cz = chunkPos.getMinBlockZ() >> 4; - int bx = 8 + (cx << 4); - int bz = 8 + (cz << 4); - IrisBiome biome = engine.getComplex().getTrueBiomeStream().get(bx, bz); - IrisRegion ireg = engine.getComplex().getRegionStream().get(bx, bz); - KList placements = new KList<>(); - if (biome != null) { - placements.addAll(biome.getStructures()); - } - if (ireg != null) { - placements.addAll(ireg.getStructures()); - } - placements.addAll(engine.getDimension().getStructures()); - if (placements.isEmpty()) { - return; - } - - long seed = world.getSeed(); - RNG rng = new RNG(Cache.key(cx, cz) + seed); - ServerLevel level = region.getLevel(); - Registry registry = world.registryAccess().lookupOrThrow(Registries.STRUCTURE); - RandomState randomState = level.getChunkSource().randomState(); - StructureTemplateManager templateManager = level.getStructureManager(); - StructureManager structureManager = level.structureManager(); - BiomeSource biomeSource = getBiomeSource(); - ResourceKey levelKey = level.dimension(); - int minY = getMinY() + 1; - int maxY = getMinY() + engine.getHeight() - 1; - - for (IrisStructurePlacement placement : placements) { - if (placement.getRoute() != StructurePlacementRoute.NATIVE_AT_POINT || placement.getVanilla().isEmpty()) { - continue; - } - if (!StructurePlacementGrid.startsInChunk(placement, cx, cz, seed, rng)) { - continue; - } - String key = placement.getVanilla().get(rng.i(0, placement.getVanilla().size() - 1)).toLowerCase(); - try { - String[] parts = key.split(":", 2); - Identifier id = parts.length > 1 ? Identifier.fromNamespaceAndPath(parts[0], parts[1]) : Identifier.fromNamespaceAndPath("minecraft", parts[0]); - Holder holder = registry.getOrThrow(ResourceKey.create(Registries.STRUCTURE, id)); - Structure structure = holder.value(); - StructureStart start = structure.generate(holder, levelKey, world.registryAccess(), this, biomeSource, randomState, templateManager, seed, chunkPos, 0, level, b -> true); - if (start == null || !start.isValid()) { - continue; - } - BoundingBox box = start.getBoundingBox(); - WorldgenRandom placeRandom = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); - for (int scx = box.minX() >> 4; scx <= box.maxX() >> 4; scx++) { - for (int scz = box.minZ() >> 4; scz <= box.maxZ() >> 4; scz++) { - ChunkPos target = new ChunkPos(scx, scz); - BoundingBox chunkBox = new BoundingBox(target.getMinBlockX(), minY, target.getMinBlockZ(), target.getMaxBlockX(), maxY, target.getMaxBlockZ()); - start.placeInChunk(world, structureManager, this, placeRandom, chunkBox, target); - } - } - } catch (Throwable e) { - Iris.reportError(e); - } - } - } - @Override public void addVanillaDecorations(WorldGenLevel level, ChunkAccess chunkAccess, StructureManager structureManager) { SectionPos sectionPos = SectionPos.of(chunkAccess.getPos(), level.getMinSectionY()); diff --git a/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java index 829b43569..300afe7ac 100644 --- a/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java +++ b/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java @@ -585,7 +585,7 @@ public class NMSBinding implements INMSBinding { if (!dimensionType.identifier().getNamespace().equals("iris")) { Iris.error("Loaded world %s with invalid dimension type! expected=%s actual=%s", world.getName(), expectedDimensionType, actualDimensionType); } else { - Iris.info("Loaded world %s with Iris dimension type %s", world.getName(), actualDimensionType); + Iris.debug("Loaded world " + world.getName() + " with Iris dimension type " + actualDimensionType); } } else { Iris.error("Loaded world %s with unknown dimension type! expected=%s", world.getName(), expectedDimensionType);